@actuate-media/cms-core 0.31.0 → 0.33.0

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.
Files changed (84) hide show
  1. package/dist/__tests__/api/form-webhooks.test.d.ts +2 -0
  2. package/dist/__tests__/api/form-webhooks.test.d.ts.map +1 -0
  3. package/dist/__tests__/api/form-webhooks.test.js +162 -0
  4. package/dist/__tests__/api/form-webhooks.test.js.map +1 -0
  5. package/dist/__tests__/api/public-forms.test.d.ts +2 -0
  6. package/dist/__tests__/api/public-forms.test.d.ts.map +1 -0
  7. package/dist/__tests__/api/public-forms.test.js +125 -0
  8. package/dist/__tests__/api/public-forms.test.js.map +1 -0
  9. package/dist/__tests__/forms/notify.test.d.ts +2 -0
  10. package/dist/__tests__/forms/notify.test.d.ts.map +1 -0
  11. package/dist/__tests__/forms/notify.test.js +114 -0
  12. package/dist/__tests__/forms/notify.test.js.map +1 -0
  13. package/dist/__tests__/forms/spam.test.d.ts +2 -0
  14. package/dist/__tests__/forms/spam.test.d.ts.map +1 -0
  15. package/dist/__tests__/forms/spam.test.js +115 -0
  16. package/dist/__tests__/forms/spam.test.js.map +1 -0
  17. package/dist/__tests__/forms/submission-files.test.d.ts +2 -0
  18. package/dist/__tests__/forms/submission-files.test.d.ts.map +1 -0
  19. package/dist/__tests__/forms/submission-files.test.js +65 -0
  20. package/dist/__tests__/forms/submission-files.test.js.map +1 -0
  21. package/dist/__tests__/forms/submission-pipeline.test.d.ts +2 -0
  22. package/dist/__tests__/forms/submission-pipeline.test.d.ts.map +1 -0
  23. package/dist/__tests__/forms/submission-pipeline.test.js +131 -0
  24. package/dist/__tests__/forms/submission-pipeline.test.js.map +1 -0
  25. package/dist/__tests__/forms/submission-validation.test.d.ts +2 -0
  26. package/dist/__tests__/forms/submission-validation.test.d.ts.map +1 -0
  27. package/dist/__tests__/forms/submission-validation.test.js +108 -0
  28. package/dist/__tests__/forms/submission-validation.test.js.map +1 -0
  29. package/dist/__tests__/forms/webhooks.test.d.ts +2 -0
  30. package/dist/__tests__/forms/webhooks.test.d.ts.map +1 -0
  31. package/dist/__tests__/forms/webhooks.test.js +203 -0
  32. package/dist/__tests__/forms/webhooks.test.js.map +1 -0
  33. package/dist/api/handler-factory.d.ts.map +1 -1
  34. package/dist/api/handler-factory.js +2 -2
  35. package/dist/api/handler-factory.js.map +1 -1
  36. package/dist/api/handlers.d.ts.map +1 -1
  37. package/dist/api/handlers.js +420 -56
  38. package/dist/api/handlers.js.map +1 -1
  39. package/dist/api/router.d.ts +2 -1
  40. package/dist/api/router.d.ts.map +1 -1
  41. package/dist/api/router.js +3 -0
  42. package/dist/api/router.js.map +1 -1
  43. package/dist/config/types.d.ts +11 -6
  44. package/dist/config/types.d.ts.map +1 -1
  45. package/dist/forms/entries.d.ts.map +1 -1
  46. package/dist/forms/entries.js +16 -1
  47. package/dist/forms/entries.js.map +1 -1
  48. package/dist/forms/files.d.ts +38 -0
  49. package/dist/forms/files.d.ts.map +1 -0
  50. package/dist/forms/files.js +134 -0
  51. package/dist/forms/files.js.map +1 -0
  52. package/dist/forms/index.d.ts +6 -0
  53. package/dist/forms/index.d.ts.map +1 -1
  54. package/dist/forms/index.js +6 -0
  55. package/dist/forms/index.js.map +1 -1
  56. package/dist/forms/notify.d.ts +37 -0
  57. package/dist/forms/notify.d.ts.map +1 -0
  58. package/dist/forms/notify.js +198 -0
  59. package/dist/forms/notify.js.map +1 -0
  60. package/dist/forms/spam.d.ts +33 -0
  61. package/dist/forms/spam.d.ts.map +1 -0
  62. package/dist/forms/spam.js +93 -0
  63. package/dist/forms/spam.js.map +1 -0
  64. package/dist/forms/submission.d.ts +116 -0
  65. package/dist/forms/submission.d.ts.map +1 -0
  66. package/dist/forms/submission.js +336 -0
  67. package/dist/forms/submission.js.map +1 -0
  68. package/dist/forms/validation.d.ts +19 -0
  69. package/dist/forms/validation.d.ts.map +1 -0
  70. package/dist/forms/validation.js +235 -0
  71. package/dist/forms/validation.js.map +1 -0
  72. package/dist/forms/webhooks.d.ts +54 -0
  73. package/dist/forms/webhooks.d.ts.map +1 -0
  74. package/dist/forms/webhooks.js +292 -0
  75. package/dist/forms/webhooks.js.map +1 -0
  76. package/dist/index.d.ts +2 -2
  77. package/dist/index.d.ts.map +1 -1
  78. package/dist/index.js +1 -1
  79. package/dist/index.js.map +1 -1
  80. package/dist/security/client-ip.d.ts +11 -21
  81. package/dist/security/client-ip.d.ts.map +1 -1
  82. package/dist/security/client-ip.js +38 -0
  83. package/dist/security/client-ip.js.map +1 -1
  84. package/package.json +1 -1
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Form submission notifications — admin alerts + submitter autoresponder.
3
+ *
4
+ * Email is sent through the configured `platform.email` adapter (the same path
5
+ * password-reset uses). When no adapter is configured we no-op so a missing
6
+ * email integration never breaks the submission. All sends are best-effort and
7
+ * never throw into the caller — the submission is already persisted by the time
8
+ * we run.
9
+ *
10
+ * Templates support `{{placeholder}}` interpolation over the submission's field
11
+ * values plus `{{formName}}` / `{{entryNumber}}`. Field values are HTML-escaped
12
+ * before interpolation, so a value can never inject markup into the email.
13
+ */
14
+ import { getActuateConfig } from '../config/runtime.js';
15
+ import { createLogger } from '../diagnostics/logger.js';
16
+ const logger = createLogger('forms-notify');
17
+ const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
18
+ function escapeHtml(value) {
19
+ return value
20
+ .replace(/&/g, '&')
21
+ .replace(/</g, '&lt;')
22
+ .replace(/>/g, '&gt;')
23
+ .replace(/"/g, '&quot;')
24
+ .replace(/'/g, '&#39;');
25
+ }
26
+ function valueToString(value) {
27
+ if (value == null)
28
+ return '';
29
+ if (Array.isArray(value))
30
+ return value.map((v) => String(v)).join(', ');
31
+ if (typeof value === 'boolean')
32
+ return value ? 'Yes' : 'No';
33
+ return String(value);
34
+ }
35
+ /** A field is sensitive when explicitly encrypted or classified high-risk PII. */
36
+ function isSensitive(field) {
37
+ return Boolean(field.encrypted) || field.piiClassification === 'high';
38
+ }
39
+ /** Interpolate `{{key}}` tokens. Values are HTML-escaped to prevent injection. */
40
+ function renderTemplate(template, vars) {
41
+ return template.replace(/\{\{\s*([a-zA-Z0-9_]+)\s*\}\}/g, (_, key) => Object.prototype.hasOwnProperty.call(vars, key) ? escapeHtml(vars[key] ?? '') : '');
42
+ }
43
+ function buildVars(ctx) {
44
+ const vars = {
45
+ formName: ctx.formName,
46
+ entryNumber: ctx.entryNumber != null ? `#${String(ctx.entryNumber).padStart(5, '0')}` : '',
47
+ senderName: ctx.senderName ?? '',
48
+ senderEmail: ctx.senderEmail ?? '',
49
+ };
50
+ for (const field of ctx.fields) {
51
+ if (field.type === 'honeypot')
52
+ continue;
53
+ vars[field.key] = valueToString(ctx.data[field.key]);
54
+ }
55
+ return vars;
56
+ }
57
+ function buildFieldsTable(ctx, excludeSensitive) {
58
+ const rows = [];
59
+ const lines = [];
60
+ for (const field of ctx.fields) {
61
+ if (field.type === 'honeypot' || field.type === 'divider' || field.type === 'richtext')
62
+ continue;
63
+ const label = escapeHtml(field.label || field.key);
64
+ const sensitive = excludeSensitive && isSensitive(field);
65
+ const raw = sensitive ? '[hidden]' : valueToString(ctx.data[field.key]);
66
+ const value = escapeHtml(raw) || '<span style="color:#999">—</span>';
67
+ rows.push(`<tr><td style="padding:6px 12px;font-weight:500;color:#111;border-bottom:1px solid #eee;vertical-align:top">${label}</td>` +
68
+ `<td style="padding:6px 12px;color:#333;border-bottom:1px solid #eee">${value}</td></tr>`);
69
+ lines.push(`${field.label || field.key}: ${sensitive ? '[hidden]' : valueToString(ctx.data[field.key])}`);
70
+ }
71
+ const html = `<table style="border-collapse:collapse;width:100%;font-family:system-ui,sans-serif;font-size:14px">${rows.join('')}</table>`;
72
+ return { html, text: lines.join('\n') };
73
+ }
74
+ function adminEntryUrl(formId) {
75
+ const config = getActuateConfig();
76
+ const siteUrl = config?.seo?.siteUrl;
77
+ if (!siteUrl)
78
+ return null;
79
+ const adminPath = config?.admin?.path ?? '/admin';
80
+ return `${siteUrl.replace(/\/$/, '')}${adminPath}/forms/${formId}/submissions`;
81
+ }
82
+ /** Build the admin notification email (subject/html/text) for a submission. */
83
+ export function buildNotificationEmail(settings, ctx) {
84
+ const vars = buildVars(ctx);
85
+ const subject = settings.subjectTemplate
86
+ ? renderTemplate(settings.subjectTemplate, vars)
87
+ : `New submission: ${ctx.formName}`;
88
+ const includeFields = settings.includeFields !== false;
89
+ const table = includeFields
90
+ ? buildFieldsTable(ctx, Boolean(settings.excludeSensitiveFields))
91
+ : { html: '', text: '' };
92
+ const intro = settings.messageTemplate
93
+ ? renderTemplate(settings.messageTemplate, vars)
94
+ : `You received a new submission${ctx.entryNumber != null ? ` (${vars.entryNumber})` : ''} on <strong>${escapeHtml(ctx.formName)}</strong>.`;
95
+ const link = adminEntryUrl(ctx.formId);
96
+ const linkHtml = link
97
+ ? `<p style="margin-top:16px"><a href="${escapeHtml(link)}" style="color:#2563eb">View in admin →</a></p>`
98
+ : '';
99
+ const html = `<div style="font-family:system-ui,sans-serif;color:#111;max-width:640px">
100
+ <p style="font-size:15px">${intro}</p>
101
+ ${table.html}
102
+ ${linkHtml}
103
+ </div>`;
104
+ const textParts = [stripTags(intro), table.text, link ? `View in admin: ${link}` : ''].filter(Boolean);
105
+ return { subject, html, text: textParts.join('\n\n') };
106
+ }
107
+ function stripTags(html) {
108
+ return html
109
+ .replace(/<[^>]+>/g, '')
110
+ .replace(/&nbsp;/g, ' ')
111
+ .trim();
112
+ }
113
+ /** Resolve the reply-to address from the configured field, if it's a valid email. */
114
+ function resolveReplyTo(settings, ctx) {
115
+ if (settings.replyToFieldKey) {
116
+ const v = valueToString(ctx.data[settings.replyToFieldKey]).trim();
117
+ if (EMAIL_RE.test(v))
118
+ return v;
119
+ }
120
+ if (ctx.senderEmail && EMAIL_RE.test(ctx.senderEmail))
121
+ return ctx.senderEmail;
122
+ return undefined;
123
+ }
124
+ /** Build the submitter autoresponder email. */
125
+ export function buildAutoresponderEmail(settings, ctx) {
126
+ const vars = buildVars(ctx);
127
+ const subject = settings.autoresponderSubject
128
+ ? renderTemplate(settings.autoresponderSubject, vars)
129
+ : `We received your submission`;
130
+ const body = settings.autoresponderBody
131
+ ? renderTemplate(settings.autoresponderBody, vars)
132
+ : `Thanks${ctx.senderName ? `, ${escapeHtml(ctx.senderName)}` : ''}! We received your submission and will be in touch soon.`;
133
+ const html = `<div style="font-family:system-ui,sans-serif;color:#111;max-width:640px"><p style="font-size:15px">${body}</p></div>`;
134
+ return { subject, html, text: stripTags(body) };
135
+ }
136
+ async function sendEmail(options) {
137
+ const email = getActuateConfig()?.platform?.email;
138
+ if (!email)
139
+ return false;
140
+ await email.send(options);
141
+ return true;
142
+ }
143
+ /**
144
+ * Send admin notifications + autoresponder for a submission. Best-effort: logs
145
+ * and swallows per-recipient failures so one bad address can't block the rest
146
+ * or surface to the public submitter.
147
+ */
148
+ export async function sendFormNotifications(settings, ctx) {
149
+ const result = { notified: 0, autoresponded: false };
150
+ if (!getActuateConfig()?.platform?.email) {
151
+ return { ...result, skippedReason: 'no-email-adapter' };
152
+ }
153
+ if (!settings?.enabled) {
154
+ return { ...result, skippedReason: 'notifications-disabled' };
155
+ }
156
+ const recipients = (settings.recipients ?? []).filter((r) => EMAIL_RE.test(r.trim()));
157
+ if (recipients.length > 0) {
158
+ const email = buildNotificationEmail(settings, ctx);
159
+ const replyTo = resolveReplyTo(settings, ctx);
160
+ for (const to of recipients) {
161
+ try {
162
+ await sendEmail({
163
+ to: to.trim(),
164
+ subject: email.subject,
165
+ html: email.html,
166
+ text: email.text,
167
+ replyTo,
168
+ });
169
+ result.notified++;
170
+ }
171
+ catch (err) {
172
+ logger.error('notification send failed', {
173
+ formId: ctx.formId,
174
+ reason: err instanceof Error ? err.message : String(err),
175
+ });
176
+ }
177
+ }
178
+ }
179
+ if (settings.autoresponderEnabled) {
180
+ const key = settings.autoresponderEmailFieldKey;
181
+ const target = key ? valueToString(ctx.data[key]).trim() : (ctx.senderEmail ?? '');
182
+ if (target && EMAIL_RE.test(target)) {
183
+ try {
184
+ const auto = buildAutoresponderEmail(settings, ctx);
185
+ await sendEmail({ to: target, subject: auto.subject, html: auto.html, text: auto.text });
186
+ result.autoresponded = true;
187
+ }
188
+ catch (err) {
189
+ logger.error('autoresponder send failed', {
190
+ formId: ctx.formId,
191
+ reason: err instanceof Error ? err.message : String(err),
192
+ });
193
+ }
194
+ }
195
+ }
196
+ return result;
197
+ }
198
+ //# sourceMappingURL=notify.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notify.js","sourceRoot":"","sources":["../../src/forms/notify.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Spam scoring for public form submissions.
3
+ *
4
+ * Each enabled strategy contributes a reason; any reason flags the submission
5
+ * as `spam` so it is hidden from the default inbox while still being persisted
6
+ * (operators can review the spam folder and recover false positives). The
7
+ * honeypot is always checked when a honeypot field exists — it is free and has
8
+ * no false-positive risk for real users.
9
+ *
10
+ * This module is pure (no I/O) so it is trivially unit-testable and cannot leak
11
+ * PII or make outbound calls.
12
+ */
13
+ import type { FormField, FormSpamSettings, SpamProtectionStrategy, SpamStatus } from './types.js';
14
+ export interface SpamScoreInput {
15
+ fields: FormField[];
16
+ /** Raw (pre-validation) values — needed so the honeypot value is still visible. */
17
+ rawValues: Record<string, unknown>;
18
+ settings?: FormSpamSettings;
19
+ /** Site-wide default strategies merged with the per-form strategy list. */
20
+ defaultStrategies?: SpamProtectionStrategy[];
21
+ senderEmail?: string | null;
22
+ /** Milliseconds between form render and submit, when the client reports it. */
23
+ elapsedMs?: number | null;
24
+ /** Request `Origin` header (preferred) and `Referer` fallback for allowlisting. */
25
+ origin?: string | null;
26
+ referrer?: string | null;
27
+ }
28
+ export interface SpamScoreResult {
29
+ status: SpamStatus;
30
+ reasons: string[];
31
+ }
32
+ export declare function scoreSubmissionSpam(input: SpamScoreInput): SpamScoreResult;
33
+ //# sourceMappingURL=spam.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spam.d.ts","sourceRoot":"","sources":["../../src/forms/spam.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,gBAAgB,EAAE,sBAAsB,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAEjG,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,SAAS,EAAE,CAAA;IACnB,mFAAmF;IACnF,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAClC,QAAQ,CAAC,EAAE,gBAAgB,CAAA;IAC3B,2EAA2E;IAC3E,iBAAiB,CAAC,EAAE,sBAAsB,EAAE,CAAA;IAC5C,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,+EAA+E;IAC/E,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,mFAAmF;IACnF,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACzB;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,UAAU,CAAA;IAClB,OAAO,EAAE,MAAM,EAAE,CAAA;CAClB;AAgCD,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,cAAc,GAAG,eAAe,CA+D1E"}
@@ -0,0 +1,93 @@
1
+ function hostOf(value) {
2
+ if (!value)
3
+ return null;
4
+ try {
5
+ return new URL(value).hostname.toLowerCase();
6
+ }
7
+ catch {
8
+ return null;
9
+ }
10
+ }
11
+ function normalizeHost(domain) {
12
+ return domain
13
+ .trim()
14
+ .toLowerCase()
15
+ .replace(/^https?:\/\//, '')
16
+ .replace(/\/.*$/, '')
17
+ .replace(/^\*\./, '');
18
+ }
19
+ /** Concatenate user-supplied string values for keyword scanning. */
20
+ function textBlob(fields, rawValues) {
21
+ const parts = [];
22
+ for (const field of fields) {
23
+ if (field.type === 'honeypot' || field.type === 'divider')
24
+ continue;
25
+ const v = rawValues[field.key];
26
+ if (typeof v === 'string')
27
+ parts.push(v);
28
+ else if (Array.isArray(v))
29
+ parts.push(v.map((x) => String(x)).join(' '));
30
+ }
31
+ return parts.join(' \n ').toLowerCase();
32
+ }
33
+ export function scoreSubmissionSpam(input) {
34
+ const reasons = [];
35
+ const settings = input.settings ?? {};
36
+ const strategies = new Set([
37
+ ...(input.defaultStrategies ?? []),
38
+ ...(settings.strategies ?? []),
39
+ ]);
40
+ // Honeypot — always evaluated when a honeypot field/key exists. A populated
41
+ // honeypot means an automated client filled a field that is hidden from
42
+ // humans via CSS.
43
+ const honeypotKey = settings.honeypotFieldKey ?? input.fields.find((f) => f.type === 'honeypot')?.key;
44
+ if (honeypotKey) {
45
+ const v = input.rawValues[honeypotKey];
46
+ const filled = typeof v === 'string' ? v.trim() !== '' : v != null && v !== false;
47
+ if (filled)
48
+ reasons.push('honeypot');
49
+ }
50
+ // Time-to-submit — humans take a few seconds; instant submits are bots.
51
+ if (strategies.has('timeToSubmit') &&
52
+ typeof settings.minSubmitSeconds === 'number' &&
53
+ settings.minSubmitSeconds > 0 &&
54
+ typeof input.elapsedMs === 'number' &&
55
+ input.elapsedMs >= 0) {
56
+ if (input.elapsedMs < settings.minSubmitSeconds * 1000)
57
+ reasons.push('too_fast');
58
+ }
59
+ // Email / domain blocklist.
60
+ if (strategies.has('blocklist')) {
61
+ const email = (input.senderEmail ?? '').trim().toLowerCase();
62
+ if (email) {
63
+ const blockedEmails = (settings.blockedEmails ?? []).map((e) => e.trim().toLowerCase());
64
+ if (blockedEmails.includes(email))
65
+ reasons.push('blocked_email');
66
+ const domain = email.split('@')[1] ?? '';
67
+ const blockedDomains = (settings.blockedDomains ?? []).map((d) => normalizeHost(d));
68
+ if (domain && blockedDomains.includes(domain))
69
+ reasons.push('blocked_domain');
70
+ }
71
+ }
72
+ // Keyword blocklist.
73
+ if (strategies.has('keyword') && (settings.blockedKeywords?.length ?? 0) > 0) {
74
+ const blob = textBlob(input.fields, input.rawValues);
75
+ for (const keyword of settings.blockedKeywords ?? []) {
76
+ const k = keyword.trim().toLowerCase();
77
+ if (k && blob.includes(k)) {
78
+ reasons.push('blocked_keyword');
79
+ break;
80
+ }
81
+ }
82
+ }
83
+ // Domain allowlist — the submitting origin must be on the allowlist.
84
+ if (strategies.has('domainAllowlist') && (settings.allowedDomains?.length ?? 0) > 0) {
85
+ const host = hostOf(input.origin) ?? hostOf(input.referrer);
86
+ const allowed = (settings.allowedDomains ?? []).map((d) => normalizeHost(d));
87
+ const ok = host != null && allowed.some((a) => host === a || host.endsWith(`.${a}`));
88
+ if (!ok)
89
+ reasons.push('origin_not_allowed');
90
+ }
91
+ return { status: reasons.length > 0 ? 'spam' : 'clean', reasons };
92
+ }
93
+ //# sourceMappingURL=spam.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spam.js","sourceRoot":"","sources":["../../src/forms/spam.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,116 @@
1
+ import { type FieldError } from './validation.js';
2
+ import { type IncomingFile } from './files.js';
3
+ import type { FormDefinition, FormField, SpamStatus } from './types.js';
4
+ export interface SubmissionConfirmation {
5
+ type: 'message' | 'redirect';
6
+ message?: string;
7
+ redirectUrl?: string;
8
+ }
9
+ export interface SubmissionContext {
10
+ /** Raw field values from the request (object keyed by field key). */
11
+ values: Record<string, unknown>;
12
+ /** Already-validated file uploads to attach after the row is created. */
13
+ files?: IncomingFile[];
14
+ ip?: string;
15
+ userAgent?: string | null;
16
+ referrer?: string | null;
17
+ origin?: string | null;
18
+ pageUrl?: string | null;
19
+ /** Opaque attribution blob captured client-side; stored as-is. */
20
+ attribution?: Record<string, unknown> | null;
21
+ /** Milliseconds between render and submit (for the time-trap spam check). */
22
+ elapsedMs?: number | null;
23
+ }
24
+ export interface SubmissionResult {
25
+ submissionId: string;
26
+ entryNumber: number | null;
27
+ spamStatus: SpamStatus;
28
+ confirmation: SubmissionConfirmation;
29
+ }
30
+ /** Thrown when field validation fails; mapped to HTTP 422 by the API layer. */
31
+ export declare class SubmissionValidationError extends Error {
32
+ fieldErrors: FieldError[];
33
+ constructor(fieldErrors: FieldError[]);
34
+ }
35
+ /** Resolve the field schema the submission should be validated against. */
36
+ declare function resolveFields(form: FormDefinition): Promise<{
37
+ fields: FormField[];
38
+ versionId: string | null;
39
+ }>;
40
+ export interface SubmissionSideEffectInput {
41
+ form: FormDefinition;
42
+ fields: FormField[];
43
+ data: Record<string, unknown>;
44
+ submissionId: string;
45
+ entryNumber: number | null;
46
+ spamStatus: SpamStatus;
47
+ sender: {
48
+ name: string | null;
49
+ email: string | null;
50
+ };
51
+ submittedAt: string;
52
+ formSlug: string;
53
+ }
54
+ /**
55
+ * Best-effort notification + webhook fan-out after a submission is persisted.
56
+ * Exported so the API layer (or tests) can await it deterministically; the
57
+ * pipeline itself fires it without blocking the public response. Every branch
58
+ * is wrapped so a single failing channel can't take down the others.
59
+ */
60
+ export declare function runSubmissionSideEffects(input: SubmissionSideEffectInput): Promise<void>;
61
+ /**
62
+ * Process a validated request into a persisted submission. Throws
63
+ * `SubmissionValidationError` when field validation fails so the caller can
64
+ * return a structured 422.
65
+ */
66
+ export declare function processSubmission(form: FormDefinition, ctx: SubmissionContext): Promise<SubmissionResult>;
67
+ export interface PublicFormField {
68
+ id: string;
69
+ key: string;
70
+ label: string;
71
+ type: string;
72
+ required: boolean;
73
+ placeholder?: string;
74
+ helpText?: string;
75
+ defaultValue?: unknown;
76
+ options?: {
77
+ label: string;
78
+ value: string;
79
+ }[];
80
+ width?: string;
81
+ sortOrder: number;
82
+ validationRules?: Record<string, unknown>;
83
+ conditionalLogic?: unknown;
84
+ }
85
+ export interface PublicForm {
86
+ id: string;
87
+ name: string;
88
+ slug: string;
89
+ description?: string;
90
+ fields: PublicFormField[];
91
+ submitPath: string;
92
+ successMessage?: string;
93
+ redirectUrl?: string;
94
+ spam: {
95
+ honeypotFieldKey?: string;
96
+ minSubmitSeconds?: number;
97
+ };
98
+ captcha?: {
99
+ provider: string;
100
+ siteKey: string;
101
+ };
102
+ schemaVersionId?: string | null;
103
+ }
104
+ /**
105
+ * Project a form definition into the public, embeddable schema. Strips every
106
+ * server-only concern (access rules, notification recipients, webhook secrets,
107
+ * blocklists, encryption flags) so nothing sensitive leaks to the browser.
108
+ */
109
+ export declare function buildPublicForm(form: FormDefinition, fields: FormField[], schemaVersionId: string | null, options?: {
110
+ captcha?: {
111
+ provider: string;
112
+ siteKey: string;
113
+ };
114
+ }): PublicForm;
115
+ export { resolveFields as resolveSubmissionFields };
116
+ //# sourceMappingURL=submission.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"submission.d.ts","sourceRoot":"","sources":["../../src/forms/submission.ts"],"names":[],"mappings":"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"}