@nextlyhq/plugin-form-builder 0.0.2-alpha.40 → 0.0.2-alpha.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/package.json +10 -10
package/README.md
CHANGED
|
@@ -63,6 +63,10 @@ import "@nextlyhq/plugin-form-builder/admin";
|
|
|
63
63
|
- **Form lifecycle states:** Draft, Published, Closed
|
|
64
64
|
- **Optional integrations:** email notifications, webhook delivery on submission, spam protection (honeypot, rate limit, reCAPTCHA v3)
|
|
65
65
|
|
|
66
|
+
### Submission privacy
|
|
67
|
+
|
|
68
|
+
Submissions carry visitor-entered content plus `ipAddress` and `userAgent`, so the `form-submissions` collection sets `webhooks: false` — its writes are never recorded to the webhook outbox or delivered to endpoints subscribed to `entry.created` or `*`. Any collection or single can opt out the same way (`webhooks: false`, or `{ record: false }`); recording is on by default for everything else. To send submission webhooks deliberately, override the submissions collection with `webhooks: true`.
|
|
69
|
+
|
|
66
70
|
## Field types
|
|
67
71
|
|
|
68
72
|
The plugin ships 13 field types. All are enabled by default; disable any by setting it to `false` in the `fields` option.
|
package/dist/index.js
CHANGED
|
@@ -431,6 +431,11 @@ function submissionsCollection(pluginConfig) {
|
|
|
431
431
|
labels,
|
|
432
432
|
fields: finalFields,
|
|
433
433
|
timestamps: true,
|
|
434
|
+
// Submissions carry visitor-entered content plus ipAddress/userAgent, so
|
|
435
|
+
// they are never recorded to the webhook outbox — otherwise every submission
|
|
436
|
+
// would be delivered to any endpoint subscribed to entry.created or `*`. An
|
|
437
|
+
// operator who wants submission webhooks can override this to `true`.
|
|
438
|
+
webhooks: false,
|
|
434
439
|
admin: {
|
|
435
440
|
isPlugin: true,
|
|
436
441
|
group: "Forms",
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/plugin.ts","../src/collections/forms.ts","../src/collections/submissions.ts","../src/config/validate-form.ts","../src/config/field-helpers.ts","../src/utils/generate-schema.ts","../src/handlers/spam-detection.ts","../src/handlers/submit-form.ts","../src/handlers/webhooks.ts","../src/index.ts"],"sourcesContent":["/**\n * Form Builder Plugin\n *\n * Visual form builder with drag-and-drop UI, submission management,\n * email notifications, and spam protection.\n *\n * @module plugin\n * @since 0.1.0\n */\n\nimport { createRequire } from \"node:module\";\n\nimport { definePlugin, type PluginDefinition } from \"@nextlyhq/plugin-sdk\";\nimport type { CollectionConfig } from \"nextly\";\n// Author against the SDK — the stable, experimental plugin boundary.\n\nimport { formsCollection } from \"./collections/forms\";\nimport { submissionsCollection } from \"./collections/submissions\";\nimport type {\n BeforeEmailFilterContext,\n FormNotification,\n FormBuilderPluginOptions,\n FormEmailNotification,\n FormDocument,\n SubmissionDocument,\n ResolvedFormBuilderConfig,\n} from \"./types\";\nimport { evaluateSingleCondition } from \"./utils/evaluate-conditions\";\nimport { exportToCSV, generateExportFilename } from \"./utils/export-formats\";\n\nexport type NextlyPlugin = PluginDefinition;\n\n// Read the version from package.json so it can never drift from the published\n// package. Node/config-side only (this module is not part of the admin bundle).\nconst { version: PLUGIN_VERSION } = createRequire(import.meta.url)(\n \"../package.json\"\n) as { version: string };\n\n/** The runtime instance passed to plugin hooks (type extracted from `init`'s parameter). */\ntype NextlyInstance = Parameters<NonNullable<NextlyPlugin[\"init\"]>>[0];\n\n/** Internal augmentation: we stash the resolved config on the nextly instance for later retrieval. */\ntype NextlyWithFormBuilderConfig = NextlyInstance & {\n __formBuilderConfig?: ResolvedFormBuilderConfig;\n};\n\n// ---------------------------------------------------------------------------\n// Configuration resolver\n// ---------------------------------------------------------------------------\n\n/**\n * Merge user options with sensible defaults.\n */\nfunction resolveConfig(\n options: FormBuilderPluginOptions\n): ResolvedFormBuilderConfig {\n const formOverrides = options.formOverrides || {};\n const submissionOverrides = options.formSubmissionOverrides || {};\n\n return {\n formOverrides: {\n ...formOverrides,\n slug: formOverrides.slug || \"forms\",\n labels: {\n singular: formOverrides.labels?.singular || \"Form\",\n plural: formOverrides.labels?.plural || \"Forms\",\n },\n },\n\n formSubmissionOverrides: {\n ...submissionOverrides,\n slug: submissionOverrides.slug || \"form-submissions\",\n labels: {\n singular: submissionOverrides.labels?.singular || \"Submission\",\n plural: submissionOverrides.labels?.plural || \"Submissions\",\n },\n },\n\n fields: {\n text: options.fields?.text ?? true,\n email: options.fields?.email ?? true,\n number: options.fields?.number ?? true,\n phone: options.fields?.phone ?? true,\n url: options.fields?.url ?? true,\n textarea: options.fields?.textarea ?? true,\n select: options.fields?.select ?? true,\n checkbox: options.fields?.checkbox ?? true,\n radio: options.fields?.radio ?? true,\n file: options.fields?.file ?? true,\n date: options.fields?.date ?? true,\n time: options.fields?.time ?? true,\n hidden: options.fields?.hidden ?? true,\n },\n\n redirectRelationships: options.redirectRelationships || [],\n beforeEmail: options.beforeEmail,\n\n notifications: {\n defaultFrom: options.notifications?.defaultFrom,\n defaultToEmail: options.notifications?.defaultToEmail,\n enabled: options.notifications?.enabled ?? true,\n },\n\n spamProtection: {\n honeypot: options.spamProtection?.honeypot ?? true,\n recaptcha: options.spamProtection?.recaptcha ?? { enabled: false },\n rateLimit: {\n maxSubmissions: options.spamProtection?.rateLimit?.maxSubmissions ?? 10,\n windowMs: options.spamProtection?.rateLimit?.windowMs ?? 60_000,\n },\n },\n\n uploads: {\n maxFileSize: options.uploads?.maxFileSize ?? 10_485_760, // 10 MB\n allowedMimeTypes: options.uploads?.allowedMimeTypes ?? [\n \"image/*\",\n \"application/pdf\",\n \"text/*\",\n ],\n uploadCollection: options.uploads?.uploadCollection ?? \"media\",\n },\n\n features: {\n builder: options.features?.builder ?? true,\n conditionalLogic: options.features?.conditionalLogic ?? true,\n fileUploads: options.features?.fileUploads ?? true,\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\nexport interface FormBuilderPluginResult {\n /** Plugin definition — pass to `defineConfig({ plugins: [...] })`. */\n plugin: NextlyPlugin;\n /** Forms & Submissions collections (also auto-added by the plugin). */\n collections: CollectionConfig[];\n /** Resolved configuration with all defaults applied. */\n config: ResolvedFormBuilderConfig;\n}\n\n/**\n * Create a Form Builder plugin instance.\n *\n * @example Basic usage\n * ```ts\n * import { defineConfig } from \"nextly/config\";\n * import { formBuilder } from \"@nextlyhq/plugin-form-builder\";\n *\n * const fb = formBuilder();\n *\n * export default defineConfig({\n * plugins: [fb.plugin],\n * });\n * ```\n *\n * @example With options\n * ```ts\n * const fb = formBuilder({\n * notifications: { defaultFrom: \"noreply@example.com\" },\n * spamProtection: { honeypot: true },\n * formOverrides: {\n * slug: \"contact-forms\",\n * labels: { singular: \"Contact Form\", plural: \"Contact Forms\" },\n * },\n * });\n * ```\n */\nexport function formBuilder(\n options: FormBuilderPluginOptions = {}\n): FormBuilderPluginResult {\n const resolvedConfig = resolveConfig(options);\n\n const formsCol = formsCollection(resolvedConfig);\n const submissionsCol = submissionsCollection(resolvedConfig);\n\n const plugin = definePlugin({\n name: \"@nextlyhq/plugin-form-builder\",\n version: PLUGIN_VERSION,\n nextly: \">=0.0.2-alpha.21\",\n // Identity metadata for the admin plugins page, mirroring package.json.\n author: \"Nextly\",\n homepage: \"https://nextlyhq.com\",\n repository: \"https://github.com/nextlyhq/nextly\",\n license: \"MIT\",\n category: \"forms\",\n tags: [\"forms\", \"submissions\", \"email-notifications\"],\n\n // Declarative schema: the merged pipeline folds these into the\n // app schema — no manual `setup()` append needed. Just register the plugin.\n contributes: {\n collections: [formsCol, submissionsCol],\n // Custom permission — gates submission export beyond CRUD. The\n // canonical example third-party plugin authors copy.\n permissions: [\n {\n action: \"export\",\n resource: \"submissions\",\n label: \"Export Submissions\",\n description: \"Export form submissions as CSV/JSON\",\n // No `group`: the admin already files this under the plugin that\n // declared it, so naming the plugin again would nest it inside\n // itself. `group` is for a plugin with enough permissions to sort\n // its own into headings, which one is not.\n //\n // `danger` because the point of the permission is to take\n // submissions — names, emails, whatever a form asked for — out of\n // the site in a file.\n danger: true,\n },\n ],\n // HTTP route — exported at\n // /api/plugins/@nextlyhq/plugin-form-builder/submissions/export. Secure by\n // default: gated by the custom `export-submissions` permission. Reads\n // via the secure-by-default service path as the authed user and\n // resolves its OWN slug through `ctx.self`. The canonical\n // contributes.routes example for third-party authors.\n routes: [\n {\n method: \"GET\",\n path: \"/submissions/export\",\n requiredPermission: \"export-submissions\",\n handler: async (req, ctx) => {\n const declaredSlug = resolvedConfig.formSubmissionOverrides.slug;\n const slug = ctx.self.collections[declaredSlug] ?? declaredSlug;\n const url = new URL(req.url);\n const format = url.searchParams.get(\"format\") ?? \"json\";\n const formId = url.searchParams.get(\"form\");\n const status = url.searchParams.get(\"status\");\n\n // Spam stays out of exports unless it is explicitly requested —\n // an export is \"what people submitted\", not \"what bots sent\".\n const where: Record<string, unknown> = {};\n if (formId) where.form = { equals: formId };\n if (status) where.status = { equals: status };\n else where.status = { not_equals: \"spam\" };\n\n const opts = { as: \"user\", user: ctx.user ?? undefined } as const;\n\n // CSV needs its form BEFORE any submissions are read: the\n // columns come from one form's fields, and a malformed request\n // must fail fast instead of paginating the whole table first.\n let form: Record<string, unknown> | undefined;\n if (format === \"csv\") {\n if (!formId) {\n return Response.json(\n {\n error: {\n code: \"VALIDATION_ERROR\",\n message: \"CSV export requires a form parameter.\",\n },\n },\n { status: 400 }\n );\n }\n const formsSlugDeclared = resolvedConfig.formOverrides.slug;\n const formsSlug =\n ctx.self.collections[formsSlugDeclared] ?? formsSlugDeclared;\n const formResult = await ctx.services.collections.listEntries(\n formsSlug,\n { where: { id: { equals: formId } } },\n opts\n );\n form = formResult.data[0];\n if (!form) {\n return Response.json(\n { error: { code: \"NOT_FOUND\", message: \"Form not found.\" } },\n { status: 404 }\n );\n }\n }\n\n // Page through the export with a hard ceiling: an unbounded loop\n // over a high-traffic form could hold the whole table in memory.\n // Hitting the ceiling is reported in a header, never silent.\n const MAX_EXPORT_ROWS = 50_000;\n const items: unknown[] = [];\n const pageSize = 200;\n let truncated = false;\n for (let page = 1; ; page += 1) {\n const result = await ctx.services.collections.listEntries(\n slug,\n { where, pagination: { limit: pageSize, page } },\n opts\n );\n items.push(...result.data);\n const pageWasFull = result.data.length === pageSize;\n if (items.length >= MAX_EXPORT_ROWS) {\n truncated = pageWasFull || items.length > MAX_EXPORT_ROWS;\n items.length = MAX_EXPORT_ROWS;\n break;\n }\n if (!pageWasFull) break;\n }\n const truncationHeaders: Record<string, string> = truncated\n ? { \"X-Export-Truncated\": \"true\" }\n : {};\n\n if (format !== \"csv\" || !form) {\n return Response.json({ items }, { headers: truncationHeaders });\n }\n\n // The service returns parsed entries; the export helpers declare\n // the document shapes — the boundary cast is through unknown.\n const csv = exportToCSV(\n items as unknown as SubmissionDocument[],\n form as unknown as FormDocument\n );\n const filename = generateExportFilename(\n typeof form.slug === \"string\" ? form.slug : \"form\",\n \"csv\"\n );\n return new Response(csv, {\n headers: {\n \"Content-Type\": \"text/csv; charset=utf-8\",\n \"Content-Disposition\": `attachment; filename=\"${filename}\"`,\n ...truncationHeaders,\n },\n });\n },\n },\n {\n // The builder UI reads the host's resolved field enable/disable map\n // from here, so the plugin option actually gates the type picker.\n // Options resolve server-side only; this is the one channel the\n // admin client has to them.\n method: \"GET\",\n path: \"/builder-config\",\n // Derived from the forms slug like the menu entry: with an\n // overridden slug, users hold read-<slug>, not read-forms.\n requiredPermission: `read-${resolvedConfig.formOverrides.slug}`,\n handler: (_req, ctx) =>\n Promise.resolve(\n Response.json({\n fields: resolvedConfig.fields,\n // Notification defaults the builder surfaces: defaultToEmail\n // seeds a new form's first rule, defaultFrom renders as the\n // inherited sender placeholder.\n notifications: {\n defaultFrom: resolvedConfig.notifications.defaultFrom,\n defaultToEmail: resolvedConfig.notifications.defaultToEmail,\n },\n // Spam defaults the Settings tab surfaces, so per-form\n // override selects can show what \"inherit\" resolves to.\n spamProtection: {\n honeypot: resolvedConfig.spamProtection.honeypot,\n recaptchaEnabled:\n resolvedConfig.spamProtection.recaptcha?.enabled ?? false,\n },\n // Runtime-resolved collection slugs (through ctx.self, so a\n // framework .rename() is honored too) — admin components\n // never hardcode \"forms\"/\"form-submissions\".\n slugs: {\n forms:\n ctx.self.collections[resolvedConfig.formOverrides.slug] ??\n resolvedConfig.formOverrides.slug,\n submissions:\n ctx.self.collections[\n resolvedConfig.formSubmissionOverrides.slug\n ] ?? resolvedConfig.formSubmissionOverrides.slug,\n },\n })\n ),\n },\n ],\n },\n\n // Forms is a first-class destination, not a plugin detail: standalone\n // placement gives it its own main-rail icon after Media, and its\n // sub-sidebar lists the plugin's collections (Forms, Submissions) —\n // no separate menu contribution, so \"Forms\" exists exactly once.\n // Hosts preferring the Plugins section override placement in config.\n // (The old admin.settings.component that rendered a second full builder\n // at /admin/plugins/<slug> is gone; the collection Edit-view override\n // is the single FormBuilderView mount.)\n admin: {\n placement: \"standalone\",\n after: \"media\",\n order: 50,\n appearance: { icon: \"FileText\", label: \"Forms\" },\n description: \"Create and manage forms with submission tracking\",\n },\n\n // -- Init ----------------------------------------------------------------\n // Registers an afterCreate hook on submissions to send email notifications.\n init(nextly: NextlyInstance) {\n // Resolve our OWN submissions slug through ctx.self, so the hook\n // follows a framework `.rename()` as well as our formSubmissionOverrides\n // option. The declared slug is the key; ctx.self maps it to the resolved\n // (possibly renamed) slug. Identity when not renamed.\n const declaredSubmissionsSlug =\n resolvedConfig.formSubmissionOverrides.slug;\n const submissionSlug =\n nextly.self.collections[declaredSubmissionsSlug] ??\n declaredSubmissionsSlug;\n\n // Hook/event subscriptions are idempotent across HMR — the platform\n // clears a plugin's prior subscriptions before re-init (B2), so the old\n // globalThis dedup guard is no longer needed.\n nextly.logger.info(\"Form Builder plugin initialized\", {\n formsCollection: resolvedConfig.formOverrides.slug,\n submissionsCollection: submissionSlug,\n enabledFields: Object.entries(resolvedConfig.fields)\n .filter(([, on]) => on)\n .map(([name]) => name),\n });\n\n (nextly as NextlyWithFormBuilderConfig).__formBuilderConfig =\n resolvedConfig;\n\n // D63: run the user's beforeEmail config as a filter on the form-builder seam.\n if (resolvedConfig.beforeEmail) {\n nextly.filters.add(\n \"form-builder.beforeEmail\",\n (emails: FormEmailNotification[], ctx: BeforeEmailFilterContext) =>\n resolvedConfig.beforeEmail!({\n emails,\n // Boundary: loose runtime documents → the user's typed contract.\n form: ctx.form as unknown as FormDocument,\n submission: ctx.submission as unknown as SubmissionDocument,\n })\n );\n }\n\n // Register afterCreate hook for email notifications\n nextly.hooks.on(\n \"afterCreate\",\n submissionSlug,\n async (context: unknown) => {\n await handleSubmissionCreated(context, resolvedConfig, nextly);\n }\n );\n\n // Stamp admin edits of submitted data: changing what the visitor\n // submitted must leave a visible trace. Registered directly on the\n // registry (like the notification hook) so it runs for every API\n // surface that updates a submission.\n nextly.hooks.on(\"beforeUpdate\", submissionSlug, (context: unknown) => {\n const ctx = context as {\n data?: Record<string, unknown>;\n user?: { id?: string };\n };\n if (ctx.data && ctx.data.data !== undefined) {\n ctx.data.editedAt = new Date();\n ctx.data.editedBy = ctx.user?.id ?? null;\n }\n return ctx.data;\n });\n\n // Inject a real submissionCount into form reads (spam excluded — the\n // number answers \"how many people submitted\", not \"how many bots\").\n const declaredFormsSlug = resolvedConfig.formOverrides.slug;\n const formsSlug =\n nextly.self.collections[declaredFormsSlug] ?? declaredFormsSlug;\n nextly.hooks.on(\"afterRead\", formsSlug, async (context: unknown) => {\n const data = (context as { data?: unknown }).data;\n // afterRead fires for single reads (one record) and list reads\n // (array); count each form either way. Counts run concurrently —\n // form lists are paginated, so this is a bounded fan-out of small\n // indexed queries, not a serial N+1 walk.\n const records = Array.isArray(data) ? data : data ? [data] : [];\n await Promise.all(\n records.map(async record => {\n const form = record as Record<string, unknown>;\n if (typeof form.id !== \"string\") return;\n try {\n // Count as system: whoever may read the form may see its\n // submission volume without holding submission read rights.\n form.submissionCount = await nextly.services.collections.count(\n submissionSlug,\n {\n where: {\n form: { equals: form.id },\n status: { not_equals: \"spam\" },\n },\n },\n { as: \"system\" }\n );\n } catch {\n // A failed count must never break reading the form itself.\n form.submissionCount = 0;\n }\n })\n );\n });\n },\n });\n\n return {\n plugin,\n collections: [formsCol, submissionsCol],\n config: resolvedConfig,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Utilities\n// ---------------------------------------------------------------------------\n\n/**\n * Retrieve the resolved Form Builder config from a Nextly plugin context.\n */\nexport function getFormBuilderConfig(\n nextly: unknown\n): ResolvedFormBuilderConfig | undefined {\n if (!nextly || typeof nextly !== \"object\") return undefined;\n return (nextly as Partial<NextlyWithFormBuilderConfig>).__formBuilderConfig;\n}\n\n/**\n * Resolve a `{{fieldName}}` template reference against submitted values.\n * Plain strings (e.g. email addresses) are returned as-is.\n */\nfunction resolveFieldRef(ref: string, data: Record<string, unknown>): string {\n const match = ref.match(/^\\{\\{(\\w+)\\}\\}$/);\n if (!match) return ref;\n const value = data[match[1]];\n return typeof value === \"string\" ? value : \"\";\n}\n\n// ---------------------------------------------------------------------------\n// Attachment collection for file fields\n// ---------------------------------------------------------------------------\n\n/**\n * Scan form fields for file-upload fields marked `attachToEmail` and\n * collect the corresponding mediaIds from the submitted data. Returns\n * an array of `{ mediaId }` objects ready to pass as `attachments` to\n * `emailService.sendWithTemplate()`.\n *\n * Skips empty/missing values silently (optional file fields). Handles\n * both single-value and `multiple: true` file fields.\n *\n * @internal Exported for testing — not part of the public plugin API.\n */\nexport function collectAttachmentInputs(\n fields: Array<Record<string, unknown>>,\n submittedData: Record<string, unknown>\n): Array<{ mediaId: string }> {\n const attachments: Array<{ mediaId: string }> = [];\n\n for (const field of fields) {\n if (field.type !== \"file\" || !field.attachToEmail) continue;\n\n const name = field.name as string;\n const value = submittedData[name];\n if (value == null) continue;\n\n if (Array.isArray(value)) {\n for (const item of value) {\n if (typeof item === \"string\" && item) {\n attachments.push({ mediaId: item });\n }\n }\n } else if (typeof value === \"string\" && value) {\n attachments.push({ mediaId: value });\n }\n }\n\n return attachments;\n}\n\n// ---------------------------------------------------------------------------\n// Submission notification handler\n// ---------------------------------------------------------------------------\n\n/**\n * Normalize a JSON column value from a raw DB row into an object. Hook\n * contexts carry the row as stored, so the value may be an already-parsed\n * object (jsonb dialects) or a serialized string (text-storage dialects).\n *\n * @internal Exported for testing — not part of the public plugin API.\n */\nexport function parseJsonColumn(value: unknown): Record<string, unknown> {\n if (typeof value === \"string\") {\n try {\n const parsed: unknown = JSON.parse(value);\n return parsed && typeof parsed === \"object\" && !Array.isArray(parsed)\n ? (parsed as Record<string, unknown>)\n : {};\n } catch {\n return {};\n }\n }\n return value && typeof value === \"object\" && !Array.isArray(value)\n ? (value as Record<string, unknown>)\n : {};\n}\n\n/** Trim an address list and drop blanks; undefined when nothing remains. */\nfunction normalizeAddressList(\n addresses: readonly string[] | undefined\n): string[] | undefined {\n if (!Array.isArray(addresses)) return undefined;\n const cleaned = addresses\n .map(address => (typeof address === \"string\" ? address.trim() : \"\"))\n .filter(Boolean);\n return cleaned.length > 0 ? cleaned : undefined;\n}\n\n/** Why a notification rule produced no email for a given submission. */\nexport interface SkippedNotification {\n notificationId: string;\n reason: \"empty-recipient\" | \"condition-unmet\" | \"no-template\";\n}\n\n/**\n * Resolve a form's notification rules against one submission into outgoing\n * email descriptors, honoring everything a rule can configure: the send\n * condition gates the rule, the recipient/reply-to resolve `{{fieldName}}`\n * references, and the sender falls back from the rule's own address to the\n * plugin's `notifications.defaultFrom` (undefined lets the template/provider\n * default apply downstream).\n *\n * @internal Exported for testing — not part of the public plugin API.\n */\nexport function buildNotificationEmails(input: {\n notifications: readonly FormNotification[];\n submittedData: Record<string, unknown>;\n formName: unknown;\n submissionId: unknown;\n defaultFrom?: string;\n}): { emails: FormEmailNotification[]; skipped: SkippedNotification[] } {\n const { notifications, submittedData, formName, submissionId, defaultFrom } =\n input;\n\n const seen = new Set<string>();\n const emails: FormEmailNotification[] = [];\n const skipped: SkippedNotification[] = [];\n\n for (const notification of notifications) {\n if (!notification.enabled) continue;\n\n if (!notification.templateSlug) {\n skipped.push({ notificationId: notification.id, reason: \"no-template\" });\n continue;\n }\n\n // Deduplicate (UI can append duplicates on repeated saves). For id-less\n // rules (API-authored), only an exact structural duplicate counts — a\n // to+template key would silently drop distinct rules that share a\n // recipient and template but differ elsewhere (e.g. their condition).\n const key = notification.id || JSON.stringify(notification);\n if (seen.has(key)) continue;\n seen.add(key);\n\n // An unmet send condition skips the rule for this submission — an\n // expected state, never an error.\n if (\n notification.condition &&\n !evaluateSingleCondition(notification.condition, submittedData)\n ) {\n skipped.push({\n notificationId: notification.id,\n reason: \"condition-unmet\",\n });\n continue;\n }\n\n // Trim every address on the way out: whitespace-only values must count\n // as empty, and stray spaces must not reach the provider as headers.\n const to = (\n notification.recipientType === \"field\"\n ? resolveFieldRef(notification.to, submittedData)\n : notification.to\n ).trim();\n\n if (!to) {\n skipped.push({\n notificationId: notification.id,\n reason: \"empty-recipient\",\n });\n continue;\n }\n\n const cc = normalizeAddressList(notification.cc);\n const bcc = normalizeAddressList(notification.bcc);\n\n // Sender resolution: the rule's own address wins, then the plugin's\n // configured default; undefined defers to the template/provider chain.\n const from =\n notification.senderEmail?.trim() || defaultFrom?.trim() || undefined;\n\n // Reply-To resolves {{fieldName}} like recipients do; a reference to a\n // field the visitor left empty degrades to \"no Reply-To header\" rather\n // than an invalid address.\n const replyTo = notification.replyTo\n ? resolveFieldRef(notification.replyTo, submittedData).trim() || undefined\n : undefined;\n\n emails.push({\n to,\n templateSlug: notification.templateSlug,\n variables: {\n ...submittedData,\n formName,\n submissionId,\n },\n providerId: notification.providerId,\n from,\n replyTo,\n cc,\n bcc,\n notificationId: notification.id,\n });\n }\n\n return { emails, skipped };\n}\n\n/**\n * Send email notifications after a form submission is created.\n */\nasync function handleSubmissionCreated(\n context: unknown,\n config: ResolvedFormBuilderConfig,\n nextly: NextlyInstance\n): Promise<void> {\n // The plugin-level kill switch: `notifications.enabled: false` turns off\n // all form emails regardless of per-rule state.\n if (!config.notifications.enabled) return;\n\n const submission = (context as { data?: Record<string, unknown> }).data;\n if (!submission) return;\n\n // Spam is stored for review, but nobody gets emailed about it — otherwise\n // every bot hit would trigger the form's notification rules.\n if (submission.status === \"spam\") return;\n\n const rawFormId = submission.form;\n let formId: string | null = null;\n if (typeof rawFormId === \"string\") {\n formId = rawFormId;\n } else if (rawFormId && typeof rawFormId === \"object\") {\n const maybeId = (rawFormId as { id?: unknown }).id;\n if (typeof maybeId === \"string\") formId = maybeId;\n }\n if (!formId) return;\n\n // Fetch the parent form\n const form = await fetchParentForm(config, formId, nextly);\n if (!form) return;\n\n const notifications = Array.isArray(form.notifications)\n ? (form.notifications as FormNotification[])\n : [];\n if (notifications.length === 0) return;\n\n const emailService = nextly.services.email;\n if (!emailService) return;\n\n // The afterCreate hook receives the raw DB row, so on dialects that store\n // JSON columns as text (e.g. SQLite) `data` arrives serialized. Without\n // parsing it, {{field}} recipients, reply-to references, and send\n // conditions all silently see an empty submission.\n const submittedData = parseJsonColumn(submission.data);\n\n // Collect mediaIds from file fields marked for email attachment\n const formFields = Array.isArray(form.fields)\n ? (form.fields as Array<Record<string, unknown>>)\n : [];\n const fileAttachments = collectAttachmentInputs(formFields, submittedData);\n\n // -- Build phase: resolve each enabled notification into an outgoing\n // descriptor (the value the D63 seam transforms).\n const { emails, skipped } = buildNotificationEmails({\n notifications,\n submittedData,\n formName: form.name,\n submissionId: submission.id,\n defaultFrom: config.notifications.defaultFrom,\n });\n\n for (const skip of skipped) {\n if (skip.reason === \"empty-recipient\") {\n nextly.logger.warn?.(\n \"Form Builder: empty recipient, skipping notification\",\n { notificationId: skip.notificationId, formSlug: form.slug }\n );\n } else {\n // Unmet conditions and missing templates are expected states, not\n // faults — surface them only at debug level.\n nextly.logger.debug?.(\"Form Builder: notification skipped\", {\n notificationId: skip.notificationId,\n formSlug: form.slug,\n reason: skip.reason,\n });\n }\n }\n\n if (emails.length === 0) return;\n\n // -- Seam: thread the outgoing notifications through the D63 filter so user\n // config (and any other registered handler) can modify/filter them.\n const finalEmails = await nextly.filters.apply<\n FormEmailNotification[],\n BeforeEmailFilterContext\n >(\"form-builder.beforeEmail\", emails, { form, submission });\n\n // -- Send phase: send the (possibly transformed) outgoing notifications.\n for (const email of finalEmails) {\n try {\n await emailService.sendWithTemplate(\n email.templateSlug,\n email.to,\n email.variables,\n {\n providerId: email.providerId,\n from: email.from,\n replyTo: email.replyTo,\n cc: email.cc,\n bcc: email.bcc,\n attachments: fileAttachments.length > 0 ? fileAttachments : undefined,\n }\n );\n\n nextly.logger.info?.(\"Form Builder: notification sent\", {\n formSlug: form.slug,\n to: email.to,\n templateSlug: email.templateSlug,\n notificationId: email.notificationId,\n attachmentCount: fileAttachments.length,\n });\n } catch (err) {\n nextly.logger.error?.(\"Form Builder: notification failed\", {\n to: email.to,\n templateSlug: email.templateSlug,\n notificationId: email.notificationId,\n error: err instanceof Error ? err.message : String(err),\n });\n }\n }\n}\n\n/**\n * Fetch the parent form document for a submission.\n */\nexport async function fetchParentForm(\n config: ResolvedFormBuilderConfig,\n formId: string,\n nextly: NextlyInstance\n): Promise<Record<string, unknown> | null> {\n try {\n // D35/D56: read the parent form through the secure managed service as\n // system — the afterCreate hook runs without an ambient user. Replaces the\n // legacy `getCollectionsHandler()` + `overrideAccess` runtime path.\n const form = await nextly.services.collections.findEntryById(\n config.formOverrides.slug,\n formId,\n { as: \"system\" }\n );\n return form;\n } catch (err) {\n nextly.logger.error?.(\"Form Builder: failed to fetch form\", {\n formId,\n error: err instanceof Error ? err.message : String(err),\n });\n return null;\n }\n}\n","/**\n * Forms Collection\n *\n * Stores form definitions created via Code-First or Schema Builder.\n * Each form contains field definitions, settings, notifications, and access control.\n *\n * @module collections/forms\n * @since 0.1.0\n */\n\nimport type { CollectionConfig, FieldConfig, HookContext } from \"nextly\";\nimport {\n text,\n textarea,\n select,\n checkbox,\n group,\n json,\n relationship,\n} from \"nextly\";\n\nimport type { ResolvedFormBuilderConfig } from \"../types\";\n\n// ============================================================\n// Type Augmentation for Custom Admin Components\n// ============================================================\n// These types extend CollectionAdminOptions to support custom admin views.\n// This augmentation will be removed once nextly exports these types.\n\n/**\n * Component path string format.\n * Format: `\"package-name/path#ExportName\"`\n */\ntype ComponentPath = string;\n\n/**\n * Custom view configuration for replacing default admin views.\n */\ninterface CollectionAdminViewConfig {\n /** Component path to the custom view component */\n Component: ComponentPath;\n}\n\n/**\n * Custom components configuration for collection admin UI.\n */\ninterface CollectionAdminComponents {\n views?: {\n Edit?: CollectionAdminViewConfig;\n List?: CollectionAdminViewConfig;\n };\n BeforeListTable?: ComponentPath;\n AfterListTable?: ComponentPath;\n BeforeEdit?: ComponentPath;\n AfterEdit?: ComponentPath;\n}\n\n/**\n * Extended CollectionConfig with custom admin components support.\n * This extends the base CollectionConfig to include the components property.\n */\ninterface ExtendedCollectionConfig extends Omit<CollectionConfig, \"admin\"> {\n admin?: CollectionConfig[\"admin\"] & {\n /** Whether this collection is provided by a plugin */\n isPlugin?: boolean;\n components?: CollectionAdminComponents;\n };\n}\n\n/**\n * Generate the Forms collection configuration.\n *\n * Creates a collection for storing form definitions with fields,\n * settings, notifications, and access control.\n *\n * @param pluginConfig - Resolved plugin configuration\n * @returns ExtendedCollectionConfig for the Forms collection (with custom admin components)\n *\n * @example\n * ```typescript\n * const formsCol = formsCollection(resolvedConfig);\n * // Returns a collection with slug \"forms\" (or custom slug from config)\n * ```\n */\nexport function formsCollection(\n pluginConfig: ResolvedFormBuilderConfig\n): ExtendedCollectionConfig {\n const {\n slug,\n labels,\n fields: additionalFields,\n access: accessOverrides,\n ...overrides\n } = pluginConfig.formOverrides;\n\n // Build settings group fields based on plugin configuration\n const settingsFields: FieldConfig[] = [\n text({\n name: \"submitButtonText\",\n label: \"Submit Button Text\",\n defaultValue: \"Submit\",\n admin: {\n description: \"Text displayed on the form submit button\",\n },\n }),\n\n select({\n name: \"confirmationType\",\n label: \"After Submission\",\n defaultValue: \"message\",\n options: [\n { label: \"Show Message\", value: \"message\" },\n { label: \"Redirect to URL\", value: \"redirect\" },\n ...(pluginConfig.redirectRelationships.length > 0\n ? [{ label: \"Redirect to Page\", value: \"relationship\" }]\n : []),\n ],\n admin: {\n description: \"What happens after a successful form submission\",\n },\n }),\n\n textarea({\n name: \"successMessage\",\n label: \"Success Message\",\n defaultValue: \"Thank you for your submission!\",\n admin: {\n description: \"Message shown after successful submission\",\n condition: {\n field: \"settings.confirmationType\",\n equals: \"message\",\n },\n },\n }),\n\n text({\n name: \"redirectUrl\",\n label: \"Redirect URL\",\n admin: {\n description: \"URL to redirect to after submission\",\n condition: {\n field: \"settings.confirmationType\",\n equals: \"redirect\",\n },\n },\n }),\n ];\n\n // Add redirect relationship field if configured\n if (pluginConfig.redirectRelationships.length > 0) {\n settingsFields.push(\n relationship({\n name: \"redirectPage\",\n label: \"Redirect Page\",\n relationTo: pluginConfig.redirectRelationships,\n admin: {\n description: \"Select a page to redirect to after submission\",\n condition: {\n field: \"settings.confirmationType\",\n equals: \"relationship\",\n },\n },\n })\n );\n }\n\n // Add remaining settings fields\n settingsFields.push(\n checkbox({\n name: \"allowMultipleSubmissions\",\n label: \"Allow Multiple Submissions\",\n defaultValue: true,\n admin: {\n description:\n \"Allow the same user/IP to submit this form multiple times\",\n },\n }),\n\n checkbox({\n name: \"honeypotEnabled\",\n label: \"Honeypot\",\n admin: {\n description:\n \"Per-form honeypot override; unset inherits the plugin default\",\n },\n }),\n\n checkbox({\n name: \"captchaEnabled\",\n label: \"Enable reCAPTCHA\",\n admin: {\n description:\n \"Per-form reCAPTCHA override; unset inherits the plugin default\",\n },\n }),\n\n text({\n name: \"captchaSiteKey\",\n label: \"reCAPTCHA Site Key\",\n admin: {\n description: \"Override the default reCAPTCHA site key for this form\",\n condition: {\n field: \"settings.captchaEnabled\",\n equals: true,\n },\n },\n })\n );\n\n // Build the default fields array\n const defaultFields: FieldConfig[] = [\n // ============================================================\n // Basic Information\n // ============================================================\n text({\n name: \"name\",\n label: \"Form Name\",\n required: true,\n admin: {\n description: \"Internal name for this form (not shown to users)\",\n },\n }),\n\n text({\n name: \"slug\",\n label: \"Slug\",\n required: true,\n unique: true,\n admin: {\n description: \"URL-friendly identifier. Used in API: /api/forms/{slug}\",\n },\n }),\n\n textarea({\n name: \"description\",\n label: \"Description\",\n admin: {\n description: \"Internal notes about this form's purpose\",\n },\n }),\n\n // ============================================================\n // Form Fields (JSON schema managed by Form Builder UI)\n // ============================================================\n json({\n name: \"fields\",\n label: \"Form Fields\",\n required: true,\n defaultValue: [],\n admin: {\n description:\n \"Form field configuration. Managed by the Form Builder UI.\",\n },\n }),\n\n // ============================================================\n // Form Settings\n // ============================================================\n group({\n name: \"settings\",\n label: \"Form Settings\",\n fields: settingsFields,\n }),\n\n // ============================================================\n // Email Notifications\n // ============================================================\n json({\n name: \"notifications\",\n label: \"Notifications\",\n defaultValue: [],\n admin: {\n description:\n \"Email notification rules. Managed by the Form Builder UI.\",\n },\n }),\n\n // ============================================================\n // Status\n // ============================================================\n select({\n name: \"status\",\n label: \"Status\",\n required: true,\n defaultValue: \"draft\",\n options: [\n { label: \"Draft\", value: \"draft\" },\n { label: \"Published\", value: \"published\" },\n { label: \"Closed\", value: \"closed\" },\n ],\n admin: {\n description:\n \"Only published forms can receive submissions. Closed forms display a message instead.\",\n },\n }),\n\n textarea({\n name: \"closedMessage\",\n label: \"Closed Form Message\",\n defaultValue: \"This form is no longer accepting submissions.\",\n admin: {\n description: \"Message shown when form status is 'closed'\",\n condition: {\n field: \"status\",\n equals: \"closed\",\n },\n },\n }),\n ];\n\n // Process additional fields from formOverrides\n // Note: Function-style overrides should be resolved in plugin.ts before reaching here\n let finalFields: FieldConfig[];\n\n if (Array.isArray(additionalFields) && additionalFields.length > 0) {\n // Merge: append additional fields to defaults\n finalFields = [...defaultFields, ...additionalFields];\n } else {\n finalFields = defaultFields;\n }\n\n // Convert hyphenated slug to underscored table name for database compatibility\n const dbName = slug.replace(/-/g, \"_\");\n\n return {\n slug,\n dbName,\n labels,\n fields: finalFields,\n timestamps: true,\n\n admin: {\n isPlugin: true,\n group: \"Forms\",\n order: 50,\n useAsTitle: \"name\",\n description:\n \"Create and manage your form templates using the visual form builder. Create and manage forms for collecting user submissions\",\n\n // Custom components for visual form builder\n components: {\n views: {\n // Replace default edit view with visual Form Builder\n Edit: {\n Component: \"@nextlyhq/plugin-form-builder/admin#FormBuilderView\",\n },\n },\n },\n\n ...overrides.admin,\n },\n\n // Access control with sensible defaults\n access: {\n // Anyone can read forms (needed for frontend rendering)\n read: accessOverrides?.read ?? true,\n // Only authenticated users can create/update forms\n create: accessOverrides?.create ?? (({ user }) => !!user),\n update: accessOverrides?.update ?? (({ user }) => !!user),\n // Only admins can delete forms (falls back to DB permissions)\n delete:\n accessOverrides?.delete ??\n (({ roles }) =>\n roles.includes(\"admin\") || roles.includes(\"super-admin\")),\n },\n\n hooks: {\n // Auto-generate slug from name if not provided\n beforeValidate: [\n (context: HookContext) => {\n const { data, operation } = context;\n\n // Auto-generate slug from name if not provided (only on create)\n if (operation === \"create\" && data && !data.slug && data.name) {\n data.slug = String(data.name)\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\");\n }\n\n // Validation: Ensure fields array is not empty\n // We check for length 0 to prevent empty forms from being saved\n if (\n data &&\n (data.fields === undefined ||\n (Array.isArray(data.fields) && data.fields.length === 0))\n ) {\n throw new Error(\"Form must have at least one field.\");\n }\n\n return data;\n },\n ],\n },\n\n // Spread any additional overrides (excluding already used properties)\n ...overrides,\n };\n}\n","/**\n * Form Submissions Collection\n *\n * Stores form submission data from user-submitted forms.\n * Each submission contains the submitted data, metadata, and status.\n *\n * @module collections/submissions\n * @since 0.1.0\n */\n\nimport type { CollectionConfig, FieldConfig, HookContext } from \"nextly\";\nimport { text, textarea, select, json, date, relationship } from \"nextly\";\n\nimport type { ResolvedFormBuilderConfig } from \"../types\";\n\n/**\n * Extended CollectionConfig with isPlugin support.\n * This extends the base CollectionConfig to include the isPlugin property.\n */\ninterface ExtendedCollectionConfig extends Omit<CollectionConfig, \"admin\"> {\n admin?: CollectionConfig[\"admin\"] & {\n /** Whether this collection is provided by a plugin */\n isPlugin?: boolean;\n };\n}\n\n/**\n * Generate the Form Submissions collection configuration.\n *\n * Creates a collection for storing form submissions with:\n * - Reference to the parent form\n * - Submitted data (JSON)\n * - Status tracking (new, read, archived)\n * - Metadata (IP, user agent, timestamp)\n * - Internal notes for admin use\n *\n * @param pluginConfig - Resolved plugin configuration\n * @returns CollectionConfig for the Submissions collection\n *\n * @example\n * ```typescript\n * const submissionsCol = submissionsCollection(resolvedConfig);\n * // Returns a collection with slug \"form-submissions\" (or custom slug from config)\n * ```\n */\nexport function submissionsCollection(\n pluginConfig: ResolvedFormBuilderConfig\n): ExtendedCollectionConfig {\n const {\n slug,\n labels,\n fields: additionalFields,\n access: accessOverrides,\n ...overrides\n } = pluginConfig.formSubmissionOverrides;\n\n // Get the forms collection slug for the relationship\n const formSlug = pluginConfig.formOverrides.slug;\n\n // Build the default fields array\n const defaultFields: FieldConfig[] = [\n // ============================================================\n // Form Relationship\n // ============================================================\n relationship({\n name: \"form\",\n label: \"Form\",\n relationTo: formSlug,\n required: true,\n admin: {\n readOnly: true,\n description: \"The form this submission belongs to\",\n },\n }),\n\n // ============================================================\n // Submission Data (JSON)\n // ============================================================\n json({\n name: \"data\",\n label: \"Submission Data\",\n required: true,\n admin: {\n readOnly: true,\n description: \"The submitted form data as JSON\",\n },\n }),\n\n // ============================================================\n // Status\n // ============================================================\n select({\n name: \"status\",\n label: \"Status\",\n required: true,\n defaultValue: \"new\",\n options: [\n { label: \"New\", value: \"new\" },\n { label: \"Read\", value: \"read\" },\n { label: \"Archived\", value: \"archived\" },\n // Spam is stored and flagged, never silently dropped: a false\n // positive stays reviewable and recoverable (\"Not spam\" = back to new).\n { label: \"Spam\", value: \"spam\" },\n ],\n admin: {\n description: \"Track whether this submission has been reviewed\",\n },\n }),\n\n text({\n name: \"spamReason\",\n label: \"Spam Reason\",\n admin: {\n readOnly: true,\n description:\n \"Which spam check flagged this submission (empty for clean submissions)\",\n },\n }),\n\n // ============================================================\n // Internal Notes\n // ============================================================\n textarea({\n name: \"notes\",\n label: \"Internal Notes\",\n admin: {\n description: \"Notes visible only to admins (not shown to submitter)\",\n },\n }),\n\n // ============================================================\n // Metadata\n // ============================================================\n text({\n name: \"ipAddress\",\n label: \"IP Address\",\n admin: {\n readOnly: true,\n description: \"IP address of the submitter\",\n },\n }),\n\n text({\n name: \"userAgent\",\n label: \"User Agent\",\n admin: {\n readOnly: true,\n description: \"Browser/device information of the submitter\",\n },\n }),\n\n date({\n name: \"submittedAt\",\n label: \"Submitted At\",\n required: true,\n admin: {\n readOnly: true,\n description: \"When the form was submitted\",\n },\n }),\n\n // Honesty stamps for admin edits of submitted data: a corrected\n // submission must never be indistinguishable from what the visitor sent.\n date({\n name: \"editedAt\",\n label: \"Edited At\",\n admin: {\n readOnly: true,\n description: \"When an admin last edited the submitted data\",\n },\n }),\n\n text({\n name: \"editedBy\",\n label: \"Edited By\",\n admin: {\n readOnly: true,\n description: \"ID of the admin who last edited the submitted data\",\n },\n }),\n ];\n\n // Process additional fields from formSubmissionOverrides\n // Note: Function-style overrides should be resolved in plugin.ts before reaching here\n let finalFields: FieldConfig[];\n\n if (Array.isArray(additionalFields) && additionalFields.length > 0) {\n // Merge: append additional fields to defaults\n finalFields = [...defaultFields, ...additionalFields];\n } else {\n finalFields = defaultFields;\n }\n\n // Convert hyphenated slug to underscored table name (e.g., \"form-submissions\" -> \"form_submissions\")\n const dbName = slug.replace(/-/g, \"_\");\n\n return {\n slug,\n dbName,\n labels,\n fields: finalFields,\n timestamps: true,\n\n admin: {\n isPlugin: true,\n group: \"Forms\",\n order: 51,\n useAsTitle: \"id\",\n description: \"View and manage form submissions\",\n // Submissions are created by visitors submitting forms — a hand-typed\n // \"New Submission\" would be fiction, so the admin never offers one.\n disableCreate: true,\n\n components: {\n // The full submissions experience (per-form columns, status tabs,\n // drawer detail, export) replaces the generic list.\n views: {\n List: {\n Component: \"@nextlyhq/plugin-form-builder/admin#SubmissionsView\",\n },\n },\n },\n\n ...overrides.admin,\n },\n\n // Access control with sensible defaults\n // Public can create (submit forms), but only authenticated users can read/update/delete\n access: {\n // Anyone can submit forms (public access)\n create: accessOverrides?.create ?? true,\n // Only authenticated users can view submissions\n read: accessOverrides?.read ?? (({ user }) => !!user),\n // Only authenticated users can update submissions (e.g., change status, add notes)\n update: accessOverrides?.update ?? (({ user }) => !!user),\n // Only admins can delete submissions (falls back to DB permissions)\n delete:\n accessOverrides?.delete ??\n (({ roles }) =>\n roles.includes(\"admin\") || roles.includes(\"super-admin\")),\n },\n\n hooks: {\n // Auto-set submittedAt timestamp on creation. (The edit stamp for\n // updates is registered in plugin.ts init — direct registry hooks are\n // the path that runs for every API surface.)\n beforeValidate: [\n (context: HookContext) => {\n const { data, operation } = context;\n if (operation === \"create\" && data && !data.submittedAt) {\n data.submittedAt = new Date();\n }\n return data;\n },\n ],\n\n afterChange: [\n (context: HookContext) => {\n const { data } = context;\n return data;\n },\n ],\n },\n\n // Spread any additional overrides (excluding already used properties)\n ...overrides,\n };\n}\n","/**\n * Form Configuration Validator\n *\n * Provides comprehensive validation for form configurations including:\n * - Slug validation (format, reserved names)\n * - Field name validation (format, duplicates)\n * - Field-specific validation (select/radio options, min/max constraints)\n * - Conditional logic validation\n * - Nested field validation\n *\n * @module config/validate-form\n * @since 1.0.0\n *\n * @example\n * ```typescript\n * import { validateFormConfig } from '@nextly/plugin-form-builder';\n *\n * const result = validateFormConfig(config);\n * if (!result.valid) {\n * console.error('Validation errors:', result.errors);\n * }\n * ```\n */\n\nimport { isPluginFieldTypeOnSurface } from \"nextly\";\n\nimport { BUILT_IN_FORM_FIELD_TYPES } from \"../types\";\nimport type { FormConfig, FormField, FormFieldType } from \"../types\";\n\n// ============================================================\n// Validation Error Types\n// ============================================================\n\n/**\n * Error codes for form validation failures.\n * Used for programmatic error handling.\n */\nexport type FormValidationErrorCode =\n // Slug errors\n | \"SLUG_REQUIRED\"\n | \"SLUG_INVALID_TYPE\"\n | \"SLUG_TOO_SHORT\"\n | \"SLUG_TOO_LONG\"\n | \"SLUG_INVALID_FORMAT\"\n | \"SLUG_RESERVED\"\n // Field errors\n | \"FIELDS_REQUIRED\"\n | \"FIELDS_INVALID_TYPE\"\n | \"FIELDS_EMPTY\"\n | \"FIELD_NAME_REQUIRED\"\n | \"FIELD_NAME_INVALID_FORMAT\"\n | \"FIELD_NAME_DUPLICATE\"\n | \"FIELD_TYPE_REQUIRED\"\n | \"FIELD_TYPE_INVALID\"\n | \"FIELD_LABEL_REQUIRED\"\n // Field-specific errors\n | \"OPTIONS_REQUIRED\"\n | \"OPTIONS_EMPTY\"\n | \"MIN_GREATER_THAN_MAX\"\n | \"MIN_LENGTH_GREATER_THAN_MAX\"\n | \"FILE_MAX_SIZE_INVALID\"\n // Conditional logic errors\n | \"CONDITIONS_REQUIRED\"\n | \"CONDITION_FIELD_REQUIRED\"\n | \"CONDITION_COMPARISON_REQUIRED\"\n // Settings errors\n | \"SETTINGS_INVALID_TYPE\"\n | \"REDIRECT_URL_REQUIRED\"\n // Access errors\n | \"ACCESS_INVALID_TYPE\"\n | \"ACCESS_FUNCTION_INVALID\";\n\n/**\n * A single validation error with path and context.\n *\n * @example\n * ```typescript\n * const error: FormValidationError = {\n * path: 'fields.email.validation',\n * message: \"Min length cannot be greater than max length\",\n * code: 'MIN_LENGTH_GREATER_THAN_MAX',\n * };\n * ```\n */\nexport interface FormValidationError {\n /**\n * Dot-notation path to the invalid property.\n * @example 'slug', 'fields.0.name', 'fields.email.options'\n */\n path: string;\n\n /**\n * Human-readable error message.\n */\n message: string;\n\n /**\n * Machine-readable error code for programmatic handling.\n */\n code: FormValidationErrorCode;\n}\n\n/**\n * Result of form config validation.\n */\nexport interface FormValidationResult {\n /**\n * Whether the configuration is valid.\n */\n valid: boolean;\n\n /**\n * Array of validation errors (empty if valid).\n */\n errors: FormValidationError[];\n}\n\n// ============================================================\n// Reserved Names\n// ============================================================\n\n/**\n * Reserved form slugs that cannot be used.\n * These are used by the system or have special meaning.\n */\nexport const RESERVED_FORM_SLUGS = [\n // API routes\n \"api\",\n \"submit\",\n \"submissions\",\n \"export\",\n // System slugs\n \"admin\",\n \"new\",\n \"create\",\n \"edit\",\n \"delete\",\n] as const;\n\n/**\n * Set for O(1) lookup of reserved slugs.\n */\nconst RESERVED_SLUGS_SET = new Set<string>(RESERVED_FORM_SLUGS);\n\n// ============================================================\n// Validation Patterns\n// ============================================================\n\n/**\n * Regex pattern for valid form slugs.\n * Must start with a lowercase letter, contain only lowercase letters,\n * numbers, hyphens, and underscores. Length: 1-50 chars.\n * Allows hyphens for URL-friendly slugs (e.g., 'contact-form').\n */\nconst SLUG_PATTERN = /^[a-z][a-z0-9_-]*$/;\n\n/**\n * Regex pattern for valid field names.\n * Must start with a letter (upper or lower), contain only letters,\n * numbers, and underscores. Supports camelCase (e.g., 'firstName').\n */\nconst FIELD_NAME_PATTERN = /^[a-zA-Z][a-zA-Z0-9_]*$/;\n\n/**\n * All valid built-in form field types (plugin types validate via the registry).\n */\nconst VALID_FIELD_TYPES: readonly FormFieldType[] = BUILT_IN_FORM_FIELD_TYPES;\n\nconst VALID_FIELD_TYPES_SET = new Set<string>(VALID_FIELD_TYPES);\n\n// ============================================================\n// Validation Helper Functions\n// ============================================================\n\n/**\n * Checks if a string is a reserved form slug.\n */\nfunction isReservedSlug(slug: string): boolean {\n return RESERVED_SLUGS_SET.has(slug.toLowerCase());\n}\n\n/**\n * Validates a form slug.\n */\nfunction validateSlug(slug: unknown, errors: FormValidationError[]): void {\n const path = \"slug\";\n\n if (!slug) {\n errors.push({\n path,\n message: \"Form slug is required\",\n code: \"SLUG_REQUIRED\",\n });\n return;\n }\n\n if (typeof slug !== \"string\") {\n errors.push({\n path,\n message: \"Form slug must be a string\",\n code: \"SLUG_INVALID_TYPE\",\n });\n return;\n }\n\n if (slug.length < 1) {\n errors.push({\n path,\n message: \"Form slug must be at least 1 character\",\n code: \"SLUG_TOO_SHORT\",\n });\n }\n\n if (slug.length > 50) {\n errors.push({\n path,\n message: \"Form slug must be at most 50 characters\",\n code: \"SLUG_TOO_LONG\",\n });\n }\n\n if (!SLUG_PATTERN.test(slug)) {\n errors.push({\n path,\n message:\n \"Form slug must start with a lowercase letter and contain only lowercase letters, numbers, hyphens, and underscores\",\n code: \"SLUG_INVALID_FORMAT\",\n });\n }\n\n if (isReservedSlug(slug)) {\n errors.push({\n path,\n message: `Form slug '${slug}' is reserved and cannot be used`,\n code: \"SLUG_RESERVED\",\n });\n }\n}\n\n/**\n * Validates a single field name.\n */\nfunction validateFieldName(\n name: unknown,\n path: string,\n errors: FormValidationError[],\n seenNames: Set<string>\n): void {\n if (!name) {\n errors.push({\n path: `${path}.name`,\n message: \"Field name is required\",\n code: \"FIELD_NAME_REQUIRED\",\n });\n return;\n }\n\n if (typeof name !== \"string\") {\n errors.push({\n path: `${path}.name`,\n message: \"Field name must be a string\",\n code: \"FIELD_NAME_REQUIRED\",\n });\n return;\n }\n\n if (!FIELD_NAME_PATTERN.test(name)) {\n errors.push({\n path: `${path}.name`,\n message: `Field name '${name}' must start with a letter and contain only letters, numbers, and underscores`,\n code: \"FIELD_NAME_INVALID_FORMAT\",\n });\n }\n\n // Check for duplicates at this level\n const nameLower = name.toLowerCase();\n if (seenNames.has(nameLower)) {\n errors.push({\n path: `${path}.name`,\n message: `Duplicate field name '${name}'`,\n code: \"FIELD_NAME_DUPLICATE\",\n });\n } else {\n seenNames.add(nameLower);\n }\n}\n\n/**\n * Validates a single field label.\n */\nfunction validateFieldLabel(\n label: unknown,\n path: string,\n errors: FormValidationError[]\n): void {\n if (!label || (typeof label === \"string\" && label.trim() === \"\")) {\n errors.push({\n path: `${path}.label`,\n message: \"Field label is required\",\n code: \"FIELD_LABEL_REQUIRED\",\n });\n }\n}\n\n/**\n * Validates select/radio/checkbox-group field options.\n */\nfunction validateOptions(\n field: FormField,\n path: string,\n errors: FormValidationError[]\n): void {\n // Type assertion for fields with options\n const fieldWithOptions = field as {\n options?: Array<{ label: string; value: string }>;\n };\n const options = fieldWithOptions.options;\n\n if (!options) {\n errors.push({\n path: `${path}.options`,\n message: `${field.type} field must have an 'options' array`,\n code: \"OPTIONS_REQUIRED\",\n });\n return;\n }\n\n if (!Array.isArray(options)) {\n errors.push({\n path: `${path}.options`,\n message: `${field.type} field 'options' must be an array`,\n code: \"OPTIONS_REQUIRED\",\n });\n return;\n }\n\n if (options.length === 0) {\n errors.push({\n path: `${path}.options`,\n message: `${field.type} field must have at least one option`,\n code: \"OPTIONS_EMPTY\",\n });\n }\n}\n\n/**\n * Validates conditional logic configuration.\n */\nfunction validateConditionalLogic(\n field: FormField,\n path: string,\n errors: FormValidationError[]\n): void {\n const { conditionalLogic } = field;\n\n if (!conditionalLogic || !conditionalLogic.enabled) {\n return;\n }\n\n if (\n !conditionalLogic.conditions ||\n conditionalLogic.conditions.length === 0\n ) {\n errors.push({\n path: `${path}.conditionalLogic.conditions`,\n message: \"Conditional logic must have at least one condition\",\n code: \"CONDITIONS_REQUIRED\",\n });\n return;\n }\n\n conditionalLogic.conditions.forEach((condition, index) => {\n const conditionPath = `${path}.conditionalLogic.conditions[${index}]`;\n\n if (!condition.field) {\n errors.push({\n path: `${conditionPath}.field`,\n message: \"Condition must specify a field name\",\n code: \"CONDITION_FIELD_REQUIRED\",\n });\n }\n\n if (!condition.comparison) {\n errors.push({\n path: `${conditionPath}.comparison`,\n message: \"Condition must specify a comparison operator\",\n code: \"CONDITION_COMPARISON_REQUIRED\",\n });\n }\n });\n}\n\n/**\n * Validates field based on its type.\n */\nfunction validateFieldByType(\n field: FormField,\n path: string,\n errors: FormValidationError[]\n): void {\n switch (field.type) {\n case \"select\":\n case \"radio\":\n validateOptions(field, path, errors);\n break;\n\n case \"number\": {\n const numberField = field;\n if (numberField.validation) {\n const { min, max } = numberField.validation;\n if (min !== undefined && max !== undefined && min > max) {\n errors.push({\n path: `${path}.validation`,\n message: \"Min value cannot be greater than max value\",\n code: \"MIN_GREATER_THAN_MAX\",\n });\n }\n }\n break;\n }\n\n case \"text\":\n case \"textarea\": {\n const textField = field;\n if (textField.validation) {\n const { minLength, maxLength } = textField.validation;\n if (\n minLength !== undefined &&\n maxLength !== undefined &&\n minLength > maxLength\n ) {\n errors.push({\n path: `${path}.validation`,\n message: \"Min length cannot be greater than max length\",\n code: \"MIN_LENGTH_GREATER_THAN_MAX\",\n });\n }\n }\n break;\n }\n\n case \"file\": {\n const fileField = field;\n if (fileField.maxFileSize !== undefined && fileField.maxFileSize <= 0) {\n errors.push({\n path: `${path}.maxFileSize`,\n message: \"File maxFileSize must be a positive number\",\n code: \"FILE_MAX_SIZE_INVALID\",\n });\n }\n break;\n }\n }\n}\n\n/**\n * Validates a single field configuration.\n */\nfunction validateField(\n field: unknown,\n path: string,\n errors: FormValidationError[],\n seenNames: Set<string>\n): void {\n if (!field || typeof field !== \"object\") {\n return;\n }\n\n const f = field as Record<string, unknown>;\n\n // Validate field type\n const fieldType = f.type;\n if (!fieldType) {\n errors.push({\n path: `${path}.type`,\n message: \"Field type is required\",\n code: \"FIELD_TYPE_REQUIRED\",\n });\n return;\n }\n\n // A plugin may contribute a field type that opted into the forms surface; it\n // is as valid here as a built-in. Built-ins keep the fast allowlist; other\n // strings fall through to the registry, surface-gated.\n const isValidType =\n typeof fieldType === \"string\" &&\n (VALID_FIELD_TYPES_SET.has(fieldType) ||\n isPluginFieldTypeOnSurface(fieldType, \"forms\"));\n\n if (!isValidType) {\n let fieldTypeRepr: string;\n if (typeof fieldType === \"object\" && fieldType !== null) {\n fieldTypeRepr = JSON.stringify(fieldType);\n } else {\n // eslint-disable-next-line @typescript-eslint/no-base-to-string -- fieldType narrowed to primitive above; rule doesn't follow control flow on unknown\n fieldTypeRepr = String(fieldType);\n }\n errors.push({\n path: `${path}.type`,\n message: `Invalid field type '${fieldTypeRepr}'. Valid types: ${VALID_FIELD_TYPES.join(\", \")}`,\n code: \"FIELD_TYPE_INVALID\",\n });\n return;\n }\n\n validateFieldName(f.name, path, errors, seenNames);\n\n // All fields except hidden require a label\n if (fieldType !== \"hidden\") {\n validateFieldLabel(f.label, path, errors);\n }\n\n // Type-specific validation\n validateFieldByType(field as FormField, path, errors);\n\n // Validate conditional logic if present\n if (f.conditionalLogic) {\n validateConditionalLogic(field as FormField, path, errors);\n }\n}\n\n/**\n * Validates an array of field configurations.\n */\nfunction validateFieldsArray(\n fields: unknown[],\n basePath: string,\n errors: FormValidationError[]\n): void {\n // Track field names for duplicate detection\n const seenNames = new Set<string>();\n\n fields.forEach((field, index) => {\n const fieldPath = `${basePath}[${index}]`;\n validateField(field, fieldPath, errors, seenNames);\n });\n}\n\n/**\n * Validates the top-level fields array.\n */\nfunction validateFields(fields: unknown, errors: FormValidationError[]): void {\n const path = \"fields\";\n\n if (!fields) {\n errors.push({\n path,\n message: \"Form must have at least one field\",\n code: \"FIELDS_REQUIRED\",\n });\n return;\n }\n\n if (!Array.isArray(fields)) {\n errors.push({\n path,\n message: \"Form fields must be an array\",\n code: \"FIELDS_INVALID_TYPE\",\n });\n return;\n }\n\n if (fields.length === 0) {\n errors.push({\n path,\n message: \"Form must have at least one field\",\n code: \"FIELDS_EMPTY\",\n });\n return;\n }\n\n validateFieldsArray(fields, path, errors);\n}\n\n/**\n * Validates form settings.\n */\nfunction validateSettings(\n settings: unknown,\n errors: FormValidationError[]\n): void {\n if (!settings) {\n return; // Settings are optional\n }\n\n if (typeof settings !== \"object\" || Array.isArray(settings)) {\n errors.push({\n path: \"settings\",\n message: \"Form settings must be an object\",\n code: \"SETTINGS_INVALID_TYPE\",\n });\n return;\n }\n\n const s = settings as Record<string, unknown>;\n\n // If confirmationType is \"redirect\", redirectUrl or redirectRelation is required\n if (s.confirmationType === \"redirect\") {\n if (!s.redirectUrl && !s.redirectRelation) {\n errors.push({\n path: \"settings.redirectUrl\",\n message:\n \"Redirect URL or redirect relation is required when confirmation type is 'redirect'\",\n code: \"REDIRECT_URL_REQUIRED\",\n });\n }\n }\n}\n\n/**\n * Validates form access control.\n */\nfunction validateAccess(access: unknown, errors: FormValidationError[]): void {\n if (!access) {\n return; // Access is optional\n }\n\n if (typeof access !== \"object\" || Array.isArray(access)) {\n errors.push({\n path: \"access\",\n message: \"Access control must be an object\",\n code: \"ACCESS_INVALID_TYPE\",\n });\n return;\n }\n\n const accessObj = access as Record<string, unknown>;\n const validAccessKeys = [\"submit\", \"read\"];\n\n for (const key of validAccessKeys) {\n const fn = accessObj[key];\n if (fn !== undefined && typeof fn !== \"function\") {\n errors.push({\n path: `access.${key}`,\n message: `Access control '${key}' must be a function`,\n code: \"ACCESS_FUNCTION_INVALID\",\n });\n }\n }\n}\n\n// ============================================================\n// Main Validation Function\n// ============================================================\n\n/**\n * Validates a complete form configuration.\n *\n * This is the main entry point for form validation. It performs\n * comprehensive validation including:\n * - Slug format and reserved name checking\n * - Field name validation\n * - Field-specific validation (options, min/max, etc.)\n * - Duplicate field name detection\n * - Conditional logic validation\n * - Settings validation\n * - Access function type validation\n *\n * @param config - The form configuration to validate\n * @returns Validation result with any errors found\n *\n * @example\n * ```typescript\n * import { validateFormConfig } from '@nextly/plugin-form-builder';\n *\n * const config = {\n * slug: 'contact-form',\n * fields: [\n * { type: 'text', name: 'firstName', label: 'First Name' },\n * { type: 'email', name: 'email', label: 'Email', required: true },\n * ],\n * };\n *\n * const result = validateFormConfig(config);\n *\n * if (!result.valid) {\n * result.errors.forEach(err => {\n * console.error(`[${err.code}] ${err.path}: ${err.message}`);\n * });\n * }\n * ```\n */\nexport function validateFormConfig(config: FormConfig): FormValidationResult {\n const errors: FormValidationError[] = [];\n\n // Validate slug\n validateSlug(config.slug, errors);\n\n // Validate fields\n validateFields(config.fields, errors);\n\n // Validate settings\n validateSettings(config.settings, errors);\n\n // Validate access control\n validateAccess(config.access, errors);\n\n return {\n valid: errors.length === 0,\n errors,\n };\n}\n\n/**\n * Throws an error if the form configuration is invalid.\n *\n * This is a convenience wrapper around `validateFormConfig` that\n * throws a descriptive error instead of returning a result object.\n *\n * @param config - The form configuration to validate\n * @throws Error if validation fails with all error messages\n *\n * @example\n * ```typescript\n * import { assertValidFormConfig } from '@nextly/plugin-form-builder';\n *\n * // Throws if invalid\n * assertValidFormConfig(config);\n * ```\n */\nexport function assertValidFormConfig(config: FormConfig): void {\n const result = validateFormConfig(config);\n\n if (!result.valid) {\n const errorMessages = result.errors\n .map(err => ` - [${err.code}] ${err.path}: ${err.message}`)\n .join(\"\\n\");\n\n throw new Error(\n `Invalid form config for '${config.slug || \"unknown\"}':\\n${errorMessages}`\n );\n }\n}\n","/**\n * Form Field Helpers\n *\n * Convenient factory functions for creating form field configurations.\n * These helpers eliminate the need to manually specify the `type` property,\n * providing a cleaner, more ergonomic API for form definitions.\n *\n * @module config/field-helpers\n *\n * @example\n * ```typescript\n * import {\n * text,\n * email,\n * select,\n * checkbox,\n * option,\n * } from '@nextly/plugin-form-builder';\n *\n * const contactFormFields = [\n * text({ name: 'firstName', label: 'First Name', required: true }),\n * text({ name: 'lastName', label: 'Last Name', required: true }),\n * email({ name: 'email', label: 'Email Address', required: true }),\n * select({\n * name: 'subject',\n * label: 'Subject',\n * options: [\n * option('General Inquiry'),\n * option('Support Request'),\n * option('Feedback'),\n * ],\n * }),\n * checkbox({ name: 'subscribe', label: 'Subscribe to newsletter' }),\n * ];\n * ```\n */\n\nimport type {\n TextFormField,\n EmailFormField,\n NumberFormField,\n PhoneFormField,\n UrlFormField,\n TextareaFormField,\n SelectFormField,\n CheckboxFormField,\n RadioFormField,\n FileFormField,\n DateFormField,\n TimeFormField,\n HiddenFormField,\n} from \"../types\";\n\n// ============================================================\n// Utility Types\n// ============================================================\n\n/**\n * Option for select and radio fields.\n */\nexport interface FormFieldOption {\n label: string;\n value: string;\n}\n\n// ============================================================\n// Utility Helpers\n// ============================================================\n\n/**\n * Creates an option for select or radio fields.\n *\n * If no value is provided, the label is converted to lowercase with\n * spaces replaced by underscores.\n *\n * @param label - Display label for the option\n * @param value - Optional value (defaults to lowercase label with underscores)\n * @returns Option object with label and value\n *\n * @example\n * ```typescript\n * // Value derived from label\n * option('United States') // { label: 'United States', value: 'united_states' }\n *\n * // Explicit value\n * option('United States', 'us') // { label: 'United States', value: 'us' }\n *\n * // Usage in select field\n * select({\n * name: 'country',\n * label: 'Country',\n * options: [\n * option('United States', 'us'),\n * option('Canada', 'ca'),\n * option('Mexico', 'mx'),\n * ],\n * })\n * ```\n */\nexport const option = (label: string, value?: string): FormFieldOption => ({\n label,\n value: value ?? label.toLowerCase().replace(/\\s+/g, \"_\"),\n});\n\n// ============================================================\n// Text Input Field Helpers\n// ============================================================\n\n/**\n * Creates a text input field configuration.\n *\n * Text fields store simple string values with optional constraints\n * like min/max length and pattern validation.\n *\n * @param config - Field configuration without the `type` property\n * @returns Complete text field configuration\n *\n * @example\n * ```typescript\n * // Basic text field\n * text({ name: 'firstName', label: 'First Name', required: true })\n *\n * // Text with validation\n * text({\n * name: 'username',\n * label: 'Username',\n * required: true,\n * placeholder: 'Enter username',\n * validation: {\n * minLength: 3,\n * maxLength: 20,\n * pattern: '^[a-zA-Z0-9_]+$',\n * },\n * })\n * ```\n */\nexport const text = (config: Omit<TextFormField, \"type\">): TextFormField => ({\n ...config,\n type: \"text\",\n});\n\n/**\n * Creates an email input field configuration.\n *\n * Email fields provide built-in email format validation.\n *\n * @param config - Field configuration without the `type` property\n * @returns Complete email field configuration\n *\n * @example\n * ```typescript\n * // Basic email field\n * email({ name: 'email', label: 'Email Address', required: true })\n *\n * // Email with placeholder\n * email({\n * name: 'workEmail',\n * label: 'Work Email',\n * required: true,\n * placeholder: 'you@nextly.local',\n * })\n * ```\n */\nexport const email = (\n config: Omit<EmailFormField, \"type\">\n): EmailFormField => ({\n ...config,\n type: \"email\",\n});\n\n/**\n * Creates a phone input field configuration.\n *\n * Phone fields are optimized for phone number entry with\n * appropriate keyboard on mobile devices.\n *\n * @param config - Field configuration without the `type` property\n * @returns Complete phone field configuration\n *\n * @example\n * ```typescript\n * // Basic phone field\n * phone({ name: 'phone', label: 'Phone Number', required: true })\n *\n * // Phone with placeholder\n * phone({\n * name: 'mobile',\n * label: 'Mobile Number',\n * placeholder: '+1 (555) 123-4567',\n * })\n * ```\n */\nexport const phone = (\n config: Omit<PhoneFormField, \"type\">\n): PhoneFormField => ({\n ...config,\n type: \"phone\",\n});\n\n/**\n * Creates a URL input field configuration.\n *\n * URL fields provide built-in URL format validation.\n *\n * @param config - Field configuration without the `type` property\n * @returns Complete URL field configuration\n *\n * @example\n * ```typescript\n * // Basic URL field\n * url({ name: 'website', label: 'Website URL' })\n *\n * // URL with placeholder\n * url({\n * name: 'portfolio',\n * label: 'Portfolio URL',\n * placeholder: 'https://example.com',\n * })\n * ```\n */\nexport const url = (config: Omit<UrlFormField, \"type\">): UrlFormField => ({\n ...config,\n type: \"url\",\n});\n\n/**\n * Creates a textarea field configuration.\n *\n * Textarea fields provide multi-line text input with configurable\n * row count and length validation.\n *\n * @param config - Field configuration without the `type` property\n * @returns Complete textarea field configuration\n *\n * @example\n * ```typescript\n * // Basic textarea\n * textarea({ name: 'message', label: 'Message', required: true })\n *\n * // Textarea with rows and validation\n * textarea({\n * name: 'description',\n * label: 'Description',\n * rows: 5,\n * validation: {\n * minLength: 10,\n * maxLength: 500,\n * },\n * })\n * ```\n */\nexport const textarea = (\n config: Omit<TextareaFormField, \"type\">\n): TextareaFormField => ({\n ...config,\n type: \"textarea\",\n});\n\n// ============================================================\n// Numeric Field Helpers\n// ============================================================\n\n/**\n * Creates a number input field configuration.\n *\n * Number fields store numeric values with optional min/max/step\n * constraints.\n *\n * @param config - Field configuration without the `type` property\n * @returns Complete number field configuration\n *\n * @example\n * ```typescript\n * // Basic number field\n * number({ name: 'age', label: 'Age', required: true })\n *\n * // Number with range validation\n * number({\n * name: 'quantity',\n * label: 'Quantity',\n * required: true,\n * validation: {\n * min: 1,\n * max: 100,\n * step: 1,\n * },\n * })\n * ```\n */\nexport const number = (\n config: Omit<NumberFormField, \"type\">\n): NumberFormField => ({\n ...config,\n type: \"number\",\n});\n\n// ============================================================\n// Selection Field Helpers\n// ============================================================\n\n/**\n * Creates a select dropdown field configuration.\n *\n * Select fields display a dropdown list of options. Use the `option()`\n * helper to create options.\n *\n * @param config - Field configuration without the `type` property\n * @returns Complete select field configuration\n *\n * @example\n * ```typescript\n * // Basic select\n * select({\n * name: 'country',\n * label: 'Country',\n * required: true,\n * options: [\n * option('United States', 'us'),\n * option('Canada', 'ca'),\n * option('Mexico', 'mx'),\n * ],\n * })\n *\n * // Multi-select\n * select({\n * name: 'interests',\n * label: 'Interests',\n * allowMultiple: true,\n * options: [\n * option('Technology'),\n * option('Design'),\n * option('Business'),\n * ],\n * })\n * ```\n */\nexport const select = (\n config: Omit<SelectFormField, \"type\">\n): SelectFormField => ({\n ...config,\n type: \"select\",\n});\n\n/**\n * Creates a single checkbox field configuration.\n *\n * Checkbox fields store boolean values (true/false).\n *\n * @param config - Field configuration without the `type` property\n * @returns Complete checkbox field configuration\n *\n * @example\n * ```typescript\n * // Terms agreement checkbox\n * checkbox({\n * name: 'agreeToTerms',\n * label: 'I agree to the terms and conditions',\n * required: true,\n * })\n *\n * // Newsletter subscription\n * checkbox({\n * name: 'subscribe',\n * label: 'Subscribe to our newsletter',\n * defaultValue: true,\n * })\n * ```\n */\nexport const checkbox = (\n config: Omit<CheckboxFormField, \"type\">\n): CheckboxFormField => ({\n ...config,\n type: \"checkbox\",\n});\n\n/**\n * Creates a radio group field configuration.\n *\n * Radio group fields allow selecting exactly one option from a list.\n * Use the `option()` helper to create options.\n *\n * @param config - Field configuration without the `type` property\n * @returns Complete radio field configuration\n *\n * @example\n * ```typescript\n * radio({\n * name: 'contactMethod',\n * label: 'Preferred Contact Method',\n * required: true,\n * options: [\n * option('Email', 'email'),\n * option('Phone', 'phone'),\n * option('SMS', 'sms'),\n * ],\n * })\n * ```\n */\nexport const radio = (\n config: Omit<RadioFormField, \"type\">\n): RadioFormField => ({\n ...config,\n type: \"radio\",\n});\n\n// ============================================================\n// File Field Helpers\n// ============================================================\n\n/**\n * Creates a file upload field configuration.\n *\n * File fields allow users to upload files with configurable\n * accepted types, size limits, and multiple file support.\n *\n * @param config - Field configuration without the `type` property\n * @returns Complete file field configuration\n *\n * @example\n * ```typescript\n * // Single file upload\n * file({\n * name: 'resume',\n * label: 'Upload Resume',\n * required: true,\n * accept: 'application/pdf,.doc,.docx',\n * maxFileSize: 5242880, // 5MB\n * })\n *\n * // Multiple images\n * file({\n * name: 'photos',\n * label: 'Upload Photos',\n * accept: 'image/*',\n * multiple: true,\n * maxFileSize: 10485760, // 10MB\n * })\n * ```\n */\nexport const file = (config: Omit<FileFormField, \"type\">): FileFormField => ({\n ...config,\n type: \"file\",\n});\n\n// ============================================================\n// Date/Time Field Helpers\n// ============================================================\n\n/**\n * Creates a date picker field configuration.\n *\n * Date fields provide a date picker with optional min/max constraints.\n *\n * @param config - Field configuration without the `type` property\n * @returns Complete date field configuration\n *\n * @example\n * ```typescript\n * // Basic date field\n * date({ name: 'birthdate', label: 'Date of Birth', required: true })\n *\n * // Date with range constraints\n * date({\n * name: 'eventDate',\n * label: 'Event Date',\n * required: true,\n * min: '2024-01-01',\n * max: '2024-12-31',\n * })\n * ```\n */\nexport const date = (config: Omit<DateFormField, \"type\">): DateFormField => ({\n ...config,\n type: \"date\",\n});\n\n/**\n * Creates a time picker field configuration.\n *\n * Time fields provide a time picker for selecting time values.\n *\n * @param config - Field configuration without the `type` property\n * @returns Complete time field configuration\n *\n * @example\n * ```typescript\n * // Basic time field\n * time({ name: 'appointmentTime', label: 'Preferred Time', required: true })\n *\n * // Time with default\n * time({\n * name: 'meetingTime',\n * label: 'Meeting Time',\n * defaultValue: '09:00',\n * })\n * ```\n */\nexport const time = (config: Omit<TimeFormField, \"type\">): TimeFormField => ({\n ...config,\n type: \"time\",\n});\n\n// ============================================================\n// Special Field Helpers\n// ============================================================\n\n/**\n * Creates a hidden field configuration.\n *\n * Hidden fields store values that are not visible to users but\n * are included in the submission data. Useful for tracking\n * metadata like source, campaign, or referrer.\n *\n * @param config - Field configuration without the `type` property\n * @returns Complete hidden field configuration\n *\n * @example\n * ```typescript\n * // Track form source\n * hidden({ name: 'source', label: 'Source', defaultValue: 'website' })\n *\n * // Campaign tracking\n * hidden({ name: 'campaign', label: 'Campaign', defaultValue: 'summer-sale' })\n * ```\n */\nexport const hidden = (\n config: Omit<HiddenFormField, \"type\">\n): HiddenFormField => ({\n ...config,\n type: \"hidden\",\n});\n","/**\n * Dynamic Zod Schema Generator\n *\n * Converts FormField[] configuration to Zod validation schemas.\n * Supports all form field types with proper validation rules.\n *\n * @module utils/generate-schema\n * @since 0.1.0\n */\n\nimport { z } from \"zod\";\n\nimport { isKnownFormField } from \"../types\";\nimport type {\n AnyFormField,\n FormField,\n TextFormField,\n EmailFormField,\n NumberFormField,\n PhoneFormField,\n UrlFormField,\n TextareaFormField,\n SelectFormField,\n CheckboxFormField,\n RadioFormField,\n FileFormField,\n DateFormField,\n TimeFormField,\n HiddenFormField,\n} from \"../types\";\n\n// ============================================================\n// Main Schema Generator\n// ============================================================\n\n/**\n * Generate a Zod validation schema from form field configuration.\n *\n * Creates a dynamic Zod schema based on the form fields array,\n * applying appropriate validation rules for each field type.\n *\n * @param fields - Array of form field configurations\n * @returns Zod object schema for validating submission data\n *\n * @example\n * ```typescript\n * const fields: FormField[] = [\n * { type: 'text', name: 'firstName', label: 'First Name', required: true },\n * { type: 'email', name: 'email', label: 'Email', required: true },\n * { type: 'number', name: 'age', label: 'Age', validation: { min: 18, max: 120 } },\n * ];\n *\n * const schema = generateZodSchema(fields);\n * const result = schema.safeParse({ firstName: 'John', email: 'john@example.com', age: 25 });\n * ```\n */\nexport function generateZodSchema(\n fields: AnyFormField[]\n): z.ZodObject<Record<string, z.ZodTypeAny>> {\n const schemaShape: Record<string, z.ZodTypeAny> = {};\n\n for (const field of fields) {\n // A plugin-contributed field's value shape is owned by the plugin: accept\n // any value, but keep the base required check so a required plugin field\n // cannot be omitted (z.unknown() alone treats a missing key as valid).\n if (!isKnownFormField(field)) {\n schemaShape[field.name] = field.required\n ? z\n .unknown()\n .refine(\n value => value !== undefined && value !== null && value !== \"\",\n { message: `${field.label || field.name} is required` }\n )\n : z.unknown();\n continue;\n }\n\n // Skip hidden fields without required flag\n if (field.type === \"hidden\" && !field.required) {\n // Still add to schema but make optional\n schemaShape[field.name] = z.string().optional();\n continue;\n }\n\n const fieldSchema = generateFieldSchema(field);\n if (fieldSchema) {\n schemaShape[field.name] = fieldSchema;\n }\n }\n\n return z.object(schemaShape);\n}\n\n// ============================================================\n// Field Schema Generators\n// ============================================================\n\n/**\n * Generate a Zod schema for a single field.\n */\nfunction generateFieldSchema(field: FormField): z.ZodTypeAny | null {\n switch (field.type) {\n case \"text\":\n return generateTextSchema(field);\n case \"email\":\n return generateEmailSchema(field);\n case \"number\":\n return generateNumberSchema(field);\n case \"phone\":\n return generatePhoneSchema(field);\n case \"url\":\n return generateUrlSchema(field);\n case \"textarea\":\n return generateTextareaSchema(field);\n case \"select\":\n return generateSelectSchema(field);\n case \"checkbox\":\n return generateCheckboxSchema(field);\n case \"radio\":\n return generateRadioSchema(field);\n case \"file\":\n return generateFileSchema(field);\n case \"date\":\n return generateDateSchema(field);\n case \"time\":\n return generateTimeSchema(field);\n case \"hidden\":\n return generateHiddenSchema(field);\n default:\n // Unreachable for a known FormField; plugin types are handled in\n // generateZodSchema before this switch is reached.\n return z.unknown();\n }\n}\n\n/**\n * Generate schema for text field.\n */\nfunction generateTextSchema(field: TextFormField): z.ZodTypeAny {\n let schema = z.string();\n\n // Apply validation rules\n if (field.validation?.minLength !== undefined) {\n schema = schema.min(\n field.validation.minLength,\n field.validation.errorMessage ||\n `Minimum ${field.validation.minLength} characters required`\n );\n }\n\n if (field.validation?.maxLength !== undefined) {\n schema = schema.max(\n field.validation.maxLength,\n field.validation.errorMessage ||\n `Maximum ${field.validation.maxLength} characters allowed`\n );\n }\n\n if (field.validation?.pattern) {\n schema = schema.regex(\n new RegExp(field.validation.pattern),\n field.validation.errorMessage || \"Invalid format\"\n );\n }\n\n return applyRequired(schema, field.required, field.validation?.errorMessage);\n}\n\n/**\n * Generate schema for email field.\n */\nfunction generateEmailSchema(field: EmailFormField): z.ZodTypeAny {\n let schema = z\n .string()\n .email(field.validation?.errorMessage || \"Invalid email address\");\n\n if (field.validation?.pattern) {\n schema = schema.regex(\n new RegExp(field.validation.pattern),\n field.validation.errorMessage || \"Invalid email format\"\n );\n }\n\n return applyRequired(schema, field.required, field.validation?.errorMessage);\n}\n\n/**\n * Generate schema for number field.\n */\nfunction generateNumberSchema(field: NumberFormField): z.ZodTypeAny {\n // Zod 4 unified message customization under a single `error` callback;\n // an undefined input is the \"missing required\" case, anything else is a\n // type mismatch.\n let schema = z.number({\n error: issue =>\n issue.input === undefined\n ? field.validation?.errorMessage || \"This field is required\"\n : \"Must be a number\",\n });\n\n if (field.validation?.min !== undefined) {\n schema = schema.min(\n field.validation.min,\n field.validation.errorMessage ||\n `Minimum value is ${field.validation.min}`\n );\n }\n\n if (field.validation?.max !== undefined) {\n schema = schema.max(\n field.validation.max,\n field.validation.errorMessage ||\n `Maximum value is ${field.validation.max}`\n );\n }\n\n if (field.required) {\n return schema;\n }\n\n return schema.optional().nullable();\n}\n\n/**\n * Generate schema for phone field.\n */\nfunction generatePhoneSchema(field: PhoneFormField): z.ZodTypeAny {\n let schema = z.string();\n\n if (field.validation?.pattern) {\n schema = schema.regex(\n new RegExp(field.validation.pattern),\n field.validation.errorMessage || \"Invalid phone number format\"\n );\n } else {\n // Default phone pattern - allows common formats\n schema = schema.regex(\n /^[\\d\\s\\-+()]+$/,\n field.validation?.errorMessage || \"Invalid phone number\"\n );\n }\n\n return applyRequired(schema, field.required, field.validation?.errorMessage);\n}\n\n/**\n * Generate schema for URL field.\n */\nfunction generateUrlSchema(field: UrlFormField): z.ZodTypeAny {\n let schema = z.string().url(field.validation?.errorMessage || \"Invalid URL\");\n\n if (field.validation?.pattern) {\n schema = schema.regex(\n new RegExp(field.validation.pattern),\n field.validation.errorMessage || \"Invalid URL format\"\n );\n }\n\n return applyRequired(schema, field.required, field.validation?.errorMessage);\n}\n\n/**\n * Generate schema for textarea field.\n */\nfunction generateTextareaSchema(field: TextareaFormField): z.ZodTypeAny {\n let schema = z.string();\n\n if (field.validation?.minLength !== undefined) {\n schema = schema.min(\n field.validation.minLength,\n field.validation.errorMessage ||\n `Minimum ${field.validation.minLength} characters required`\n );\n }\n\n if (field.validation?.maxLength !== undefined) {\n schema = schema.max(\n field.validation.maxLength,\n field.validation.errorMessage ||\n `Maximum ${field.validation.maxLength} characters allowed`\n );\n }\n\n return applyRequired(schema, field.required, field.validation?.errorMessage);\n}\n\n/**\n * Generate schema for select field.\n */\nfunction generateSelectSchema(field: SelectFormField): z.ZodTypeAny {\n const validValues = field.options.map(opt => opt.value);\n\n if (field.allowMultiple) {\n // Multi-select: array of valid values\n const schema = z.array(z.enum(validValues as [string, ...string[]]));\n\n if (field.required) {\n return schema.min(\n 1,\n field.validation?.errorMessage || \"Please select at least one option\"\n );\n }\n\n return schema.optional();\n }\n\n // Single select\n const schema = z.enum(validValues as [string, ...string[]], {\n error: () =>\n field.validation?.errorMessage || \"Please select a valid option\",\n });\n\n if (field.required) {\n return schema;\n }\n\n return schema.optional();\n}\n\n/**\n * Generate schema for checkbox field.\n */\nfunction generateCheckboxSchema(field: CheckboxFormField): z.ZodTypeAny {\n const schema = z.boolean();\n\n if (field.required) {\n // For required checkbox, value must be true\n return schema.refine(val => val === true, {\n message: field.validation?.errorMessage || \"This field is required\",\n });\n }\n\n return schema.optional();\n}\n\n/**\n * Generate schema for radio field.\n */\nfunction generateRadioSchema(field: RadioFormField): z.ZodTypeAny {\n const validValues = field.options.map(opt => opt.value);\n\n const schema = z.enum(validValues as [string, ...string[]], {\n error: () => field.validation?.errorMessage || \"Please select an option\",\n });\n\n if (field.required) {\n return schema;\n }\n\n return schema.optional();\n}\n\n/**\n * Generate schema for file field.\n *\n * Note: File validation is typically handled server-side.\n * This schema validates file references (IDs or URLs).\n */\nfunction generateFileSchema(field: FileFormField): z.ZodTypeAny {\n if (field.multiple) {\n const schema = z.array(z.string());\n\n if (field.required) {\n return schema.min(\n 1,\n field.validation?.errorMessage || \"Please upload at least one file\"\n );\n }\n\n return schema.optional();\n }\n\n // Single file\n const schema = z.string();\n return applyRequired(schema, field.required, field.validation?.errorMessage);\n}\n\n/**\n * Generate schema for date field.\n */\nfunction generateDateSchema(field: DateFormField): z.ZodTypeAny {\n // Start with base string schema\n let schema: z.ZodTypeAny = z.string();\n\n // Add date format validation\n schema = (schema as z.ZodString).refine(\n val => {\n if (!val) return true; // Allow empty for optional fields\n const date = new Date(val);\n return !isNaN(date.getTime());\n },\n { message: field.validation?.errorMessage || \"Invalid date\" }\n );\n\n // Min/max date validation\n if (field.min || field.max) {\n schema = schema.refine(\n (val: unknown) => {\n if (!val) return true;\n const date = new Date(val as string);\n if (field.min && date < new Date(field.min)) return false;\n if (field.max && date > new Date(field.max)) return false;\n return true;\n },\n {\n message:\n field.validation?.errorMessage ||\n `Date must be between ${field.min || \"any\"} and ${field.max || \"any\"}`,\n }\n );\n }\n\n // Handle required vs optional\n if (field.required) {\n // Add non-empty check for required fields\n return schema.refine(\n (val: unknown) => val !== undefined && val !== null && val !== \"\",\n { message: field.validation?.errorMessage || \"This field is required\" }\n );\n }\n\n return schema.optional().or(z.literal(\"\"));\n}\n\n/**\n * Generate schema for time field.\n */\nfunction generateTimeSchema(field: TimeFormField): z.ZodTypeAny {\n const schema = z\n .string()\n .regex(\n /^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$/,\n field.validation?.errorMessage || \"Invalid time format (HH:mm)\"\n );\n\n return applyRequired(schema, field.required, field.validation?.errorMessage);\n}\n\n/**\n * Generate schema for hidden field.\n */\nfunction generateHiddenSchema(field: HiddenFormField): z.ZodTypeAny {\n const schema = z.string();\n return applyRequired(schema, field.required, field.validation?.errorMessage);\n}\n\n// ============================================================\n// Helper Functions\n// ============================================================\n\n/**\n * Apply required/optional to a schema.\n */\nfunction applyRequired(\n schema: z.ZodString,\n required?: boolean,\n errorMessage?: string\n): z.ZodTypeAny {\n if (required) {\n return schema.min(1, errorMessage || \"This field is required\");\n }\n\n return schema.optional().or(z.literal(\"\"));\n}\n\n// ============================================================\n// Data Transformation\n// ============================================================\n\n/**\n * Transform form data before validation.\n *\n * Handles type coercion and normalization:\n * - Converts string numbers to actual numbers\n * - Converts string booleans to actual booleans\n * - Trims string values\n * - Handles empty strings\n *\n * @param data - Raw form data\n * @param fields - Form field configurations\n * @returns Transformed data ready for validation\n */\nexport function transformFormData(\n data: Record<string, unknown>,\n fields: AnyFormField[]\n): Record<string, unknown> {\n const transformed: Record<string, unknown> = {};\n\n for (const field of fields) {\n const value = data[field.name];\n\n // Skip undefined values\n if (value === undefined) {\n continue;\n }\n\n // A plugin field's value passes through untransformed — the plugin owns its\n // value shape; only built-in types get type coercion.\n transformed[field.name] = isKnownFormField(field)\n ? transformFieldValue(value, field)\n : value;\n }\n\n return transformed;\n}\n\n/**\n * Transform a single field value based on its type.\n */\nfunction transformFieldValue(value: unknown, field: FormField): unknown {\n // Handle null/undefined\n if (value === null || value === undefined) {\n return value;\n }\n\n switch (field.type) {\n case \"number\":\n // Coerce to number\n if (typeof value === \"string\") {\n const num = parseFloat(value);\n return isNaN(num) ? value : num;\n }\n return value;\n\n case \"checkbox\":\n // Coerce to boolean\n if (typeof value === \"string\") {\n return value === \"true\" || value === \"1\" || value === \"on\";\n }\n return Boolean(value);\n\n case \"text\":\n case \"email\":\n case \"phone\":\n case \"url\":\n case \"textarea\":\n case \"hidden\":\n // Trim strings\n if (typeof value === \"string\") {\n return value.trim();\n }\n return value;\n\n case \"select\":\n // Ensure array for multi-select\n if (field.allowMultiple) {\n if (!Array.isArray(value)) {\n return value ? [value] : [];\n }\n }\n return value;\n\n default:\n return value;\n }\n}\n\n// ============================================================\n// Validation Helper\n// ============================================================\n\n/**\n * Validate form data against a form's field configuration.\n *\n * Combines transformation and validation in one step.\n *\n * @param data - Raw form data\n * @param fields - Form field configurations\n * @returns Zod safe parse result\n *\n * @example\n * ```typescript\n * const result = validateFormData({ name: 'John', email: 'john@example.com' }, fields);\n *\n * if (result.success) {\n * console.log('Valid data:', result.data);\n * } else {\n * console.log('Validation errors:', result.error.flatten());\n * }\n * ```\n */\nexport function validateFormData(\n data: Record<string, unknown>,\n fields: AnyFormField[]\n): z.ZodSafeParseResult<Record<string, unknown>> {\n const transformed = transformFormData(data, fields);\n const schema = generateZodSchema(fields);\n return schema.safeParse(transformed);\n}\n\n/**\n * Get validation errors as a flat object.\n *\n * @param result - Zod safe parse result\n * @returns Object mapping field names to error messages\n */\nexport function getValidationErrors(\n result: z.ZodSafeParseResult<unknown>\n): Record<string, string> {\n if (result.success) {\n return {};\n }\n\n const errors: Record<string, string> = {};\n\n // Zod 4 renamed ZodError.errors to ZodError.issues.\n for (const error of result.error.issues) {\n const path = error.path.join(\".\");\n if (!errors[path]) {\n errors[path] = error.message;\n }\n }\n\n return errors;\n}\n","/**\n * Spam Detection\n *\n * Implements honeypot and rate limiting spam protection for form submissions.\n * Provides a pluggable approach to spam detection that can be extended.\n *\n * @module handlers/spam-detection\n * @since 0.1.0\n */\n\n// ============================================================\n// Types\n// ============================================================\n\n/**\n * Configuration for spam detection.\n */\nexport interface SpamCheckConfig {\n /**\n * Enable honeypot field detection.\n * When enabled, submissions with filled honeypot fields are marked as spam.\n */\n honeypot?: boolean;\n\n /**\n * Rate limiting configuration.\n */\n rateLimit?: {\n /** Maximum submissions allowed per window */\n maxSubmissions: number;\n /** Time window in milliseconds */\n windowMs: number;\n };\n\n /**\n * reCAPTCHA configuration (for future implementation).\n */\n recaptcha?: {\n enabled: boolean;\n secretKey?: string;\n scoreThreshold?: number;\n };\n}\n\n/**\n * Options for spam check.\n */\nexport interface SpamCheckOptions {\n /** Form submission data */\n data: Record<string, unknown>;\n\n /** Submitter's IP address */\n ipAddress?: string;\n\n /** Form slug for rate limiting key */\n formSlug: string;\n\n /** Spam protection configuration */\n config: SpamCheckConfig;\n}\n\n/**\n * Result of spam check.\n */\nexport interface SpamCheckResult {\n /** Whether the submission is detected as spam */\n isSpam: boolean;\n\n /** Reason for spam detection (for logging, not exposed to user) */\n reason?: \"honeypot\" | \"rate_limit\" | \"recaptcha\";\n\n /** Additional details (for logging) */\n details?: string;\n}\n\n// ============================================================\n// Rate Limit Store\n// ============================================================\n\n/**\n * In-memory rate limit store.\n *\n * Note: For multi-instance deployments (serverless, load-balanced),\n * consider using Redis or a database-backed store instead.\n * This in-memory implementation works well for single-instance deployments.\n */\ninterface RateLimitEntry {\n count: number;\n timestamp: number;\n}\n\nconst rateLimitStore = new Map<string, RateLimitEntry>();\n\n// ============================================================\n// Main Spam Check Function\n// ============================================================\n\n/**\n * Check a form submission for spam indicators.\n *\n * Performs multiple spam checks based on configuration:\n * 1. Honeypot field detection\n * 2. Rate limiting by IP/form\n * 3. reCAPTCHA verification (future)\n *\n * @param options - Spam check options\n * @returns Spam check result\n *\n * @example\n * ```typescript\n * const result = await checkSpam({\n * data: formData,\n * ipAddress: req.ip,\n * formSlug: 'contact-form',\n * config: {\n * honeypot: true,\n * rateLimit: { maxSubmissions: 5, windowMs: 60000 },\n * },\n * });\n *\n * if (result.isSpam) {\n * // Silently reject (return fake success to bot)\n * return { success: true };\n * }\n * ```\n */\n// eslint-disable-next-line @typescript-eslint/require-await -- public API: declared return type is `Promise<SpamCheckResult>` and callers `await` it; removing `async` would force a return-type change\nexport async function checkSpam(\n options: SpamCheckOptions\n): Promise<SpamCheckResult> {\n const { data, ipAddress, formSlug, config } = options;\n\n // 1. Honeypot check\n if (config.honeypot) {\n const honeypotResult = checkHoneypot(data);\n if (honeypotResult.isSpam) {\n return honeypotResult;\n }\n }\n\n // 2. Rate limiting check\n if (config.rateLimit && ipAddress) {\n const rateLimitResult = checkRateLimit(\n ipAddress,\n formSlug,\n config.rateLimit\n );\n if (rateLimitResult.isSpam) {\n return rateLimitResult;\n }\n }\n\n return { isSpam: false };\n}\n\n// ============================================================\n// Honeypot Detection\n// ============================================================\n\n/**\n * Standard honeypot field names to check.\n * These fields should be hidden from users but filled by bots.\n */\nconst HONEYPOT_FIELDS = [\n \"__honeypot\",\n \"_honeypot\",\n \"honeypot\",\n \"__hp\",\n \"_hp\",\n \"website\", // Common honeypot field name\n \"url_field\", // Another common honeypot\n \"fax_number\", // Rarely used by humans\n];\n\n/**\n * Check for honeypot field presence.\n *\n * Honeypot fields are hidden form fields that legitimate users\n * won't see or fill, but bots often fill automatically.\n *\n * @param data - Form submission data\n * @returns Spam check result\n */\nfunction checkHoneypot(data: Record<string, unknown>): SpamCheckResult {\n for (const fieldName of HONEYPOT_FIELDS) {\n const value = data[fieldName];\n\n // Check if honeypot field has a non-empty value\n if (value !== undefined && value !== null && value !== \"\") {\n let raw: string;\n if (typeof value === \"object\") {\n raw = JSON.stringify(value);\n } else {\n // eslint-disable-next-line @typescript-eslint/no-base-to-string -- value narrowed to primitive above; rule doesn't follow control flow on unknown\n raw = String(value);\n }\n const stringValue = raw.trim();\n if (stringValue.length > 0) {\n return {\n isSpam: true,\n reason: \"honeypot\",\n details: `Honeypot field \"${fieldName}\" was filled`,\n };\n }\n }\n }\n\n return { isSpam: false };\n}\n\n// ============================================================\n// Rate Limiting\n// ============================================================\n\n/**\n * Check rate limit for a given IP and form combination.\n *\n * Uses a sliding window approach where submissions are counted\n * within a time window. If the count exceeds the maximum,\n * further submissions are blocked.\n *\n * @param ipAddress - Submitter's IP address\n * @param formSlug - Form identifier\n * @param config - Rate limit configuration\n * @returns Spam check result\n */\nfunction checkRateLimit(\n ipAddress: string,\n formSlug: string,\n config: { maxSubmissions: number; windowMs: number }\n): SpamCheckResult {\n const key = `${formSlug}:${ipAddress}`;\n const now = Date.now();\n const entry = rateLimitStore.get(key);\n\n if (entry) {\n // Check if window has expired\n if (now - entry.timestamp > config.windowMs) {\n // Reset counter for new window\n rateLimitStore.set(key, { count: 1, timestamp: now });\n return { isSpam: false };\n }\n\n // Check if limit exceeded\n if (entry.count >= config.maxSubmissions) {\n return {\n isSpam: true,\n reason: \"rate_limit\",\n details: `Rate limit exceeded: ${entry.count}/${config.maxSubmissions} in ${config.windowMs}ms`,\n };\n }\n\n // Increment counter\n rateLimitStore.set(key, {\n count: entry.count + 1,\n timestamp: entry.timestamp,\n });\n } else {\n // First submission from this IP/form\n rateLimitStore.set(key, { count: 1, timestamp: now });\n }\n\n return { isSpam: false };\n}\n\n// ============================================================\n// Utility Functions\n// ============================================================\n\n/**\n * Clean up expired rate limit entries.\n *\n * Call this periodically (e.g., every 5 minutes) to prevent\n * memory leaks from accumulated rate limit entries.\n *\n * @param maxAgeMs - Maximum age of entries to keep (default: 5 minutes)\n * @returns Number of entries removed\n *\n * @example\n * ```typescript\n * // Set up periodic cleanup\n * setInterval(() => {\n * const removed = cleanupRateLimitStore();\n * console.log(`Cleaned up ${removed} expired rate limit entries`);\n * }, 5 * 60 * 1000); // Every 5 minutes\n * ```\n */\nexport function cleanupRateLimitStore(\n maxAgeMs: number = 5 * 60 * 1000\n): number {\n const now = Date.now();\n let removed = 0;\n\n for (const [key, entry] of rateLimitStore.entries()) {\n if (now - entry.timestamp > maxAgeMs) {\n rateLimitStore.delete(key);\n removed++;\n }\n }\n\n return removed;\n}\n\n/**\n * Get the current size of the rate limit store.\n *\n * Useful for monitoring and debugging.\n *\n * @returns Number of entries in the store\n */\nexport function getRateLimitStoreSize(): number {\n return rateLimitStore.size;\n}\n\n/**\n * Clear all rate limit entries.\n *\n * Useful for testing or resetting state.\n */\nexport function clearRateLimitStore(): void {\n rateLimitStore.clear();\n}\n\n/**\n * Check if a specific IP/form is currently rate limited.\n *\n * @param ipAddress - IP address to check\n * @param formSlug - Form slug\n * @param config - Rate limit configuration\n * @returns Whether the IP is currently rate limited\n */\nexport function isRateLimited(\n ipAddress: string,\n formSlug: string,\n config: { maxSubmissions: number; windowMs: number }\n): boolean {\n const key = `${formSlug}:${ipAddress}`;\n const now = Date.now();\n const entry = rateLimitStore.get(key);\n\n if (!entry) {\n return false;\n }\n\n // Check if window has expired\n if (now - entry.timestamp > config.windowMs) {\n return false;\n }\n\n return entry.count >= config.maxSubmissions;\n}\n\n// ============================================================\n// reCAPTCHA Support (Future Implementation)\n// ============================================================\n\n/**\n * Verify reCAPTCHA v3 token.\n *\n * Note: This is a placeholder for future implementation.\n * reCAPTCHA verification requires server-side API calls to Google.\n *\n * @param data - Form data containing reCAPTCHA token\n * @param config - reCAPTCHA configuration\n * @returns Spam check result\n */\n// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/require-await -- declared return type is `Promise<SpamCheckResult>`; placeholder for unimplemented Google siteverify fetch\nasync function verifyRecaptcha(\n _data: Record<string, unknown>,\n _config: { secretKey?: string; scoreThreshold?: number }\n): Promise<SpamCheckResult> {\n // Placeholder - always pass for now\n return { isSpam: false };\n}\n","/**\n * Form Submission Handler\n *\n * Processes form submissions including validation, spam detection,\n * storage, and notification triggering.\n *\n * @module handlers/submit-form\n * @since 0.1.0\n */\n\nimport type { PluginContext } from \"nextly\";\n\nimport type {\n FormDocument,\n SubmissionDocument,\n ResolvedFormBuilderConfig,\n AnyFormField,\n} from \"../types\";\nimport { normalizeFormSettings } from \"../utils/form-settings\";\nimport {\n generateZodSchema,\n transformFormData,\n getValidationErrors,\n} from \"../utils/generate-schema\";\n\nimport { checkSpam } from \"./spam-detection\";\n\n// ============================================================\n// Types\n// ============================================================\n\n/**\n * Options for submitting a form.\n */\nexport interface SubmitFormOptions {\n /** Form slug (URL-friendly identifier) */\n formSlug: string;\n\n /** Form submission data */\n data: Record<string, unknown>;\n\n /** Optional metadata about the submission */\n metadata?: {\n /** Submitter's IP address */\n ipAddress?: string;\n /** Submitter's user agent string */\n userAgent?: string;\n };\n}\n\n/**\n * Result of form submission.\n */\nexport interface SubmitFormResult {\n /** Whether the submission was successful */\n success: boolean;\n\n /** The created submission document (on success) */\n submission?: SubmissionDocument;\n\n /** Error message (on failure) */\n error?: string;\n\n /** Field-level validation errors */\n validationErrors?: Record<string, string>;\n\n /** Redirect URL (if form configured for redirect on success) */\n redirect?: string;\n}\n\n/**\n * Context for the submission handler.\n */\nexport interface SubmitFormContext {\n /** Plugin context with access to services */\n pluginContext: PluginContext;\n\n /** Resolved plugin configuration */\n pluginConfig: ResolvedFormBuilderConfig;\n}\n\n// ============================================================\n// Main Submission Handler\n// ============================================================\n\n/**\n * Process a form submission.\n *\n * This is the main entry point for handling form submissions.\n * It performs the following steps:\n *\n * 1. Fetch form configuration by slug\n * 2. Verify form status (must be \"published\")\n * 3. Transform and validate submission data\n * 4. Check for spam (honeypot, rate limiting)\n * 5. Create submission record in database\n * 6. Return result with redirect info if configured\n *\n * Note: Email notifications are triggered separately via collection\n * hooks in the submissions collection (see Phase 4.2).\n *\n * @param options - Submission options\n * @param context - Handler context with services\n * @returns Submission result\n *\n * @example\n * ```typescript\n * const result = await submitForm(\n * {\n * formSlug: 'contact-form',\n * data: { name: 'John', email: 'john@example.com', message: 'Hello!' },\n * metadata: { ipAddress: '127.0.0.1', userAgent: 'Mozilla/5.0...' },\n * },\n * { pluginContext, pluginConfig }\n * );\n *\n * if (result.success) {\n * console.log('Submission created:', result.submission?.id);\n * if (result.redirect) {\n * // Redirect user\n * }\n * } else {\n * console.error('Submission failed:', result.error);\n * }\n * ```\n */\nexport async function submitForm(\n options: SubmitFormOptions,\n context: SubmitFormContext\n): Promise<SubmitFormResult> {\n const { formSlug, data, metadata } = options;\n const { pluginContext, pluginConfig } = context;\n const { collections } = pluginContext.services;\n const { logger } = pluginContext;\n\n try {\n // 1. Fetch form configuration\n const form = await fetchFormBySlug(formSlug, pluginConfig, pluginContext);\n\n if (!form) {\n logger.warn?.(\"Form submission attempted for non-existent form\", {\n formSlug,\n });\n return {\n success: false,\n error: \"Form not found\",\n };\n }\n\n // 2. Check form status\n if (form.status !== \"published\") {\n logger.info?.(\"Form submission rejected - form not published\", {\n formSlug,\n status: form.status,\n });\n return {\n success: false,\n error: \"This form is not currently accepting submissions\",\n };\n }\n\n // 2b. Settings come through the one canonical reader — stored settings\n // may carry legacy keys from earlier builder versions.\n const settings = normalizeFormSettings(form.settings);\n\n // 2c. Single-submission forms: the same visitor (by IP) submits once.\n // Checked before spam so a repeat visitor gets the honest message\n // instead of an inexplicable success that stored nothing new. This is a\n // best-effort deterrent, not a guarantee: IPs are shared (NAT/proxies),\n // and the check-then-create pair is not atomic, so two truly concurrent\n // submissions can both pass — an airtight version needs a partial\n // unique index the portable field config cannot express yet.\n if (settings.allowMultipleSubmissions === false && !metadata?.ipAddress) {\n // Without an identifier there is nothing to deduplicate on. Failing\n // closed would block every server-to-server integration, so the gate\n // stays open — loudly.\n logger.warn?.(\n \"Single-submission form received a submission without an IP — the restriction cannot apply\",\n { formSlug }\n );\n }\n if (settings.allowMultipleSubmissions === false && metadata?.ipAddress) {\n const existing = await collections.count(\n pluginConfig.formSubmissionOverrides.slug,\n {\n where: {\n form: { equals: form.id },\n ipAddress: { equals: metadata.ipAddress },\n status: { not_equals: \"spam\" },\n },\n },\n { as: \"system\" }\n );\n if (existing > 0) {\n logger.info?.(\"Repeat submission rejected (single-submission form)\", {\n formSlug,\n ipAddress: metadata.ipAddress,\n });\n return {\n success: false,\n error: \"You have already submitted this form.\",\n };\n }\n }\n\n // 3. Spam detection — BEFORE validation, so a bot that trips the trap\n // while also failing validation still receives the same fake success as\n // any other bot instead of a distinguishable validation error.\n // Honeypot fields are by definition NOT declared form fields, so the\n // probe is the raw payload minus the declared schema — a declared field\n // (e.g. a real \"website\" input) must never trip the trap.\n const declaredFieldNames = new Set(form.fields.map(field => field.name));\n const undeclaredData = Object.fromEntries(\n Object.entries(data).filter(([key]) => !declaredFieldNames.has(key))\n );\n\n const spamResult = await checkSpam({\n data: undeclaredData,\n ipAddress: metadata?.ipAddress,\n formSlug,\n config: {\n // Per-form overrides win where set; blank inherits the plugin config.\n honeypot:\n settings.honeypotEnabled ?? pluginConfig.spamProtection.honeypot,\n rateLimit: pluginConfig.spamProtection.rateLimit,\n recaptcha: {\n ...pluginConfig.spamProtection.recaptcha,\n enabled:\n settings.captchaEnabled ??\n pluginConfig.spamProtection.recaptcha?.enabled ??\n false,\n },\n },\n });\n\n // Rate-limit hits are pure volume — storing them would turn the limiter\n // into a database DoS, so they are rejected without a trace (still with\n // fake success so the client learns nothing).\n if (spamResult.isSpam && spamResult.reason === \"rate_limit\") {\n logger.info?.(\"Spam submission rejected (rate limit)\", {\n formSlug,\n ipAddress: metadata?.ipAddress,\n });\n return { success: true };\n }\n\n const isContentSpam = spamResult.isSpam;\n\n // 4. Transform + validate. Content spam skips validation entirely — it\n // is stored for review exactly as the bot shaped it (declared fields\n // only, sanitized), and requiring validity would drop the evidence.\n const transformedData = transformFormData(data, form.fields);\n let storedData: Record<string, unknown>;\n\n if (isContentSpam) {\n logger.info?.(\"Spam submission detected — storing flagged\", {\n formSlug,\n reason: spamResult.reason,\n ipAddress: metadata?.ipAddress,\n });\n sanitizeSubmissionData(transformedData, form.fields);\n storedData = transformedData;\n } else {\n const schema = generateZodSchema(form.fields);\n const validationResult = schema.safeParse(transformedData);\n\n if (!validationResult.success) {\n const validationErrors = getValidationErrors(validationResult);\n logger.debug?.(\"Form validation failed\", {\n formSlug,\n errors: validationErrors,\n });\n return {\n success: false,\n error: \"Validation failed\",\n validationErrors,\n };\n }\n\n // Sanitize validated submission data (strip HTML from free-text fields)\n sanitizeSubmissionData(validationResult.data, form.fields);\n storedData = validationResult.data;\n }\n\n // 5. Create submission record. Content spam (honeypot/recaptcha) is\n // stored FLAGGED, never silently dropped: a false positive stays\n // reviewable in the Spam view and recoverable via \"Not spam\".\n const submissionData = {\n form: form.id,\n data: storedData,\n status: isContentSpam ? (\"spam\" as const) : (\"new\" as const),\n spamReason: isContentSpam ? (spamResult.reason ?? null) : null,\n ipAddress: metadata?.ipAddress || null,\n userAgent: metadata?.userAgent || null,\n submittedAt: new Date(),\n };\n\n const submission = await collections.createEntry(\n pluginConfig.formSubmissionOverrides.slug,\n submissionData,\n // Public form submission — create as system. No ambient user; an\n // empty context already resolves to system, but be explicit.\n { as: \"system\" }\n );\n\n logger.info?.(\"Form submission created successfully\", {\n formSlug,\n submissionId: submission.id,\n flaggedAsSpam: isContentSpam,\n });\n\n // Email notifications are sent via the afterCreate hook registered in\n // plugin.ts init(), so they fire for all submission paths (HTTP + direct).\n // The hook itself skips spam-flagged rows.\n\n // 6. Determine redirect URL. Spam gets the same success shape as a real\n // submission (minus the stored row reference) so bots can't diff the two.\n const redirect = determineRedirectUrl(form);\n\n if (isContentSpam) {\n return { success: true, redirect };\n }\n\n return {\n success: true,\n submission: submission as unknown as SubmissionDocument,\n redirect,\n };\n } catch (error) {\n logger.error?.(\"Form submission error\", {\n formSlug,\n error: error instanceof Error ? error.message : String(error),\n });\n\n return {\n success: false,\n error: \"An error occurred processing your submission. Please try again.\",\n };\n }\n}\n\n// ============================================================\n// Helper Functions\n// ============================================================\n\n/**\n * Fetch a form by its slug via a service-level `where` query.\n *\n * @param slug - Form slug\n * @param pluginConfig - Plugin configuration\n * @param pluginContext - Plugin context\n * @returns Form document or null if not found\n */\nexport async function fetchFormBySlug(\n slug: string,\n pluginConfig: ResolvedFormBuilderConfig,\n pluginContext: PluginContext\n): Promise<FormDocument | null> {\n try {\n const { collections } = pluginContext.services;\n\n // D56: resolve the form by slug via a service-level `where` query\n // instead of fetching every form and filtering client-side. Forms are\n // public config the plugin owns, so the read runs as system.\n const result = await collections.listEntries(\n pluginConfig.formOverrides.slug,\n { where: { slug: { equals: slug } }, pagination: { limit: 1 } },\n { as: \"system\" }\n );\n\n const form = result.data?.[0];\n return form ? (form as unknown as FormDocument) : null;\n } catch {\n return null;\n }\n}\n\n/**\n * Determine the redirect URL based on form settings.\n *\n * @param form - Form document\n * @returns Redirect URL or undefined\n */\nfunction determineRedirectUrl(form: FormDocument): string | undefined {\n if (!form.settings) {\n return undefined;\n }\n\n // Check confirmation type\n if (form.settings.confirmationType !== \"redirect\") {\n return undefined;\n }\n\n // Direct URL redirect\n if (form.settings.redirectUrl) {\n return form.settings.redirectUrl;\n }\n\n // Relationship-based redirect (requires additional lookup)\n // Note: For relationship redirects, the admin UI should resolve\n // the URL before saving to redirectUrl, or the frontend should\n // handle the relationship lookup.\n if (form.settings.redirectRelation) {\n // Return a placeholder that the frontend can resolve\n // Format: relationship://{collection}/{id}\n const { relationTo, value } = form.settings.redirectRelation;\n return `relationship://${relationTo}/${value}`;\n }\n\n return undefined;\n}\n\n// ============================================================\n// Validation-Only Function\n// ============================================================\n\n/**\n * Validate form data without creating a submission.\n *\n * Useful for real-time validation in the frontend.\n *\n * @param formSlug - Form slug\n * @param data - Form data to validate\n * @param context - Handler context\n * @returns Validation result\n *\n * @example\n * ```typescript\n * const result = await validateSubmission(\n * 'contact-form',\n * { name: 'John', email: 'invalid-email' },\n * { pluginContext, pluginConfig }\n * );\n *\n * if (!result.valid) {\n * console.log('Validation errors:', result.errors);\n * }\n * ```\n */\nexport async function validateSubmission(\n formSlug: string,\n data: Record<string, unknown>,\n context: SubmitFormContext\n): Promise<{ valid: boolean; errors?: Record<string, string> }> {\n const { pluginContext, pluginConfig } = context;\n\n // Fetch form\n const form = await fetchFormBySlug(formSlug, pluginConfig, pluginContext);\n\n if (!form) {\n return { valid: false, errors: { _form: \"Form not found\" } };\n }\n\n // Transform and validate\n const transformedData = transformFormData(data, form.fields);\n const schema = generateZodSchema(form.fields);\n const result = schema.safeParse(transformedData);\n\n if (result.success) {\n return { valid: true };\n }\n\n return {\n valid: false,\n errors: getValidationErrors(result),\n };\n}\n\n// ============================================================\n// Form Status Check\n// ============================================================\n\n/**\n * Check if a form is accepting submissions.\n *\n * @param formSlug - Form slug\n * @param context - Handler context\n * @returns Whether the form accepts submissions\n */\nexport async function isFormAcceptingSubmissions(\n formSlug: string,\n context: SubmitFormContext\n): Promise<boolean> {\n const { pluginContext, pluginConfig } = context;\n\n const form = await fetchFormBySlug(formSlug, pluginConfig, pluginContext);\n\n if (!form) {\n return false;\n }\n\n return form.status === \"published\";\n}\n\n// ============================================================\n// Submission Data Sanitization\n// ============================================================\n\n/**\n * Form field types that accept free-text string input from end users.\n * These fields can contain HTML injection vectors and must be sanitized.\n *\n * Fields NOT in this set (select, radio, checkbox, number, date, time, file)\n * are constrained by Zod enum/type validation and don't need sanitization.\n */\nconst TEXT_FORM_FIELDS = new Set([\n \"text\",\n \"email\",\n \"textarea\",\n \"phone\",\n \"url\",\n \"hidden\",\n]);\n\n/**\n * Remove all HTML tags from a string, collapse whitespace, and trim.\n *\n * Uses a regex that matches both complete tags (`<b>`) and unclosed tags\n * at end-of-string (`<script`) to prevent browsers from interpreting\n * incomplete markup.\n */\nfunction stripHtmlTags(input: string): string {\n return input\n .replace(/<[^>]*(?:>|$)/g, \"\")\n .replace(/\\s+/g, \" \")\n .trim();\n}\n\n/**\n * Sanitize form submission data by stripping HTML tags from free-text fields.\n *\n * Iterates over the form's field definitions and applies `stripHtmlTags()`\n * to values whose field type is in `TEXT_FORM_FIELDS`. Non-string values\n * and constrained fields (select, radio, checkbox, etc.) are left unchanged.\n *\n * Mutates the data object in place for efficiency.\n *\n * @param data - Validated submission data (mutated in place)\n * @param fields - Form field definitions (used for type-aware dispatch)\n */\nfunction sanitizeSubmissionData(\n data: Record<string, unknown>,\n fields: AnyFormField[]\n): void {\n for (const field of fields) {\n // Plugin field types are not in TEXT_FORM_FIELDS, so they skip naturally.\n if (!TEXT_FORM_FIELDS.has(field.type)) continue;\n\n const value = data[field.name];\n if (typeof value !== \"string\") continue;\n\n data[field.name] = stripHtmlTags(value);\n }\n}\n\n// ============================================================\n// Bulk Operations (Admin)\n// ============================================================\n\n/**\n * Get submission statistics for a form.\n *\n * Note: This function uses client-side filtering which works for\n * small-medium submission counts. For large-scale deployments,\n * consider using direct database queries with COUNT aggregation.\n *\n * @param formSlug - Form slug\n * @param context - Handler context\n * @returns Submission statistics\n */\nexport async function getFormSubmissionStats(\n formSlug: string,\n context: SubmitFormContext\n): Promise<{\n total: number;\n new: number;\n read: number;\n archived: number;\n} | null> {\n const { pluginContext, pluginConfig } = context;\n const { collections } = pluginContext.services;\n\n // Get form ID\n const form = await fetchFormBySlug(formSlug, pluginConfig, pluginContext);\n if (!form) {\n return null;\n }\n\n try {\n const submissionsSlug = pluginConfig.formSubmissionOverrides.slug;\n const sys = { as: \"system\" } as const;\n const base = { form: { equals: form.id } };\n\n // D56: count per status server-side instead of listing every\n // submission and counting client-side. Stats run as system (plugin-owned\n // aggregate over submissions for this form).\n const [total, newCount, read, archived] = await Promise.all([\n collections.count(submissionsSlug, { where: base }, sys),\n collections.count(\n submissionsSlug,\n { where: { ...base, status: { equals: \"new\" } } },\n sys\n ),\n collections.count(\n submissionsSlug,\n { where: { ...base, status: { equals: \"read\" } } },\n sys\n ),\n collections.count(\n submissionsSlug,\n { where: { ...base, status: { equals: \"archived\" } } },\n sys\n ),\n ]);\n\n return { total, new: newCount, read, archived };\n } catch {\n return null;\n }\n}\n","/**\n * Webhook Handler\n *\n * Triggers webhook notifications for form events.\n * Webhooks are fired asynchronously and do not block form submission.\n *\n * @module handlers/webhooks\n * @since 0.1.0\n */\n\nimport { safeFetch, ExternalUrlError } from \"nextly\";\n\nimport type {\n FormDocument,\n SubmissionDocument,\n WebhookConfig,\n WebhookEvent,\n} from \"../types\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\n/**\n * Webhook payload sent to configured endpoints.\n */\nexport interface WebhookPayload {\n /** Event type that triggered the webhook */\n event: WebhookEvent;\n\n /** ISO 8601 timestamp when the event occurred */\n timestamp: string;\n\n /** Form information */\n form: {\n id: string;\n slug: string;\n name: string;\n };\n\n /** Submission information (full data or minimal based on config) */\n submission:\n | SubmissionDocument\n | {\n id: string;\n submittedAt: Date;\n };\n}\n\n/**\n * Options for triggering webhooks.\n */\nexport interface TriggerWebhooksOptions {\n /** Event type */\n event: WebhookEvent;\n\n /** Form document */\n form: FormDocument;\n\n /** Submission document */\n submission: SubmissionDocument;\n}\n\n/**\n * Result of a single webhook delivery attempt.\n */\nexport interface WebhookDeliveryResult {\n /** Webhook URL */\n url: string;\n\n /** Whether the delivery was successful */\n success: boolean;\n\n /** HTTP status code (if request completed) */\n statusCode?: number;\n\n /** Error message (if delivery failed) */\n error?: string;\n\n /** Time taken in milliseconds */\n durationMs: number;\n}\n\n/**\n * Result of triggering webhooks for an event.\n */\nexport interface TriggerWebhooksResult {\n /** Number of webhooks triggered */\n triggered: number;\n\n /** Number of successful deliveries */\n successful: number;\n\n /** Number of failed deliveries */\n failed: number;\n\n /** Individual delivery results */\n results: WebhookDeliveryResult[];\n}\n\n// ============================================================================\n// Constants\n// ============================================================================\n\n/** Default timeout for webhook requests (10 seconds) */\nconst WEBHOOK_TIMEOUT_MS = 10_000;\n\n/** Header name for webhook signature */\nconst SIGNATURE_HEADER = \"X-Webhook-Signature\";\n\n/** Header name for event type */\nconst EVENT_HEADER = \"X-Webhook-Event\";\n\n/** Header name for delivery ID (submission ID) */\nconst DELIVERY_ID_HEADER = \"X-Webhook-Delivery-ID\";\n\n// ============================================================================\n// Signature Generation\n// ============================================================================\n\n/**\n * Generate HMAC-SHA256 signature for webhook payload.\n *\n * @param payload - The webhook payload as a string\n * @param secret - The secret key for signing\n * @returns Signature in format \"sha256=<hex>\"\n */\nasync function generateSignature(\n payload: string,\n secret: string\n): Promise<string> {\n // Use Web Crypto API (works in both Node.js 18+ and browsers)\n const encoder = new TextEncoder();\n const keyData = encoder.encode(secret);\n const messageData = encoder.encode(payload);\n\n // Import the key\n const cryptoKey = await crypto.subtle.importKey(\n \"raw\",\n keyData,\n { name: \"HMAC\", hash: \"SHA-256\" },\n false,\n [\"sign\"]\n );\n\n // Sign the message\n const signature = await crypto.subtle.sign(\"HMAC\", cryptoKey, messageData);\n\n // Convert to hex string\n const hashArray = Array.from(new Uint8Array(signature));\n const hashHex = hashArray.map(b => b.toString(16).padStart(2, \"0\")).join(\"\");\n\n return `sha256=${hashHex}`;\n}\n\n// ============================================================================\n// Webhook Delivery\n// ============================================================================\n\n/**\n * Send a single webhook request.\n *\n * @param webhook - Webhook configuration\n * @param payload - Webhook payload\n * @returns Delivery result\n */\nasync function sendWebhook(\n webhook: WebhookConfig,\n payload: WebhookPayload\n): Promise<WebhookDeliveryResult> {\n const startTime = Date.now();\n const payloadString = JSON.stringify(payload);\n\n try {\n // Build headers\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n [EVENT_HEADER]: payload.event,\n [DELIVERY_ID_HEADER]: payload.submission.id,\n ...webhook.headers,\n };\n\n // Add signature if secret is configured\n if (webhook.secret) {\n headers[SIGNATURE_HEADER] = await generateSignature(\n payloadString,\n webhook.secret\n );\n }\n\n // Create abort controller for timeout\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), WEBHOOK_TIMEOUT_MS);\n\n try {\n const response = await safeFetch(webhook.url, {\n method: webhook.method || \"POST\",\n headers,\n body: payloadString,\n signal: controller.signal,\n allowLocalhost: process.env.NODE_ENV !== \"production\",\n });\n\n clearTimeout(timeoutId);\n\n const durationMs = Date.now() - startTime;\n\n // Consider 2xx status codes as success\n if (response.ok) {\n return {\n url: webhook.url,\n success: true,\n statusCode: response.status,\n durationMs,\n };\n }\n\n return {\n url: webhook.url,\n success: false,\n statusCode: response.status,\n error: `HTTP ${response.status}: ${response.statusText}`,\n durationMs,\n };\n } finally {\n clearTimeout(timeoutId);\n }\n } catch (error) {\n const durationMs = Date.now() - startTime;\n\n // Handle abort (timeout)\n if (error instanceof Error && error.name === \"AbortError\") {\n return {\n url: webhook.url,\n success: false,\n error: `Timeout after ${WEBHOOK_TIMEOUT_MS}ms`,\n durationMs,\n };\n }\n\n // The webhook URL pointed at an internal / non-public address\n // and was refused before the request left the host. Surface as a\n // distinct delivery failure.\n if (error instanceof ExternalUrlError) {\n return {\n url: webhook.url,\n success: false,\n error: `Webhook URL rejected for SSRF safety: ${error.message}`,\n durationMs,\n };\n }\n\n // Handle other errors\n return {\n url: webhook.url,\n success: false,\n error: error instanceof Error ? error.message : String(error),\n durationMs,\n };\n }\n}\n\n// ============================================================================\n// Main Webhook Trigger Function\n// ============================================================================\n\n/**\n * Trigger webhooks for a form event.\n *\n * This function fires all matching webhooks asynchronously and does not\n * block form submission. Failed webhooks are logged but do not cause\n * submission failures.\n *\n * @param options - Trigger options\n * @returns Result of webhook triggering\n *\n * @example\n * ```typescript\n * // After creating a submission\n * const result = await triggerWebhooks({\n * event: 'submission.created',\n * form,\n * submission,\n * });\n *\n * console.log(`Triggered ${result.triggered} webhooks`);\n * console.log(`Successful: ${result.successful}, Failed: ${result.failed}`);\n * ```\n */\nexport async function triggerWebhooks(\n options: TriggerWebhooksOptions\n): Promise<TriggerWebhooksResult> {\n const { event, form, submission } = options;\n\n // Get webhooks configured on the form\n const webhooks = form.webhooks || [];\n\n // Filter webhooks that subscribe to this event\n const relevantWebhooks = webhooks.filter(w => w.events.includes(event));\n\n if (relevantWebhooks.length === 0) {\n return {\n triggered: 0,\n successful: 0,\n failed: 0,\n results: [],\n };\n }\n\n // Build payloads and send webhooks in parallel\n const deliveryPromises = relevantWebhooks.map(async webhook => {\n // Build payload based on includeData setting\n const payload: WebhookPayload = {\n event,\n timestamp: new Date().toISOString(),\n form: {\n id: form.id,\n slug: form.slug,\n name: form.name,\n },\n submission:\n webhook.includeData !== false\n ? submission\n : {\n id: submission.id,\n submittedAt: submission.submittedAt,\n },\n };\n\n return sendWebhook(webhook, payload);\n });\n\n // Wait for all webhooks to complete (or fail)\n const results = await Promise.all(deliveryPromises);\n\n // Count successes and failures\n const successful = results.filter(r => r.success).length;\n const failed = results.filter(r => !r.success).length;\n\n return {\n triggered: relevantWebhooks.length,\n successful,\n failed,\n results,\n };\n}\n\n// ============================================================================\n// Fire-and-Forget Helper\n// ============================================================================\n\n/**\n * Trigger webhooks without waiting for completion.\n *\n * This is the recommended way to call webhooks from submission handlers\n * as it doesn't block the response. Errors are logged but not thrown.\n *\n * @param options - Trigger options\n * @param logger - Optional logger for error reporting\n *\n * @example\n * ```typescript\n * // In submission handler - fire and forget\n * fireWebhooks(\n * { event: 'submission.created', form, submission },\n * pluginContext.logger\n * );\n *\n * // Continue with response immediately\n * return { success: true, submission };\n * ```\n */\nexport function fireWebhooks(\n options: TriggerWebhooksOptions,\n logger?: {\n info?: (message: string, data?: Record<string, unknown>) => void;\n warn?: (message: string, data?: Record<string, unknown>) => void;\n error?: (message: string, data?: Record<string, unknown>) => void;\n }\n): void {\n // Fire webhooks in background\n triggerWebhooks(options)\n .then(result => {\n if (result.triggered > 0) {\n if (result.failed > 0) {\n logger?.warn?.(\"Some webhooks failed\", {\n event: options.event,\n formSlug: options.form.slug,\n submissionId: options.submission.id,\n triggered: result.triggered,\n successful: result.successful,\n failed: result.failed,\n errors: result.results\n .filter(r => !r.success)\n .map(r => ({ url: r.url, error: r.error })),\n });\n } else {\n logger?.info?.(\"Webhooks triggered successfully\", {\n event: options.event,\n formSlug: options.form.slug,\n submissionId: options.submission.id,\n count: result.triggered,\n });\n }\n }\n })\n .catch(error => {\n // This shouldn't happen since triggerWebhooks catches all errors,\n // but handle it just in case\n logger?.error?.(\"Unexpected error triggering webhooks\", {\n event: options.event,\n formSlug: options.form.slug,\n submissionId: options.submission.id,\n error: error instanceof Error ? error.message : String(error),\n });\n });\n}\n\n// ============================================================================\n// Utility Functions\n// ============================================================================\n\n/**\n * Validate a webhook URL.\n *\n * @param url - URL to validate\n * @returns Whether the URL is valid for webhooks\n */\nexport function isValidWebhookUrl(url: string): boolean {\n try {\n const parsed = new URL(url);\n // Only allow HTTPS in production (HTTP allowed for localhost)\n if (\n parsed.protocol !== \"https:\" &&\n !parsed.hostname.match(/^(localhost|127\\.0\\.0\\.1)$/)\n ) {\n return false;\n }\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Get the list of supported webhook events.\n *\n * @returns Array of supported event types\n */\nexport function getSupportedWebhookEvents(): WebhookEvent[] {\n return [\"submission.created\", \"submission.updated\", \"submission.deleted\"];\n}\n","/**\n * @nextly/plugin-form-builder\n *\n * Visual form builder plugin for Nextly CMS.\n * Provides drag-and-drop form building, submission management,\n * email notifications, and spam protection.\n *\n * @packageDocumentation\n */\n\n// ============================================================\n// Main Plugin Export\n// ============================================================\n\n// ============================================================\n// Default Plugin Instance\n// ============================================================\n\n/**\n * Pre-configured form builder plugin for simple usage.\n *\n * This is a default instance with all defaults applied.\n * For customization, use `formBuilder()` instead.\n *\n * @example Simple usage - just add the plugin!\n * ```typescript\n * // nextly.config.ts\n * import { defineConfig } from 'nextly';\n * import { formBuilderPlugin } from '@nextly/plugin-form-builder';\n *\n * export default defineConfig({\n * plugins: [formBuilderPlugin],\n * collections: [Posts, Users, Media], // Your collections only\n * });\n * ```\n *\n * @example With customization\n * ```typescript\n * import { formBuilder } from '@nextly/plugin-form-builder';\n *\n * const myFormPlugin = formBuilder({\n * notifications: { defaultFrom: 'noreply@example.com' },\n * });\n *\n * export default defineConfig({\n * plugins: [myFormPlugin.plugin],\n * });\n * ```\n */\nimport { formBuilder } from \"./plugin\";\n\nexport {\n formBuilder,\n getFormBuilderConfig,\n type NextlyPlugin,\n type FormBuilderPluginResult,\n} from \"./plugin\";\nexport const formBuilderPlugin = formBuilder().plugin;\n\n// ============================================================\n// Type Exports\n// ============================================================\n\n// Plugin configuration types\nexport type {\n FormBuilderPluginOptions,\n ResolvedFormBuilderConfig,\n FieldBlockConfig,\n} from \"./types\";\n\n// Form field types (discriminated union)\nexport type {\n FormField,\n FormFieldType,\n FormFieldTypeId,\n AnyFormField,\n CustomFormField,\n BaseFormField,\n TextFormField,\n EmailFormField,\n NumberFormField,\n PhoneFormField,\n UrlFormField,\n TextareaFormField,\n SelectFormField,\n CheckboxFormField,\n RadioFormField,\n FileFormField,\n DateFormField,\n TimeFormField,\n HiddenFormField,\n} from \"./types\";\nexport { isKnownFormField, BUILT_IN_FORM_FIELD_TYPES } from \"./types\";\n\n// Form configuration types\nexport type {\n FormConfig,\n FormSettings,\n FormNotifications,\n FormAccess,\n FormHooks,\n FormHookContext,\n} from \"./types\";\n\n// Document types\nexport type { FormDocument, SubmissionDocument, FormSubmission } from \"./types\";\n\n// Notification rule type (stored on the form; edited by the builder UI)\nexport type { FormNotification, FormNotificationItem } from \"./types\";\n\n// Validation and conditional logic types\nexport type {\n ValidationRules,\n ConditionalLogic,\n ConditionalLogicCondition,\n} from \"./types\";\n\n// Email types\nexport type { EmailConfig } from \"./types\";\n\n// Webhook types\nexport type { WebhookConfig, WebhookEvent } from \"./types\";\n\n// ============================================================\n// Utility Exports\n// ============================================================\n\n// Type guards\nexport {\n isFormField,\n isFormFieldType,\n isTextFormField,\n isEmailFormField,\n isSelectFormField,\n isCheckboxFormField,\n isFileFormField,\n} from \"./types\";\n\n// ============================================================\n// Config Utilities (validation & defaults)\n// ============================================================\n\n// Validation utilities\nexport {\n validateFormConfig,\n assertValidFormConfig,\n RESERVED_FORM_SLUGS,\n type FormValidationError,\n type FormValidationErrorCode,\n type FormValidationResult,\n} from \"./config\";\n\n// Default application utilities\nexport {\n applyFormDefaults,\n applyFieldDefaults,\n createFormConfig,\n toTitleCase,\n pluralize,\n DEFAULT_FORM_SETTINGS,\n DEFAULT_NOTIFICATION_SETTINGS,\n} from \"./config\";\n\n// ============================================================\n// Field Helper Functions\n// ============================================================\n\n// Utility helper\nexport { option, type FormFieldOption } from \"./config\";\n\n// Text input field helpers\nexport { text, email, phone, url, textarea } from \"./config\";\n\n// Numeric field helpers\nexport { number } from \"./config\";\n\n// Selection field helpers\nexport { select, checkbox, radio } from \"./config\";\n\n// File field helpers\nexport { file } from \"./config\";\n\n// Date/time field helpers\nexport { date, time } from \"./config\";\n\n// Special field helpers\nexport { hidden } from \"./config\";\n\n// ============================================================\n// Conditional Logic Utilities\n// ============================================================\n\n// Conditional logic evaluation\nexport {\n evaluateConditions,\n isValidComparisonOperator,\n getSupportedComparisonOperators,\n type ComparisonOperator,\n} from \"./utils\";\n\n// ============================================================\n// Schema Generation & Validation Utilities\n// ============================================================\n\n// Zod schema generation for form validation\nexport {\n generateZodSchema,\n transformFormData,\n validateFormData,\n getValidationErrors,\n} from \"./utils\";\n\n// ============================================================\n// Export Utilities (CSV, JSON)\n// ============================================================\n\n// Export format utilities\nexport {\n exportToCSV,\n exportToJSON,\n formatExportValue,\n downloadFile,\n generateExportFilename,\n exportAndDownload,\n type CSVExportOptions,\n type JSONExportOptions,\n type ExportedJSON,\n} from \"./utils\";\n\n// ============================================================\n// Form Submission Handlers\n// ============================================================\n\n// Main submission handler\nexport {\n submitForm,\n validateSubmission,\n isFormAcceptingSubmissions,\n getFormSubmissionStats,\n type SubmitFormOptions,\n type SubmitFormResult,\n type SubmitFormContext,\n} from \"./handlers\";\n\n// Spam detection utilities\nexport {\n checkSpam,\n cleanupRateLimitStore,\n getRateLimitStoreSize,\n clearRateLimitStore,\n isRateLimited,\n type SpamCheckConfig,\n type SpamCheckOptions,\n type SpamCheckResult,\n} from \"./handlers\";\n\n// Webhook utilities\nexport {\n triggerWebhooks,\n fireWebhooks,\n isValidWebhookUrl,\n getSupportedWebhookEvents,\n type WebhookPayload,\n type TriggerWebhooksOptions,\n type WebhookDeliveryResult,\n type TriggerWebhooksResult,\n} from \"./handlers\";\n\n// ============================================================\n// Collection Exports (for advanced usage)\n// ============================================================\n\nexport { formsCollection, submissionsCollection } from \"./collections\";\n"],"mappings":"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","names":["text","textarea","select","json","relationship","url","email","text","textarea","select","checkbox","date","schema","date","url"]}
|
|
1
|
+
{"version":3,"sources":["../src/plugin.ts","../src/collections/forms.ts","../src/collections/submissions.ts","../src/config/validate-form.ts","../src/config/field-helpers.ts","../src/utils/generate-schema.ts","../src/handlers/spam-detection.ts","../src/handlers/submit-form.ts","../src/handlers/webhooks.ts","../src/index.ts"],"sourcesContent":["/**\n * Form Builder Plugin\n *\n * Visual form builder with drag-and-drop UI, submission management,\n * email notifications, and spam protection.\n *\n * @module plugin\n * @since 0.1.0\n */\n\nimport { createRequire } from \"node:module\";\n\nimport { definePlugin, type PluginDefinition } from \"@nextlyhq/plugin-sdk\";\nimport type { CollectionConfig } from \"nextly\";\n// Author against the SDK — the stable, experimental plugin boundary.\n\nimport { formsCollection } from \"./collections/forms\";\nimport { submissionsCollection } from \"./collections/submissions\";\nimport type {\n BeforeEmailFilterContext,\n FormNotification,\n FormBuilderPluginOptions,\n FormEmailNotification,\n FormDocument,\n SubmissionDocument,\n ResolvedFormBuilderConfig,\n} from \"./types\";\nimport { evaluateSingleCondition } from \"./utils/evaluate-conditions\";\nimport { exportToCSV, generateExportFilename } from \"./utils/export-formats\";\n\nexport type NextlyPlugin = PluginDefinition;\n\n// Read the version from package.json so it can never drift from the published\n// package. Node/config-side only (this module is not part of the admin bundle).\nconst { version: PLUGIN_VERSION } = createRequire(import.meta.url)(\n \"../package.json\"\n) as { version: string };\n\n/** The runtime instance passed to plugin hooks (type extracted from `init`'s parameter). */\ntype NextlyInstance = Parameters<NonNullable<NextlyPlugin[\"init\"]>>[0];\n\n/** Internal augmentation: we stash the resolved config on the nextly instance for later retrieval. */\ntype NextlyWithFormBuilderConfig = NextlyInstance & {\n __formBuilderConfig?: ResolvedFormBuilderConfig;\n};\n\n// ---------------------------------------------------------------------------\n// Configuration resolver\n// ---------------------------------------------------------------------------\n\n/**\n * Merge user options with sensible defaults.\n */\nfunction resolveConfig(\n options: FormBuilderPluginOptions\n): ResolvedFormBuilderConfig {\n const formOverrides = options.formOverrides || {};\n const submissionOverrides = options.formSubmissionOverrides || {};\n\n return {\n formOverrides: {\n ...formOverrides,\n slug: formOverrides.slug || \"forms\",\n labels: {\n singular: formOverrides.labels?.singular || \"Form\",\n plural: formOverrides.labels?.plural || \"Forms\",\n },\n },\n\n formSubmissionOverrides: {\n ...submissionOverrides,\n slug: submissionOverrides.slug || \"form-submissions\",\n labels: {\n singular: submissionOverrides.labels?.singular || \"Submission\",\n plural: submissionOverrides.labels?.plural || \"Submissions\",\n },\n },\n\n fields: {\n text: options.fields?.text ?? true,\n email: options.fields?.email ?? true,\n number: options.fields?.number ?? true,\n phone: options.fields?.phone ?? true,\n url: options.fields?.url ?? true,\n textarea: options.fields?.textarea ?? true,\n select: options.fields?.select ?? true,\n checkbox: options.fields?.checkbox ?? true,\n radio: options.fields?.radio ?? true,\n file: options.fields?.file ?? true,\n date: options.fields?.date ?? true,\n time: options.fields?.time ?? true,\n hidden: options.fields?.hidden ?? true,\n },\n\n redirectRelationships: options.redirectRelationships || [],\n beforeEmail: options.beforeEmail,\n\n notifications: {\n defaultFrom: options.notifications?.defaultFrom,\n defaultToEmail: options.notifications?.defaultToEmail,\n enabled: options.notifications?.enabled ?? true,\n },\n\n spamProtection: {\n honeypot: options.spamProtection?.honeypot ?? true,\n recaptcha: options.spamProtection?.recaptcha ?? { enabled: false },\n rateLimit: {\n maxSubmissions: options.spamProtection?.rateLimit?.maxSubmissions ?? 10,\n windowMs: options.spamProtection?.rateLimit?.windowMs ?? 60_000,\n },\n },\n\n uploads: {\n maxFileSize: options.uploads?.maxFileSize ?? 10_485_760, // 10 MB\n allowedMimeTypes: options.uploads?.allowedMimeTypes ?? [\n \"image/*\",\n \"application/pdf\",\n \"text/*\",\n ],\n uploadCollection: options.uploads?.uploadCollection ?? \"media\",\n },\n\n features: {\n builder: options.features?.builder ?? true,\n conditionalLogic: options.features?.conditionalLogic ?? true,\n fileUploads: options.features?.fileUploads ?? true,\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\nexport interface FormBuilderPluginResult {\n /** Plugin definition — pass to `defineConfig({ plugins: [...] })`. */\n plugin: NextlyPlugin;\n /** Forms & Submissions collections (also auto-added by the plugin). */\n collections: CollectionConfig[];\n /** Resolved configuration with all defaults applied. */\n config: ResolvedFormBuilderConfig;\n}\n\n/**\n * Create a Form Builder plugin instance.\n *\n * @example Basic usage\n * ```ts\n * import { defineConfig } from \"nextly/config\";\n * import { formBuilder } from \"@nextlyhq/plugin-form-builder\";\n *\n * const fb = formBuilder();\n *\n * export default defineConfig({\n * plugins: [fb.plugin],\n * });\n * ```\n *\n * @example With options\n * ```ts\n * const fb = formBuilder({\n * notifications: { defaultFrom: \"noreply@example.com\" },\n * spamProtection: { honeypot: true },\n * formOverrides: {\n * slug: \"contact-forms\",\n * labels: { singular: \"Contact Form\", plural: \"Contact Forms\" },\n * },\n * });\n * ```\n */\nexport function formBuilder(\n options: FormBuilderPluginOptions = {}\n): FormBuilderPluginResult {\n const resolvedConfig = resolveConfig(options);\n\n const formsCol = formsCollection(resolvedConfig);\n const submissionsCol = submissionsCollection(resolvedConfig);\n\n const plugin = definePlugin({\n name: \"@nextlyhq/plugin-form-builder\",\n version: PLUGIN_VERSION,\n nextly: \">=0.0.2-alpha.21\",\n // Identity metadata for the admin plugins page, mirroring package.json.\n author: \"Nextly\",\n homepage: \"https://nextlyhq.com\",\n repository: \"https://github.com/nextlyhq/nextly\",\n license: \"MIT\",\n category: \"forms\",\n tags: [\"forms\", \"submissions\", \"email-notifications\"],\n\n // Declarative schema: the merged pipeline folds these into the\n // app schema — no manual `setup()` append needed. Just register the plugin.\n contributes: {\n collections: [formsCol, submissionsCol],\n // Custom permission — gates submission export beyond CRUD. The\n // canonical example third-party plugin authors copy.\n permissions: [\n {\n action: \"export\",\n resource: \"submissions\",\n label: \"Export Submissions\",\n description: \"Export form submissions as CSV/JSON\",\n // No `group`: the admin already files this under the plugin that\n // declared it, so naming the plugin again would nest it inside\n // itself. `group` is for a plugin with enough permissions to sort\n // its own into headings, which one is not.\n //\n // `danger` because the point of the permission is to take\n // submissions — names, emails, whatever a form asked for — out of\n // the site in a file.\n danger: true,\n },\n ],\n // HTTP route — exported at\n // /api/plugins/@nextlyhq/plugin-form-builder/submissions/export. Secure by\n // default: gated by the custom `export-submissions` permission. Reads\n // via the secure-by-default service path as the authed user and\n // resolves its OWN slug through `ctx.self`. The canonical\n // contributes.routes example for third-party authors.\n routes: [\n {\n method: \"GET\",\n path: \"/submissions/export\",\n requiredPermission: \"export-submissions\",\n handler: async (req, ctx) => {\n const declaredSlug = resolvedConfig.formSubmissionOverrides.slug;\n const slug = ctx.self.collections[declaredSlug] ?? declaredSlug;\n const url = new URL(req.url);\n const format = url.searchParams.get(\"format\") ?? \"json\";\n const formId = url.searchParams.get(\"form\");\n const status = url.searchParams.get(\"status\");\n\n // Spam stays out of exports unless it is explicitly requested —\n // an export is \"what people submitted\", not \"what bots sent\".\n const where: Record<string, unknown> = {};\n if (formId) where.form = { equals: formId };\n if (status) where.status = { equals: status };\n else where.status = { not_equals: \"spam\" };\n\n const opts = { as: \"user\", user: ctx.user ?? undefined } as const;\n\n // CSV needs its form BEFORE any submissions are read: the\n // columns come from one form's fields, and a malformed request\n // must fail fast instead of paginating the whole table first.\n let form: Record<string, unknown> | undefined;\n if (format === \"csv\") {\n if (!formId) {\n return Response.json(\n {\n error: {\n code: \"VALIDATION_ERROR\",\n message: \"CSV export requires a form parameter.\",\n },\n },\n { status: 400 }\n );\n }\n const formsSlugDeclared = resolvedConfig.formOverrides.slug;\n const formsSlug =\n ctx.self.collections[formsSlugDeclared] ?? formsSlugDeclared;\n const formResult = await ctx.services.collections.listEntries(\n formsSlug,\n { where: { id: { equals: formId } } },\n opts\n );\n form = formResult.data[0];\n if (!form) {\n return Response.json(\n { error: { code: \"NOT_FOUND\", message: \"Form not found.\" } },\n { status: 404 }\n );\n }\n }\n\n // Page through the export with a hard ceiling: an unbounded loop\n // over a high-traffic form could hold the whole table in memory.\n // Hitting the ceiling is reported in a header, never silent.\n const MAX_EXPORT_ROWS = 50_000;\n const items: unknown[] = [];\n const pageSize = 200;\n let truncated = false;\n for (let page = 1; ; page += 1) {\n const result = await ctx.services.collections.listEntries(\n slug,\n { where, pagination: { limit: pageSize, page } },\n opts\n );\n items.push(...result.data);\n const pageWasFull = result.data.length === pageSize;\n if (items.length >= MAX_EXPORT_ROWS) {\n truncated = pageWasFull || items.length > MAX_EXPORT_ROWS;\n items.length = MAX_EXPORT_ROWS;\n break;\n }\n if (!pageWasFull) break;\n }\n const truncationHeaders: Record<string, string> = truncated\n ? { \"X-Export-Truncated\": \"true\" }\n : {};\n\n if (format !== \"csv\" || !form) {\n return Response.json({ items }, { headers: truncationHeaders });\n }\n\n // The service returns parsed entries; the export helpers declare\n // the document shapes — the boundary cast is through unknown.\n const csv = exportToCSV(\n items as unknown as SubmissionDocument[],\n form as unknown as FormDocument\n );\n const filename = generateExportFilename(\n typeof form.slug === \"string\" ? form.slug : \"form\",\n \"csv\"\n );\n return new Response(csv, {\n headers: {\n \"Content-Type\": \"text/csv; charset=utf-8\",\n \"Content-Disposition\": `attachment; filename=\"${filename}\"`,\n ...truncationHeaders,\n },\n });\n },\n },\n {\n // The builder UI reads the host's resolved field enable/disable map\n // from here, so the plugin option actually gates the type picker.\n // Options resolve server-side only; this is the one channel the\n // admin client has to them.\n method: \"GET\",\n path: \"/builder-config\",\n // Derived from the forms slug like the menu entry: with an\n // overridden slug, users hold read-<slug>, not read-forms.\n requiredPermission: `read-${resolvedConfig.formOverrides.slug}`,\n handler: (_req, ctx) =>\n Promise.resolve(\n Response.json({\n fields: resolvedConfig.fields,\n // Notification defaults the builder surfaces: defaultToEmail\n // seeds a new form's first rule, defaultFrom renders as the\n // inherited sender placeholder.\n notifications: {\n defaultFrom: resolvedConfig.notifications.defaultFrom,\n defaultToEmail: resolvedConfig.notifications.defaultToEmail,\n },\n // Spam defaults the Settings tab surfaces, so per-form\n // override selects can show what \"inherit\" resolves to.\n spamProtection: {\n honeypot: resolvedConfig.spamProtection.honeypot,\n recaptchaEnabled:\n resolvedConfig.spamProtection.recaptcha?.enabled ?? false,\n },\n // Runtime-resolved collection slugs (through ctx.self, so a\n // framework .rename() is honored too) — admin components\n // never hardcode \"forms\"/\"form-submissions\".\n slugs: {\n forms:\n ctx.self.collections[resolvedConfig.formOverrides.slug] ??\n resolvedConfig.formOverrides.slug,\n submissions:\n ctx.self.collections[\n resolvedConfig.formSubmissionOverrides.slug\n ] ?? resolvedConfig.formSubmissionOverrides.slug,\n },\n })\n ),\n },\n ],\n },\n\n // Forms is a first-class destination, not a plugin detail: standalone\n // placement gives it its own main-rail icon after Media, and its\n // sub-sidebar lists the plugin's collections (Forms, Submissions) —\n // no separate menu contribution, so \"Forms\" exists exactly once.\n // Hosts preferring the Plugins section override placement in config.\n // (The old admin.settings.component that rendered a second full builder\n // at /admin/plugins/<slug> is gone; the collection Edit-view override\n // is the single FormBuilderView mount.)\n admin: {\n placement: \"standalone\",\n after: \"media\",\n order: 50,\n appearance: { icon: \"FileText\", label: \"Forms\" },\n description: \"Create and manage forms with submission tracking\",\n },\n\n // -- Init ----------------------------------------------------------------\n // Registers an afterCreate hook on submissions to send email notifications.\n init(nextly: NextlyInstance) {\n // Resolve our OWN submissions slug through ctx.self, so the hook\n // follows a framework `.rename()` as well as our formSubmissionOverrides\n // option. The declared slug is the key; ctx.self maps it to the resolved\n // (possibly renamed) slug. Identity when not renamed.\n const declaredSubmissionsSlug =\n resolvedConfig.formSubmissionOverrides.slug;\n const submissionSlug =\n nextly.self.collections[declaredSubmissionsSlug] ??\n declaredSubmissionsSlug;\n\n // Hook/event subscriptions are idempotent across HMR — the platform\n // clears a plugin's prior subscriptions before re-init (B2), so the old\n // globalThis dedup guard is no longer needed.\n nextly.logger.info(\"Form Builder plugin initialized\", {\n formsCollection: resolvedConfig.formOverrides.slug,\n submissionsCollection: submissionSlug,\n enabledFields: Object.entries(resolvedConfig.fields)\n .filter(([, on]) => on)\n .map(([name]) => name),\n });\n\n (nextly as NextlyWithFormBuilderConfig).__formBuilderConfig =\n resolvedConfig;\n\n // D63: run the user's beforeEmail config as a filter on the form-builder seam.\n if (resolvedConfig.beforeEmail) {\n nextly.filters.add(\n \"form-builder.beforeEmail\",\n (emails: FormEmailNotification[], ctx: BeforeEmailFilterContext) =>\n resolvedConfig.beforeEmail!({\n emails,\n // Boundary: loose runtime documents → the user's typed contract.\n form: ctx.form as unknown as FormDocument,\n submission: ctx.submission as unknown as SubmissionDocument,\n })\n );\n }\n\n // Register afterCreate hook for email notifications\n nextly.hooks.on(\n \"afterCreate\",\n submissionSlug,\n async (context: unknown) => {\n await handleSubmissionCreated(context, resolvedConfig, nextly);\n }\n );\n\n // Stamp admin edits of submitted data: changing what the visitor\n // submitted must leave a visible trace. Registered directly on the\n // registry (like the notification hook) so it runs for every API\n // surface that updates a submission.\n nextly.hooks.on(\"beforeUpdate\", submissionSlug, (context: unknown) => {\n const ctx = context as {\n data?: Record<string, unknown>;\n user?: { id?: string };\n };\n if (ctx.data && ctx.data.data !== undefined) {\n ctx.data.editedAt = new Date();\n ctx.data.editedBy = ctx.user?.id ?? null;\n }\n return ctx.data;\n });\n\n // Inject a real submissionCount into form reads (spam excluded — the\n // number answers \"how many people submitted\", not \"how many bots\").\n const declaredFormsSlug = resolvedConfig.formOverrides.slug;\n const formsSlug =\n nextly.self.collections[declaredFormsSlug] ?? declaredFormsSlug;\n nextly.hooks.on(\"afterRead\", formsSlug, async (context: unknown) => {\n const data = (context as { data?: unknown }).data;\n // afterRead fires for single reads (one record) and list reads\n // (array); count each form either way. Counts run concurrently —\n // form lists are paginated, so this is a bounded fan-out of small\n // indexed queries, not a serial N+1 walk.\n const records = Array.isArray(data) ? data : data ? [data] : [];\n await Promise.all(\n records.map(async record => {\n const form = record as Record<string, unknown>;\n if (typeof form.id !== \"string\") return;\n try {\n // Count as system: whoever may read the form may see its\n // submission volume without holding submission read rights.\n form.submissionCount = await nextly.services.collections.count(\n submissionSlug,\n {\n where: {\n form: { equals: form.id },\n status: { not_equals: \"spam\" },\n },\n },\n { as: \"system\" }\n );\n } catch {\n // A failed count must never break reading the form itself.\n form.submissionCount = 0;\n }\n })\n );\n });\n },\n });\n\n return {\n plugin,\n collections: [formsCol, submissionsCol],\n config: resolvedConfig,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Utilities\n// ---------------------------------------------------------------------------\n\n/**\n * Retrieve the resolved Form Builder config from a Nextly plugin context.\n */\nexport function getFormBuilderConfig(\n nextly: unknown\n): ResolvedFormBuilderConfig | undefined {\n if (!nextly || typeof nextly !== \"object\") return undefined;\n return (nextly as Partial<NextlyWithFormBuilderConfig>).__formBuilderConfig;\n}\n\n/**\n * Resolve a `{{fieldName}}` template reference against submitted values.\n * Plain strings (e.g. email addresses) are returned as-is.\n */\nfunction resolveFieldRef(ref: string, data: Record<string, unknown>): string {\n const match = ref.match(/^\\{\\{(\\w+)\\}\\}$/);\n if (!match) return ref;\n const value = data[match[1]];\n return typeof value === \"string\" ? value : \"\";\n}\n\n// ---------------------------------------------------------------------------\n// Attachment collection for file fields\n// ---------------------------------------------------------------------------\n\n/**\n * Scan form fields for file-upload fields marked `attachToEmail` and\n * collect the corresponding mediaIds from the submitted data. Returns\n * an array of `{ mediaId }` objects ready to pass as `attachments` to\n * `emailService.sendWithTemplate()`.\n *\n * Skips empty/missing values silently (optional file fields). Handles\n * both single-value and `multiple: true` file fields.\n *\n * @internal Exported for testing — not part of the public plugin API.\n */\nexport function collectAttachmentInputs(\n fields: Array<Record<string, unknown>>,\n submittedData: Record<string, unknown>\n): Array<{ mediaId: string }> {\n const attachments: Array<{ mediaId: string }> = [];\n\n for (const field of fields) {\n if (field.type !== \"file\" || !field.attachToEmail) continue;\n\n const name = field.name as string;\n const value = submittedData[name];\n if (value == null) continue;\n\n if (Array.isArray(value)) {\n for (const item of value) {\n if (typeof item === \"string\" && item) {\n attachments.push({ mediaId: item });\n }\n }\n } else if (typeof value === \"string\" && value) {\n attachments.push({ mediaId: value });\n }\n }\n\n return attachments;\n}\n\n// ---------------------------------------------------------------------------\n// Submission notification handler\n// ---------------------------------------------------------------------------\n\n/**\n * Normalize a JSON column value from a raw DB row into an object. Hook\n * contexts carry the row as stored, so the value may be an already-parsed\n * object (jsonb dialects) or a serialized string (text-storage dialects).\n *\n * @internal Exported for testing — not part of the public plugin API.\n */\nexport function parseJsonColumn(value: unknown): Record<string, unknown> {\n if (typeof value === \"string\") {\n try {\n const parsed: unknown = JSON.parse(value);\n return parsed && typeof parsed === \"object\" && !Array.isArray(parsed)\n ? (parsed as Record<string, unknown>)\n : {};\n } catch {\n return {};\n }\n }\n return value && typeof value === \"object\" && !Array.isArray(value)\n ? (value as Record<string, unknown>)\n : {};\n}\n\n/** Trim an address list and drop blanks; undefined when nothing remains. */\nfunction normalizeAddressList(\n addresses: readonly string[] | undefined\n): string[] | undefined {\n if (!Array.isArray(addresses)) return undefined;\n const cleaned = addresses\n .map(address => (typeof address === \"string\" ? address.trim() : \"\"))\n .filter(Boolean);\n return cleaned.length > 0 ? cleaned : undefined;\n}\n\n/** Why a notification rule produced no email for a given submission. */\nexport interface SkippedNotification {\n notificationId: string;\n reason: \"empty-recipient\" | \"condition-unmet\" | \"no-template\";\n}\n\n/**\n * Resolve a form's notification rules against one submission into outgoing\n * email descriptors, honoring everything a rule can configure: the send\n * condition gates the rule, the recipient/reply-to resolve `{{fieldName}}`\n * references, and the sender falls back from the rule's own address to the\n * plugin's `notifications.defaultFrom` (undefined lets the template/provider\n * default apply downstream).\n *\n * @internal Exported for testing — not part of the public plugin API.\n */\nexport function buildNotificationEmails(input: {\n notifications: readonly FormNotification[];\n submittedData: Record<string, unknown>;\n formName: unknown;\n submissionId: unknown;\n defaultFrom?: string;\n}): { emails: FormEmailNotification[]; skipped: SkippedNotification[] } {\n const { notifications, submittedData, formName, submissionId, defaultFrom } =\n input;\n\n const seen = new Set<string>();\n const emails: FormEmailNotification[] = [];\n const skipped: SkippedNotification[] = [];\n\n for (const notification of notifications) {\n if (!notification.enabled) continue;\n\n if (!notification.templateSlug) {\n skipped.push({ notificationId: notification.id, reason: \"no-template\" });\n continue;\n }\n\n // Deduplicate (UI can append duplicates on repeated saves). For id-less\n // rules (API-authored), only an exact structural duplicate counts — a\n // to+template key would silently drop distinct rules that share a\n // recipient and template but differ elsewhere (e.g. their condition).\n const key = notification.id || JSON.stringify(notification);\n if (seen.has(key)) continue;\n seen.add(key);\n\n // An unmet send condition skips the rule for this submission — an\n // expected state, never an error.\n if (\n notification.condition &&\n !evaluateSingleCondition(notification.condition, submittedData)\n ) {\n skipped.push({\n notificationId: notification.id,\n reason: \"condition-unmet\",\n });\n continue;\n }\n\n // Trim every address on the way out: whitespace-only values must count\n // as empty, and stray spaces must not reach the provider as headers.\n const to = (\n notification.recipientType === \"field\"\n ? resolveFieldRef(notification.to, submittedData)\n : notification.to\n ).trim();\n\n if (!to) {\n skipped.push({\n notificationId: notification.id,\n reason: \"empty-recipient\",\n });\n continue;\n }\n\n const cc = normalizeAddressList(notification.cc);\n const bcc = normalizeAddressList(notification.bcc);\n\n // Sender resolution: the rule's own address wins, then the plugin's\n // configured default; undefined defers to the template/provider chain.\n const from =\n notification.senderEmail?.trim() || defaultFrom?.trim() || undefined;\n\n // Reply-To resolves {{fieldName}} like recipients do; a reference to a\n // field the visitor left empty degrades to \"no Reply-To header\" rather\n // than an invalid address.\n const replyTo = notification.replyTo\n ? resolveFieldRef(notification.replyTo, submittedData).trim() || undefined\n : undefined;\n\n emails.push({\n to,\n templateSlug: notification.templateSlug,\n variables: {\n ...submittedData,\n formName,\n submissionId,\n },\n providerId: notification.providerId,\n from,\n replyTo,\n cc,\n bcc,\n notificationId: notification.id,\n });\n }\n\n return { emails, skipped };\n}\n\n/**\n * Send email notifications after a form submission is created.\n */\nasync function handleSubmissionCreated(\n context: unknown,\n config: ResolvedFormBuilderConfig,\n nextly: NextlyInstance\n): Promise<void> {\n // The plugin-level kill switch: `notifications.enabled: false` turns off\n // all form emails regardless of per-rule state.\n if (!config.notifications.enabled) return;\n\n const submission = (context as { data?: Record<string, unknown> }).data;\n if (!submission) return;\n\n // Spam is stored for review, but nobody gets emailed about it — otherwise\n // every bot hit would trigger the form's notification rules.\n if (submission.status === \"spam\") return;\n\n const rawFormId = submission.form;\n let formId: string | null = null;\n if (typeof rawFormId === \"string\") {\n formId = rawFormId;\n } else if (rawFormId && typeof rawFormId === \"object\") {\n const maybeId = (rawFormId as { id?: unknown }).id;\n if (typeof maybeId === \"string\") formId = maybeId;\n }\n if (!formId) return;\n\n // Fetch the parent form\n const form = await fetchParentForm(config, formId, nextly);\n if (!form) return;\n\n const notifications = Array.isArray(form.notifications)\n ? (form.notifications as FormNotification[])\n : [];\n if (notifications.length === 0) return;\n\n const emailService = nextly.services.email;\n if (!emailService) return;\n\n // The afterCreate hook receives the raw DB row, so on dialects that store\n // JSON columns as text (e.g. SQLite) `data` arrives serialized. Without\n // parsing it, {{field}} recipients, reply-to references, and send\n // conditions all silently see an empty submission.\n const submittedData = parseJsonColumn(submission.data);\n\n // Collect mediaIds from file fields marked for email attachment\n const formFields = Array.isArray(form.fields)\n ? (form.fields as Array<Record<string, unknown>>)\n : [];\n const fileAttachments = collectAttachmentInputs(formFields, submittedData);\n\n // -- Build phase: resolve each enabled notification into an outgoing\n // descriptor (the value the D63 seam transforms).\n const { emails, skipped } = buildNotificationEmails({\n notifications,\n submittedData,\n formName: form.name,\n submissionId: submission.id,\n defaultFrom: config.notifications.defaultFrom,\n });\n\n for (const skip of skipped) {\n if (skip.reason === \"empty-recipient\") {\n nextly.logger.warn?.(\n \"Form Builder: empty recipient, skipping notification\",\n { notificationId: skip.notificationId, formSlug: form.slug }\n );\n } else {\n // Unmet conditions and missing templates are expected states, not\n // faults — surface them only at debug level.\n nextly.logger.debug?.(\"Form Builder: notification skipped\", {\n notificationId: skip.notificationId,\n formSlug: form.slug,\n reason: skip.reason,\n });\n }\n }\n\n if (emails.length === 0) return;\n\n // -- Seam: thread the outgoing notifications through the D63 filter so user\n // config (and any other registered handler) can modify/filter them.\n const finalEmails = await nextly.filters.apply<\n FormEmailNotification[],\n BeforeEmailFilterContext\n >(\"form-builder.beforeEmail\", emails, { form, submission });\n\n // -- Send phase: send the (possibly transformed) outgoing notifications.\n for (const email of finalEmails) {\n try {\n await emailService.sendWithTemplate(\n email.templateSlug,\n email.to,\n email.variables,\n {\n providerId: email.providerId,\n from: email.from,\n replyTo: email.replyTo,\n cc: email.cc,\n bcc: email.bcc,\n attachments: fileAttachments.length > 0 ? fileAttachments : undefined,\n }\n );\n\n nextly.logger.info?.(\"Form Builder: notification sent\", {\n formSlug: form.slug,\n to: email.to,\n templateSlug: email.templateSlug,\n notificationId: email.notificationId,\n attachmentCount: fileAttachments.length,\n });\n } catch (err) {\n nextly.logger.error?.(\"Form Builder: notification failed\", {\n to: email.to,\n templateSlug: email.templateSlug,\n notificationId: email.notificationId,\n error: err instanceof Error ? err.message : String(err),\n });\n }\n }\n}\n\n/**\n * Fetch the parent form document for a submission.\n */\nexport async function fetchParentForm(\n config: ResolvedFormBuilderConfig,\n formId: string,\n nextly: NextlyInstance\n): Promise<Record<string, unknown> | null> {\n try {\n // D35/D56: read the parent form through the secure managed service as\n // system — the afterCreate hook runs without an ambient user. Replaces the\n // legacy `getCollectionsHandler()` + `overrideAccess` runtime path.\n const form = await nextly.services.collections.findEntryById(\n config.formOverrides.slug,\n formId,\n { as: \"system\" }\n );\n return form;\n } catch (err) {\n nextly.logger.error?.(\"Form Builder: failed to fetch form\", {\n formId,\n error: err instanceof Error ? err.message : String(err),\n });\n return null;\n }\n}\n","/**\n * Forms Collection\n *\n * Stores form definitions created via Code-First or Schema Builder.\n * Each form contains field definitions, settings, notifications, and access control.\n *\n * @module collections/forms\n * @since 0.1.0\n */\n\nimport type { CollectionConfig, FieldConfig, HookContext } from \"nextly\";\nimport {\n text,\n textarea,\n select,\n checkbox,\n group,\n json,\n relationship,\n} from \"nextly\";\n\nimport type { ResolvedFormBuilderConfig } from \"../types\";\n\n// ============================================================\n// Type Augmentation for Custom Admin Components\n// ============================================================\n// These types extend CollectionAdminOptions to support custom admin views.\n// This augmentation will be removed once nextly exports these types.\n\n/**\n * Component path string format.\n * Format: `\"package-name/path#ExportName\"`\n */\ntype ComponentPath = string;\n\n/**\n * Custom view configuration for replacing default admin views.\n */\ninterface CollectionAdminViewConfig {\n /** Component path to the custom view component */\n Component: ComponentPath;\n}\n\n/**\n * Custom components configuration for collection admin UI.\n */\ninterface CollectionAdminComponents {\n views?: {\n Edit?: CollectionAdminViewConfig;\n List?: CollectionAdminViewConfig;\n };\n BeforeListTable?: ComponentPath;\n AfterListTable?: ComponentPath;\n BeforeEdit?: ComponentPath;\n AfterEdit?: ComponentPath;\n}\n\n/**\n * Extended CollectionConfig with custom admin components support.\n * This extends the base CollectionConfig to include the components property.\n */\ninterface ExtendedCollectionConfig extends Omit<CollectionConfig, \"admin\"> {\n admin?: CollectionConfig[\"admin\"] & {\n /** Whether this collection is provided by a plugin */\n isPlugin?: boolean;\n components?: CollectionAdminComponents;\n };\n}\n\n/**\n * Generate the Forms collection configuration.\n *\n * Creates a collection for storing form definitions with fields,\n * settings, notifications, and access control.\n *\n * @param pluginConfig - Resolved plugin configuration\n * @returns ExtendedCollectionConfig for the Forms collection (with custom admin components)\n *\n * @example\n * ```typescript\n * const formsCol = formsCollection(resolvedConfig);\n * // Returns a collection with slug \"forms\" (or custom slug from config)\n * ```\n */\nexport function formsCollection(\n pluginConfig: ResolvedFormBuilderConfig\n): ExtendedCollectionConfig {\n const {\n slug,\n labels,\n fields: additionalFields,\n access: accessOverrides,\n ...overrides\n } = pluginConfig.formOverrides;\n\n // Build settings group fields based on plugin configuration\n const settingsFields: FieldConfig[] = [\n text({\n name: \"submitButtonText\",\n label: \"Submit Button Text\",\n defaultValue: \"Submit\",\n admin: {\n description: \"Text displayed on the form submit button\",\n },\n }),\n\n select({\n name: \"confirmationType\",\n label: \"After Submission\",\n defaultValue: \"message\",\n options: [\n { label: \"Show Message\", value: \"message\" },\n { label: \"Redirect to URL\", value: \"redirect\" },\n ...(pluginConfig.redirectRelationships.length > 0\n ? [{ label: \"Redirect to Page\", value: \"relationship\" }]\n : []),\n ],\n admin: {\n description: \"What happens after a successful form submission\",\n },\n }),\n\n textarea({\n name: \"successMessage\",\n label: \"Success Message\",\n defaultValue: \"Thank you for your submission!\",\n admin: {\n description: \"Message shown after successful submission\",\n condition: {\n field: \"settings.confirmationType\",\n equals: \"message\",\n },\n },\n }),\n\n text({\n name: \"redirectUrl\",\n label: \"Redirect URL\",\n admin: {\n description: \"URL to redirect to after submission\",\n condition: {\n field: \"settings.confirmationType\",\n equals: \"redirect\",\n },\n },\n }),\n ];\n\n // Add redirect relationship field if configured\n if (pluginConfig.redirectRelationships.length > 0) {\n settingsFields.push(\n relationship({\n name: \"redirectPage\",\n label: \"Redirect Page\",\n relationTo: pluginConfig.redirectRelationships,\n admin: {\n description: \"Select a page to redirect to after submission\",\n condition: {\n field: \"settings.confirmationType\",\n equals: \"relationship\",\n },\n },\n })\n );\n }\n\n // Add remaining settings fields\n settingsFields.push(\n checkbox({\n name: \"allowMultipleSubmissions\",\n label: \"Allow Multiple Submissions\",\n defaultValue: true,\n admin: {\n description:\n \"Allow the same user/IP to submit this form multiple times\",\n },\n }),\n\n checkbox({\n name: \"honeypotEnabled\",\n label: \"Honeypot\",\n admin: {\n description:\n \"Per-form honeypot override; unset inherits the plugin default\",\n },\n }),\n\n checkbox({\n name: \"captchaEnabled\",\n label: \"Enable reCAPTCHA\",\n admin: {\n description:\n \"Per-form reCAPTCHA override; unset inherits the plugin default\",\n },\n }),\n\n text({\n name: \"captchaSiteKey\",\n label: \"reCAPTCHA Site Key\",\n admin: {\n description: \"Override the default reCAPTCHA site key for this form\",\n condition: {\n field: \"settings.captchaEnabled\",\n equals: true,\n },\n },\n })\n );\n\n // Build the default fields array\n const defaultFields: FieldConfig[] = [\n // ============================================================\n // Basic Information\n // ============================================================\n text({\n name: \"name\",\n label: \"Form Name\",\n required: true,\n admin: {\n description: \"Internal name for this form (not shown to users)\",\n },\n }),\n\n text({\n name: \"slug\",\n label: \"Slug\",\n required: true,\n unique: true,\n admin: {\n description: \"URL-friendly identifier. Used in API: /api/forms/{slug}\",\n },\n }),\n\n textarea({\n name: \"description\",\n label: \"Description\",\n admin: {\n description: \"Internal notes about this form's purpose\",\n },\n }),\n\n // ============================================================\n // Form Fields (JSON schema managed by Form Builder UI)\n // ============================================================\n json({\n name: \"fields\",\n label: \"Form Fields\",\n required: true,\n defaultValue: [],\n admin: {\n description:\n \"Form field configuration. Managed by the Form Builder UI.\",\n },\n }),\n\n // ============================================================\n // Form Settings\n // ============================================================\n group({\n name: \"settings\",\n label: \"Form Settings\",\n fields: settingsFields,\n }),\n\n // ============================================================\n // Email Notifications\n // ============================================================\n json({\n name: \"notifications\",\n label: \"Notifications\",\n defaultValue: [],\n admin: {\n description:\n \"Email notification rules. Managed by the Form Builder UI.\",\n },\n }),\n\n // ============================================================\n // Status\n // ============================================================\n select({\n name: \"status\",\n label: \"Status\",\n required: true,\n defaultValue: \"draft\",\n options: [\n { label: \"Draft\", value: \"draft\" },\n { label: \"Published\", value: \"published\" },\n { label: \"Closed\", value: \"closed\" },\n ],\n admin: {\n description:\n \"Only published forms can receive submissions. Closed forms display a message instead.\",\n },\n }),\n\n textarea({\n name: \"closedMessage\",\n label: \"Closed Form Message\",\n defaultValue: \"This form is no longer accepting submissions.\",\n admin: {\n description: \"Message shown when form status is 'closed'\",\n condition: {\n field: \"status\",\n equals: \"closed\",\n },\n },\n }),\n ];\n\n // Process additional fields from formOverrides\n // Note: Function-style overrides should be resolved in plugin.ts before reaching here\n let finalFields: FieldConfig[];\n\n if (Array.isArray(additionalFields) && additionalFields.length > 0) {\n // Merge: append additional fields to defaults\n finalFields = [...defaultFields, ...additionalFields];\n } else {\n finalFields = defaultFields;\n }\n\n // Convert hyphenated slug to underscored table name for database compatibility\n const dbName = slug.replace(/-/g, \"_\");\n\n return {\n slug,\n dbName,\n labels,\n fields: finalFields,\n timestamps: true,\n\n admin: {\n isPlugin: true,\n group: \"Forms\",\n order: 50,\n useAsTitle: \"name\",\n description:\n \"Create and manage your form templates using the visual form builder. Create and manage forms for collecting user submissions\",\n\n // Custom components for visual form builder\n components: {\n views: {\n // Replace default edit view with visual Form Builder\n Edit: {\n Component: \"@nextlyhq/plugin-form-builder/admin#FormBuilderView\",\n },\n },\n },\n\n ...overrides.admin,\n },\n\n // Access control with sensible defaults\n access: {\n // Anyone can read forms (needed for frontend rendering)\n read: accessOverrides?.read ?? true,\n // Only authenticated users can create/update forms\n create: accessOverrides?.create ?? (({ user }) => !!user),\n update: accessOverrides?.update ?? (({ user }) => !!user),\n // Only admins can delete forms (falls back to DB permissions)\n delete:\n accessOverrides?.delete ??\n (({ roles }) =>\n roles.includes(\"admin\") || roles.includes(\"super-admin\")),\n },\n\n hooks: {\n // Auto-generate slug from name if not provided\n beforeValidate: [\n (context: HookContext) => {\n const { data, operation } = context;\n\n // Auto-generate slug from name if not provided (only on create)\n if (operation === \"create\" && data && !data.slug && data.name) {\n data.slug = String(data.name)\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\");\n }\n\n // Validation: Ensure fields array is not empty\n // We check for length 0 to prevent empty forms from being saved\n if (\n data &&\n (data.fields === undefined ||\n (Array.isArray(data.fields) && data.fields.length === 0))\n ) {\n throw new Error(\"Form must have at least one field.\");\n }\n\n return data;\n },\n ],\n },\n\n // Spread any additional overrides (excluding already used properties)\n ...overrides,\n };\n}\n","/**\n * Form Submissions Collection\n *\n * Stores form submission data from user-submitted forms.\n * Each submission contains the submitted data, metadata, and status.\n *\n * @module collections/submissions\n * @since 0.1.0\n */\n\nimport type { CollectionConfig, FieldConfig, HookContext } from \"nextly\";\nimport { text, textarea, select, json, date, relationship } from \"nextly\";\n\nimport type { ResolvedFormBuilderConfig } from \"../types\";\n\n/**\n * Extended CollectionConfig with isPlugin support.\n * This extends the base CollectionConfig to include the isPlugin property.\n */\ninterface ExtendedCollectionConfig extends Omit<CollectionConfig, \"admin\"> {\n admin?: CollectionConfig[\"admin\"] & {\n /** Whether this collection is provided by a plugin */\n isPlugin?: boolean;\n };\n}\n\n/**\n * Generate the Form Submissions collection configuration.\n *\n * Creates a collection for storing form submissions with:\n * - Reference to the parent form\n * - Submitted data (JSON)\n * - Status tracking (new, read, archived)\n * - Metadata (IP, user agent, timestamp)\n * - Internal notes for admin use\n *\n * @param pluginConfig - Resolved plugin configuration\n * @returns CollectionConfig for the Submissions collection\n *\n * @example\n * ```typescript\n * const submissionsCol = submissionsCollection(resolvedConfig);\n * // Returns a collection with slug \"form-submissions\" (or custom slug from config)\n * ```\n */\nexport function submissionsCollection(\n pluginConfig: ResolvedFormBuilderConfig\n): ExtendedCollectionConfig {\n const {\n slug,\n labels,\n fields: additionalFields,\n access: accessOverrides,\n ...overrides\n } = pluginConfig.formSubmissionOverrides;\n\n // Get the forms collection slug for the relationship\n const formSlug = pluginConfig.formOverrides.slug;\n\n // Build the default fields array\n const defaultFields: FieldConfig[] = [\n // ============================================================\n // Form Relationship\n // ============================================================\n relationship({\n name: \"form\",\n label: \"Form\",\n relationTo: formSlug,\n required: true,\n admin: {\n readOnly: true,\n description: \"The form this submission belongs to\",\n },\n }),\n\n // ============================================================\n // Submission Data (JSON)\n // ============================================================\n json({\n name: \"data\",\n label: \"Submission Data\",\n required: true,\n admin: {\n readOnly: true,\n description: \"The submitted form data as JSON\",\n },\n }),\n\n // ============================================================\n // Status\n // ============================================================\n select({\n name: \"status\",\n label: \"Status\",\n required: true,\n defaultValue: \"new\",\n options: [\n { label: \"New\", value: \"new\" },\n { label: \"Read\", value: \"read\" },\n { label: \"Archived\", value: \"archived\" },\n // Spam is stored and flagged, never silently dropped: a false\n // positive stays reviewable and recoverable (\"Not spam\" = back to new).\n { label: \"Spam\", value: \"spam\" },\n ],\n admin: {\n description: \"Track whether this submission has been reviewed\",\n },\n }),\n\n text({\n name: \"spamReason\",\n label: \"Spam Reason\",\n admin: {\n readOnly: true,\n description:\n \"Which spam check flagged this submission (empty for clean submissions)\",\n },\n }),\n\n // ============================================================\n // Internal Notes\n // ============================================================\n textarea({\n name: \"notes\",\n label: \"Internal Notes\",\n admin: {\n description: \"Notes visible only to admins (not shown to submitter)\",\n },\n }),\n\n // ============================================================\n // Metadata\n // ============================================================\n text({\n name: \"ipAddress\",\n label: \"IP Address\",\n admin: {\n readOnly: true,\n description: \"IP address of the submitter\",\n },\n }),\n\n text({\n name: \"userAgent\",\n label: \"User Agent\",\n admin: {\n readOnly: true,\n description: \"Browser/device information of the submitter\",\n },\n }),\n\n date({\n name: \"submittedAt\",\n label: \"Submitted At\",\n required: true,\n admin: {\n readOnly: true,\n description: \"When the form was submitted\",\n },\n }),\n\n // Honesty stamps for admin edits of submitted data: a corrected\n // submission must never be indistinguishable from what the visitor sent.\n date({\n name: \"editedAt\",\n label: \"Edited At\",\n admin: {\n readOnly: true,\n description: \"When an admin last edited the submitted data\",\n },\n }),\n\n text({\n name: \"editedBy\",\n label: \"Edited By\",\n admin: {\n readOnly: true,\n description: \"ID of the admin who last edited the submitted data\",\n },\n }),\n ];\n\n // Process additional fields from formSubmissionOverrides\n // Note: Function-style overrides should be resolved in plugin.ts before reaching here\n let finalFields: FieldConfig[];\n\n if (Array.isArray(additionalFields) && additionalFields.length > 0) {\n // Merge: append additional fields to defaults\n finalFields = [...defaultFields, ...additionalFields];\n } else {\n finalFields = defaultFields;\n }\n\n // Convert hyphenated slug to underscored table name (e.g., \"form-submissions\" -> \"form_submissions\")\n const dbName = slug.replace(/-/g, \"_\");\n\n return {\n slug,\n dbName,\n labels,\n fields: finalFields,\n timestamps: true,\n\n // Submissions carry visitor-entered content plus ipAddress/userAgent, so\n // they are never recorded to the webhook outbox — otherwise every submission\n // would be delivered to any endpoint subscribed to entry.created or `*`. An\n // operator who wants submission webhooks can override this to `true`.\n webhooks: false,\n\n admin: {\n isPlugin: true,\n group: \"Forms\",\n order: 51,\n useAsTitle: \"id\",\n description: \"View and manage form submissions\",\n // Submissions are created by visitors submitting forms — a hand-typed\n // \"New Submission\" would be fiction, so the admin never offers one.\n disableCreate: true,\n\n components: {\n // The full submissions experience (per-form columns, status tabs,\n // drawer detail, export) replaces the generic list.\n views: {\n List: {\n Component: \"@nextlyhq/plugin-form-builder/admin#SubmissionsView\",\n },\n },\n },\n\n ...overrides.admin,\n },\n\n // Access control with sensible defaults\n // Public can create (submit forms), but only authenticated users can read/update/delete\n access: {\n // Anyone can submit forms (public access)\n create: accessOverrides?.create ?? true,\n // Only authenticated users can view submissions\n read: accessOverrides?.read ?? (({ user }) => !!user),\n // Only authenticated users can update submissions (e.g., change status, add notes)\n update: accessOverrides?.update ?? (({ user }) => !!user),\n // Only admins can delete submissions (falls back to DB permissions)\n delete:\n accessOverrides?.delete ??\n (({ roles }) =>\n roles.includes(\"admin\") || roles.includes(\"super-admin\")),\n },\n\n hooks: {\n // Auto-set submittedAt timestamp on creation. (The edit stamp for\n // updates is registered in plugin.ts init — direct registry hooks are\n // the path that runs for every API surface.)\n beforeValidate: [\n (context: HookContext) => {\n const { data, operation } = context;\n if (operation === \"create\" && data && !data.submittedAt) {\n data.submittedAt = new Date();\n }\n return data;\n },\n ],\n\n afterChange: [\n (context: HookContext) => {\n const { data } = context;\n return data;\n },\n ],\n },\n\n // Spread any additional overrides (excluding already used properties)\n ...overrides,\n };\n}\n","/**\n * Form Configuration Validator\n *\n * Provides comprehensive validation for form configurations including:\n * - Slug validation (format, reserved names)\n * - Field name validation (format, duplicates)\n * - Field-specific validation (select/radio options, min/max constraints)\n * - Conditional logic validation\n * - Nested field validation\n *\n * @module config/validate-form\n * @since 1.0.0\n *\n * @example\n * ```typescript\n * import { validateFormConfig } from '@nextly/plugin-form-builder';\n *\n * const result = validateFormConfig(config);\n * if (!result.valid) {\n * console.error('Validation errors:', result.errors);\n * }\n * ```\n */\n\nimport { isPluginFieldTypeOnSurface } from \"nextly\";\n\nimport { BUILT_IN_FORM_FIELD_TYPES } from \"../types\";\nimport type { FormConfig, FormField, FormFieldType } from \"../types\";\n\n// ============================================================\n// Validation Error Types\n// ============================================================\n\n/**\n * Error codes for form validation failures.\n * Used for programmatic error handling.\n */\nexport type FormValidationErrorCode =\n // Slug errors\n | \"SLUG_REQUIRED\"\n | \"SLUG_INVALID_TYPE\"\n | \"SLUG_TOO_SHORT\"\n | \"SLUG_TOO_LONG\"\n | \"SLUG_INVALID_FORMAT\"\n | \"SLUG_RESERVED\"\n // Field errors\n | \"FIELDS_REQUIRED\"\n | \"FIELDS_INVALID_TYPE\"\n | \"FIELDS_EMPTY\"\n | \"FIELD_NAME_REQUIRED\"\n | \"FIELD_NAME_INVALID_FORMAT\"\n | \"FIELD_NAME_DUPLICATE\"\n | \"FIELD_TYPE_REQUIRED\"\n | \"FIELD_TYPE_INVALID\"\n | \"FIELD_LABEL_REQUIRED\"\n // Field-specific errors\n | \"OPTIONS_REQUIRED\"\n | \"OPTIONS_EMPTY\"\n | \"MIN_GREATER_THAN_MAX\"\n | \"MIN_LENGTH_GREATER_THAN_MAX\"\n | \"FILE_MAX_SIZE_INVALID\"\n // Conditional logic errors\n | \"CONDITIONS_REQUIRED\"\n | \"CONDITION_FIELD_REQUIRED\"\n | \"CONDITION_COMPARISON_REQUIRED\"\n // Settings errors\n | \"SETTINGS_INVALID_TYPE\"\n | \"REDIRECT_URL_REQUIRED\"\n // Access errors\n | \"ACCESS_INVALID_TYPE\"\n | \"ACCESS_FUNCTION_INVALID\";\n\n/**\n * A single validation error with path and context.\n *\n * @example\n * ```typescript\n * const error: FormValidationError = {\n * path: 'fields.email.validation',\n * message: \"Min length cannot be greater than max length\",\n * code: 'MIN_LENGTH_GREATER_THAN_MAX',\n * };\n * ```\n */\nexport interface FormValidationError {\n /**\n * Dot-notation path to the invalid property.\n * @example 'slug', 'fields.0.name', 'fields.email.options'\n */\n path: string;\n\n /**\n * Human-readable error message.\n */\n message: string;\n\n /**\n * Machine-readable error code for programmatic handling.\n */\n code: FormValidationErrorCode;\n}\n\n/**\n * Result of form config validation.\n */\nexport interface FormValidationResult {\n /**\n * Whether the configuration is valid.\n */\n valid: boolean;\n\n /**\n * Array of validation errors (empty if valid).\n */\n errors: FormValidationError[];\n}\n\n// ============================================================\n// Reserved Names\n// ============================================================\n\n/**\n * Reserved form slugs that cannot be used.\n * These are used by the system or have special meaning.\n */\nexport const RESERVED_FORM_SLUGS = [\n // API routes\n \"api\",\n \"submit\",\n \"submissions\",\n \"export\",\n // System slugs\n \"admin\",\n \"new\",\n \"create\",\n \"edit\",\n \"delete\",\n] as const;\n\n/**\n * Set for O(1) lookup of reserved slugs.\n */\nconst RESERVED_SLUGS_SET = new Set<string>(RESERVED_FORM_SLUGS);\n\n// ============================================================\n// Validation Patterns\n// ============================================================\n\n/**\n * Regex pattern for valid form slugs.\n * Must start with a lowercase letter, contain only lowercase letters,\n * numbers, hyphens, and underscores. Length: 1-50 chars.\n * Allows hyphens for URL-friendly slugs (e.g., 'contact-form').\n */\nconst SLUG_PATTERN = /^[a-z][a-z0-9_-]*$/;\n\n/**\n * Regex pattern for valid field names.\n * Must start with a letter (upper or lower), contain only letters,\n * numbers, and underscores. Supports camelCase (e.g., 'firstName').\n */\nconst FIELD_NAME_PATTERN = /^[a-zA-Z][a-zA-Z0-9_]*$/;\n\n/**\n * All valid built-in form field types (plugin types validate via the registry).\n */\nconst VALID_FIELD_TYPES: readonly FormFieldType[] = BUILT_IN_FORM_FIELD_TYPES;\n\nconst VALID_FIELD_TYPES_SET = new Set<string>(VALID_FIELD_TYPES);\n\n// ============================================================\n// Validation Helper Functions\n// ============================================================\n\n/**\n * Checks if a string is a reserved form slug.\n */\nfunction isReservedSlug(slug: string): boolean {\n return RESERVED_SLUGS_SET.has(slug.toLowerCase());\n}\n\n/**\n * Validates a form slug.\n */\nfunction validateSlug(slug: unknown, errors: FormValidationError[]): void {\n const path = \"slug\";\n\n if (!slug) {\n errors.push({\n path,\n message: \"Form slug is required\",\n code: \"SLUG_REQUIRED\",\n });\n return;\n }\n\n if (typeof slug !== \"string\") {\n errors.push({\n path,\n message: \"Form slug must be a string\",\n code: \"SLUG_INVALID_TYPE\",\n });\n return;\n }\n\n if (slug.length < 1) {\n errors.push({\n path,\n message: \"Form slug must be at least 1 character\",\n code: \"SLUG_TOO_SHORT\",\n });\n }\n\n if (slug.length > 50) {\n errors.push({\n path,\n message: \"Form slug must be at most 50 characters\",\n code: \"SLUG_TOO_LONG\",\n });\n }\n\n if (!SLUG_PATTERN.test(slug)) {\n errors.push({\n path,\n message:\n \"Form slug must start with a lowercase letter and contain only lowercase letters, numbers, hyphens, and underscores\",\n code: \"SLUG_INVALID_FORMAT\",\n });\n }\n\n if (isReservedSlug(slug)) {\n errors.push({\n path,\n message: `Form slug '${slug}' is reserved and cannot be used`,\n code: \"SLUG_RESERVED\",\n });\n }\n}\n\n/**\n * Validates a single field name.\n */\nfunction validateFieldName(\n name: unknown,\n path: string,\n errors: FormValidationError[],\n seenNames: Set<string>\n): void {\n if (!name) {\n errors.push({\n path: `${path}.name`,\n message: \"Field name is required\",\n code: \"FIELD_NAME_REQUIRED\",\n });\n return;\n }\n\n if (typeof name !== \"string\") {\n errors.push({\n path: `${path}.name`,\n message: \"Field name must be a string\",\n code: \"FIELD_NAME_REQUIRED\",\n });\n return;\n }\n\n if (!FIELD_NAME_PATTERN.test(name)) {\n errors.push({\n path: `${path}.name`,\n message: `Field name '${name}' must start with a letter and contain only letters, numbers, and underscores`,\n code: \"FIELD_NAME_INVALID_FORMAT\",\n });\n }\n\n // Check for duplicates at this level\n const nameLower = name.toLowerCase();\n if (seenNames.has(nameLower)) {\n errors.push({\n path: `${path}.name`,\n message: `Duplicate field name '${name}'`,\n code: \"FIELD_NAME_DUPLICATE\",\n });\n } else {\n seenNames.add(nameLower);\n }\n}\n\n/**\n * Validates a single field label.\n */\nfunction validateFieldLabel(\n label: unknown,\n path: string,\n errors: FormValidationError[]\n): void {\n if (!label || (typeof label === \"string\" && label.trim() === \"\")) {\n errors.push({\n path: `${path}.label`,\n message: \"Field label is required\",\n code: \"FIELD_LABEL_REQUIRED\",\n });\n }\n}\n\n/**\n * Validates select/radio/checkbox-group field options.\n */\nfunction validateOptions(\n field: FormField,\n path: string,\n errors: FormValidationError[]\n): void {\n // Type assertion for fields with options\n const fieldWithOptions = field as {\n options?: Array<{ label: string; value: string }>;\n };\n const options = fieldWithOptions.options;\n\n if (!options) {\n errors.push({\n path: `${path}.options`,\n message: `${field.type} field must have an 'options' array`,\n code: \"OPTIONS_REQUIRED\",\n });\n return;\n }\n\n if (!Array.isArray(options)) {\n errors.push({\n path: `${path}.options`,\n message: `${field.type} field 'options' must be an array`,\n code: \"OPTIONS_REQUIRED\",\n });\n return;\n }\n\n if (options.length === 0) {\n errors.push({\n path: `${path}.options`,\n message: `${field.type} field must have at least one option`,\n code: \"OPTIONS_EMPTY\",\n });\n }\n}\n\n/**\n * Validates conditional logic configuration.\n */\nfunction validateConditionalLogic(\n field: FormField,\n path: string,\n errors: FormValidationError[]\n): void {\n const { conditionalLogic } = field;\n\n if (!conditionalLogic || !conditionalLogic.enabled) {\n return;\n }\n\n if (\n !conditionalLogic.conditions ||\n conditionalLogic.conditions.length === 0\n ) {\n errors.push({\n path: `${path}.conditionalLogic.conditions`,\n message: \"Conditional logic must have at least one condition\",\n code: \"CONDITIONS_REQUIRED\",\n });\n return;\n }\n\n conditionalLogic.conditions.forEach((condition, index) => {\n const conditionPath = `${path}.conditionalLogic.conditions[${index}]`;\n\n if (!condition.field) {\n errors.push({\n path: `${conditionPath}.field`,\n message: \"Condition must specify a field name\",\n code: \"CONDITION_FIELD_REQUIRED\",\n });\n }\n\n if (!condition.comparison) {\n errors.push({\n path: `${conditionPath}.comparison`,\n message: \"Condition must specify a comparison operator\",\n code: \"CONDITION_COMPARISON_REQUIRED\",\n });\n }\n });\n}\n\n/**\n * Validates field based on its type.\n */\nfunction validateFieldByType(\n field: FormField,\n path: string,\n errors: FormValidationError[]\n): void {\n switch (field.type) {\n case \"select\":\n case \"radio\":\n validateOptions(field, path, errors);\n break;\n\n case \"number\": {\n const numberField = field;\n if (numberField.validation) {\n const { min, max } = numberField.validation;\n if (min !== undefined && max !== undefined && min > max) {\n errors.push({\n path: `${path}.validation`,\n message: \"Min value cannot be greater than max value\",\n code: \"MIN_GREATER_THAN_MAX\",\n });\n }\n }\n break;\n }\n\n case \"text\":\n case \"textarea\": {\n const textField = field;\n if (textField.validation) {\n const { minLength, maxLength } = textField.validation;\n if (\n minLength !== undefined &&\n maxLength !== undefined &&\n minLength > maxLength\n ) {\n errors.push({\n path: `${path}.validation`,\n message: \"Min length cannot be greater than max length\",\n code: \"MIN_LENGTH_GREATER_THAN_MAX\",\n });\n }\n }\n break;\n }\n\n case \"file\": {\n const fileField = field;\n if (fileField.maxFileSize !== undefined && fileField.maxFileSize <= 0) {\n errors.push({\n path: `${path}.maxFileSize`,\n message: \"File maxFileSize must be a positive number\",\n code: \"FILE_MAX_SIZE_INVALID\",\n });\n }\n break;\n }\n }\n}\n\n/**\n * Validates a single field configuration.\n */\nfunction validateField(\n field: unknown,\n path: string,\n errors: FormValidationError[],\n seenNames: Set<string>\n): void {\n if (!field || typeof field !== \"object\") {\n return;\n }\n\n const f = field as Record<string, unknown>;\n\n // Validate field type\n const fieldType = f.type;\n if (!fieldType) {\n errors.push({\n path: `${path}.type`,\n message: \"Field type is required\",\n code: \"FIELD_TYPE_REQUIRED\",\n });\n return;\n }\n\n // A plugin may contribute a field type that opted into the forms surface; it\n // is as valid here as a built-in. Built-ins keep the fast allowlist; other\n // strings fall through to the registry, surface-gated.\n const isValidType =\n typeof fieldType === \"string\" &&\n (VALID_FIELD_TYPES_SET.has(fieldType) ||\n isPluginFieldTypeOnSurface(fieldType, \"forms\"));\n\n if (!isValidType) {\n let fieldTypeRepr: string;\n if (typeof fieldType === \"object\" && fieldType !== null) {\n fieldTypeRepr = JSON.stringify(fieldType);\n } else {\n // eslint-disable-next-line @typescript-eslint/no-base-to-string -- fieldType narrowed to primitive above; rule doesn't follow control flow on unknown\n fieldTypeRepr = String(fieldType);\n }\n errors.push({\n path: `${path}.type`,\n message: `Invalid field type '${fieldTypeRepr}'. Valid types: ${VALID_FIELD_TYPES.join(\", \")}`,\n code: \"FIELD_TYPE_INVALID\",\n });\n return;\n }\n\n validateFieldName(f.name, path, errors, seenNames);\n\n // All fields except hidden require a label\n if (fieldType !== \"hidden\") {\n validateFieldLabel(f.label, path, errors);\n }\n\n // Type-specific validation\n validateFieldByType(field as FormField, path, errors);\n\n // Validate conditional logic if present\n if (f.conditionalLogic) {\n validateConditionalLogic(field as FormField, path, errors);\n }\n}\n\n/**\n * Validates an array of field configurations.\n */\nfunction validateFieldsArray(\n fields: unknown[],\n basePath: string,\n errors: FormValidationError[]\n): void {\n // Track field names for duplicate detection\n const seenNames = new Set<string>();\n\n fields.forEach((field, index) => {\n const fieldPath = `${basePath}[${index}]`;\n validateField(field, fieldPath, errors, seenNames);\n });\n}\n\n/**\n * Validates the top-level fields array.\n */\nfunction validateFields(fields: unknown, errors: FormValidationError[]): void {\n const path = \"fields\";\n\n if (!fields) {\n errors.push({\n path,\n message: \"Form must have at least one field\",\n code: \"FIELDS_REQUIRED\",\n });\n return;\n }\n\n if (!Array.isArray(fields)) {\n errors.push({\n path,\n message: \"Form fields must be an array\",\n code: \"FIELDS_INVALID_TYPE\",\n });\n return;\n }\n\n if (fields.length === 0) {\n errors.push({\n path,\n message: \"Form must have at least one field\",\n code: \"FIELDS_EMPTY\",\n });\n return;\n }\n\n validateFieldsArray(fields, path, errors);\n}\n\n/**\n * Validates form settings.\n */\nfunction validateSettings(\n settings: unknown,\n errors: FormValidationError[]\n): void {\n if (!settings) {\n return; // Settings are optional\n }\n\n if (typeof settings !== \"object\" || Array.isArray(settings)) {\n errors.push({\n path: \"settings\",\n message: \"Form settings must be an object\",\n code: \"SETTINGS_INVALID_TYPE\",\n });\n return;\n }\n\n const s = settings as Record<string, unknown>;\n\n // If confirmationType is \"redirect\", redirectUrl or redirectRelation is required\n if (s.confirmationType === \"redirect\") {\n if (!s.redirectUrl && !s.redirectRelation) {\n errors.push({\n path: \"settings.redirectUrl\",\n message:\n \"Redirect URL or redirect relation is required when confirmation type is 'redirect'\",\n code: \"REDIRECT_URL_REQUIRED\",\n });\n }\n }\n}\n\n/**\n * Validates form access control.\n */\nfunction validateAccess(access: unknown, errors: FormValidationError[]): void {\n if (!access) {\n return; // Access is optional\n }\n\n if (typeof access !== \"object\" || Array.isArray(access)) {\n errors.push({\n path: \"access\",\n message: \"Access control must be an object\",\n code: \"ACCESS_INVALID_TYPE\",\n });\n return;\n }\n\n const accessObj = access as Record<string, unknown>;\n const validAccessKeys = [\"submit\", \"read\"];\n\n for (const key of validAccessKeys) {\n const fn = accessObj[key];\n if (fn !== undefined && typeof fn !== \"function\") {\n errors.push({\n path: `access.${key}`,\n message: `Access control '${key}' must be a function`,\n code: \"ACCESS_FUNCTION_INVALID\",\n });\n }\n }\n}\n\n// ============================================================\n// Main Validation Function\n// ============================================================\n\n/**\n * Validates a complete form configuration.\n *\n * This is the main entry point for form validation. It performs\n * comprehensive validation including:\n * - Slug format and reserved name checking\n * - Field name validation\n * - Field-specific validation (options, min/max, etc.)\n * - Duplicate field name detection\n * - Conditional logic validation\n * - Settings validation\n * - Access function type validation\n *\n * @param config - The form configuration to validate\n * @returns Validation result with any errors found\n *\n * @example\n * ```typescript\n * import { validateFormConfig } from '@nextly/plugin-form-builder';\n *\n * const config = {\n * slug: 'contact-form',\n * fields: [\n * { type: 'text', name: 'firstName', label: 'First Name' },\n * { type: 'email', name: 'email', label: 'Email', required: true },\n * ],\n * };\n *\n * const result = validateFormConfig(config);\n *\n * if (!result.valid) {\n * result.errors.forEach(err => {\n * console.error(`[${err.code}] ${err.path}: ${err.message}`);\n * });\n * }\n * ```\n */\nexport function validateFormConfig(config: FormConfig): FormValidationResult {\n const errors: FormValidationError[] = [];\n\n // Validate slug\n validateSlug(config.slug, errors);\n\n // Validate fields\n validateFields(config.fields, errors);\n\n // Validate settings\n validateSettings(config.settings, errors);\n\n // Validate access control\n validateAccess(config.access, errors);\n\n return {\n valid: errors.length === 0,\n errors,\n };\n}\n\n/**\n * Throws an error if the form configuration is invalid.\n *\n * This is a convenience wrapper around `validateFormConfig` that\n * throws a descriptive error instead of returning a result object.\n *\n * @param config - The form configuration to validate\n * @throws Error if validation fails with all error messages\n *\n * @example\n * ```typescript\n * import { assertValidFormConfig } from '@nextly/plugin-form-builder';\n *\n * // Throws if invalid\n * assertValidFormConfig(config);\n * ```\n */\nexport function assertValidFormConfig(config: FormConfig): void {\n const result = validateFormConfig(config);\n\n if (!result.valid) {\n const errorMessages = result.errors\n .map(err => ` - [${err.code}] ${err.path}: ${err.message}`)\n .join(\"\\n\");\n\n throw new Error(\n `Invalid form config for '${config.slug || \"unknown\"}':\\n${errorMessages}`\n );\n }\n}\n","/**\n * Form Field Helpers\n *\n * Convenient factory functions for creating form field configurations.\n * These helpers eliminate the need to manually specify the `type` property,\n * providing a cleaner, more ergonomic API for form definitions.\n *\n * @module config/field-helpers\n *\n * @example\n * ```typescript\n * import {\n * text,\n * email,\n * select,\n * checkbox,\n * option,\n * } from '@nextly/plugin-form-builder';\n *\n * const contactFormFields = [\n * text({ name: 'firstName', label: 'First Name', required: true }),\n * text({ name: 'lastName', label: 'Last Name', required: true }),\n * email({ name: 'email', label: 'Email Address', required: true }),\n * select({\n * name: 'subject',\n * label: 'Subject',\n * options: [\n * option('General Inquiry'),\n * option('Support Request'),\n * option('Feedback'),\n * ],\n * }),\n * checkbox({ name: 'subscribe', label: 'Subscribe to newsletter' }),\n * ];\n * ```\n */\n\nimport type {\n TextFormField,\n EmailFormField,\n NumberFormField,\n PhoneFormField,\n UrlFormField,\n TextareaFormField,\n SelectFormField,\n CheckboxFormField,\n RadioFormField,\n FileFormField,\n DateFormField,\n TimeFormField,\n HiddenFormField,\n} from \"../types\";\n\n// ============================================================\n// Utility Types\n// ============================================================\n\n/**\n * Option for select and radio fields.\n */\nexport interface FormFieldOption {\n label: string;\n value: string;\n}\n\n// ============================================================\n// Utility Helpers\n// ============================================================\n\n/**\n * Creates an option for select or radio fields.\n *\n * If no value is provided, the label is converted to lowercase with\n * spaces replaced by underscores.\n *\n * @param label - Display label for the option\n * @param value - Optional value (defaults to lowercase label with underscores)\n * @returns Option object with label and value\n *\n * @example\n * ```typescript\n * // Value derived from label\n * option('United States') // { label: 'United States', value: 'united_states' }\n *\n * // Explicit value\n * option('United States', 'us') // { label: 'United States', value: 'us' }\n *\n * // Usage in select field\n * select({\n * name: 'country',\n * label: 'Country',\n * options: [\n * option('United States', 'us'),\n * option('Canada', 'ca'),\n * option('Mexico', 'mx'),\n * ],\n * })\n * ```\n */\nexport const option = (label: string, value?: string): FormFieldOption => ({\n label,\n value: value ?? label.toLowerCase().replace(/\\s+/g, \"_\"),\n});\n\n// ============================================================\n// Text Input Field Helpers\n// ============================================================\n\n/**\n * Creates a text input field configuration.\n *\n * Text fields store simple string values with optional constraints\n * like min/max length and pattern validation.\n *\n * @param config - Field configuration without the `type` property\n * @returns Complete text field configuration\n *\n * @example\n * ```typescript\n * // Basic text field\n * text({ name: 'firstName', label: 'First Name', required: true })\n *\n * // Text with validation\n * text({\n * name: 'username',\n * label: 'Username',\n * required: true,\n * placeholder: 'Enter username',\n * validation: {\n * minLength: 3,\n * maxLength: 20,\n * pattern: '^[a-zA-Z0-9_]+$',\n * },\n * })\n * ```\n */\nexport const text = (config: Omit<TextFormField, \"type\">): TextFormField => ({\n ...config,\n type: \"text\",\n});\n\n/**\n * Creates an email input field configuration.\n *\n * Email fields provide built-in email format validation.\n *\n * @param config - Field configuration without the `type` property\n * @returns Complete email field configuration\n *\n * @example\n * ```typescript\n * // Basic email field\n * email({ name: 'email', label: 'Email Address', required: true })\n *\n * // Email with placeholder\n * email({\n * name: 'workEmail',\n * label: 'Work Email',\n * required: true,\n * placeholder: 'you@nextly.local',\n * })\n * ```\n */\nexport const email = (\n config: Omit<EmailFormField, \"type\">\n): EmailFormField => ({\n ...config,\n type: \"email\",\n});\n\n/**\n * Creates a phone input field configuration.\n *\n * Phone fields are optimized for phone number entry with\n * appropriate keyboard on mobile devices.\n *\n * @param config - Field configuration without the `type` property\n * @returns Complete phone field configuration\n *\n * @example\n * ```typescript\n * // Basic phone field\n * phone({ name: 'phone', label: 'Phone Number', required: true })\n *\n * // Phone with placeholder\n * phone({\n * name: 'mobile',\n * label: 'Mobile Number',\n * placeholder: '+1 (555) 123-4567',\n * })\n * ```\n */\nexport const phone = (\n config: Omit<PhoneFormField, \"type\">\n): PhoneFormField => ({\n ...config,\n type: \"phone\",\n});\n\n/**\n * Creates a URL input field configuration.\n *\n * URL fields provide built-in URL format validation.\n *\n * @param config - Field configuration without the `type` property\n * @returns Complete URL field configuration\n *\n * @example\n * ```typescript\n * // Basic URL field\n * url({ name: 'website', label: 'Website URL' })\n *\n * // URL with placeholder\n * url({\n * name: 'portfolio',\n * label: 'Portfolio URL',\n * placeholder: 'https://example.com',\n * })\n * ```\n */\nexport const url = (config: Omit<UrlFormField, \"type\">): UrlFormField => ({\n ...config,\n type: \"url\",\n});\n\n/**\n * Creates a textarea field configuration.\n *\n * Textarea fields provide multi-line text input with configurable\n * row count and length validation.\n *\n * @param config - Field configuration without the `type` property\n * @returns Complete textarea field configuration\n *\n * @example\n * ```typescript\n * // Basic textarea\n * textarea({ name: 'message', label: 'Message', required: true })\n *\n * // Textarea with rows and validation\n * textarea({\n * name: 'description',\n * label: 'Description',\n * rows: 5,\n * validation: {\n * minLength: 10,\n * maxLength: 500,\n * },\n * })\n * ```\n */\nexport const textarea = (\n config: Omit<TextareaFormField, \"type\">\n): TextareaFormField => ({\n ...config,\n type: \"textarea\",\n});\n\n// ============================================================\n// Numeric Field Helpers\n// ============================================================\n\n/**\n * Creates a number input field configuration.\n *\n * Number fields store numeric values with optional min/max/step\n * constraints.\n *\n * @param config - Field configuration without the `type` property\n * @returns Complete number field configuration\n *\n * @example\n * ```typescript\n * // Basic number field\n * number({ name: 'age', label: 'Age', required: true })\n *\n * // Number with range validation\n * number({\n * name: 'quantity',\n * label: 'Quantity',\n * required: true,\n * validation: {\n * min: 1,\n * max: 100,\n * step: 1,\n * },\n * })\n * ```\n */\nexport const number = (\n config: Omit<NumberFormField, \"type\">\n): NumberFormField => ({\n ...config,\n type: \"number\",\n});\n\n// ============================================================\n// Selection Field Helpers\n// ============================================================\n\n/**\n * Creates a select dropdown field configuration.\n *\n * Select fields display a dropdown list of options. Use the `option()`\n * helper to create options.\n *\n * @param config - Field configuration without the `type` property\n * @returns Complete select field configuration\n *\n * @example\n * ```typescript\n * // Basic select\n * select({\n * name: 'country',\n * label: 'Country',\n * required: true,\n * options: [\n * option('United States', 'us'),\n * option('Canada', 'ca'),\n * option('Mexico', 'mx'),\n * ],\n * })\n *\n * // Multi-select\n * select({\n * name: 'interests',\n * label: 'Interests',\n * allowMultiple: true,\n * options: [\n * option('Technology'),\n * option('Design'),\n * option('Business'),\n * ],\n * })\n * ```\n */\nexport const select = (\n config: Omit<SelectFormField, \"type\">\n): SelectFormField => ({\n ...config,\n type: \"select\",\n});\n\n/**\n * Creates a single checkbox field configuration.\n *\n * Checkbox fields store boolean values (true/false).\n *\n * @param config - Field configuration without the `type` property\n * @returns Complete checkbox field configuration\n *\n * @example\n * ```typescript\n * // Terms agreement checkbox\n * checkbox({\n * name: 'agreeToTerms',\n * label: 'I agree to the terms and conditions',\n * required: true,\n * })\n *\n * // Newsletter subscription\n * checkbox({\n * name: 'subscribe',\n * label: 'Subscribe to our newsletter',\n * defaultValue: true,\n * })\n * ```\n */\nexport const checkbox = (\n config: Omit<CheckboxFormField, \"type\">\n): CheckboxFormField => ({\n ...config,\n type: \"checkbox\",\n});\n\n/**\n * Creates a radio group field configuration.\n *\n * Radio group fields allow selecting exactly one option from a list.\n * Use the `option()` helper to create options.\n *\n * @param config - Field configuration without the `type` property\n * @returns Complete radio field configuration\n *\n * @example\n * ```typescript\n * radio({\n * name: 'contactMethod',\n * label: 'Preferred Contact Method',\n * required: true,\n * options: [\n * option('Email', 'email'),\n * option('Phone', 'phone'),\n * option('SMS', 'sms'),\n * ],\n * })\n * ```\n */\nexport const radio = (\n config: Omit<RadioFormField, \"type\">\n): RadioFormField => ({\n ...config,\n type: \"radio\",\n});\n\n// ============================================================\n// File Field Helpers\n// ============================================================\n\n/**\n * Creates a file upload field configuration.\n *\n * File fields allow users to upload files with configurable\n * accepted types, size limits, and multiple file support.\n *\n * @param config - Field configuration without the `type` property\n * @returns Complete file field configuration\n *\n * @example\n * ```typescript\n * // Single file upload\n * file({\n * name: 'resume',\n * label: 'Upload Resume',\n * required: true,\n * accept: 'application/pdf,.doc,.docx',\n * maxFileSize: 5242880, // 5MB\n * })\n *\n * // Multiple images\n * file({\n * name: 'photos',\n * label: 'Upload Photos',\n * accept: 'image/*',\n * multiple: true,\n * maxFileSize: 10485760, // 10MB\n * })\n * ```\n */\nexport const file = (config: Omit<FileFormField, \"type\">): FileFormField => ({\n ...config,\n type: \"file\",\n});\n\n// ============================================================\n// Date/Time Field Helpers\n// ============================================================\n\n/**\n * Creates a date picker field configuration.\n *\n * Date fields provide a date picker with optional min/max constraints.\n *\n * @param config - Field configuration without the `type` property\n * @returns Complete date field configuration\n *\n * @example\n * ```typescript\n * // Basic date field\n * date({ name: 'birthdate', label: 'Date of Birth', required: true })\n *\n * // Date with range constraints\n * date({\n * name: 'eventDate',\n * label: 'Event Date',\n * required: true,\n * min: '2024-01-01',\n * max: '2024-12-31',\n * })\n * ```\n */\nexport const date = (config: Omit<DateFormField, \"type\">): DateFormField => ({\n ...config,\n type: \"date\",\n});\n\n/**\n * Creates a time picker field configuration.\n *\n * Time fields provide a time picker for selecting time values.\n *\n * @param config - Field configuration without the `type` property\n * @returns Complete time field configuration\n *\n * @example\n * ```typescript\n * // Basic time field\n * time({ name: 'appointmentTime', label: 'Preferred Time', required: true })\n *\n * // Time with default\n * time({\n * name: 'meetingTime',\n * label: 'Meeting Time',\n * defaultValue: '09:00',\n * })\n * ```\n */\nexport const time = (config: Omit<TimeFormField, \"type\">): TimeFormField => ({\n ...config,\n type: \"time\",\n});\n\n// ============================================================\n// Special Field Helpers\n// ============================================================\n\n/**\n * Creates a hidden field configuration.\n *\n * Hidden fields store values that are not visible to users but\n * are included in the submission data. Useful for tracking\n * metadata like source, campaign, or referrer.\n *\n * @param config - Field configuration without the `type` property\n * @returns Complete hidden field configuration\n *\n * @example\n * ```typescript\n * // Track form source\n * hidden({ name: 'source', label: 'Source', defaultValue: 'website' })\n *\n * // Campaign tracking\n * hidden({ name: 'campaign', label: 'Campaign', defaultValue: 'summer-sale' })\n * ```\n */\nexport const hidden = (\n config: Omit<HiddenFormField, \"type\">\n): HiddenFormField => ({\n ...config,\n type: \"hidden\",\n});\n","/**\n * Dynamic Zod Schema Generator\n *\n * Converts FormField[] configuration to Zod validation schemas.\n * Supports all form field types with proper validation rules.\n *\n * @module utils/generate-schema\n * @since 0.1.0\n */\n\nimport { z } from \"zod\";\n\nimport { isKnownFormField } from \"../types\";\nimport type {\n AnyFormField,\n FormField,\n TextFormField,\n EmailFormField,\n NumberFormField,\n PhoneFormField,\n UrlFormField,\n TextareaFormField,\n SelectFormField,\n CheckboxFormField,\n RadioFormField,\n FileFormField,\n DateFormField,\n TimeFormField,\n HiddenFormField,\n} from \"../types\";\n\n// ============================================================\n// Main Schema Generator\n// ============================================================\n\n/**\n * Generate a Zod validation schema from form field configuration.\n *\n * Creates a dynamic Zod schema based on the form fields array,\n * applying appropriate validation rules for each field type.\n *\n * @param fields - Array of form field configurations\n * @returns Zod object schema for validating submission data\n *\n * @example\n * ```typescript\n * const fields: FormField[] = [\n * { type: 'text', name: 'firstName', label: 'First Name', required: true },\n * { type: 'email', name: 'email', label: 'Email', required: true },\n * { type: 'number', name: 'age', label: 'Age', validation: { min: 18, max: 120 } },\n * ];\n *\n * const schema = generateZodSchema(fields);\n * const result = schema.safeParse({ firstName: 'John', email: 'john@example.com', age: 25 });\n * ```\n */\nexport function generateZodSchema(\n fields: AnyFormField[]\n): z.ZodObject<Record<string, z.ZodTypeAny>> {\n const schemaShape: Record<string, z.ZodTypeAny> = {};\n\n for (const field of fields) {\n // A plugin-contributed field's value shape is owned by the plugin: accept\n // any value, but keep the base required check so a required plugin field\n // cannot be omitted (z.unknown() alone treats a missing key as valid).\n if (!isKnownFormField(field)) {\n schemaShape[field.name] = field.required\n ? z\n .unknown()\n .refine(\n value => value !== undefined && value !== null && value !== \"\",\n { message: `${field.label || field.name} is required` }\n )\n : z.unknown();\n continue;\n }\n\n // Skip hidden fields without required flag\n if (field.type === \"hidden\" && !field.required) {\n // Still add to schema but make optional\n schemaShape[field.name] = z.string().optional();\n continue;\n }\n\n const fieldSchema = generateFieldSchema(field);\n if (fieldSchema) {\n schemaShape[field.name] = fieldSchema;\n }\n }\n\n return z.object(schemaShape);\n}\n\n// ============================================================\n// Field Schema Generators\n// ============================================================\n\n/**\n * Generate a Zod schema for a single field.\n */\nfunction generateFieldSchema(field: FormField): z.ZodTypeAny | null {\n switch (field.type) {\n case \"text\":\n return generateTextSchema(field);\n case \"email\":\n return generateEmailSchema(field);\n case \"number\":\n return generateNumberSchema(field);\n case \"phone\":\n return generatePhoneSchema(field);\n case \"url\":\n return generateUrlSchema(field);\n case \"textarea\":\n return generateTextareaSchema(field);\n case \"select\":\n return generateSelectSchema(field);\n case \"checkbox\":\n return generateCheckboxSchema(field);\n case \"radio\":\n return generateRadioSchema(field);\n case \"file\":\n return generateFileSchema(field);\n case \"date\":\n return generateDateSchema(field);\n case \"time\":\n return generateTimeSchema(field);\n case \"hidden\":\n return generateHiddenSchema(field);\n default:\n // Unreachable for a known FormField; plugin types are handled in\n // generateZodSchema before this switch is reached.\n return z.unknown();\n }\n}\n\n/**\n * Generate schema for text field.\n */\nfunction generateTextSchema(field: TextFormField): z.ZodTypeAny {\n let schema = z.string();\n\n // Apply validation rules\n if (field.validation?.minLength !== undefined) {\n schema = schema.min(\n field.validation.minLength,\n field.validation.errorMessage ||\n `Minimum ${field.validation.minLength} characters required`\n );\n }\n\n if (field.validation?.maxLength !== undefined) {\n schema = schema.max(\n field.validation.maxLength,\n field.validation.errorMessage ||\n `Maximum ${field.validation.maxLength} characters allowed`\n );\n }\n\n if (field.validation?.pattern) {\n schema = schema.regex(\n new RegExp(field.validation.pattern),\n field.validation.errorMessage || \"Invalid format\"\n );\n }\n\n return applyRequired(schema, field.required, field.validation?.errorMessage);\n}\n\n/**\n * Generate schema for email field.\n */\nfunction generateEmailSchema(field: EmailFormField): z.ZodTypeAny {\n let schema = z\n .string()\n .email(field.validation?.errorMessage || \"Invalid email address\");\n\n if (field.validation?.pattern) {\n schema = schema.regex(\n new RegExp(field.validation.pattern),\n field.validation.errorMessage || \"Invalid email format\"\n );\n }\n\n return applyRequired(schema, field.required, field.validation?.errorMessage);\n}\n\n/**\n * Generate schema for number field.\n */\nfunction generateNumberSchema(field: NumberFormField): z.ZodTypeAny {\n // Zod 4 unified message customization under a single `error` callback;\n // an undefined input is the \"missing required\" case, anything else is a\n // type mismatch.\n let schema = z.number({\n error: issue =>\n issue.input === undefined\n ? field.validation?.errorMessage || \"This field is required\"\n : \"Must be a number\",\n });\n\n if (field.validation?.min !== undefined) {\n schema = schema.min(\n field.validation.min,\n field.validation.errorMessage ||\n `Minimum value is ${field.validation.min}`\n );\n }\n\n if (field.validation?.max !== undefined) {\n schema = schema.max(\n field.validation.max,\n field.validation.errorMessage ||\n `Maximum value is ${field.validation.max}`\n );\n }\n\n if (field.required) {\n return schema;\n }\n\n return schema.optional().nullable();\n}\n\n/**\n * Generate schema for phone field.\n */\nfunction generatePhoneSchema(field: PhoneFormField): z.ZodTypeAny {\n let schema = z.string();\n\n if (field.validation?.pattern) {\n schema = schema.regex(\n new RegExp(field.validation.pattern),\n field.validation.errorMessage || \"Invalid phone number format\"\n );\n } else {\n // Default phone pattern - allows common formats\n schema = schema.regex(\n /^[\\d\\s\\-+()]+$/,\n field.validation?.errorMessage || \"Invalid phone number\"\n );\n }\n\n return applyRequired(schema, field.required, field.validation?.errorMessage);\n}\n\n/**\n * Generate schema for URL field.\n */\nfunction generateUrlSchema(field: UrlFormField): z.ZodTypeAny {\n let schema = z.string().url(field.validation?.errorMessage || \"Invalid URL\");\n\n if (field.validation?.pattern) {\n schema = schema.regex(\n new RegExp(field.validation.pattern),\n field.validation.errorMessage || \"Invalid URL format\"\n );\n }\n\n return applyRequired(schema, field.required, field.validation?.errorMessage);\n}\n\n/**\n * Generate schema for textarea field.\n */\nfunction generateTextareaSchema(field: TextareaFormField): z.ZodTypeAny {\n let schema = z.string();\n\n if (field.validation?.minLength !== undefined) {\n schema = schema.min(\n field.validation.minLength,\n field.validation.errorMessage ||\n `Minimum ${field.validation.minLength} characters required`\n );\n }\n\n if (field.validation?.maxLength !== undefined) {\n schema = schema.max(\n field.validation.maxLength,\n field.validation.errorMessage ||\n `Maximum ${field.validation.maxLength} characters allowed`\n );\n }\n\n return applyRequired(schema, field.required, field.validation?.errorMessage);\n}\n\n/**\n * Generate schema for select field.\n */\nfunction generateSelectSchema(field: SelectFormField): z.ZodTypeAny {\n const validValues = field.options.map(opt => opt.value);\n\n if (field.allowMultiple) {\n // Multi-select: array of valid values\n const schema = z.array(z.enum(validValues as [string, ...string[]]));\n\n if (field.required) {\n return schema.min(\n 1,\n field.validation?.errorMessage || \"Please select at least one option\"\n );\n }\n\n return schema.optional();\n }\n\n // Single select\n const schema = z.enum(validValues as [string, ...string[]], {\n error: () =>\n field.validation?.errorMessage || \"Please select a valid option\",\n });\n\n if (field.required) {\n return schema;\n }\n\n return schema.optional();\n}\n\n/**\n * Generate schema for checkbox field.\n */\nfunction generateCheckboxSchema(field: CheckboxFormField): z.ZodTypeAny {\n const schema = z.boolean();\n\n if (field.required) {\n // For required checkbox, value must be true\n return schema.refine(val => val === true, {\n message: field.validation?.errorMessage || \"This field is required\",\n });\n }\n\n return schema.optional();\n}\n\n/**\n * Generate schema for radio field.\n */\nfunction generateRadioSchema(field: RadioFormField): z.ZodTypeAny {\n const validValues = field.options.map(opt => opt.value);\n\n const schema = z.enum(validValues as [string, ...string[]], {\n error: () => field.validation?.errorMessage || \"Please select an option\",\n });\n\n if (field.required) {\n return schema;\n }\n\n return schema.optional();\n}\n\n/**\n * Generate schema for file field.\n *\n * Note: File validation is typically handled server-side.\n * This schema validates file references (IDs or URLs).\n */\nfunction generateFileSchema(field: FileFormField): z.ZodTypeAny {\n if (field.multiple) {\n const schema = z.array(z.string());\n\n if (field.required) {\n return schema.min(\n 1,\n field.validation?.errorMessage || \"Please upload at least one file\"\n );\n }\n\n return schema.optional();\n }\n\n // Single file\n const schema = z.string();\n return applyRequired(schema, field.required, field.validation?.errorMessage);\n}\n\n/**\n * Generate schema for date field.\n */\nfunction generateDateSchema(field: DateFormField): z.ZodTypeAny {\n // Start with base string schema\n let schema: z.ZodTypeAny = z.string();\n\n // Add date format validation\n schema = (schema as z.ZodString).refine(\n val => {\n if (!val) return true; // Allow empty for optional fields\n const date = new Date(val);\n return !isNaN(date.getTime());\n },\n { message: field.validation?.errorMessage || \"Invalid date\" }\n );\n\n // Min/max date validation\n if (field.min || field.max) {\n schema = schema.refine(\n (val: unknown) => {\n if (!val) return true;\n const date = new Date(val as string);\n if (field.min && date < new Date(field.min)) return false;\n if (field.max && date > new Date(field.max)) return false;\n return true;\n },\n {\n message:\n field.validation?.errorMessage ||\n `Date must be between ${field.min || \"any\"} and ${field.max || \"any\"}`,\n }\n );\n }\n\n // Handle required vs optional\n if (field.required) {\n // Add non-empty check for required fields\n return schema.refine(\n (val: unknown) => val !== undefined && val !== null && val !== \"\",\n { message: field.validation?.errorMessage || \"This field is required\" }\n );\n }\n\n return schema.optional().or(z.literal(\"\"));\n}\n\n/**\n * Generate schema for time field.\n */\nfunction generateTimeSchema(field: TimeFormField): z.ZodTypeAny {\n const schema = z\n .string()\n .regex(\n /^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$/,\n field.validation?.errorMessage || \"Invalid time format (HH:mm)\"\n );\n\n return applyRequired(schema, field.required, field.validation?.errorMessage);\n}\n\n/**\n * Generate schema for hidden field.\n */\nfunction generateHiddenSchema(field: HiddenFormField): z.ZodTypeAny {\n const schema = z.string();\n return applyRequired(schema, field.required, field.validation?.errorMessage);\n}\n\n// ============================================================\n// Helper Functions\n// ============================================================\n\n/**\n * Apply required/optional to a schema.\n */\nfunction applyRequired(\n schema: z.ZodString,\n required?: boolean,\n errorMessage?: string\n): z.ZodTypeAny {\n if (required) {\n return schema.min(1, errorMessage || \"This field is required\");\n }\n\n return schema.optional().or(z.literal(\"\"));\n}\n\n// ============================================================\n// Data Transformation\n// ============================================================\n\n/**\n * Transform form data before validation.\n *\n * Handles type coercion and normalization:\n * - Converts string numbers to actual numbers\n * - Converts string booleans to actual booleans\n * - Trims string values\n * - Handles empty strings\n *\n * @param data - Raw form data\n * @param fields - Form field configurations\n * @returns Transformed data ready for validation\n */\nexport function transformFormData(\n data: Record<string, unknown>,\n fields: AnyFormField[]\n): Record<string, unknown> {\n const transformed: Record<string, unknown> = {};\n\n for (const field of fields) {\n const value = data[field.name];\n\n // Skip undefined values\n if (value === undefined) {\n continue;\n }\n\n // A plugin field's value passes through untransformed — the plugin owns its\n // value shape; only built-in types get type coercion.\n transformed[field.name] = isKnownFormField(field)\n ? transformFieldValue(value, field)\n : value;\n }\n\n return transformed;\n}\n\n/**\n * Transform a single field value based on its type.\n */\nfunction transformFieldValue(value: unknown, field: FormField): unknown {\n // Handle null/undefined\n if (value === null || value === undefined) {\n return value;\n }\n\n switch (field.type) {\n case \"number\":\n // Coerce to number\n if (typeof value === \"string\") {\n const num = parseFloat(value);\n return isNaN(num) ? value : num;\n }\n return value;\n\n case \"checkbox\":\n // Coerce to boolean\n if (typeof value === \"string\") {\n return value === \"true\" || value === \"1\" || value === \"on\";\n }\n return Boolean(value);\n\n case \"text\":\n case \"email\":\n case \"phone\":\n case \"url\":\n case \"textarea\":\n case \"hidden\":\n // Trim strings\n if (typeof value === \"string\") {\n return value.trim();\n }\n return value;\n\n case \"select\":\n // Ensure array for multi-select\n if (field.allowMultiple) {\n if (!Array.isArray(value)) {\n return value ? [value] : [];\n }\n }\n return value;\n\n default:\n return value;\n }\n}\n\n// ============================================================\n// Validation Helper\n// ============================================================\n\n/**\n * Validate form data against a form's field configuration.\n *\n * Combines transformation and validation in one step.\n *\n * @param data - Raw form data\n * @param fields - Form field configurations\n * @returns Zod safe parse result\n *\n * @example\n * ```typescript\n * const result = validateFormData({ name: 'John', email: 'john@example.com' }, fields);\n *\n * if (result.success) {\n * console.log('Valid data:', result.data);\n * } else {\n * console.log('Validation errors:', result.error.flatten());\n * }\n * ```\n */\nexport function validateFormData(\n data: Record<string, unknown>,\n fields: AnyFormField[]\n): z.ZodSafeParseResult<Record<string, unknown>> {\n const transformed = transformFormData(data, fields);\n const schema = generateZodSchema(fields);\n return schema.safeParse(transformed);\n}\n\n/**\n * Get validation errors as a flat object.\n *\n * @param result - Zod safe parse result\n * @returns Object mapping field names to error messages\n */\nexport function getValidationErrors(\n result: z.ZodSafeParseResult<unknown>\n): Record<string, string> {\n if (result.success) {\n return {};\n }\n\n const errors: Record<string, string> = {};\n\n // Zod 4 renamed ZodError.errors to ZodError.issues.\n for (const error of result.error.issues) {\n const path = error.path.join(\".\");\n if (!errors[path]) {\n errors[path] = error.message;\n }\n }\n\n return errors;\n}\n","/**\n * Spam Detection\n *\n * Implements honeypot and rate limiting spam protection for form submissions.\n * Provides a pluggable approach to spam detection that can be extended.\n *\n * @module handlers/spam-detection\n * @since 0.1.0\n */\n\n// ============================================================\n// Types\n// ============================================================\n\n/**\n * Configuration for spam detection.\n */\nexport interface SpamCheckConfig {\n /**\n * Enable honeypot field detection.\n * When enabled, submissions with filled honeypot fields are marked as spam.\n */\n honeypot?: boolean;\n\n /**\n * Rate limiting configuration.\n */\n rateLimit?: {\n /** Maximum submissions allowed per window */\n maxSubmissions: number;\n /** Time window in milliseconds */\n windowMs: number;\n };\n\n /**\n * reCAPTCHA configuration (for future implementation).\n */\n recaptcha?: {\n enabled: boolean;\n secretKey?: string;\n scoreThreshold?: number;\n };\n}\n\n/**\n * Options for spam check.\n */\nexport interface SpamCheckOptions {\n /** Form submission data */\n data: Record<string, unknown>;\n\n /** Submitter's IP address */\n ipAddress?: string;\n\n /** Form slug for rate limiting key */\n formSlug: string;\n\n /** Spam protection configuration */\n config: SpamCheckConfig;\n}\n\n/**\n * Result of spam check.\n */\nexport interface SpamCheckResult {\n /** Whether the submission is detected as spam */\n isSpam: boolean;\n\n /** Reason for spam detection (for logging, not exposed to user) */\n reason?: \"honeypot\" | \"rate_limit\" | \"recaptcha\";\n\n /** Additional details (for logging) */\n details?: string;\n}\n\n// ============================================================\n// Rate Limit Store\n// ============================================================\n\n/**\n * In-memory rate limit store.\n *\n * Note: For multi-instance deployments (serverless, load-balanced),\n * consider using Redis or a database-backed store instead.\n * This in-memory implementation works well for single-instance deployments.\n */\ninterface RateLimitEntry {\n count: number;\n timestamp: number;\n}\n\nconst rateLimitStore = new Map<string, RateLimitEntry>();\n\n// ============================================================\n// Main Spam Check Function\n// ============================================================\n\n/**\n * Check a form submission for spam indicators.\n *\n * Performs multiple spam checks based on configuration:\n * 1. Honeypot field detection\n * 2. Rate limiting by IP/form\n * 3. reCAPTCHA verification (future)\n *\n * @param options - Spam check options\n * @returns Spam check result\n *\n * @example\n * ```typescript\n * const result = await checkSpam({\n * data: formData,\n * ipAddress: req.ip,\n * formSlug: 'contact-form',\n * config: {\n * honeypot: true,\n * rateLimit: { maxSubmissions: 5, windowMs: 60000 },\n * },\n * });\n *\n * if (result.isSpam) {\n * // Silently reject (return fake success to bot)\n * return { success: true };\n * }\n * ```\n */\n// eslint-disable-next-line @typescript-eslint/require-await -- public API: declared return type is `Promise<SpamCheckResult>` and callers `await` it; removing `async` would force a return-type change\nexport async function checkSpam(\n options: SpamCheckOptions\n): Promise<SpamCheckResult> {\n const { data, ipAddress, formSlug, config } = options;\n\n // 1. Honeypot check\n if (config.honeypot) {\n const honeypotResult = checkHoneypot(data);\n if (honeypotResult.isSpam) {\n return honeypotResult;\n }\n }\n\n // 2. Rate limiting check\n if (config.rateLimit && ipAddress) {\n const rateLimitResult = checkRateLimit(\n ipAddress,\n formSlug,\n config.rateLimit\n );\n if (rateLimitResult.isSpam) {\n return rateLimitResult;\n }\n }\n\n return { isSpam: false };\n}\n\n// ============================================================\n// Honeypot Detection\n// ============================================================\n\n/**\n * Standard honeypot field names to check.\n * These fields should be hidden from users but filled by bots.\n */\nconst HONEYPOT_FIELDS = [\n \"__honeypot\",\n \"_honeypot\",\n \"honeypot\",\n \"__hp\",\n \"_hp\",\n \"website\", // Common honeypot field name\n \"url_field\", // Another common honeypot\n \"fax_number\", // Rarely used by humans\n];\n\n/**\n * Check for honeypot field presence.\n *\n * Honeypot fields are hidden form fields that legitimate users\n * won't see or fill, but bots often fill automatically.\n *\n * @param data - Form submission data\n * @returns Spam check result\n */\nfunction checkHoneypot(data: Record<string, unknown>): SpamCheckResult {\n for (const fieldName of HONEYPOT_FIELDS) {\n const value = data[fieldName];\n\n // Check if honeypot field has a non-empty value\n if (value !== undefined && value !== null && value !== \"\") {\n let raw: string;\n if (typeof value === \"object\") {\n raw = JSON.stringify(value);\n } else {\n // eslint-disable-next-line @typescript-eslint/no-base-to-string -- value narrowed to primitive above; rule doesn't follow control flow on unknown\n raw = String(value);\n }\n const stringValue = raw.trim();\n if (stringValue.length > 0) {\n return {\n isSpam: true,\n reason: \"honeypot\",\n details: `Honeypot field \"${fieldName}\" was filled`,\n };\n }\n }\n }\n\n return { isSpam: false };\n}\n\n// ============================================================\n// Rate Limiting\n// ============================================================\n\n/**\n * Check rate limit for a given IP and form combination.\n *\n * Uses a sliding window approach where submissions are counted\n * within a time window. If the count exceeds the maximum,\n * further submissions are blocked.\n *\n * @param ipAddress - Submitter's IP address\n * @param formSlug - Form identifier\n * @param config - Rate limit configuration\n * @returns Spam check result\n */\nfunction checkRateLimit(\n ipAddress: string,\n formSlug: string,\n config: { maxSubmissions: number; windowMs: number }\n): SpamCheckResult {\n const key = `${formSlug}:${ipAddress}`;\n const now = Date.now();\n const entry = rateLimitStore.get(key);\n\n if (entry) {\n // Check if window has expired\n if (now - entry.timestamp > config.windowMs) {\n // Reset counter for new window\n rateLimitStore.set(key, { count: 1, timestamp: now });\n return { isSpam: false };\n }\n\n // Check if limit exceeded\n if (entry.count >= config.maxSubmissions) {\n return {\n isSpam: true,\n reason: \"rate_limit\",\n details: `Rate limit exceeded: ${entry.count}/${config.maxSubmissions} in ${config.windowMs}ms`,\n };\n }\n\n // Increment counter\n rateLimitStore.set(key, {\n count: entry.count + 1,\n timestamp: entry.timestamp,\n });\n } else {\n // First submission from this IP/form\n rateLimitStore.set(key, { count: 1, timestamp: now });\n }\n\n return { isSpam: false };\n}\n\n// ============================================================\n// Utility Functions\n// ============================================================\n\n/**\n * Clean up expired rate limit entries.\n *\n * Call this periodically (e.g., every 5 minutes) to prevent\n * memory leaks from accumulated rate limit entries.\n *\n * @param maxAgeMs - Maximum age of entries to keep (default: 5 minutes)\n * @returns Number of entries removed\n *\n * @example\n * ```typescript\n * // Set up periodic cleanup\n * setInterval(() => {\n * const removed = cleanupRateLimitStore();\n * console.log(`Cleaned up ${removed} expired rate limit entries`);\n * }, 5 * 60 * 1000); // Every 5 minutes\n * ```\n */\nexport function cleanupRateLimitStore(\n maxAgeMs: number = 5 * 60 * 1000\n): number {\n const now = Date.now();\n let removed = 0;\n\n for (const [key, entry] of rateLimitStore.entries()) {\n if (now - entry.timestamp > maxAgeMs) {\n rateLimitStore.delete(key);\n removed++;\n }\n }\n\n return removed;\n}\n\n/**\n * Get the current size of the rate limit store.\n *\n * Useful for monitoring and debugging.\n *\n * @returns Number of entries in the store\n */\nexport function getRateLimitStoreSize(): number {\n return rateLimitStore.size;\n}\n\n/**\n * Clear all rate limit entries.\n *\n * Useful for testing or resetting state.\n */\nexport function clearRateLimitStore(): void {\n rateLimitStore.clear();\n}\n\n/**\n * Check if a specific IP/form is currently rate limited.\n *\n * @param ipAddress - IP address to check\n * @param formSlug - Form slug\n * @param config - Rate limit configuration\n * @returns Whether the IP is currently rate limited\n */\nexport function isRateLimited(\n ipAddress: string,\n formSlug: string,\n config: { maxSubmissions: number; windowMs: number }\n): boolean {\n const key = `${formSlug}:${ipAddress}`;\n const now = Date.now();\n const entry = rateLimitStore.get(key);\n\n if (!entry) {\n return false;\n }\n\n // Check if window has expired\n if (now - entry.timestamp > config.windowMs) {\n return false;\n }\n\n return entry.count >= config.maxSubmissions;\n}\n\n// ============================================================\n// reCAPTCHA Support (Future Implementation)\n// ============================================================\n\n/**\n * Verify reCAPTCHA v3 token.\n *\n * Note: This is a placeholder for future implementation.\n * reCAPTCHA verification requires server-side API calls to Google.\n *\n * @param data - Form data containing reCAPTCHA token\n * @param config - reCAPTCHA configuration\n * @returns Spam check result\n */\n// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/require-await -- declared return type is `Promise<SpamCheckResult>`; placeholder for unimplemented Google siteverify fetch\nasync function verifyRecaptcha(\n _data: Record<string, unknown>,\n _config: { secretKey?: string; scoreThreshold?: number }\n): Promise<SpamCheckResult> {\n // Placeholder - always pass for now\n return { isSpam: false };\n}\n","/**\n * Form Submission Handler\n *\n * Processes form submissions including validation, spam detection,\n * storage, and notification triggering.\n *\n * @module handlers/submit-form\n * @since 0.1.0\n */\n\nimport type { PluginContext } from \"nextly\";\n\nimport type {\n FormDocument,\n SubmissionDocument,\n ResolvedFormBuilderConfig,\n AnyFormField,\n} from \"../types\";\nimport { normalizeFormSettings } from \"../utils/form-settings\";\nimport {\n generateZodSchema,\n transformFormData,\n getValidationErrors,\n} from \"../utils/generate-schema\";\n\nimport { checkSpam } from \"./spam-detection\";\n\n// ============================================================\n// Types\n// ============================================================\n\n/**\n * Options for submitting a form.\n */\nexport interface SubmitFormOptions {\n /** Form slug (URL-friendly identifier) */\n formSlug: string;\n\n /** Form submission data */\n data: Record<string, unknown>;\n\n /** Optional metadata about the submission */\n metadata?: {\n /** Submitter's IP address */\n ipAddress?: string;\n /** Submitter's user agent string */\n userAgent?: string;\n };\n}\n\n/**\n * Result of form submission.\n */\nexport interface SubmitFormResult {\n /** Whether the submission was successful */\n success: boolean;\n\n /** The created submission document (on success) */\n submission?: SubmissionDocument;\n\n /** Error message (on failure) */\n error?: string;\n\n /** Field-level validation errors */\n validationErrors?: Record<string, string>;\n\n /** Redirect URL (if form configured for redirect on success) */\n redirect?: string;\n}\n\n/**\n * Context for the submission handler.\n */\nexport interface SubmitFormContext {\n /** Plugin context with access to services */\n pluginContext: PluginContext;\n\n /** Resolved plugin configuration */\n pluginConfig: ResolvedFormBuilderConfig;\n}\n\n// ============================================================\n// Main Submission Handler\n// ============================================================\n\n/**\n * Process a form submission.\n *\n * This is the main entry point for handling form submissions.\n * It performs the following steps:\n *\n * 1. Fetch form configuration by slug\n * 2. Verify form status (must be \"published\")\n * 3. Transform and validate submission data\n * 4. Check for spam (honeypot, rate limiting)\n * 5. Create submission record in database\n * 6. Return result with redirect info if configured\n *\n * Note: Email notifications are triggered separately via collection\n * hooks in the submissions collection (see Phase 4.2).\n *\n * @param options - Submission options\n * @param context - Handler context with services\n * @returns Submission result\n *\n * @example\n * ```typescript\n * const result = await submitForm(\n * {\n * formSlug: 'contact-form',\n * data: { name: 'John', email: 'john@example.com', message: 'Hello!' },\n * metadata: { ipAddress: '127.0.0.1', userAgent: 'Mozilla/5.0...' },\n * },\n * { pluginContext, pluginConfig }\n * );\n *\n * if (result.success) {\n * console.log('Submission created:', result.submission?.id);\n * if (result.redirect) {\n * // Redirect user\n * }\n * } else {\n * console.error('Submission failed:', result.error);\n * }\n * ```\n */\nexport async function submitForm(\n options: SubmitFormOptions,\n context: SubmitFormContext\n): Promise<SubmitFormResult> {\n const { formSlug, data, metadata } = options;\n const { pluginContext, pluginConfig } = context;\n const { collections } = pluginContext.services;\n const { logger } = pluginContext;\n\n try {\n // 1. Fetch form configuration\n const form = await fetchFormBySlug(formSlug, pluginConfig, pluginContext);\n\n if (!form) {\n logger.warn?.(\"Form submission attempted for non-existent form\", {\n formSlug,\n });\n return {\n success: false,\n error: \"Form not found\",\n };\n }\n\n // 2. Check form status\n if (form.status !== \"published\") {\n logger.info?.(\"Form submission rejected - form not published\", {\n formSlug,\n status: form.status,\n });\n return {\n success: false,\n error: \"This form is not currently accepting submissions\",\n };\n }\n\n // 2b. Settings come through the one canonical reader — stored settings\n // may carry legacy keys from earlier builder versions.\n const settings = normalizeFormSettings(form.settings);\n\n // 2c. Single-submission forms: the same visitor (by IP) submits once.\n // Checked before spam so a repeat visitor gets the honest message\n // instead of an inexplicable success that stored nothing new. This is a\n // best-effort deterrent, not a guarantee: IPs are shared (NAT/proxies),\n // and the check-then-create pair is not atomic, so two truly concurrent\n // submissions can both pass — an airtight version needs a partial\n // unique index the portable field config cannot express yet.\n if (settings.allowMultipleSubmissions === false && !metadata?.ipAddress) {\n // Without an identifier there is nothing to deduplicate on. Failing\n // closed would block every server-to-server integration, so the gate\n // stays open — loudly.\n logger.warn?.(\n \"Single-submission form received a submission without an IP — the restriction cannot apply\",\n { formSlug }\n );\n }\n if (settings.allowMultipleSubmissions === false && metadata?.ipAddress) {\n const existing = await collections.count(\n pluginConfig.formSubmissionOverrides.slug,\n {\n where: {\n form: { equals: form.id },\n ipAddress: { equals: metadata.ipAddress },\n status: { not_equals: \"spam\" },\n },\n },\n { as: \"system\" }\n );\n if (existing > 0) {\n logger.info?.(\"Repeat submission rejected (single-submission form)\", {\n formSlug,\n ipAddress: metadata.ipAddress,\n });\n return {\n success: false,\n error: \"You have already submitted this form.\",\n };\n }\n }\n\n // 3. Spam detection — BEFORE validation, so a bot that trips the trap\n // while also failing validation still receives the same fake success as\n // any other bot instead of a distinguishable validation error.\n // Honeypot fields are by definition NOT declared form fields, so the\n // probe is the raw payload minus the declared schema — a declared field\n // (e.g. a real \"website\" input) must never trip the trap.\n const declaredFieldNames = new Set(form.fields.map(field => field.name));\n const undeclaredData = Object.fromEntries(\n Object.entries(data).filter(([key]) => !declaredFieldNames.has(key))\n );\n\n const spamResult = await checkSpam({\n data: undeclaredData,\n ipAddress: metadata?.ipAddress,\n formSlug,\n config: {\n // Per-form overrides win where set; blank inherits the plugin config.\n honeypot:\n settings.honeypotEnabled ?? pluginConfig.spamProtection.honeypot,\n rateLimit: pluginConfig.spamProtection.rateLimit,\n recaptcha: {\n ...pluginConfig.spamProtection.recaptcha,\n enabled:\n settings.captchaEnabled ??\n pluginConfig.spamProtection.recaptcha?.enabled ??\n false,\n },\n },\n });\n\n // Rate-limit hits are pure volume — storing them would turn the limiter\n // into a database DoS, so they are rejected without a trace (still with\n // fake success so the client learns nothing).\n if (spamResult.isSpam && spamResult.reason === \"rate_limit\") {\n logger.info?.(\"Spam submission rejected (rate limit)\", {\n formSlug,\n ipAddress: metadata?.ipAddress,\n });\n return { success: true };\n }\n\n const isContentSpam = spamResult.isSpam;\n\n // 4. Transform + validate. Content spam skips validation entirely — it\n // is stored for review exactly as the bot shaped it (declared fields\n // only, sanitized), and requiring validity would drop the evidence.\n const transformedData = transformFormData(data, form.fields);\n let storedData: Record<string, unknown>;\n\n if (isContentSpam) {\n logger.info?.(\"Spam submission detected — storing flagged\", {\n formSlug,\n reason: spamResult.reason,\n ipAddress: metadata?.ipAddress,\n });\n sanitizeSubmissionData(transformedData, form.fields);\n storedData = transformedData;\n } else {\n const schema = generateZodSchema(form.fields);\n const validationResult = schema.safeParse(transformedData);\n\n if (!validationResult.success) {\n const validationErrors = getValidationErrors(validationResult);\n logger.debug?.(\"Form validation failed\", {\n formSlug,\n errors: validationErrors,\n });\n return {\n success: false,\n error: \"Validation failed\",\n validationErrors,\n };\n }\n\n // Sanitize validated submission data (strip HTML from free-text fields)\n sanitizeSubmissionData(validationResult.data, form.fields);\n storedData = validationResult.data;\n }\n\n // 5. Create submission record. Content spam (honeypot/recaptcha) is\n // stored FLAGGED, never silently dropped: a false positive stays\n // reviewable in the Spam view and recoverable via \"Not spam\".\n const submissionData = {\n form: form.id,\n data: storedData,\n status: isContentSpam ? (\"spam\" as const) : (\"new\" as const),\n spamReason: isContentSpam ? (spamResult.reason ?? null) : null,\n ipAddress: metadata?.ipAddress || null,\n userAgent: metadata?.userAgent || null,\n submittedAt: new Date(),\n };\n\n const submission = await collections.createEntry(\n pluginConfig.formSubmissionOverrides.slug,\n submissionData,\n // Public form submission — create as system. No ambient user; an\n // empty context already resolves to system, but be explicit.\n { as: \"system\" }\n );\n\n logger.info?.(\"Form submission created successfully\", {\n formSlug,\n submissionId: submission.id,\n flaggedAsSpam: isContentSpam,\n });\n\n // Email notifications are sent via the afterCreate hook registered in\n // plugin.ts init(), so they fire for all submission paths (HTTP + direct).\n // The hook itself skips spam-flagged rows.\n\n // 6. Determine redirect URL. Spam gets the same success shape as a real\n // submission (minus the stored row reference) so bots can't diff the two.\n const redirect = determineRedirectUrl(form);\n\n if (isContentSpam) {\n return { success: true, redirect };\n }\n\n return {\n success: true,\n submission: submission as unknown as SubmissionDocument,\n redirect,\n };\n } catch (error) {\n logger.error?.(\"Form submission error\", {\n formSlug,\n error: error instanceof Error ? error.message : String(error),\n });\n\n return {\n success: false,\n error: \"An error occurred processing your submission. Please try again.\",\n };\n }\n}\n\n// ============================================================\n// Helper Functions\n// ============================================================\n\n/**\n * Fetch a form by its slug via a service-level `where` query.\n *\n * @param slug - Form slug\n * @param pluginConfig - Plugin configuration\n * @param pluginContext - Plugin context\n * @returns Form document or null if not found\n */\nexport async function fetchFormBySlug(\n slug: string,\n pluginConfig: ResolvedFormBuilderConfig,\n pluginContext: PluginContext\n): Promise<FormDocument | null> {\n try {\n const { collections } = pluginContext.services;\n\n // D56: resolve the form by slug via a service-level `where` query\n // instead of fetching every form and filtering client-side. Forms are\n // public config the plugin owns, so the read runs as system.\n const result = await collections.listEntries(\n pluginConfig.formOverrides.slug,\n { where: { slug: { equals: slug } }, pagination: { limit: 1 } },\n { as: \"system\" }\n );\n\n const form = result.data?.[0];\n return form ? (form as unknown as FormDocument) : null;\n } catch {\n return null;\n }\n}\n\n/**\n * Determine the redirect URL based on form settings.\n *\n * @param form - Form document\n * @returns Redirect URL or undefined\n */\nfunction determineRedirectUrl(form: FormDocument): string | undefined {\n if (!form.settings) {\n return undefined;\n }\n\n // Check confirmation type\n if (form.settings.confirmationType !== \"redirect\") {\n return undefined;\n }\n\n // Direct URL redirect\n if (form.settings.redirectUrl) {\n return form.settings.redirectUrl;\n }\n\n // Relationship-based redirect (requires additional lookup)\n // Note: For relationship redirects, the admin UI should resolve\n // the URL before saving to redirectUrl, or the frontend should\n // handle the relationship lookup.\n if (form.settings.redirectRelation) {\n // Return a placeholder that the frontend can resolve\n // Format: relationship://{collection}/{id}\n const { relationTo, value } = form.settings.redirectRelation;\n return `relationship://${relationTo}/${value}`;\n }\n\n return undefined;\n}\n\n// ============================================================\n// Validation-Only Function\n// ============================================================\n\n/**\n * Validate form data without creating a submission.\n *\n * Useful for real-time validation in the frontend.\n *\n * @param formSlug - Form slug\n * @param data - Form data to validate\n * @param context - Handler context\n * @returns Validation result\n *\n * @example\n * ```typescript\n * const result = await validateSubmission(\n * 'contact-form',\n * { name: 'John', email: 'invalid-email' },\n * { pluginContext, pluginConfig }\n * );\n *\n * if (!result.valid) {\n * console.log('Validation errors:', result.errors);\n * }\n * ```\n */\nexport async function validateSubmission(\n formSlug: string,\n data: Record<string, unknown>,\n context: SubmitFormContext\n): Promise<{ valid: boolean; errors?: Record<string, string> }> {\n const { pluginContext, pluginConfig } = context;\n\n // Fetch form\n const form = await fetchFormBySlug(formSlug, pluginConfig, pluginContext);\n\n if (!form) {\n return { valid: false, errors: { _form: \"Form not found\" } };\n }\n\n // Transform and validate\n const transformedData = transformFormData(data, form.fields);\n const schema = generateZodSchema(form.fields);\n const result = schema.safeParse(transformedData);\n\n if (result.success) {\n return { valid: true };\n }\n\n return {\n valid: false,\n errors: getValidationErrors(result),\n };\n}\n\n// ============================================================\n// Form Status Check\n// ============================================================\n\n/**\n * Check if a form is accepting submissions.\n *\n * @param formSlug - Form slug\n * @param context - Handler context\n * @returns Whether the form accepts submissions\n */\nexport async function isFormAcceptingSubmissions(\n formSlug: string,\n context: SubmitFormContext\n): Promise<boolean> {\n const { pluginContext, pluginConfig } = context;\n\n const form = await fetchFormBySlug(formSlug, pluginConfig, pluginContext);\n\n if (!form) {\n return false;\n }\n\n return form.status === \"published\";\n}\n\n// ============================================================\n// Submission Data Sanitization\n// ============================================================\n\n/**\n * Form field types that accept free-text string input from end users.\n * These fields can contain HTML injection vectors and must be sanitized.\n *\n * Fields NOT in this set (select, radio, checkbox, number, date, time, file)\n * are constrained by Zod enum/type validation and don't need sanitization.\n */\nconst TEXT_FORM_FIELDS = new Set([\n \"text\",\n \"email\",\n \"textarea\",\n \"phone\",\n \"url\",\n \"hidden\",\n]);\n\n/**\n * Remove all HTML tags from a string, collapse whitespace, and trim.\n *\n * Uses a regex that matches both complete tags (`<b>`) and unclosed tags\n * at end-of-string (`<script`) to prevent browsers from interpreting\n * incomplete markup.\n */\nfunction stripHtmlTags(input: string): string {\n return input\n .replace(/<[^>]*(?:>|$)/g, \"\")\n .replace(/\\s+/g, \" \")\n .trim();\n}\n\n/**\n * Sanitize form submission data by stripping HTML tags from free-text fields.\n *\n * Iterates over the form's field definitions and applies `stripHtmlTags()`\n * to values whose field type is in `TEXT_FORM_FIELDS`. Non-string values\n * and constrained fields (select, radio, checkbox, etc.) are left unchanged.\n *\n * Mutates the data object in place for efficiency.\n *\n * @param data - Validated submission data (mutated in place)\n * @param fields - Form field definitions (used for type-aware dispatch)\n */\nfunction sanitizeSubmissionData(\n data: Record<string, unknown>,\n fields: AnyFormField[]\n): void {\n for (const field of fields) {\n // Plugin field types are not in TEXT_FORM_FIELDS, so they skip naturally.\n if (!TEXT_FORM_FIELDS.has(field.type)) continue;\n\n const value = data[field.name];\n if (typeof value !== \"string\") continue;\n\n data[field.name] = stripHtmlTags(value);\n }\n}\n\n// ============================================================\n// Bulk Operations (Admin)\n// ============================================================\n\n/**\n * Get submission statistics for a form.\n *\n * Note: This function uses client-side filtering which works for\n * small-medium submission counts. For large-scale deployments,\n * consider using direct database queries with COUNT aggregation.\n *\n * @param formSlug - Form slug\n * @param context - Handler context\n * @returns Submission statistics\n */\nexport async function getFormSubmissionStats(\n formSlug: string,\n context: SubmitFormContext\n): Promise<{\n total: number;\n new: number;\n read: number;\n archived: number;\n} | null> {\n const { pluginContext, pluginConfig } = context;\n const { collections } = pluginContext.services;\n\n // Get form ID\n const form = await fetchFormBySlug(formSlug, pluginConfig, pluginContext);\n if (!form) {\n return null;\n }\n\n try {\n const submissionsSlug = pluginConfig.formSubmissionOverrides.slug;\n const sys = { as: \"system\" } as const;\n const base = { form: { equals: form.id } };\n\n // D56: count per status server-side instead of listing every\n // submission and counting client-side. Stats run as system (plugin-owned\n // aggregate over submissions for this form).\n const [total, newCount, read, archived] = await Promise.all([\n collections.count(submissionsSlug, { where: base }, sys),\n collections.count(\n submissionsSlug,\n { where: { ...base, status: { equals: \"new\" } } },\n sys\n ),\n collections.count(\n submissionsSlug,\n { where: { ...base, status: { equals: \"read\" } } },\n sys\n ),\n collections.count(\n submissionsSlug,\n { where: { ...base, status: { equals: \"archived\" } } },\n sys\n ),\n ]);\n\n return { total, new: newCount, read, archived };\n } catch {\n return null;\n }\n}\n","/**\n * Webhook Handler\n *\n * Triggers webhook notifications for form events.\n * Webhooks are fired asynchronously and do not block form submission.\n *\n * @module handlers/webhooks\n * @since 0.1.0\n */\n\nimport { safeFetch, ExternalUrlError } from \"nextly\";\n\nimport type {\n FormDocument,\n SubmissionDocument,\n WebhookConfig,\n WebhookEvent,\n} from \"../types\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\n/**\n * Webhook payload sent to configured endpoints.\n */\nexport interface WebhookPayload {\n /** Event type that triggered the webhook */\n event: WebhookEvent;\n\n /** ISO 8601 timestamp when the event occurred */\n timestamp: string;\n\n /** Form information */\n form: {\n id: string;\n slug: string;\n name: string;\n };\n\n /** Submission information (full data or minimal based on config) */\n submission:\n | SubmissionDocument\n | {\n id: string;\n submittedAt: Date;\n };\n}\n\n/**\n * Options for triggering webhooks.\n */\nexport interface TriggerWebhooksOptions {\n /** Event type */\n event: WebhookEvent;\n\n /** Form document */\n form: FormDocument;\n\n /** Submission document */\n submission: SubmissionDocument;\n}\n\n/**\n * Result of a single webhook delivery attempt.\n */\nexport interface WebhookDeliveryResult {\n /** Webhook URL */\n url: string;\n\n /** Whether the delivery was successful */\n success: boolean;\n\n /** HTTP status code (if request completed) */\n statusCode?: number;\n\n /** Error message (if delivery failed) */\n error?: string;\n\n /** Time taken in milliseconds */\n durationMs: number;\n}\n\n/**\n * Result of triggering webhooks for an event.\n */\nexport interface TriggerWebhooksResult {\n /** Number of webhooks triggered */\n triggered: number;\n\n /** Number of successful deliveries */\n successful: number;\n\n /** Number of failed deliveries */\n failed: number;\n\n /** Individual delivery results */\n results: WebhookDeliveryResult[];\n}\n\n// ============================================================================\n// Constants\n// ============================================================================\n\n/** Default timeout for webhook requests (10 seconds) */\nconst WEBHOOK_TIMEOUT_MS = 10_000;\n\n/** Header name for webhook signature */\nconst SIGNATURE_HEADER = \"X-Webhook-Signature\";\n\n/** Header name for event type */\nconst EVENT_HEADER = \"X-Webhook-Event\";\n\n/** Header name for delivery ID (submission ID) */\nconst DELIVERY_ID_HEADER = \"X-Webhook-Delivery-ID\";\n\n// ============================================================================\n// Signature Generation\n// ============================================================================\n\n/**\n * Generate HMAC-SHA256 signature for webhook payload.\n *\n * @param payload - The webhook payload as a string\n * @param secret - The secret key for signing\n * @returns Signature in format \"sha256=<hex>\"\n */\nasync function generateSignature(\n payload: string,\n secret: string\n): Promise<string> {\n // Use Web Crypto API (works in both Node.js 18+ and browsers)\n const encoder = new TextEncoder();\n const keyData = encoder.encode(secret);\n const messageData = encoder.encode(payload);\n\n // Import the key\n const cryptoKey = await crypto.subtle.importKey(\n \"raw\",\n keyData,\n { name: \"HMAC\", hash: \"SHA-256\" },\n false,\n [\"sign\"]\n );\n\n // Sign the message\n const signature = await crypto.subtle.sign(\"HMAC\", cryptoKey, messageData);\n\n // Convert to hex string\n const hashArray = Array.from(new Uint8Array(signature));\n const hashHex = hashArray.map(b => b.toString(16).padStart(2, \"0\")).join(\"\");\n\n return `sha256=${hashHex}`;\n}\n\n// ============================================================================\n// Webhook Delivery\n// ============================================================================\n\n/**\n * Send a single webhook request.\n *\n * @param webhook - Webhook configuration\n * @param payload - Webhook payload\n * @returns Delivery result\n */\nasync function sendWebhook(\n webhook: WebhookConfig,\n payload: WebhookPayload\n): Promise<WebhookDeliveryResult> {\n const startTime = Date.now();\n const payloadString = JSON.stringify(payload);\n\n try {\n // Build headers\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n [EVENT_HEADER]: payload.event,\n [DELIVERY_ID_HEADER]: payload.submission.id,\n ...webhook.headers,\n };\n\n // Add signature if secret is configured\n if (webhook.secret) {\n headers[SIGNATURE_HEADER] = await generateSignature(\n payloadString,\n webhook.secret\n );\n }\n\n // Create abort controller for timeout\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), WEBHOOK_TIMEOUT_MS);\n\n try {\n const response = await safeFetch(webhook.url, {\n method: webhook.method || \"POST\",\n headers,\n body: payloadString,\n signal: controller.signal,\n allowLocalhost: process.env.NODE_ENV !== \"production\",\n });\n\n clearTimeout(timeoutId);\n\n const durationMs = Date.now() - startTime;\n\n // Consider 2xx status codes as success\n if (response.ok) {\n return {\n url: webhook.url,\n success: true,\n statusCode: response.status,\n durationMs,\n };\n }\n\n return {\n url: webhook.url,\n success: false,\n statusCode: response.status,\n error: `HTTP ${response.status}: ${response.statusText}`,\n durationMs,\n };\n } finally {\n clearTimeout(timeoutId);\n }\n } catch (error) {\n const durationMs = Date.now() - startTime;\n\n // Handle abort (timeout)\n if (error instanceof Error && error.name === \"AbortError\") {\n return {\n url: webhook.url,\n success: false,\n error: `Timeout after ${WEBHOOK_TIMEOUT_MS}ms`,\n durationMs,\n };\n }\n\n // The webhook URL pointed at an internal / non-public address\n // and was refused before the request left the host. Surface as a\n // distinct delivery failure.\n if (error instanceof ExternalUrlError) {\n return {\n url: webhook.url,\n success: false,\n error: `Webhook URL rejected for SSRF safety: ${error.message}`,\n durationMs,\n };\n }\n\n // Handle other errors\n return {\n url: webhook.url,\n success: false,\n error: error instanceof Error ? error.message : String(error),\n durationMs,\n };\n }\n}\n\n// ============================================================================\n// Main Webhook Trigger Function\n// ============================================================================\n\n/**\n * Trigger webhooks for a form event.\n *\n * This function fires all matching webhooks asynchronously and does not\n * block form submission. Failed webhooks are logged but do not cause\n * submission failures.\n *\n * @param options - Trigger options\n * @returns Result of webhook triggering\n *\n * @example\n * ```typescript\n * // After creating a submission\n * const result = await triggerWebhooks({\n * event: 'submission.created',\n * form,\n * submission,\n * });\n *\n * console.log(`Triggered ${result.triggered} webhooks`);\n * console.log(`Successful: ${result.successful}, Failed: ${result.failed}`);\n * ```\n */\nexport async function triggerWebhooks(\n options: TriggerWebhooksOptions\n): Promise<TriggerWebhooksResult> {\n const { event, form, submission } = options;\n\n // Get webhooks configured on the form\n const webhooks = form.webhooks || [];\n\n // Filter webhooks that subscribe to this event\n const relevantWebhooks = webhooks.filter(w => w.events.includes(event));\n\n if (relevantWebhooks.length === 0) {\n return {\n triggered: 0,\n successful: 0,\n failed: 0,\n results: [],\n };\n }\n\n // Build payloads and send webhooks in parallel\n const deliveryPromises = relevantWebhooks.map(async webhook => {\n // Build payload based on includeData setting\n const payload: WebhookPayload = {\n event,\n timestamp: new Date().toISOString(),\n form: {\n id: form.id,\n slug: form.slug,\n name: form.name,\n },\n submission:\n webhook.includeData !== false\n ? submission\n : {\n id: submission.id,\n submittedAt: submission.submittedAt,\n },\n };\n\n return sendWebhook(webhook, payload);\n });\n\n // Wait for all webhooks to complete (or fail)\n const results = await Promise.all(deliveryPromises);\n\n // Count successes and failures\n const successful = results.filter(r => r.success).length;\n const failed = results.filter(r => !r.success).length;\n\n return {\n triggered: relevantWebhooks.length,\n successful,\n failed,\n results,\n };\n}\n\n// ============================================================================\n// Fire-and-Forget Helper\n// ============================================================================\n\n/**\n * Trigger webhooks without waiting for completion.\n *\n * This is the recommended way to call webhooks from submission handlers\n * as it doesn't block the response. Errors are logged but not thrown.\n *\n * @param options - Trigger options\n * @param logger - Optional logger for error reporting\n *\n * @example\n * ```typescript\n * // In submission handler - fire and forget\n * fireWebhooks(\n * { event: 'submission.created', form, submission },\n * pluginContext.logger\n * );\n *\n * // Continue with response immediately\n * return { success: true, submission };\n * ```\n */\nexport function fireWebhooks(\n options: TriggerWebhooksOptions,\n logger?: {\n info?: (message: string, data?: Record<string, unknown>) => void;\n warn?: (message: string, data?: Record<string, unknown>) => void;\n error?: (message: string, data?: Record<string, unknown>) => void;\n }\n): void {\n // Fire webhooks in background\n triggerWebhooks(options)\n .then(result => {\n if (result.triggered > 0) {\n if (result.failed > 0) {\n logger?.warn?.(\"Some webhooks failed\", {\n event: options.event,\n formSlug: options.form.slug,\n submissionId: options.submission.id,\n triggered: result.triggered,\n successful: result.successful,\n failed: result.failed,\n errors: result.results\n .filter(r => !r.success)\n .map(r => ({ url: r.url, error: r.error })),\n });\n } else {\n logger?.info?.(\"Webhooks triggered successfully\", {\n event: options.event,\n formSlug: options.form.slug,\n submissionId: options.submission.id,\n count: result.triggered,\n });\n }\n }\n })\n .catch(error => {\n // This shouldn't happen since triggerWebhooks catches all errors,\n // but handle it just in case\n logger?.error?.(\"Unexpected error triggering webhooks\", {\n event: options.event,\n formSlug: options.form.slug,\n submissionId: options.submission.id,\n error: error instanceof Error ? error.message : String(error),\n });\n });\n}\n\n// ============================================================================\n// Utility Functions\n// ============================================================================\n\n/**\n * Validate a webhook URL.\n *\n * @param url - URL to validate\n * @returns Whether the URL is valid for webhooks\n */\nexport function isValidWebhookUrl(url: string): boolean {\n try {\n const parsed = new URL(url);\n // Only allow HTTPS in production (HTTP allowed for localhost)\n if (\n parsed.protocol !== \"https:\" &&\n !parsed.hostname.match(/^(localhost|127\\.0\\.0\\.1)$/)\n ) {\n return false;\n }\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Get the list of supported webhook events.\n *\n * @returns Array of supported event types\n */\nexport function getSupportedWebhookEvents(): WebhookEvent[] {\n return [\"submission.created\", \"submission.updated\", \"submission.deleted\"];\n}\n","/**\n * @nextly/plugin-form-builder\n *\n * Visual form builder plugin for Nextly CMS.\n * Provides drag-and-drop form building, submission management,\n * email notifications, and spam protection.\n *\n * @packageDocumentation\n */\n\n// ============================================================\n// Main Plugin Export\n// ============================================================\n\n// ============================================================\n// Default Plugin Instance\n// ============================================================\n\n/**\n * Pre-configured form builder plugin for simple usage.\n *\n * This is a default instance with all defaults applied.\n * For customization, use `formBuilder()` instead.\n *\n * @example Simple usage - just add the plugin!\n * ```typescript\n * // nextly.config.ts\n * import { defineConfig } from 'nextly';\n * import { formBuilderPlugin } from '@nextly/plugin-form-builder';\n *\n * export default defineConfig({\n * plugins: [formBuilderPlugin],\n * collections: [Posts, Users, Media], // Your collections only\n * });\n * ```\n *\n * @example With customization\n * ```typescript\n * import { formBuilder } from '@nextly/plugin-form-builder';\n *\n * const myFormPlugin = formBuilder({\n * notifications: { defaultFrom: 'noreply@example.com' },\n * });\n *\n * export default defineConfig({\n * plugins: [myFormPlugin.plugin],\n * });\n * ```\n */\nimport { formBuilder } from \"./plugin\";\n\nexport {\n formBuilder,\n getFormBuilderConfig,\n type NextlyPlugin,\n type FormBuilderPluginResult,\n} from \"./plugin\";\nexport const formBuilderPlugin = formBuilder().plugin;\n\n// ============================================================\n// Type Exports\n// ============================================================\n\n// Plugin configuration types\nexport type {\n FormBuilderPluginOptions,\n ResolvedFormBuilderConfig,\n FieldBlockConfig,\n} from \"./types\";\n\n// Form field types (discriminated union)\nexport type {\n FormField,\n FormFieldType,\n FormFieldTypeId,\n AnyFormField,\n CustomFormField,\n BaseFormField,\n TextFormField,\n EmailFormField,\n NumberFormField,\n PhoneFormField,\n UrlFormField,\n TextareaFormField,\n SelectFormField,\n CheckboxFormField,\n RadioFormField,\n FileFormField,\n DateFormField,\n TimeFormField,\n HiddenFormField,\n} from \"./types\";\nexport { isKnownFormField, BUILT_IN_FORM_FIELD_TYPES } from \"./types\";\n\n// Form configuration types\nexport type {\n FormConfig,\n FormSettings,\n FormNotifications,\n FormAccess,\n FormHooks,\n FormHookContext,\n} from \"./types\";\n\n// Document types\nexport type { FormDocument, SubmissionDocument, FormSubmission } from \"./types\";\n\n// Notification rule type (stored on the form; edited by the builder UI)\nexport type { FormNotification, FormNotificationItem } from \"./types\";\n\n// Validation and conditional logic types\nexport type {\n ValidationRules,\n ConditionalLogic,\n ConditionalLogicCondition,\n} from \"./types\";\n\n// Email types\nexport type { EmailConfig } from \"./types\";\n\n// Webhook types\nexport type { WebhookConfig, WebhookEvent } from \"./types\";\n\n// ============================================================\n// Utility Exports\n// ============================================================\n\n// Type guards\nexport {\n isFormField,\n isFormFieldType,\n isTextFormField,\n isEmailFormField,\n isSelectFormField,\n isCheckboxFormField,\n isFileFormField,\n} from \"./types\";\n\n// ============================================================\n// Config Utilities (validation & defaults)\n// ============================================================\n\n// Validation utilities\nexport {\n validateFormConfig,\n assertValidFormConfig,\n RESERVED_FORM_SLUGS,\n type FormValidationError,\n type FormValidationErrorCode,\n type FormValidationResult,\n} from \"./config\";\n\n// Default application utilities\nexport {\n applyFormDefaults,\n applyFieldDefaults,\n createFormConfig,\n toTitleCase,\n pluralize,\n DEFAULT_FORM_SETTINGS,\n DEFAULT_NOTIFICATION_SETTINGS,\n} from \"./config\";\n\n// ============================================================\n// Field Helper Functions\n// ============================================================\n\n// Utility helper\nexport { option, type FormFieldOption } from \"./config\";\n\n// Text input field helpers\nexport { text, email, phone, url, textarea } from \"./config\";\n\n// Numeric field helpers\nexport { number } from \"./config\";\n\n// Selection field helpers\nexport { select, checkbox, radio } from \"./config\";\n\n// File field helpers\nexport { file } from \"./config\";\n\n// Date/time field helpers\nexport { date, time } from \"./config\";\n\n// Special field helpers\nexport { hidden } from \"./config\";\n\n// ============================================================\n// Conditional Logic Utilities\n// ============================================================\n\n// Conditional logic evaluation\nexport {\n evaluateConditions,\n isValidComparisonOperator,\n getSupportedComparisonOperators,\n type ComparisonOperator,\n} from \"./utils\";\n\n// ============================================================\n// Schema Generation & Validation Utilities\n// ============================================================\n\n// Zod schema generation for form validation\nexport {\n generateZodSchema,\n transformFormData,\n validateFormData,\n getValidationErrors,\n} from \"./utils\";\n\n// ============================================================\n// Export Utilities (CSV, JSON)\n// ============================================================\n\n// Export format utilities\nexport {\n exportToCSV,\n exportToJSON,\n formatExportValue,\n downloadFile,\n generateExportFilename,\n exportAndDownload,\n type CSVExportOptions,\n type JSONExportOptions,\n type ExportedJSON,\n} from \"./utils\";\n\n// ============================================================\n// Form Submission Handlers\n// ============================================================\n\n// Main submission handler\nexport {\n submitForm,\n validateSubmission,\n isFormAcceptingSubmissions,\n getFormSubmissionStats,\n type SubmitFormOptions,\n type SubmitFormResult,\n type SubmitFormContext,\n} from \"./handlers\";\n\n// Spam detection utilities\nexport {\n checkSpam,\n cleanupRateLimitStore,\n getRateLimitStoreSize,\n clearRateLimitStore,\n isRateLimited,\n type SpamCheckConfig,\n type SpamCheckOptions,\n type SpamCheckResult,\n} from \"./handlers\";\n\n// Webhook utilities\nexport {\n triggerWebhooks,\n fireWebhooks,\n isValidWebhookUrl,\n getSupportedWebhookEvents,\n type WebhookPayload,\n type TriggerWebhooksOptions,\n type WebhookDeliveryResult,\n type TriggerWebhooksResult,\n} from \"./handlers\";\n\n// ============================================================\n// Collection Exports (for advanced usage)\n// ============================================================\n\nexport { formsCollection, submissionsCollection } from \"./collections\";\n"],"mappings":"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","names":["text","textarea","select","json","relationship","url","email","text","textarea","select","checkbox","date","schema","date","url"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nextlyhq/plugin-form-builder",
|
|
3
|
-
"version": "0.0.2-alpha.
|
|
3
|
+
"version": "0.0.2-alpha.42",
|
|
4
4
|
"description": "Visual form builder plugin for Nextly with Code-First and Schema Builder support",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -31,14 +31,14 @@
|
|
|
31
31
|
"zod": "^4.1.12"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
|
-
"@nextlyhq/admin": "0.0.2-alpha.
|
|
34
|
+
"@nextlyhq/admin": "0.0.2-alpha.42",
|
|
35
35
|
"@tanstack/react-query": ">=5.0.0",
|
|
36
36
|
"lucide-react": ">=0.400.0",
|
|
37
37
|
"next": "^14.0.0 || ^15.0.0 || ^16.0.0",
|
|
38
|
-
"nextly": "0.0.2-alpha.
|
|
38
|
+
"nextly": "0.0.2-alpha.42",
|
|
39
39
|
"react": "^18.0.0 || ^19.0.0",
|
|
40
|
-
"@nextlyhq/plugin-sdk": "0.0.2-alpha.
|
|
41
|
-
"@nextlyhq/ui": "^0.0.2-alpha.
|
|
40
|
+
"@nextlyhq/plugin-sdk": "0.0.2-alpha.42",
|
|
41
|
+
"@nextlyhq/ui": "^0.0.2-alpha.42"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@tanstack/react-query": "^5.62.0",
|
|
@@ -48,11 +48,11 @@
|
|
|
48
48
|
"tsup": "^8.5.0",
|
|
49
49
|
"typescript": "^5.9.3",
|
|
50
50
|
"vitest": "^4.1.0",
|
|
51
|
-
"@nextlyhq/admin": "0.0.2-alpha.
|
|
52
|
-
"@nextlyhq/plugin-sdk": "0.0.2-alpha.
|
|
53
|
-
"@nextlyhq/tsconfig": "0.0.2-alpha.
|
|
54
|
-
"@nextlyhq/ui": "0.0.2-alpha.
|
|
55
|
-
"nextly": "0.0.2-alpha.
|
|
51
|
+
"@nextlyhq/admin": "0.0.2-alpha.42",
|
|
52
|
+
"@nextlyhq/plugin-sdk": "0.0.2-alpha.42",
|
|
53
|
+
"@nextlyhq/tsconfig": "0.0.2-alpha.42",
|
|
54
|
+
"@nextlyhq/ui": "0.0.2-alpha.42",
|
|
55
|
+
"nextly": "0.0.2-alpha.42"
|
|
56
56
|
},
|
|
57
57
|
"engines": {
|
|
58
58
|
"node": ">=20.0.0"
|