@nextlyhq/plugin-form-builder 0.0.2-alpha.60 → 0.0.2-alpha.62
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/dist/admin/index.d.ts +18 -2
- package/dist/admin/index.js +1796 -1306
- package/dist/admin/index.js.map +1 -1
- package/dist/{chunk-XCICDELK.js → chunk-H5X3FKOZ.js} +131 -1
- package/dist/chunk-H5X3FKOZ.js.map +1 -0
- package/dist/index.d.ts +3 -18
- package/dist/index.js +601 -166
- package/dist/index.js.map +1 -1
- package/dist/{types-DuEFdLNU.d.ts → types-hLfLe81K.d.ts} +122 -75
- package/package.json +11 -11
- package/dist/chunk-XCICDELK.js.map +0 -1
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/plugin.ts","../src/collections/forms.ts","../src/collections/submissions.ts","../src/document-shapes.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 {\n asFormDocument,\n asSubmissionDocument,\n asSubmissionDocuments,\n} from \"./document-shapes\";\nimport type {\n BeforeEmailFilterContext,\n FormNotification,\n FormBuilderPluginOptions,\n FormEmailNotification,\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 asSubmissionDocuments(items),\n asFormDocument(form)\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: asFormDocument(ctx.form),\n submission: asSubmissionDocument(ctx.submission),\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 { NextlyError } from \"@nextlyhq/plugin-sdk\";\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 // A form must have at least one field, checked against what the\n // write actually sets. An update carries the patch rather than the\n // merged document, so treating an absent `fields` as empty rejects\n // every partial update -- renaming a form, or changing a setting --\n // even though its fields are untouched and still there.\n const setsFields =\n operation === \"create\" || data?.fields !== undefined;\n if (\n data &&\n setsFields &&\n (data.fields === undefined ||\n (Array.isArray(data.fields) && data.fields.length === 0))\n ) {\n // Typed, so the rejection survives as a validation failure with\n // its field issue rather than being read as a crash and replaced\n // with a generic server-fault message.\n throw NextlyError.validation({\n errors: [\n {\n path: \"fields\",\n code: \"REQUIRED\",\n message: \"Form must have at least one field.\",\n },\n ],\n });\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 answers plus ipAddress/userAgent, so the\n // default `entry.*` events stay suppressed — otherwise every submission would\n // be delivered to any endpoint subscribed to `entry.created` or `*`. Instead\n // emit a curated `form.submission.created` carrying ONLY safe metadata (which\n // form, when, and the status) — never the answers, IP, or user agent.\n webhooks: {\n record: false,\n emit: {\n event: \"form.submission.created\",\n fields: [\"form\", \"submittedAt\", \"status\"],\n },\n },\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 * Reading a stored row as one of this plugin's documents.\n *\n * The services layer answers with a loose row -- `id`, `createdAt`, `updatedAt`\n * and an `unknown` index signature -- while this plugin declares concrete\n * documents. There is no overlap for TypeScript to check between the two, so\n * the conversion goes through `unknown` and **nothing here is verified at\n * compile time**. A single assertion does not compile:\n *\n * ```\n * error TS2352: Conversion of type 'CollectionEntry' to type\n * 'SubmissionDocument' may be a mistake because neither type sufficiently\n * overlaps with the other.\n * ```\n *\n * The parameters are therefore `unknown`, which is what the rows honestly are\n * at this boundary. One assertion from `unknown` is legal where two from a\n * partially-known shape were needed, so the widening removes a cast rather\n * than adding one -- but it removes the notation, not the risk.\n *\n * The point of this module is therefore not safety it cannot provide. It is\n * that the unchecked step happens in ONE place a reviewer can find, rather than\n * at six call sites where it reads as ordinary code.\n *\n * @module document-shapes\n */\n\nimport type { FormDocument, SubmissionDocument } from \"./types\";\n\n/**\n * A row as the services layer hands it over.\n *\n * `unknown` rather than `Record<string, unknown>`: the read paths type their\n * results loosely enough that a narrower parameter only moves the assertion to\n * the call site, which is the thing this module exists to stop.\n */\ntype StoredRow = unknown;\n\n/**\n * Read a stored row as a submission.\n *\n * Does not validate, and deliberately cannot throw: every caller is on a path\n * where the row has already committed, so rejecting here would turn a saved\n * submission into an error the visitor is invited to retry.\n */\nexport function asSubmissionDocument(row: StoredRow): SubmissionDocument {\n return row as SubmissionDocument;\n}\n\n/** Read a list of stored rows as submissions. */\nexport function asSubmissionDocuments(\n rows: readonly StoredRow[]\n): SubmissionDocument[] {\n return rows as SubmissionDocument[];\n}\n\n/** Read a stored row as a form. Same contract as {@link asSubmissionDocument}. */\nexport function asFormDocument(row: StoredRow): FormDocument {\n return row as FormDocument;\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 { asFormDocument, asSubmissionDocument } from \"../document-shapes\";\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.item.id,\n flaggedAsSpam: isContentSpam,\n });\n\n // A post-commit hook cannot un-save the submission, so a failure there is\n // logged against the row rather than turned into a failed submission: the\n // notification email is the one that fails this way, and telling the\n // visitor their submission did not go through would have them send it\n // again.\n if (submission.warnings) {\n logger.warn?.(\"Form submission side effects failed\", {\n formSlug,\n submissionId: submission.item.id,\n warnings: submission.warnings,\n });\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 // The created ROW, not the envelope around it. Through the shared\n // conversion, which is where the unchecked step lives and says so; the\n // integration test reads `submission.id` off the result because nothing\n // at compile time will.\n submission: asSubmissionDocument(submission.item),\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 ? asFormDocument(form) : 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/utils/redirect-target.ts","../src/collections/access-defaults.ts","../src/collections/submissions.ts","../src/document-shapes.ts","../src/config/validate-form.ts","../src/config/field-helpers.ts","../src/utils/generate-schema.ts","../src/handlers/submit-form.ts","../src/handlers/spam-detection.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 {\n asFormDocument,\n asSubmissionDocument,\n asSubmissionDocuments,\n} from \"./document-shapes\";\nimport type {\n BeforeEmailFilterContext,\n FormNotification,\n FormBuilderPluginOptions,\n FormEmailNotification,\n ResolvedFormBuilderConfig,\n} from \"./types\";\nimport { evaluateSingleCondition } from \"./utils/evaluate-conditions\";\nimport { exportToCSV, generateExportFilename } from \"./utils/export-formats\";\nimport { parseFieldRef } from \"./utils/field-references\";\nimport { normalizeRedirectRelationships } from \"./utils/redirect-target\";\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: normalizeRedirectRelationships(\n options.redirectRelationships\n ),\n // Filled at `init` from each collection's own metadata. Empty until then,\n // and a slug that stays absent reads as unknown rather than as not\n // localized — see `ResolvedFormBuilderConfig`.\n redirectTargetLocalization: {},\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 asSubmissionDocuments(items),\n asFormDocument(form)\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 // Names only. A pattern may be a function, which does not\n // survive JSON and which the browser has no use for — the\n // admin picks a document, the server builds the URL.\n redirectCollections: Object.keys(\n resolvedConfig.redirectRelationships\n ),\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 async 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 // Ask each redirect target collection whether it is localized. The save\n // rule and the submission path both need it and neither can reach the\n // registry themselves: a collection hook is handed `req.nextly`, which\n // is the Direct API and carries no metadata accessor. Resolved once here\n // and written onto the config both of them already close over.\n //\n // Per collection rather than in one call: a slug that cannot be read\n // must stay ABSENT from the map, and a single failing lookup would\n // otherwise take its readable siblings with it.\n // Emptied first. `init` runs again on HMR and re-registration against the\n // SAME config object, so a lookup that succeeded on the previous boot and\n // fails on this one would leave its old answer standing while the catch\n // below believes it left the slug absent. A stale `false` then reads a\n // localized target as unreachable and drops its redirect; a stale `true`\n // lets a plain draft target through.\n for (const slug of Object.keys(\n resolvedConfig.redirectTargetLocalization\n )) {\n delete resolvedConfig.redirectTargetLocalization[slug];\n }\n\n // AWAITED, not fired and forgotten: a write reaching the save rule before\n // this resolves would find the map empty and read every target as\n // undecided, which is the refusal silently not happening.\n await Promise.all(\n Object.keys(resolvedConfig.redirectRelationships).map(\n async declared => {\n const slug = nextly.self.collections[declared] ?? declared;\n try {\n // `getCollection` reads registered metadata and does not consult\n // the context — its parameter is declared for a future ACL hook\n // and is unused today — so an empty context is the honest\n // argument rather than a fabricated user.\n const collection =\n await nextly.services.collections.getCollection(slug, {});\n const localized = (collection as { localized?: unknown })\n ?.localized;\n if (typeof localized === \"boolean\") {\n resolvedConfig.redirectTargetLocalization[declared] = localized;\n }\n } catch {\n // Left absent on purpose: unknown, not \"not localized\".\n nextly.logger.debug?.(\n \"Redirect target localization could not be read\",\n { collection: slug }\n );\n }\n }\n )\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: asFormDocument(ctx.form),\n submission: asSubmissionDocument(ctx.submission),\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 // The pattern is shared with the admin, which decides whether a value is a\n // reference at all. Written twice, a value the editor accepted and this did\n // not would be delivered with its braces intact.\n const name = parseFieldRef(ref);\n if (name === null) return ref;\n const value = data[name];\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 { NextlyError } from \"@nextlyhq/plugin-sdk\";\nimport type { CollectionConfig, FieldConfig, HookContext } from \"nextly\";\nimport {\n text,\n textarea,\n select,\n checkbox,\n date,\n group,\n json,\n relationship,\n} from \"nextly\";\n\nimport type { ResolvedFormBuilderConfig } from \"../types\";\nimport {\n documentReachability,\n parseRedirectReference,\n pickedDocumentField,\n type PickedDocumentField,\n} from \"../utils/redirect-reference\";\nimport {\n applyRedirectPattern,\n type RedirectRelationships,\n type RedirectTargetDocument,\n} from \"../utils/redirect-target\";\n\nimport { accessWithDefaults } from \"./access-defaults\";\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 */\n/**\n * The plugin's hooks, with a host's own appended per phase.\n *\n * A host may pass `formOverrides.hooks`, and the trailing `...overrides` spread\n * would otherwise replace this object outright — taking with it the slug\n * generation, the at-least-one-field rule and the redirect check, none of\n * which are the host's to remove. Those are invariants of the collection this\n * plugin ships; an override extends it rather than disarming it.\n *\n * Handlers in `pluginHooks` run FIRST — they are transformations a host may\n * want to build on, like the generated slug. Handlers in `trailing` run LAST,\n * after every host handler in that phase, which is where an INVARIANT has to\n * sit: one placed first is checked against a payload the host can still\n * rewrite afterwards.\n */\nfunction withHostHooks<T>(\n pluginHooks: T,\n hostHooks: T | undefined,\n trailing: Partial<Record<string, unknown[]>> = {}\n): T {\n // Annotated rather than asserted. `hostHooks ?? {}` widens to `T | {}`\n // under the generic and cannot be indexed by phase name; a cast says the\n // same thing but reads as removable, and `--fix` removes it.\n const host: Record<string, unknown> = { ...hostHooks };\n\n const asList = (value: unknown) =>\n Array.isArray(value) ? value : value === undefined ? [] : [value];\n\n const merged: Record<string, unknown> = { ...host };\n for (const [phase, handlers] of Object.entries(\n pluginHooks as Record<string, unknown>\n )) {\n merged[phase] = [...asList(handlers), ...asList(host[phase])];\n }\n for (const [phase, handlers] of Object.entries(trailing)) {\n merged[phase] = [...asList(merged[phase]), ...asList(handlers)];\n }\n return merged as T;\n}\n\n/**\n * Refuses a write whose redirect target the resolver could not use.\n *\n * One function, called from two phases. `beforeValidate` so an author gets the\n * error where the rest of the form's errors appear, and LAST in `beforeChange`\n * so the guarantee survives a host hook: a check placed before host handlers\n * judges a payload they can still rewrite, and one that can be rewritten past\n * is not an invariant.\n *\n * `setsSettings` mirrors the fields rule above. An update carries the patch\n * rather than the merged document, so treating an absent `settings` as empty\n * would reject every partial update — renaming a form — for a setting it never\n * touched.\n */\n/**\n * The settings a write is SETTING to a page redirect, or null for every other\n * write.\n *\n * `setsSettings` mirrors the fields rule above. An update carries the patch\n * rather than the merged document, so treating an absent `settings` as empty\n * would refuse a rename for a setting it never touched.\n */\nfunction documentRedirectSettings(\n settings: unknown\n): { settings: Record<string, unknown>; field: PickedDocumentField } | null {\n if (typeof settings !== \"object\" || settings === null) return null;\n const record = settings as Record<string, unknown>;\n const field = pickedDocumentField(record);\n return field ? { settings: record, field } : null;\n}\n\nfunction pageRedirectSettings(\n data: Record<string, unknown> | undefined,\n operation: string\n): { settings: Record<string, unknown>; field: PickedDocumentField } | null {\n if (!data) return null;\n if (operation !== \"create\" && data.settings === undefined) return null;\n return documentRedirectSettings(data.settings);\n}\n\n/**\n * Whether the form this write leaves behind can receive a submission.\n *\n * A form's `status` is its own lifecycle — draft, published, closed — and only\n * a published form accepts submissions, so only a published form can send a\n * visitor anywhere at all. That is the whole reason this rule is conditional:\n * a draft form pointing at a draft page is fine, because the two go live\n * together.\n *\n * The stored status is consulted when the write does not carry one. Publishing\n * a form and editing its settings are separate saves, and an update that never\n * mentions `status` leaves the stored one standing — so reading an absent\n * field as \"not published\" would wave through exactly the case this rule\n * exists to catch.\n */\nexport function formAcceptsSubmissions(\n data: Record<string, unknown> | undefined,\n originalData: Record<string, unknown> | undefined\n): boolean {\n return (data?.status ?? originalData?.status) === \"published\";\n}\n\n/**\n * The stored `settings`, which is not the shape the payload uses.\n *\n * Measured on 2026-08-25: the incoming write carries `settings` as an OBJECT,\n * and the same field on `originalData` comes back as JSON TEXT. Reading the\n * stored row with the payload's reader answers \"no redirect configured\" for\n * every form that has one — silently, and indistinguishably from a form that\n * genuinely has none.\n */\nfunction storedSettings(\n value: unknown\n): { settings: Record<string, unknown>; field: PickedDocumentField } | null {\n if (typeof value === \"string\") {\n try {\n return documentRedirectSettings(JSON.parse(value));\n } catch {\n return null;\n }\n }\n return documentRedirectSettings(value);\n}\n\n/**\n * The redirect target already stored on the form, for a write that does not\n * mention it.\n *\n * Publishing a form is such a write: it carries `status` and nothing else, and\n * the target it inherits is the one a visitor will be sent to. Without this,\n * the rule would guard the save that PICKS a draft page and ignore the save\n * that makes that same pairing reachable — protecting one path while appearing\n * to protect both.\n *\n * Nothing here is written back. `originalData` is the stored row, not the\n * payload; normalising it would edit a document this write never touched.\n */\nfunction storedRedirectReference(\n originalData: Record<string, unknown> | undefined,\n patterns: RedirectRelationships\n): {\n reference: { collection: string; id: string };\n field: PickedDocumentField;\n /** Whether that collection is still a configured redirect target. */\n configured: boolean;\n} | null {\n const picked = storedSettings(originalData?.settings);\n if (!picked) return null;\n\n const collections = Object.keys(patterns);\n const reference = parseRedirectReference(\n picked.settings[picked.field],\n collections\n );\n if (!reference) return null;\n\n // A collection dropped from `redirectRelationships` since the form was saved\n // is REPORTED, not discarded. Discarding it made the rule skip a form whose\n // destination can no longer be built at all, so publishing succeeded and\n // every submission afterwards resolved to no redirect — the silent version\n // of the failure the rule exists to prevent.\n return {\n reference,\n field: picked.field,\n configured: collections.includes(reference.collection),\n };\n}\n\n/**\n * Writes the parser's normalised names back into the row being saved.\n *\n * Not merely read: trimming only the parsed copy leaves the padded name in\n * `data`, so this rule accepts the write and the framework's own relationship\n * validator downstream still sees `\" pages \"`. The plugin and the framework\n * then disagree about a value this parser has already decided.\n */\nfunction normalizeStoredReference(\n settings: Record<string, unknown>,\n field: PickedDocumentField,\n reference: { collection: string; id: string }\n) {\n const stored = settings[field];\n\n if (typeof stored === \"string\") {\n if (stored !== reference.id) settings[field] = reference.id;\n return;\n }\n if (typeof stored !== \"object\" || stored === null || Array.isArray(stored)) {\n return;\n }\n\n const record = stored as Record<string, unknown>;\n if (record.relationTo !== reference.collection) {\n settings[field] = { ...record, relationTo: reference.collection };\n }\n}\n\nfunction namedRedirectReference(\n data: Record<string, unknown> | undefined,\n operation: string,\n patterns: RedirectRelationships\n): {\n settings: Record<string, unknown>;\n field: PickedDocumentField;\n /** Null when the settings name a redirect this cannot read. */\n reference: { collection: string; id: string } | null;\n /** Whether that collection is still a configured redirect target. */\n configured: boolean;\n} | null {\n const picked = pageRedirectSettings(data, operation);\n if (!picked) return null;\n const { settings, field } = picked;\n\n const collections = Object.keys(patterns);\n const reference = parseRedirectReference(settings[field], collections);\n if (reference) normalizeStoredReference(settings, field, reference);\n\n return {\n settings,\n field,\n reference,\n configured:\n reference !== null && collections.includes(reference.collection),\n };\n}\n\n/**\n * The same rule, plus the one question that needs a read: can this document\n * actually fill its collection's pattern?\n *\n * Split from the shape check so the cheap half stays synchronous — the early\n * `beforeValidate` call wants an error, not a database round trip, and making\n * that hook async changes what a caller has to do to observe its rejection.\n *\n * Best effort by design: `req.nextly` is optional, and a slug can be emptied\n * AFTER this form is saved. The submit path keeps its own check and its log\n * line. What this buys is telling the author NOW, while the page they picked\n * is in front of them, rather than leaving a form that saves cleanly and\n * redirects nobody.\n */\n/**\n * Whether this write leaves a form that would send visitors to a page they\n * cannot reach.\n *\n * One question, so it has one implementation and one name. It is the whole\n * rule: a form that accepts submissions is the only kind that redirects\n * anyone, and an unreachable target is the only thing that 404s them. A draft\n * form pointing at a draft page satisfies neither half and is allowed — the\n * picker offers unpublished pages on purpose, because a form is usually\n * configured beside the page it points at and the two go live together.\n */\nexport function wouldStrandVisitors(\n data: Record<string, unknown> | undefined,\n originalData: Record<string, unknown> | undefined,\n target: RedirectTargetDocument,\n targetCollectionIsLocalized: boolean | undefined\n): boolean {\n return (\n formAcceptsSubmissions(data, originalData) &&\n documentReachability(target, targetCollectionIsLocalized) === \"unreachable\"\n );\n}\n\n/**\n * Whether this write is the one that makes the form able to receive\n * submissions.\n *\n * Distinct from {@link formAcceptsSubmissions}, which answers what the form\n * WILL be: this asks what the write CHANGES, which is what decides whether an\n * inherited target is any of its business. A form that was already published\n * is not made reachable again by an edit that merely restates its status.\n */\nfunction publishesForm(\n data: Record<string, unknown> | undefined,\n originalData: Record<string, unknown> | undefined\n): boolean {\n if (data?.status !== \"published\") return false;\n // Already published before this write, so this write is not what makes the\n // form reachable. The admin sends `status` on every save exactly as it sends\n // `settings`, so reading the value alone treats every edit to a live form as\n // the moment it went live.\n return originalData?.status !== \"published\";\n}\n\n/**\n * The page this write leaves the form pointing at, and whether the write\n * CHOSE it.\n *\n * The distinction decides which refusals apply. A write that names a page\n * answers for that page; a write that only publishes the form, or renames it,\n * inherits whatever is stored and must not be refused for it.\n */\n/**\n * Whether two readings name the same document in the same field.\n *\n * `null` on either side is not a match: a reference this cannot read has not\n * been shown to be the stored one.\n */\nfunction sameTarget(\n named: {\n field: PickedDocumentField;\n reference: { collection: string; id: string } | null;\n },\n stored: {\n field: PickedDocumentField;\n reference: { collection: string; id: string };\n } | null\n): boolean {\n if (!named.reference || !stored) return false;\n return (\n stored.field === named.field &&\n stored.reference.collection === named.reference.collection &&\n stored.reference.id === named.reference.id\n );\n}\n\n/**\n * What a write that NAMES a target is answering for.\n *\n * NAMING is not CHOOSING, and the refusal has to come after that distinction\n * rather than before it. The admin's save sends the whole `settings` object on\n * every write, so a rename carries the redirect it never touched — judging the\n * mention meant a form whose collection had since been unconfigured could not\n * be edited at all.\n */\nfunction judgeNamedReference(\n named: {\n field: PickedDocumentField;\n reference: { collection: string; id: string } | null;\n configured: boolean;\n },\n stored: {\n field: PickedDocumentField;\n reference: { collection: string; id: string };\n } | null\n): {\n reference: { collection: string; id: string };\n field: PickedDocumentField;\n chosen: boolean;\n configured: boolean;\n} {\n if (sameTarget(named, stored) && named.reference) {\n return {\n reference: named.reference,\n field: named.field,\n chosen: false,\n configured: named.configured,\n };\n }\n\n // This write moved the target, so it answers for what it moved it to.\n if (!named.reference || !named.configured) {\n throw redirectRefusal(\n \"Choose a page to redirect to, or pick a different confirmation.\",\n named.field\n );\n }\n return {\n reference: named.reference,\n field: named.field,\n chosen: true,\n configured: true,\n };\n}\n\nfunction redirectReferenceForWrite(\n context: HookContext,\n patterns: RedirectRelationships\n): {\n reference: { collection: string; id: string };\n field: PickedDocumentField;\n chosen: boolean;\n /** Whether the target's collection is still a configured redirect target. */\n configured: boolean;\n} | null {\n const stored = storedRedirectReference(\n context.originalData as Record<string, unknown> | undefined,\n patterns\n );\n const named = namedRedirectReference(\n context.data,\n context.operation,\n patterns\n );\n\n if (named) return judgeNamedReference(named, stored);\n\n // A write that REPLACES `settings` has answered the question: this form no\n // longer redirects to a page. Inheriting the old target there would refuse\n // the very edit that removes an invalid redirect, which is the one edit an\n // author in that state needs to make. Only a write that omits `settings`\n // inherits.\n const data = context.data as Record<string, unknown> | undefined;\n if (data?.settings !== undefined) return null;\n\n return stored ? { ...stored, chosen: false } : null;\n}\n\n/**\n * Whether the collection's configured pattern can build a URL for this\n * document.\n *\n * A pattern may be a FUNCTION, which is host code and can throw. Authoring\n * must not be blocked by that: the submission path contains the same throw and\n * degrades to no redirect with a log line, so a broken pattern costs a\n * destination rather than the ability to edit forms.\n */\nfunction assertPatternBuildsUrl(\n patterns: RedirectRelationships,\n reference: { collection: string; id: string },\n field: PickedDocumentField,\n target: RedirectTargetDocument\n) {\n let url: string | undefined;\n try {\n url = applyRedirectPattern(patterns[reference.collection], target);\n } catch {\n return;\n }\n if (url) return;\n\n // Pattern-agnostic. The configured pattern may depend on any field, or be a\n // function that declines for its own reasons, so naming a slug states a\n // remedy that often cannot fix the actual failure — and the save is\n // correctly blocked either way, leaving the author following wrong advice.\n throw redirectRefusal(\n `No URL could be built for that page from the pattern configured for ` +\n `\"${reference.collection}\". Choose another page, or ask a developer ` +\n `to check that collection's redirect pattern.`,\n field\n );\n}\n\nexport async function assertRedirectTargetUsable(\n context: HookContext,\n patterns: RedirectRelationships,\n localization: Record<string, boolean>\n) {\n const write = redirectReferenceForWrite(context, patterns);\n if (!write) return;\n const { reference, field, chosen, configured } = write;\n\n // Nothing can be built for a collection that is no longer a redirect target,\n // so there is nothing to read either. Only the write that makes the form\n // reachable answers for it; an unrelated edit inherits the state as it does\n // any other.\n if (!configured) {\n if (\n publishesForm(\n context.data as Record<string, unknown> | undefined,\n context.originalData as Record<string, unknown> | undefined\n )\n ) {\n throw redirectRefusal(\n `This form redirects to \"${reference.collection}\", which is no longer ` +\n `configured as a redirect target, so it has nowhere to send anyone. ` +\n `Choose another page, or pick a different confirmation.`,\n field\n );\n }\n return;\n }\n\n const nextly = context.req?.nextly;\n if (!nextly) return;\n\n // Not inside a caller-owned transaction. `context.executor` is present only\n // there, and the contract on it is explicit: a hook that reads the database\n // must use that executor, because a second pooled connection can stall\n // against a small pool while the caller's transaction holds the only one —\n // and a pooled read cannot see the transaction's own uncommitted rows, so a\n // page created in the same transaction would read as missing and this would\n // refuse a correct write.\n //\n // `findByID` takes no executor, and reaching for Drizzle directly here would\n // be raw database access inside a plugin. So the transactional path keeps\n // the shape check and skips the read; the submit path is the backstop it\n // already was.\n if (context.executor) return;\n\n const found = await readRedirectTarget(nextly, reference);\n if (found.status === \"unreadable\") return;\n if (found.status === \"missing\") {\n // A write that neither NAMES the page nor makes the form able to send\n // anyone there does not answer for it: a rename inherits whatever is\n // stored, and refusing it would block an edit for a setting it never\n // touched.\n //\n // Publishing is not that write. It is the moment the pairing starts\n // reaching visitors, and a deleted target is a stricter case than the\n // unpublished one refused below — every submission resolves it to no\n // redirect at all. Returning here on `!chosen` alone let the worse case\n // through while the milder one was refused.\n if (\n !chosen &&\n !publishesForm(\n context.data as Record<string, unknown> | undefined,\n context.originalData as Record<string, unknown> | undefined\n )\n ) {\n return;\n }\n throw redirectRefusal(\n `That page no longer exists in \"${reference.collection}\". Pick another.`,\n field\n );\n }\n\n // An inherited target is only judged by the write that PUBLISHES the form.\n // A published form can acquire a draft target without being touched — the\n // page is unpublished later, or the pairing predates this rule — and\n // refusing every subsequent rename would hold the form hostage to a state\n // the write neither created nor mentions. A write that CHOOSES a target\n // answers for it whatever else it does.\n const judged =\n chosen ||\n publishesForm(\n context.data as Record<string, unknown> | undefined,\n context.originalData as Record<string, unknown> | undefined\n );\n\n if (\n judged &&\n wouldStrandVisitors(\n context.data as Record<string, unknown> | undefined,\n context.originalData as Record<string, unknown> | undefined,\n found.document,\n localization[reference.collection]\n )\n ) {\n throw redirectRefusal(\n `That page is not published yet, and this form is. A visitor sent ` +\n `there would reach a \"page not found\". Publish the page, or save ` +\n `this form as a draft until you do.`,\n field\n );\n }\n\n // The pattern belongs to the write that CHOOSES a page. A form being\n // published does not get refused for a pattern its author never edited.\n if (chosen)\n assertPatternBuildsUrl(patterns, reference, field, found.document);\n}\n\n/**\n * The target document, as one of three outcomes.\n *\n * Three rather than a nullable document, because \"the page is not there\" and\n * \"I could not ask\" are different answers and only the first is the author's\n * to fix. Collapsing them has been wrong in both directions on this rule:\n * refusing on any failure blocks a save a retry would complete, and skipping\n * on any failure loses the deleted-page refusal entirely.\n */\nasync function readRedirectTarget(\n nextly: NonNullable<NonNullable<HookContext[\"req\"]>[\"nextly\"]>,\n reference: { collection: string; id: string }\n): Promise<\n | { status: \"ok\"; document: RedirectTargetDocument }\n | { status: \"missing\" }\n | { status: \"unreadable\" }\n> {\n try {\n const row = (await nextly.findByID({\n collection: reference.collection,\n id: reference.id,\n })) as RedirectTargetDocument | null;\n return row ? { status: \"ok\", document: row } : { status: \"missing\" };\n } catch (error) {\n return isMissingTarget(error)\n ? { status: \"missing\" }\n : { status: \"unreadable\" };\n }\n}\n\n/**\n * Whether a failed read means the document is GONE, or that it could not be\n * asked for.\n *\n * Both arrive as exceptions — `findByID` throws `NOT_FOUND` for a missing\n * entry rather than returning null, despite a signature that says `| null` —\n * so the two are separated by the error's own code. Not by its message, which\n * is prose, and not by catching everything, which has now been wrong in both\n * directions: refusing on any failure blocks a save a retry would complete,\n * and skipping on any failure loses the deleted-page refusal.\n */\nexport function isMissingTarget(error: unknown): boolean {\n return (\n typeof error === \"object\" &&\n error !== null &&\n (error as { code?: unknown }).code === \"NOT_FOUND\"\n );\n}\n\n/**\n * Records the moment a form first goes live.\n *\n * `status` on this collection is an ordinary select, not the framework's\n * publish lifecycle, so nothing else writes down that a form was ever public.\n * Without that, `closed` stands for two different histories — a form an author\n * released and later stopped, and one created closed that nobody ever saw —\n * and the public by-slug endpoint served both.\n *\n * Written once. A form republished after being closed keeps the original date,\n * because the question this answers is \"has this ever been public\", not \"when\n * was it last\".\n */\nfunction stampFirstPublish(context: HookContext): void {\n const data = context.data as Record<string, unknown> | undefined;\n if (!data) return;\n const stored = context.originalData as Record<string, unknown> | undefined;\n\n // Written here or not at all. Duplicating an entry copies every non-system\n // field, so a copy of a closed form arrives carrying the original's stamp\n // and would qualify as previously public at a slug nobody has ever seen. The\n // same goes for any caller that simply sends the field. Removing it from the\n // patch leaves a stamp that IS stored untouched, because a column absent\n // from an update is a column not written.\n delete data.wentLiveAt;\n\n if (recordedStamp(stored) !== undefined) return;\n\n // Either side of the transition is proof. A form live before this field\n // existed carries no stamp, so its first edit is the only chance to record\n // one — and if that edit is the one CLOSING it, reading the incoming status\n // alone would miss it and take a form that had been public offline.\n if (data.status === \"published\" || stored?.status === \"published\") {\n data.wentLiveAt = new Date();\n }\n}\n\n/** The stamp a form already carries, treating blank as absent. */\nfunction recordedStamp(stored: Record<string, unknown> | undefined): unknown {\n const value = stored?.wentLiveAt;\n return value === null || value === \"\" ? undefined : value;\n}\n\n/** One shape for every refusal this rule makes, so callers see one field. */\nfunction redirectRefusal(message: string, field: PickedDocumentField) {\n // The path names the field the author actually filled in. Reporting every\n // refusal against `redirectPage` points a form configured through the URL\n // option at a control it does not show.\n return NextlyError.validation({\n errors: [{ path: `settings.${field}`, code: \"REQUIRED\", message }],\n });\n}\n\nexport function formsCollection(\n pluginConfig: ResolvedFormBuilderConfig\n): ExtendedCollectionConfig {\n const {\n slug,\n labels,\n fields: additionalFields,\n access: accessOverrides,\n hooks: hookOverrides,\n admin: adminOverrides,\n ...overrides\n } = pluginConfig.formOverrides;\n\n // Derived from the map rather than carried beside it: the collections a form\n // may redirect to are exactly the ones with a URL pattern configured, so\n // there is no second list to fall out of step with the first.\n const redirectCollections = Object.keys(pluginConfig.redirectRelationships);\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 ...(redirectCollections.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 (redirectCollections.length > 0) {\n settingsFields.push(\n relationship({\n name: \"redirectPage\",\n label: \"Redirect Page\",\n relationTo: redirectCollections,\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 // NOT `firstPublishedAt`, which is the obvious name and the one to reach\n // for next. The framework owns that name on collections it manages the\n // publish lifecycle for, and a write naming it from a plugin collection is\n // stripped on the way to the insert — silently, leaving the column null and\n // every closed form reading as one that was never public.\n date({\n name: \"wentLiveAt\",\n label: \"First Published\",\n admin: {\n readOnly: true,\n description:\n \"When this form first went live. Written once and never changed.\",\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 ...adminOverrides,\n },\n\n // Access control with sensible defaults\n // `read: true` — a form is public because a site renders it. The rest are\n // the shared defaults.\n access: accessWithDefaults(accessOverrides, { read: true }),\n\n hooks: withHostHooks(\n {\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 // A form must have at least one field, checked against what the\n // write actually sets. An update carries the patch rather than the\n // merged document, so treating an absent `fields` as empty rejects\n // every partial update -- renaming a form, or changing a setting --\n // even though its fields are untouched and still there.\n const setsFields =\n operation === \"create\" || data?.fields !== undefined;\n if (\n data &&\n setsFields &&\n (data.fields === undefined ||\n (Array.isArray(data.fields) && data.fields.length === 0))\n ) {\n // Typed, so the rejection survives as a validation failure with\n // its field issue rather than being read as a crash and replaced\n // with a generic server-fault message.\n throw NextlyError.validation({\n errors: [\n {\n path: \"fields\",\n code: \"REQUIRED\",\n message: \"Form must have at least one field.\",\n },\n ],\n });\n }\n\n // Reported here so an author sees it beside the form's other\n // errors. The GUARANTEE is the trailing `beforeChange` call below,\n // which a host hook cannot run after.\n // Shape only at this phase: the same reading, without the\n // stored document a refusal for an unconfigured collection would\n // need. `assertRedirectTargetUsable` below makes that call.\n namedRedirectReference(\n data,\n operation,\n pluginConfig.redirectRelationships\n );\n\n return data;\n },\n ],\n },\n hookOverrides,\n {\n // The last COLLECTION-level phase, after every host handler in it. A\n // host `beforeValidate` may rewrite the payload once the earlier call\n // has passed, so the check has to run again here.\n //\n // It is not the last mutating point in the write, and saying so would\n // be false: `collection-mutation-service` runs FIELD-level\n // `beforeChange` hooks after this one, and a host may contribute a\n // field through `formOverrides.fields`. Nothing collection-level can\n // sit after that — the next steps are hashing and the insert. What\n // bounds the consequence is the submit path, which resolves an\n // unusable target to NO redirect and logs it, never to a wrong one.\n beforeChange: [\n async (context: HookContext) => {\n await assertRedirectTargetUsable(\n context,\n pluginConfig.redirectRelationships,\n pluginConfig.redirectTargetLocalization\n );\n stampFirstPublish(context);\n return context.data;\n },\n ],\n }\n ),\n\n // Spread any additional overrides (excluding already used properties)\n ...overrides,\n };\n}\n","/**\n * Where a form sends the visitor after a successful submission, when the\n * author picked a PAGE rather than typing a URL.\n *\n * The plugin cannot derive this. A site's URL structure belongs to the site:\n * the playground serves the `pages` collection at `/{slug}` from its own\n * catch-all route, and another app may serve the same collection under\n * `/blog/{slug}` or behind a locale segment. Nothing inside a CMS package can\n * read that decision off the collection, so the mapping is configuration.\n *\n * A pattern covers the common case and a function covers the rest — a path\n * that depends on a parent document, a locale, or anything else the site\n * knows and this package does not.\n *\n * The pattern syntax is not invented here. `previewUrlFromTemplate` already\n * turns a `{field}` path into a URL for preview links, for the same reason and\n * with the same constraint, so this uses it rather than substituting\n * alongside it: two rules that agree today would drift, and a wrong URL still\n * looks like a URL. Encoding and the treatment of `0` and `false` come with\n * it.\n */\nimport { previewUrlFromTemplate } from \"nextly/config\";\n\nimport type { RedirectTargetDocument } from \"./redirect-reference\";\n\nexport type { RedirectTargetDocument } from \"./redirect-reference\";\nexport {\n documentReachability,\n type Reachability,\n hasPublishLifecycle,\n parseRedirectReference,\n pickedDocumentField,\n type PickedDocumentField,\n type RedirectReference,\n} from \"./redirect-reference\";\n\n/**\n * How documents in one collection become URLs.\n *\n * A string is a path with `{field}` placeholders filled from the document.\n * A function receives the document and returns the path itself.\n */\nexport type RedirectUrlPattern =\n | string\n | ((document: RedirectTargetDocument) => string | undefined);\n\n/** The collection-to-pattern map, after the array shorthand is normalised. */\nexport type RedirectRelationships = Record<string, RedirectUrlPattern>;\n\n/**\n * The pattern assumed for the `string[]` shorthand.\n *\n * `redirectRelationships: [\"pages\"]` means \"pages are redirect targets\" and\n * says nothing about their URLs, so it needs a default. This one matches the\n * routing the page builder ships and the scaffold templates generate.\n */\nexport const DEFAULT_REDIRECT_PATTERN = \"/{slug}\";\n\n/**\n * Both accepted spellings of the option reduced to the map.\n *\n * Normalised at the edge so everything downstream reads ONE shape — including\n * the list of collections, which is `Object.keys` of this rather than a second\n * field that agrees with it today.\n */\nexport function normalizeRedirectRelationships(\n option: string[] | RedirectRelationships | undefined\n): RedirectRelationships {\n if (!option) return {};\n const entries = Array.isArray(option)\n ? option.map(collection => [collection, DEFAULT_REDIRECT_PATTERN] as const)\n : Object.entries(option);\n\n // An empty pattern is dropped rather than kept, so that being OFFERED as a\n // redirect target and being able to PRODUCE a URL are one decision. Kept,\n // the collection appears in the picker, validation accepts a reference into\n // it, and every submission then resolves to nothing — a form that passes\n // every check and sends nobody anywhere.\n return Object.fromEntries(\n entries.filter(\n ([, pattern]) => typeof pattern !== \"string\" || pattern.trim()\n )\n );\n}\n\n/**\n * The pattern applied to a document, or undefined when it cannot be.\n *\n * Undefined rather than a partial string: a placeholder whose field is empty\n * would otherwise produce `/undefined` or `/`, and a form that sends every\n * visitor to a broken URL is worse than one that sends them nowhere. The\n * caller reports the difference.\n */\nexport function applyRedirectPattern(\n pattern: RedirectUrlPattern,\n document: RedirectTargetDocument\n): string | undefined {\n if (typeof pattern === \"function\") {\n const produced = pattern(document);\n return produced ? produced : undefined;\n }\n return previewUrlFromTemplate(pattern)(document) ?? undefined;\n}\n","/**\n * The access rules both collections this plugin contributes fall back to.\n *\n * Written out per collection they were byte-identical apart from `read`, and\n * two copies of an access policy is the copy that matters most: they agree\n * today, one gets tightened later, and the other silently keeps the old rule\n * while looking deliberate.\n *\n * `read` and `create` are the axes they genuinely differ on, so both are\n * parameters: a form is publicly READABLE because a site renders it, and a\n * submission is publicly CREATABLE because a visitor makes it. Update and\n * delete are the same policy in both collections.\n *\n * @module collections/access-defaults\n */\n\nimport type { CollectionAccessControl } from \"nextly\";\n\n/** A single rule, in either form the collection config accepts. */\ntype AccessRule = NonNullable<CollectionAccessControl[\"read\"]>;\n\n/** The subset of a collection's access declaration these defaults supply. */\nexport type AccessOverrides = Pick<\n CollectionAccessControl,\n \"read\" | \"create\" | \"update\" | \"delete\"\n>;\n\n/** Signed in. */\nconst authenticated: AccessRule = ({ user }) => !!user;\n\n/** Signed in AND holding an administrative role. */\nconst administrative: AccessRule = ({ roles }) =>\n roles.includes(\"admin\") || roles.includes(\"super-admin\");\n\n/**\n * Host overrides applied over this plugin's defaults, per operation.\n *\n * Each override is taken with `??`, so a host may pass `false` to close an\n * operation it would otherwise be given — which `||` would have silently\n * replaced with the default.\n */\nexport function accessWithDefaults(\n overrides: AccessOverrides | undefined,\n defaults: { read?: AccessRule; create?: AccessRule } = {}\n): AccessOverrides {\n return {\n read: overrides?.read ?? defaults.read ?? authenticated,\n create: overrides?.create ?? defaults.create ?? authenticated,\n update: overrides?.update ?? authenticated,\n // Deleting is administrative in both collections; the DB permission layer\n // remains the backstop underneath it.\n delete: overrides?.delete ?? administrative,\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\nimport { accessWithDefaults } from \"./access-defaults\";\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 answers plus ipAddress/userAgent, so the\n // default `entry.*` events stay suppressed — otherwise every submission would\n // be delivered to any endpoint subscribed to `entry.created` or `*`. Instead\n // emit a curated `form.submission.created` carrying ONLY safe metadata (which\n // form, when, and the status) — never the answers, IP, or user agent.\n webhooks: {\n record: false,\n emit: {\n event: \"form.submission.created\",\n fields: [\"form\", \"submittedAt\", \"status\"],\n },\n },\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 // `create: true` — a visitor submits without an account. Reading, updating\n // and deleting are the shared defaults.\n access: accessWithDefaults(accessOverrides, { create: true }),\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 * Reading a stored row as one of this plugin's documents.\n *\n * The services layer answers with a loose row -- `id`, `createdAt`, `updatedAt`\n * and an `unknown` index signature -- while this plugin declares concrete\n * documents. There is no overlap for TypeScript to check between the two, so\n * the conversion goes through `unknown` and **nothing here is verified at\n * compile time**. A single assertion does not compile:\n *\n * ```\n * error TS2352: Conversion of type 'CollectionEntry' to type\n * 'SubmissionDocument' may be a mistake because neither type sufficiently\n * overlaps with the other.\n * ```\n *\n * The parameters are therefore `unknown`, which is what the rows honestly are\n * at this boundary. One assertion from `unknown` is legal where two from a\n * partially-known shape were needed, so the widening removes a cast rather\n * than adding one -- but it removes the notation, not the risk.\n *\n * The point of this module is therefore not safety it cannot provide. It is\n * that the unchecked step happens in ONE place a reviewer can find, rather than\n * at six call sites where it reads as ordinary code.\n *\n * @module document-shapes\n */\n\nimport type { FormDocument, SubmissionDocument } from \"./types\";\n\n/**\n * A row as the services layer hands it over.\n *\n * `unknown` rather than `Record<string, unknown>`: the read paths type their\n * results loosely enough that a narrower parameter only moves the assertion to\n * the call site, which is the thing this module exists to stop.\n */\ntype StoredRow = unknown;\n\n/**\n * Read a stored row as a submission.\n *\n * Does not validate, and deliberately cannot throw: every caller is on a path\n * where the row has already committed, so rejecting here would turn a saved\n * submission into an error the visitor is invited to retry.\n */\nexport function asSubmissionDocument(row: StoredRow): SubmissionDocument {\n return row as SubmissionDocument;\n}\n\n/** Read a list of stored rows as submissions. */\nexport function asSubmissionDocuments(\n rows: readonly StoredRow[]\n): SubmissionDocument[] {\n return rows as SubmissionDocument[];\n}\n\n/** Read a stored row as a form. Same contract as {@link asSubmissionDocument}. */\nexport function asFormDocument(row: StoredRow): FormDocument {\n return row as FormDocument;\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\";\nimport { parseRedirectReference } from \"../utils/redirect-reference\";\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 | \"REDIRECT_PAGE_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 // A form set to redirect to a page needs the page. Without this the form\n // saves, the admin shows it as redirecting, and every submission finishes\n // with nowhere to go — the destination is missing at submit time, when\n // there is no one to tell.\n //\n // The SHAPE, not truthiness. `{}` and `{ relationTo: \"pages\" }` are truthy\n // and name no document, so a presence test admits precisely the values that\n // resolve to nothing. `null` for the collection list because this entry\n // point does not have the plugin config: a reference carrying its own\n // `relationTo` is checkable without it, and a bare id is not — which is the\n // honest limit of validating a form in isolation.\n if (\n s.confirmationType === \"relationship\" &&\n !namesADocument(s.redirectPage)\n ) {\n errors.push({\n path: \"settings.redirectPage\",\n message: \"A page is required when confirmation type is 'relationship'\",\n code: \"REDIRECT_PAGE_REQUIRED\",\n });\n }\n}\n\n/**\n * Whether a stored redirect value names a document at all.\n *\n * Derived from the shared reader rather than restating its rules, with an\n * empty collection list: that accepts every shape carrying its own\n * `relationTo` and rejects the ones naming nothing. A bare id is the case this\n * cannot judge here, since which collection it belongs to is plugin\n * configuration this entry point never sees — so it is accepted rather than\n * reported, because refusing a valid form is the worse error.\n */\nfunction namesADocument(value: unknown): boolean {\n if (typeof value === \"string\") return value.length > 0;\n return parseRedirectReference(value, []) !== null;\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 type { FieldValidationRule } from \"nextly/field-catalog\";\nimport { z } from \"zod\";\n\nimport { isKnownFormField } from \"../types\";\nimport type {\n AnyFieldValidation,\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\nimport { ENFORCED_VALIDATION_RULES } from \"./enforced-validation\";\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 */\n/**\n * The author's own wording for a failure, or `undefined` when this field's type\n * does not enforce `message`.\n *\n * Asked of the row rather than read off `validation` directly, for the reason\n * the table exists at all: the editor decides whether to OFFER the control from\n * that row, so a generator honouring a stored message regardless would enforce\n * a value the author was never shown a box for — and, worse, the table would\n * describe a rule it did not govern.\n *\n * `message` was the one rule that stayed outside this. It agreed with the table\n * only because every row happens to list it, which is agreement by coincidence:\n * drop `message` from a row and the control disappears while the wording went\n * on being applied.\n */\nfunction customMessage(field: FormField): string | undefined {\n const enforced = ENFORCED_VALIDATION_RULES[field.type] ?? [];\n if (!enforced.includes(\"message\")) return undefined;\n return field.validation?.errorMessage;\n}\n\n/**\n * A field's stored bounds, with the custom message already resolved by the row.\n *\n * Handed to the appliers so they cannot read an ungated message: they take a\n * validation object, and giving them one whose `errorMessage` has been through\n * {@link customMessage} makes the rule hold for every applier without each of\n * them having to remember it.\n */\nfunction enforcedValidation(field: FormField): AnyFieldValidation {\n const stored = (field.validation ?? {}) as AnyFieldValidation;\n return { ...stored, errorMessage: customMessage(field) };\n}\n\n/**\n * Which rules a type's schema applies, and how each one constrains a string.\n *\n * The WHICH comes from {@link ENFORCED_VALIDATION_RULES}, which the field\n * editor also reads to decide what to offer. One definition answers both\n * questions, so a rule cannot be offered by the editor and ignored here, or\n * honoured here and hidden there — the two used to state the same thing\n * separately and could only agree by being edited together.\n *\n * The HOW stays here, because it is about zod and the table is deliberately\n * free of it: the editor imports the table, and pulling the schema builder in\n * behind it would ship this file and its validation library to every admin\n * client before anyone opened a field.\n */\nconst STRING_RULE_APPLIERS: Partial<\n Record<\n FieldValidationRule,\n (\n schema: z.ZodString,\n validation: AnyFieldValidation,\n fallback: string\n ) => z.ZodString\n >\n> = {\n minLength: (schema, validation) =>\n validation.minLength === undefined\n ? schema\n : schema.min(\n validation.minLength,\n validation.errorMessage ||\n `Minimum ${validation.minLength} characters required`\n ),\n maxLength: (schema, validation) =>\n validation.maxLength === undefined\n ? schema\n : schema.max(\n validation.maxLength,\n validation.errorMessage ||\n `Maximum ${validation.maxLength} characters allowed`\n ),\n pattern: (schema, validation, fallback) =>\n validation.pattern\n ? schema.regex(\n new RegExp(validation.pattern),\n validation.errorMessage || fallback\n )\n : schema,\n};\n\n/** The same, for the bounds a number carries. */\nconst NUMBER_RULE_APPLIERS: Partial<\n Record<\n FieldValidationRule,\n (schema: z.ZodNumber, validation: AnyFieldValidation) => z.ZodNumber\n >\n> = {\n min: (schema, validation) =>\n validation.min === undefined\n ? schema\n : schema.min(\n validation.min,\n validation.errorMessage || `Minimum value is ${validation.min}`\n ),\n max: (schema, validation) =>\n validation.max === undefined\n ? schema\n : schema.max(\n validation.max,\n validation.errorMessage || `Maximum value is ${validation.max}`\n ),\n};\n\n/**\n * Whether an author-supplied pattern actually replaces a type's intrinsic one.\n *\n * Two conditions, and both are load-bearing: a pattern must be stored, AND the\n * type's row must enforce `pattern`. A type that keeps its own shape check —\n * a phone, a URL — stands it down only for an override that will really be\n * applied, because standing down for one that will not leaves nothing behind.\n */\nfunction overridesPattern(field: FormField): boolean {\n const enforced = ENFORCED_VALIDATION_RULES[field.type] ?? [];\n if (!enforced.includes(\"pattern\")) return false;\n const validation = (field.validation ?? {}) as AnyFieldValidation;\n return Boolean(validation.pattern);\n}\n\n/**\n * Constrain a string schema by the rules this field's TYPE enforces.\n *\n * A rule the type does not list is not applied even when the field carries a\n * value for it, which is the point: a `pattern` stored on a textarea is a bound\n * the editor never offered, and honouring it here would enforce a restriction\n * its author could not see.\n *\n * `patternMessage` is the type's own wording for a failed pattern — \"Invalid\n * URL format\" reads differently from \"Invalid format\" — so the rule set decides\n * WHICH constraints apply while each type keeps its own voice.\n */\nfunction applyStringRules(\n schema: z.ZodString,\n field: FormField,\n patternMessage: string\n): z.ZodString {\n const validation = enforcedValidation(field);\n let constrained = schema;\n for (const rule of ENFORCED_VALIDATION_RULES[field.type] ?? []) {\n const apply = STRING_RULE_APPLIERS[rule];\n if (apply) constrained = apply(constrained, validation, patternMessage);\n }\n return constrained;\n}\n\nfunction generateTextSchema(field: TextFormField): z.ZodTypeAny {\n const schema = applyStringRules(z.string(), field, \"Invalid format\");\n return applyRequired(schema, field.required, customMessage(field));\n}\n\n/**\n * Generate schema for email field.\n */\nfunction generateEmailSchema(field: EmailFormField): z.ZodTypeAny {\n // The format check is the type's own, not a validation rule: an email field\n // is an email field whether or not its author bounded it further.\n const schema = applyStringRules(\n z.string().email(customMessage(field) || \"Invalid email address\"),\n field,\n \"Invalid email format\"\n );\n\n return applyRequired(schema, field.required, customMessage(field));\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 ? customMessage(field) || \"This field is required\"\n : \"Must be a number\",\n });\n\n // The same rule set the editor reads decides which bounds apply here.\n const validation = enforcedValidation(field);\n for (const rule of ENFORCED_VALIDATION_RULES[field.type] ?? []) {\n const apply = NUMBER_RULE_APPLIERS[rule];\n if (apply) schema = apply(schema, validation);\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 // The default pattern is part of what a phone field IS, so it belongs to the\n // base rather than to the rule set: it applies when the author supplied none,\n // and the author's own pattern replaces it through the rule below.\n //\n // Whether the author CAN replace it is the row's decision, not the stored\n // value's. Standing the base down because a pattern happens to be stored,\n // while `applyStringRules` then declines to apply it because the row does not\n // list `pattern`, would leave the field with no check at all — every string\n // accepted, on the one type whose whole purpose is a shape.\n const base = overridesPattern(field)\n ? z.string()\n : z\n .string()\n .regex(\n /^[\\d\\s\\-+()]+$/,\n customMessage(field) || \"Invalid phone number\"\n );\n\n const schema = applyStringRules(base, field, \"Invalid phone number format\");\n\n return applyRequired(schema, field.required, customMessage(field));\n}\n\n/**\n * Generate schema for URL field.\n */\nfunction generateUrlSchema(field: UrlFormField): z.ZodTypeAny {\n const schema = applyStringRules(\n z.string().url(customMessage(field) || \"Invalid URL\"),\n field,\n \"Invalid URL format\"\n );\n\n return applyRequired(schema, field.required, customMessage(field));\n}\n\n/**\n * Generate schema for textarea field.\n */\nfunction generateTextareaSchema(field: TextareaFormField): z.ZodTypeAny {\n const schema = applyStringRules(z.string(), field, \"Invalid format\");\n return applyRequired(schema, field.required, customMessage(field));\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 customMessage(field) || \"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: () => customMessage(field) || \"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: customMessage(field) || \"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: () => customMessage(field) || \"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 customMessage(field) || \"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, customMessage(field));\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: customMessage(field) || \"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 customMessage(field) ||\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: customMessage(field) || \"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 customMessage(field) || \"Invalid time format (HH:mm)\"\n );\n\n return applyRequired(schema, field.required, customMessage(field));\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, customMessage(field));\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 * 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 { formAvailability, NO_SUCH_FORM } from \"nextly\";\nimport type { PluginContext } from \"nextly\";\n\nimport { asFormDocument, asSubmissionDocument } from \"../document-shapes\";\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\";\nimport {\n applyRedirectPattern,\n documentReachability,\n parseRedirectReference,\n pickedDocumentField,\n type RedirectTargetDocument,\n type RedirectUrlPattern,\n} from \"../utils/redirect-target\";\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 { success: false, error: NO_SUCH_FORM };\n }\n\n // 2. Check form status. The same reading the HTTP and Direct API paths do,\n // so what a visitor is told does not depend on which entry point their\n // client used.\n const availability = formAvailability(form);\n if (availability.kind !== \"open\") {\n logger.info?.(\"Form submission rejected - form not accepting\", {\n formSlug,\n status: form.status,\n availability: availability.kind,\n });\n // `absent` answers exactly as a slug nobody used does. Saying \"not\n // currently accepting submissions\" here would confirm the form exists,\n // which is the enumeration this whole reading exists to prevent — and it\n // would disagree with the REST and Direct API paths, which is how the\n // four came to give four different answers in the first place.\n return {\n success: false,\n error:\n availability.kind === \"closed\" ? availability.message : NO_SUCH_FORM,\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.item.id,\n flaggedAsSpam: isContentSpam,\n });\n\n // A post-commit hook cannot un-save the submission, so a failure there is\n // logged against the row rather than turned into a failed submission: the\n // notification email is the one that fails this way, and telling the\n // visitor their submission did not go through would have them send it\n // again.\n if (submission.warnings) {\n logger.warn?.(\"Form submission side effects failed\", {\n formSlug,\n submissionId: submission.item.id,\n warnings: submission.warnings,\n });\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 = await resolveRedirectUrl(\n form,\n pluginConfig,\n pluginContext\n );\n\n if (isContentSpam) {\n return { success: true, redirect };\n }\n\n return {\n success: true,\n // The created ROW, not the envelope around it. Through the shared\n // conversion, which is where the unchecked step lives and says so; the\n // integration test reads `submission.id` off the result because nothing\n // at compile time will.\n submission: asSubmissionDocument(submission.item),\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 ? asFormDocument(form) : null;\n } catch {\n return null;\n }\n}\n\n/**\n * Where a successful submission sends the visitor, or undefined for nowhere.\n *\n * A destination is either TYPED as a URL or PICKED as a document, and the two\n * are answered differently: a typed URL is returned verbatim, since it may\n * legitimately point off-site, while a picked document has to be read and put\n * through its collection's configured URL pattern.\n *\n * Two settings hold a picked document — `redirectPage` under the\n * \"Redirect to Page\" option, and `redirectRelation` under the URL option —\n * and both resolve through the same path, because they ask the same question.\n *\n * Every way of failing to produce a URL is logged. A form whose destination\n * cannot be built behaves exactly like one configured to stay put, so without\n * a line in the log the two are indistinguishable from the outside.\n */\nasync function resolveRedirectUrl(\n form: FormDocument,\n pluginConfig: ResolvedFormBuilderConfig,\n pluginContext: PluginContext\n): Promise<string | undefined> {\n const settings = form.settings;\n if (!settings) return undefined;\n\n // A typed URL is returned verbatim: it is the whole point of that option,\n // and it may legitimately point off-site.\n if (settings.confirmationType === \"redirect\" && settings.redirectUrl) {\n return settings.redirectUrl;\n }\n\n // Otherwise whichever key names a document, read through the SAME function\n // the save-time rule uses. Two readers here would let this path redirect to\n // a document the rule never inspected — which it did: the rule matched only\n // `relationship`, so a `redirect`-with-relation form was resolved here and\n // guarded nowhere.\n const field = pickedDocumentField(settings as Record<string, unknown>);\n if (!field) return undefined;\n\n return urlForPickedDocument(\n (settings as Record<string, unknown>)[field],\n form,\n pluginConfig,\n pluginContext\n );\n}\n\n/**\n * The URL for a picked document, or undefined with a reason in the log.\n *\n * Every branch reports. A form whose destination cannot be built behaves\n * exactly like one configured to stay put, so without a line here the two are\n * indistinguishable from outside — which is how a redirect that never fired\n * went unnoticed.\n */\nasync function urlForPickedDocument(\n stored: unknown,\n form: FormDocument,\n pluginConfig: ResolvedFormBuilderConfig,\n pluginContext: PluginContext\n): Promise<string | undefined> {\n const { logger } = pluginContext;\n const patterns = pluginConfig.redirectRelationships;\n const reference = parseRedirectReference(stored, Object.keys(patterns));\n\n if (!reference) {\n logger.warn?.(\"Form redirects to a page, but names no readable document\", {\n form: form.slug,\n });\n return undefined;\n }\n\n const pattern = patterns[reference.collection];\n if (!pattern) {\n logger.warn?.(\n \"Form redirects to a collection that is not in redirectRelationships\",\n { form: form.slug, collection: reference.collection }\n );\n return undefined;\n }\n\n const target = await readTarget(reference, form, pluginContext);\n if (!target) return undefined;\n\n // Reachability is re-decided HERE, not inherited from the save. Nothing runs\n // a forms hook when the target itself changes, so a page that was published\n // when the form was saved can be unpublished afterwards and the save-time\n // rule never sees it. Sending a visitor there produces exactly the \"page not\n // found\" that rule exists to prevent, so this degrades to no redirect — the\n // submission is still stored and still succeeds.\n // The target collection's own `localized` setting, resolved at plugin init.\n // Absent means it could not be read, which reports as undecided rather than\n // as \"not localized\" — dropping a redirect on that guess would strand a\n // destination a translation is still serving.\n if (\n documentReachability(\n target,\n pluginConfig.redirectTargetLocalization[reference.collection]\n ) === \"unreachable\"\n ) {\n logger.warn?.(\"Form redirects to a page that has never been published\", {\n form: form.slug,\n collection: reference.collection,\n target: reference.id,\n });\n return undefined;\n }\n\n return buildUrl(pattern, target, reference.collection, form, pluginContext);\n}\n\n/**\n * The pattern applied, or undefined with the reason logged.\n *\n * Separate because a configured pattern may be a FUNCTION — host code, which\n * can throw. Letting that escape would reach `submitForm`'s outer catch AFTER\n * the submission row exists, reporting a failure the caller may retry into a\n * duplicate of a submission that was already saved.\n */\nfunction buildUrl(\n pattern: RedirectUrlPattern,\n target: RedirectTargetDocument,\n collection: string,\n form: FormDocument,\n pluginContext: PluginContext\n): string | undefined {\n const { logger } = pluginContext;\n try {\n const url = applyRedirectPattern(pattern, target);\n if (!url) {\n logger.warn?.(\n \"Form redirect pattern could not be filled from the target document\",\n { form: form.slug, collection }\n );\n }\n return url;\n } catch (error) {\n logger.warn?.(\"Form redirect pattern threw while building a URL\", {\n form: form.slug,\n collection,\n error: error instanceof Error ? error.message : String(error),\n });\n return undefined;\n }\n}\n\n/** The target row, or undefined — a deleted target and a failed read differ. */\nasync function readTarget(\n reference: { collection: string; id: string },\n form: FormDocument,\n pluginContext: PluginContext\n): Promise<RedirectTargetDocument | undefined> {\n const { logger } = pluginContext;\n const context = {\n form: form.slug,\n collection: reference.collection,\n id: reference.id,\n };\n\n let row: unknown;\n try {\n row = await pluginContext.services.collections.findEntryById(\n reference.collection,\n reference.id,\n { as: \"system\" }\n );\n } catch (error) {\n // The submission already succeeded, so this degrades to \"no redirect\"\n // rather than failing the request — reported, because a broken read and a\n // deleted target are different problems with the same symptom.\n logger.warn?.(\"Form redirect target could not be read\", {\n ...context,\n error: error instanceof Error ? error.message : String(error),\n });\n return undefined;\n }\n\n if (!row || typeof row !== \"object\") {\n logger.warn?.(\"Form redirect target no longer exists\", context);\n return undefined;\n }\n return row as RedirectTargetDocument;\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 * 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 * 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":["option","url","text","textarea","select","json","date","relationship","relationship","json","select","text","textarea","date","url","email","text","textarea","select","checkbox","date","schema","date","url","url"]}
|