フォームとリード獲得:コードまたは管理画面で作成、スパム対策、GDPR 同意、メール通知、署名付き Webhook、CSV
インストール
genpm add @core/forms含まれるもの
- src/lib/forms/ にソースコード(11 ファイル)。 (32.3 KB)
- src/lib/forms/AGENTS.md に AI ルール、加えて IDE 用のルールファイル。
- .env.example に追加される環境変数: FORMS_NOTIFY_TO, FORMS_WEBHOOK_SECRET。
- @core/antispam, @core/content, @core/contracts, @core/db, @core/email, @core/jobs を自動で解決します。
README
このパッケージには README がありません。
これは AI が src/lib/forms で作業するときに読む内容そのものです。それ以外はコンテキストに追加されません。
@core/forms — rules for AI agents
Purpose
Contact and lead forms end to end: forms defined in code (defineForm with zod) or built by editors in the admin
(forms content collection), anti-spam (@core/antispam), GDPR consent with a versioned text, stored submissions with
source page and campaign parameters, and background processing (@core/jobs): team alert email, auto-reply,
HMAC-signed webhook and onSubmission handlers. CSV export, retention and erasure helpers. Table: form_submissions.
Map
index.ts— public API:defineForm,submitForm,onSubmission,formsAdminResources,exportSubmissionsCsv,pruneSubmissions,deleteSubmissionsByEmail.definitions.ts— code and admin-built definitions.submit.ts— submission and theforms.processjob.admin.ts— admin, CSV, privacy.react.ts—<Form formKey searchParams>(async server component; works without JavaScript).adapters/hono.ts—formRoutes().adapters/next.ts—formRoute(POST; HTML posts get a 303 back to the page).
Integration
- Env:
EMAIL_FROM(@core/email), optionalFORMS_NOTIFY_TO(comma-separated default recipients) andFORMS_WEBHOOK_SECRET(≥ 32 chars, only with webhooks). Migrations as insrc/lib/db/AGENTS.md; the @core/jobs cron must run. - Define code forms in a module imported at startup (e.g.
src/genpm/forms.ts):defineForm({ key: 'contact', label: 'Contact', schema: z.object({ name: z.string().min(1), email: z.email(), message: z.string().min(5) }), emailField: 'email', consent: { required: true, version: '2026-10', text: '…' } }). - Mount
formRoutes()orformRouteat/api/forms/<key>and render<Form formKey="contact" searchParams={await searchParams} />: it includes the anti-spam fields, the consent checkbox (never pre-checked) and shows the result after the 303. Custom markup: post to the same endpoint withhoneypotProps,_tsandconsent; JSON callers get{ ok, reason, errors }. In Next.js Server Actions callsubmitForm(key, formData, { headers: await headers() })instead. - Translate the messages with
labels(keys:formLabels). - Add
...formsAdminResources()tosrc/genpm/admin.ts. - Verify: submit the form, run the cron, the team receives the email and the submission appears in the admin.
Conventions
- Field names are the keys stored in
data; keep them stable (exports and webhooks depend on them). forms.processrecords each finished step inform_submissions.processed(notify,auto_reply,webhook,handler:<n>): a retry skips them, so emails and webhooks aren't repeated. KeeponSubmissionhandlers in a stable order.- Webhook receivers must verify
x-genpm-signature(t=<unix>,v1=<hmac>over"<t>.<body>") and reject old timestamps. - Ask only for the data you need; define a retention period and schedule
pruneSubmissions(days). - Permissions:
submissions:read|update|delete|export,forms:*for admin-built forms.
Don't
- Don't send emails or call webhooks inside the request;
submitFormqueues them. - Don't put submission values into HTML without escaping, or open CSV exports from other tools without the formula guard.
- Don't add hidden fields that collect data the user didn't type.
# @core/forms — rules for AI agents
## Purpose
Contact and lead forms end to end: forms defined in code (`defineForm` with zod) or built by editors in the admin
(`forms` content collection), anti-spam (@core/antispam), GDPR consent with a versioned text, stored submissions with
source page and campaign parameters, and background processing (@core/jobs): team alert email, auto-reply,
HMAC-signed webhook and `onSubmission` handlers. CSV export, retention and erasure helpers. Table: `form_submissions`.
## Map
- `index.ts` — public API: `defineForm`, `submitForm`, `onSubmission`, `formsAdminResources`, `exportSubmissionsCsv`, `pruneSubmissions`, `deleteSubmissionsByEmail`.
- `definitions.ts` — code and admin-built definitions. `submit.ts` — submission and the `forms.process` job. `admin.ts` — admin, CSV, privacy.
- `react.ts` — `<Form formKey searchParams>` (async server component; works without JavaScript).
- `adapters/hono.ts` — `formRoutes()`. `adapters/next.ts` — `formRoute` (POST; HTML posts get a 303 back to the page).
## Integration
1. Env: `EMAIL_FROM` (@core/email), optional `FORMS_NOTIFY_TO` (comma-separated default recipients) and `FORMS_WEBHOOK_SECRET` (≥ 32 chars, only with webhooks). Migrations as in `src/lib/db/AGENTS.md`; the @core/jobs cron must run.
2. Define code forms in a module imported at startup (e.g. `src/genpm/forms.ts`):
`defineForm({ key: 'contact', label: 'Contact', schema: z.object({ name: z.string().min(1), email: z.email(), message: z.string().min(5) }), emailField: 'email', consent: { required: true, version: '2026-10', text: '…' } })`.
3. Mount `formRoutes()` or `formRoute` at `/api/forms/<key>` and render `<Form formKey="contact" searchParams={await searchParams} />`:
it includes the anti-spam fields, the consent checkbox (never pre-checked) and shows the result after the 303.
Custom markup: post to the same endpoint with `honeypotProps`, `_ts` and `consent`; JSON callers get `{ ok, reason, errors }`.
In Next.js Server Actions call `submitForm(key, formData, { headers: await headers() })` instead.
4. Translate the messages with `labels` (keys: `formLabels`).
5. Add `...formsAdminResources()` to `src/genpm/admin.ts`.
6. Verify: submit the form, run the cron, the team receives the email and the submission appears in the admin.
## Conventions
- Field names are the keys stored in `data`; keep them stable (exports and webhooks depend on them).
- `forms.process` records each finished step in `form_submissions.processed` (`notify`, `auto_reply`, `webhook`,
`handler:<n>`): a retry skips them, so emails and webhooks aren't repeated. Keep `onSubmission` handlers in a stable order.
- Webhook receivers must verify `x-genpm-signature` (`t=<unix>,v1=<hmac>` over `"<t>.<body>"`) and reject old timestamps.
- Ask only for the data you need; define a retention period and schedule `pruneSubmissions(days)`.
- Permissions: `submissions:read|update|delete|export`, `forms:*` for admin-built forms.
## Don't
- Don't send emails or call webhooks inside the request; `submitForm` queues them.
- Don't put submission values into HTML without escaping, or open CSV exports from other tools without the formula guard.
- Don't add hidden fields that collect data the user didn't type.
.genpmignore 適用後に組み込まれる正確なツリーです。固定先:
// Definición de formularios: en código (`defineForm`) o desde el panel (colección `forms` de @core/content).
import { z } from 'zod';
import { defineCollection, getEntry } from '../content/index.ts';
export type ConsentConfig = { required: boolean; version: string; text: string };
export type FormDefinition = {
key: string;
label: string;
schema: z.ZodObject;
/** Emails del equipo que reciben cada envío. Por defecto `FORMS_NOTIFY_TO`. */
notify?: string[];
/** Campo con el email de quien envía: se usa como reply-to y para la respuesta automática. */
emailField?: string;
autoReply?: { subject: string; text: string };
/** Webhook https que recibe cada envío firmado con `FORMS_WEBHOOK_SECRET`. */
webhook?: string;
consent?: ConsentConfig;
/** Campos para pintar el formulario (`<Form>`). Los de código se deducen del esquema si faltan. */
fields?: BuilderField[];
};
const KEY_RE = /^[a-z][a-z0-9-]{0,47}$/;
const registry = new Map<string, FormDefinition>();
export function defineForm(def: FormDefinition): FormDefinition {
if (!KEY_RE.test(def.key)) throw new Error(`invalid form key: ${def.key}`);
if (def.webhook && !def.webhook.startsWith('https://')) throw new Error('form webhooks must use https');
registry.set(def.key, def);
return def;
}
/** Solo tests. */
export const clearForms = () => registry.clear();
// Formularios creados desde el panel: los campos se describen como datos y se convierten a zod al usarlos.
const FieldType = z.enum(['text', 'email', 'tel', 'textarea', 'number', 'select', 'checkbox']);
const BuilderField = z.object({
name: z.string().regex(/^[a-zA-Z][a-zA-Z0-9_]{0,39}$/),
label: z.string().min(1).max(120),
type: FieldType,
required: z.boolean().default(false),
options: z.array(z.string().min(1).max(120)).max(50).optional(),
maxLength: z.number().int().min(1).max(10_000).optional(),
});
export type BuilderField = z.infer<typeof BuilderField>;
export const FormBuilderSchema = z.object({
label: z.string().min(1).max(120),
fields: z.array(BuilderField).min(1).max(30),
notify: z.array(z.email()).max(10).default([]),
emailField: z.string().optional(),
autoReply: z.object({ subject: z.string().max(200), text: z.string().max(5000) }).optional(),
webhook: z.url().refine((u) => u.startsWith('https://'), 'https only').optional(),
consent: z.object({ required: z.boolean(), version: z.string().max(40), text: z.string().max(2000) }).optional(),
});
export const forms = defineCollection('forms', {
schema: FormBuilderSchema,
label: { singular: 'Form', plural: 'Forms' },
titleField: 'label',
localized: false,
});
const blank = (v: unknown) => typeof v === 'string' && v.trim() === '';
export function schemaFromFields(fields: BuilderField[]): z.ZodObject {
const shape: Record<string, z.ZodType> = {};
for (const f of fields) {
let t: z.ZodType;
const max = f.maxLength ?? (f.type === 'textarea' ? 5000 : 300);
if (f.type === 'email') t = z.email().max(254);
// `''` (campo vacío de un <form>) no es 0: se trata como ausente, también en los obligatorios.
else if (f.type === 'number') t = z.preprocess((v) => (blank(v) ? undefined : v), z.coerce.number());
else if (f.type === 'checkbox') t = z.preprocess((v) => v === true || v === 'on' || v === 'true', z.boolean());
else if (f.type === 'select' && f.options?.length) t = z.enum(f.options as [string, ...string[]]);
else t = z.string().trim().max(max);
if (f.type === 'checkbox') shape[f.name] = f.required ? t.refine((v) => v === true, 'Required') : t;
else if (f.required) shape[f.name] = f.type === 'number' || f.type === 'select' || f.type === 'email' ? t : (t as z.ZodString).min(1, 'Required');
else shape[f.name] = z.preprocess((v) => (blank(v) ? undefined : v), t.optional());
}
return z.object(shape);
}
/** Definición por clave: primero las de código, luego las publicadas desde el panel. */
export async function getForm(key: string): Promise<FormDefinition | null> {
const coded = registry.get(key);
if (coded) return coded;
if (!KEY_RE.test(key)) return null;
const entry = await getEntry<z.infer<typeof FormBuilderSchema>>('forms', key);
if (!entry) return null;
const d = entry.data;
return { key, label: d.label, schema: schemaFromFields(d.fields), notify: d.notify, emailField: d.emailField, autoReply: d.autoReply, webhook: d.webhook, consent: d.consent, fields: d.fields };
}
このパッケージは MCP サーバーを宣言していません。
| バージョン | コミット | 公開日 | スキャン |
|---|---|---|---|
| 1.1.0 | 76db1c0 | 2 時間前 | スキャン合格 |
- genpm
- @core/antispam ^1.0.0@core/content ^1.0.0@core/contracts ^1.0.0@core/db ^1.0.0@core/email ^1.0.1@core/jobs ^1.0.0
- npm
- zod ^4.0.0
- 提案
- GenPM は npm コマンドを提案し、あなたが承認した場合にのみ実行します。
- 利用元(1)
- @core/kit-cms ^1.0.0
- スキャン
- スキャン合格 · 指摘 0 件
- コミット
- v1.1.0 → 76db1c0f7e6244a5ee1cab65a5d4e23826f56190 · 取得後に検証済み
- スクリプト
- なし。GenPM はパッケージのコードを実行しません。
- ライセンス
- MIT
- 品質
- 100/100
- 認識されたライセンス達成
- AGENTS.md に目的の説明がある達成
- AGENTS.md に統合手順がある達成
- AGENTS.md に規約や禁止事項がある達成
- テストを含む達成
- セキュリティスキャンに合格達成
- 過去 6 か月以内に公開達成
- 認証済みの公開者達成
- 概要とキーワード達成
- 報告
- 問題を見つけましたか?