payload-dynamic-forms 0.0.0-stage → 0.2.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 (99) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +461 -2
  3. package/dist/access.d.ts +7 -0
  4. package/dist/access.js +10 -0
  5. package/dist/access.js.map +1 -0
  6. package/dist/collections/attachments.d.ts +6 -0
  7. package/dist/collections/attachments.js +67 -0
  8. package/dist/collections/attachments.js.map +1 -0
  9. package/dist/collections/blocks.d.ts +27 -0
  10. package/dist/collections/blocks.js +495 -0
  11. package/dist/collections/blocks.js.map +1 -0
  12. package/dist/collections/forms.d.ts +3 -0
  13. package/dist/collections/forms.js +558 -0
  14. package/dist/collections/forms.js.map +1 -0
  15. package/dist/collections/overrides.d.ts +7 -0
  16. package/dist/collections/overrides.js +49 -0
  17. package/dist/collections/overrides.js.map +1 -0
  18. package/dist/collections/ruleFields.d.ts +6 -0
  19. package/dist/collections/ruleFields.js +97 -0
  20. package/dist/collections/ruleFields.js.map +1 -0
  21. package/dist/collections/submissions.d.ts +7 -0
  22. package/dist/collections/submissions.js +225 -0
  23. package/dist/collections/submissions.js.map +1 -0
  24. package/dist/delivery/run.d.ts +37 -0
  25. package/dist/delivery/run.js +234 -0
  26. package/dist/delivery/run.js.map +1 -0
  27. package/dist/delivery/webhook.d.ts +41 -0
  28. package/dist/delivery/webhook.js +120 -0
  29. package/dist/delivery/webhook.js.map +1 -0
  30. package/dist/endpoints/public.d.ts +4 -0
  31. package/dist/endpoints/public.js +27 -0
  32. package/dist/endpoints/public.js.map +1 -0
  33. package/dist/endpoints/resend.d.ts +4 -0
  34. package/dist/endpoints/resend.js +58 -0
  35. package/dist/endpoints/resend.js.map +1 -0
  36. package/dist/endpoints/submit.d.ts +4 -0
  37. package/dist/endpoints/submit.js +320 -0
  38. package/dist/endpoints/submit.js.map +1 -0
  39. package/dist/exports/shared.d.ts +13 -0
  40. package/dist/exports/shared.js +13 -0
  41. package/dist/exports/shared.js.map +1 -0
  42. package/dist/formBlock.d.ts +13 -0
  43. package/dist/formBlock.js +25 -0
  44. package/dist/formBlock.js.map +1 -0
  45. package/dist/hooks/resendTick.d.ts +8 -0
  46. package/dist/hooks/resendTick.js +55 -0
  47. package/dist/hooks/resendTick.js.map +1 -0
  48. package/dist/hooks/validateForm.d.ts +8 -0
  49. package/dist/hooks/validateForm.js +251 -0
  50. package/dist/hooks/validateForm.js.map +1 -0
  51. package/dist/index.d.ts +18 -0
  52. package/dist/index.js +130 -0
  53. package/dist/index.js.map +1 -0
  54. package/dist/security/crypto.d.ts +9 -0
  55. package/dist/security/crypto.js +54 -0
  56. package/dist/security/crypto.js.map +1 -0
  57. package/dist/security/rateLimit.d.ts +13 -0
  58. package/dist/security/rateLimit.js +28 -0
  59. package/dist/security/rateLimit.js.map +1 -0
  60. package/dist/security/request.d.ts +3 -0
  61. package/dist/security/request.js +13 -0
  62. package/dist/security/request.js.map +1 -0
  63. package/dist/security/turnstile.d.ts +28 -0
  64. package/dist/security/turnstile.js +56 -0
  65. package/dist/security/turnstile.js.map +1 -0
  66. package/dist/server/forms.d.ts +73 -0
  67. package/dist/server/forms.js +215 -0
  68. package/dist/server/forms.js.map +1 -0
  69. package/dist/server/resend.d.ts +27 -0
  70. package/dist/server/resend.js +84 -0
  71. package/dist/server/resend.js.map +1 -0
  72. package/dist/shared/answers.d.ts +47 -0
  73. package/dist/shared/answers.js +282 -0
  74. package/dist/shared/answers.js.map +1 -0
  75. package/dist/shared/operators.d.ts +41 -0
  76. package/dist/shared/operators.js +54 -0
  77. package/dist/shared/operators.js.map +1 -0
  78. package/dist/shared/rules.d.ts +58 -0
  79. package/dist/shared/rules.js +324 -0
  80. package/dist/shared/rules.js.map +1 -0
  81. package/dist/shared/steps.d.ts +28 -0
  82. package/dist/shared/steps.js +109 -0
  83. package/dist/shared/steps.js.map +1 -0
  84. package/dist/shared/tokens.d.ts +25 -0
  85. package/dist/shared/tokens.js +96 -0
  86. package/dist/shared/tokens.js.map +1 -0
  87. package/dist/shared/types.d.ts +181 -0
  88. package/dist/shared/types.js +5 -0
  89. package/dist/shared/types.js.map +1 -0
  90. package/dist/shared/whatsapp.d.ts +20 -0
  91. package/dist/shared/whatsapp.js +26 -0
  92. package/dist/shared/whatsapp.js.map +1 -0
  93. package/dist/shared/words.d.ts +9 -0
  94. package/dist/shared/words.js +41 -0
  95. package/dist/shared/words.js.map +1 -0
  96. package/dist/types.d.ts +182 -0
  97. package/dist/types.js +3 -0
  98. package/dist/types.js.map +1 -0
  99. package/package.json +114 -4
@@ -0,0 +1,41 @@
1
+ export type WebhookSettings = {
2
+ /**
3
+ * Headers applied under the admin `headers` (e.g. resolved from the plugin config). A later
4
+ * header replaces an earlier one of the same name, in any case.
5
+ */
6
+ configHeaders?: null | Record<string, string>;
7
+ /** The webhook's own (admin) secret headers. */
8
+ headers?: Array<{
9
+ key?: null | string;
10
+ value?: null | string;
11
+ }> | null;
12
+ method?: 'PATCH' | 'POST' | 'PUT' | null;
13
+ retries?: null | number;
14
+ /** The secret to sign with, already resolved. Empty: the request is sent unsigned. */
15
+ signingSecret?: null | string;
16
+ timeoutMs?: null | number;
17
+ url?: null | string;
18
+ /**
19
+ * The Standard Webhooks `webhook-id`, the same on every try. Missing: `msg_` plus a random id
20
+ * made once per call. Characters outside `[A-Za-z0-9_]` become `_`.
21
+ */
22
+ webhookId?: null | string;
23
+ };
24
+ export type WebhookResult = {
25
+ error?: string;
26
+ ok: boolean;
27
+ status?: number;
28
+ tries: number;
29
+ };
30
+ /** Pauses between tries: 0.5 s, 1 s, 2 s. */
31
+ export declare const RETRY_PAUSES_MS: number[];
32
+ /** `X-Form-Signature` value: `t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<body>">`. */
33
+ export declare function signWebhookBody(secret: string, body: string, now?: number): Promise<string>;
34
+ /** `msg_<submissionId>_<outputId>` when both exist, else `msg_` plus a random id; only `[A-Za-z0-9_]`. */
35
+ export declare function webhookMessageId(submissionId?: null | number | string, outputId?: null | string): string;
36
+ /**
37
+ * Sends one webhook: JSON body, config headers under the secret headers, an optional signature
38
+ * (given resolved), a timeout per try (default 8 s) and up to 3 retries with growing pauses. A
39
+ * non-2xx answer or a timeout is a failed try.
40
+ */
41
+ export declare function sendWebhook(settings: WebhookSettings, body: unknown): Promise<WebhookResult>;
@@ -0,0 +1,120 @@
1
+ import { hmacSha256Base64, hmacSha256Hex, standardWebhooksKey } from '../security/crypto.js';
2
+ /** Pauses between tries: 0.5 s, 1 s, 2 s. */ export const RETRY_PAUSES_MS = [
3
+ 500,
4
+ 1000,
5
+ 2000
6
+ ];
7
+ const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
8
+ /** `X-Form-Signature` value: `t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<body>">`. */ export async function signWebhookBody(secret, body, now = Date.now()) {
9
+ const t = Math.floor(now / 1000);
10
+ return `t=${t},v1=${await hmacSha256Hex(secret, `${t}.${body}`)}`;
11
+ }
12
+ /** `msg_<submissionId>_<outputId>` when both exist, else `msg_` plus a random id; only `[A-Za-z0-9_]`. */ export function webhookMessageId(submissionId, outputId) {
13
+ const raw = submissionId !== undefined && submissionId !== null && submissionId !== '' && outputId ? `msg_${submissionId}_${outputId}` : `msg_${crypto.randomUUID().replace(/-/g, '')}`;
14
+ return raw.replace(/\W/g, '_');
15
+ }
16
+ /**
17
+ * Request headers in order, a later one replacing an earlier one whose name matches in any case:
18
+ * `Content-Type`, then each group in turn.
19
+ */ function mergeHeaders(...groups) {
20
+ const merged = new Map([
21
+ [
22
+ 'content-type',
23
+ [
24
+ 'Content-Type',
25
+ 'application/json'
26
+ ]
27
+ ]
28
+ ]);
29
+ for (const group of groups){
30
+ for (const [key, value] of group){
31
+ merged.set(key.toLowerCase(), [
32
+ key,
33
+ value
34
+ ]);
35
+ }
36
+ }
37
+ return Object.fromEntries(merged.values());
38
+ }
39
+ /**
40
+ * Sends one webhook: JSON body, config headers under the secret headers, an optional signature
41
+ * (given resolved), a timeout per try (default 8 s) and up to 3 retries with growing pauses. A
42
+ * non-2xx answer or a timeout is a failed try.
43
+ */ export async function sendWebhook(settings, body) {
44
+ const json = JSON.stringify(body);
45
+ const timeoutMs = Math.min(30_000, Math.max(500, Number(settings.timeoutMs) || 8000));
46
+ const retries = Math.min(3, Math.max(0, Math.floor(Number(settings.retries) || 0)));
47
+ const configHeaders = Object.entries(settings.configHeaders || {}).filter((entry)=>Boolean(entry[0]) && typeof entry[1] === 'string');
48
+ const adminHeaders = (settings.headers || []).flatMap((header)=>header?.key && header.value ? [
49
+ [
50
+ header.key,
51
+ header.value
52
+ ]
53
+ ] : []);
54
+ const secret = settings.signingSecret || '';
55
+ const webhookId = settings.webhookId ? settings.webhookId.replace(/\W/g, '_') : webhookMessageId();
56
+ const key = secret ? standardWebhooksKey(secret) : null;
57
+ let lastError = '';
58
+ let lastStatus;
59
+ let tries = 0;
60
+ for(let attempt = 0; attempt <= retries; attempt += 1){
61
+ if (attempt > 0) {
62
+ await sleep(RETRY_PAUSES_MS[Math.min(attempt - 1, RETRY_PAUSES_MS.length - 1)]);
63
+ }
64
+ tries += 1;
65
+ // Signature headers come last: neither config nor admin headers can replace them.
66
+ const signature = [
67
+ [
68
+ 'webhook-id',
69
+ webhookId
70
+ ]
71
+ ];
72
+ if (secret && key) {
73
+ const now = Date.now();
74
+ const t = Math.floor(now / 1000);
75
+ signature.push([
76
+ 'X-Form-Signature',
77
+ await signWebhookBody(secret, json, now)
78
+ ], [
79
+ 'webhook-timestamp',
80
+ String(t)
81
+ ], [
82
+ 'webhook-signature',
83
+ `v1,${await hmacSha256Base64(key, `${webhookId}.${t}.${json}`)}`
84
+ ]);
85
+ }
86
+ const headers = mergeHeaders(configHeaders, adminHeaders, signature);
87
+ const controller = new AbortController();
88
+ const timer = setTimeout(()=>controller.abort(), timeoutMs);
89
+ try {
90
+ const response = await fetch(String(settings.url || ''), {
91
+ body: json,
92
+ headers,
93
+ method: settings.method || 'POST',
94
+ signal: controller.signal
95
+ });
96
+ lastStatus = response.status;
97
+ if (response.ok) {
98
+ return {
99
+ ok: true,
100
+ status: response.status,
101
+ tries
102
+ };
103
+ }
104
+ lastError = `Webhook answered ${response.status}.`;
105
+ } catch (error) {
106
+ lastStatus = undefined;
107
+ lastError = controller.signal.aborted ? `Webhook timed out after ${timeoutMs} ms.` : error instanceof Error ? error.message : String(error);
108
+ } finally{
109
+ clearTimeout(timer);
110
+ }
111
+ }
112
+ return {
113
+ error: lastError,
114
+ ok: false,
115
+ status: lastStatus,
116
+ tries
117
+ };
118
+ }
119
+
120
+ //# sourceMappingURL=webhook.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/delivery/webhook.ts"],"sourcesContent":["import { hmacSha256Base64, hmacSha256Hex, standardWebhooksKey } from '../security/crypto.js'\n\nexport type WebhookSettings = {\n /**\n * Headers applied under the admin `headers` (e.g. resolved from the plugin config). A later\n * header replaces an earlier one of the same name, in any case.\n */\n configHeaders?: null | Record<string, string>\n /** The webhook's own (admin) secret headers. */\n headers?: Array<{ key?: null | string; value?: null | string }> | null\n method?: 'PATCH' | 'POST' | 'PUT' | null\n retries?: null | number\n /** The secret to sign with, already resolved. Empty: the request is sent unsigned. */\n signingSecret?: null | string\n timeoutMs?: null | number\n url?: null | string\n /**\n * The Standard Webhooks `webhook-id`, the same on every try. Missing: `msg_` plus a random id\n * made once per call. Characters outside `[A-Za-z0-9_]` become `_`.\n */\n webhookId?: null | string\n}\n\nexport type WebhookResult = { error?: string; ok: boolean; status?: number; tries: number }\n\n/** Pauses between tries: 0.5 s, 1 s, 2 s. */\nexport const RETRY_PAUSES_MS = [500, 1000, 2000]\n\nconst sleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms))\n\n/** `X-Form-Signature` value: `t=<unix seconds>,v1=<hex HMAC-SHA256 of \"<t>.<body>\">`. */\nexport async function signWebhookBody(secret: string, body: string, now = Date.now()): Promise<string> {\n const t = Math.floor(now / 1000)\n return `t=${t},v1=${await hmacSha256Hex(secret, `${t}.${body}`)}`\n}\n\n/** `msg_<submissionId>_<outputId>` when both exist, else `msg_` plus a random id; only `[A-Za-z0-9_]`. */\nexport function webhookMessageId(submissionId?: null | number | string, outputId?: null | string): string {\n const raw =\n submissionId !== undefined && submissionId !== null && submissionId !== '' && outputId\n ? `msg_${submissionId}_${outputId}`\n : `msg_${crypto.randomUUID().replace(/-/g, '')}`\n return raw.replace(/\\W/g, '_')\n}\n\n/**\n * Request headers in order, a later one replacing an earlier one whose name matches in any case:\n * `Content-Type`, then each group in turn.\n */\nfunction mergeHeaders(...groups: Array<Array<[string, string]>>): Record<string, string> {\n const merged = new Map<string, [string, string]>([['content-type', ['Content-Type', 'application/json']]])\n for (const group of groups) {\n for (const [key, value] of group) {\n merged.set(key.toLowerCase(), [key, value])\n }\n }\n return Object.fromEntries(merged.values())\n}\n\n/**\n * Sends one webhook: JSON body, config headers under the secret headers, an optional signature\n * (given resolved), a timeout per try (default 8 s) and up to 3 retries with growing pauses. A\n * non-2xx answer or a timeout is a failed try.\n */\nexport async function sendWebhook(settings: WebhookSettings, body: unknown): Promise<WebhookResult> {\n const json = JSON.stringify(body)\n const timeoutMs = Math.min(30_000, Math.max(500, Number(settings.timeoutMs) || 8000))\n const retries = Math.min(3, Math.max(0, Math.floor(Number(settings.retries) || 0)))\n const configHeaders = Object.entries(settings.configHeaders || {}).filter(\n (entry): entry is [string, string] => Boolean(entry[0]) && typeof entry[1] === 'string',\n )\n const adminHeaders = (settings.headers || []).flatMap((header): Array<[string, string]> =>\n header?.key && header.value ? [[header.key, header.value]] : [],\n )\n const secret = settings.signingSecret || ''\n const webhookId = settings.webhookId ? settings.webhookId.replace(/\\W/g, '_') : webhookMessageId()\n const key = secret ? standardWebhooksKey(secret) : null\n let lastError = ''\n let lastStatus: number | undefined\n let tries = 0\n for (let attempt = 0; attempt <= retries; attempt += 1) {\n if (attempt > 0) {\n await sleep(RETRY_PAUSES_MS[Math.min(attempt - 1, RETRY_PAUSES_MS.length - 1)])\n }\n tries += 1\n // Signature headers come last: neither config nor admin headers can replace them.\n const signature: Array<[string, string]> = [['webhook-id', webhookId]]\n if (secret && key) {\n const now = Date.now()\n const t = Math.floor(now / 1000)\n signature.push(\n ['X-Form-Signature', await signWebhookBody(secret, json, now)],\n ['webhook-timestamp', String(t)],\n ['webhook-signature', `v1,${await hmacSha256Base64(key, `${webhookId}.${t}.${json}`)}`],\n )\n }\n const headers = mergeHeaders(configHeaders, adminHeaders, signature)\n const controller = new AbortController()\n const timer = setTimeout(() => controller.abort(), timeoutMs)\n try {\n const response = await fetch(String(settings.url || ''), {\n body: json,\n headers,\n method: settings.method || 'POST',\n signal: controller.signal,\n })\n lastStatus = response.status\n if (response.ok) {\n return { ok: true, status: response.status, tries }\n }\n lastError = `Webhook answered ${response.status}.`\n } catch (error) {\n lastStatus = undefined\n lastError = controller.signal.aborted\n ? `Webhook timed out after ${timeoutMs} ms.`\n : error instanceof Error\n ? error.message\n : String(error)\n } finally {\n clearTimeout(timer)\n }\n }\n return { error: lastError, ok: false, status: lastStatus, tries }\n}\n"],"names":["hmacSha256Base64","hmacSha256Hex","standardWebhooksKey","RETRY_PAUSES_MS","sleep","ms","Promise","resolve","setTimeout","signWebhookBody","secret","body","now","Date","t","Math","floor","webhookMessageId","submissionId","outputId","raw","undefined","crypto","randomUUID","replace","mergeHeaders","groups","merged","Map","group","key","value","set","toLowerCase","Object","fromEntries","values","sendWebhook","settings","json","JSON","stringify","timeoutMs","min","max","Number","retries","configHeaders","entries","filter","entry","Boolean","adminHeaders","headers","flatMap","header","signingSecret","webhookId","lastError","lastStatus","tries","attempt","length","signature","push","String","controller","AbortController","timer","abort","response","fetch","url","method","signal","status","ok","error","aborted","Error","message","clearTimeout"],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ import type { Endpoint } from 'payload';
2
+ import type { ResolvedOptions } from '../types.js';
3
+ /** `GET /api/<forms>/public/:slug` — the published form, ready to render, without delivery settings. */
4
+ export declare function publicFormEndpoint(options: ResolvedOptions): Endpoint;
@@ -0,0 +1,27 @@
1
+ import { getPublicForm } from '../server/forms.js';
2
+ /** `GET /api/<forms>/public/:slug` — the published form, ready to render, without delivery settings. */ export function publicFormEndpoint(options) {
3
+ return {
4
+ handler: async (req)=>{
5
+ const slug = String(req.routeParams?.slug || '');
6
+ const form = slug ? await getPublicForm(req.payload, slug, {
7
+ locale: req.locale || undefined,
8
+ req
9
+ }) : null;
10
+ if (!form) {
11
+ return Response.json({
12
+ message: options.words.notFound,
13
+ ok: false
14
+ }, {
15
+ status: 404
16
+ });
17
+ }
18
+ return Response.json(form, {
19
+ status: 200
20
+ });
21
+ },
22
+ method: 'get',
23
+ path: '/public/:slug'
24
+ };
25
+ }
26
+
27
+ //# sourceMappingURL=public.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/endpoints/public.ts"],"sourcesContent":["import type { Endpoint } from 'payload'\n\nimport type { ResolvedOptions } from '../types.js'\n\nimport { getPublicForm } from '../server/forms.js'\n\n/** `GET /api/<forms>/public/:slug` — the published form, ready to render, without delivery settings. */\nexport function publicFormEndpoint(options: ResolvedOptions): Endpoint {\n return {\n handler: async (req) => {\n const slug = String(req.routeParams?.slug || '')\n const form = slug ? await getPublicForm(req.payload, slug, { locale: (req.locale || undefined) as never, req }) : null\n if (!form) {\n return Response.json({ message: options.words.notFound, ok: false }, { status: 404 })\n }\n return Response.json(form, { status: 200 })\n },\n method: 'get',\n path: '/public/:slug',\n }\n}\n"],"names":["getPublicForm","publicFormEndpoint","options","handler","req","slug","String","routeParams","form","payload","locale","undefined","Response","json","message","words","notFound","ok","status","method","path"],"mappings":"AAIA,SAASA,aAAa,QAAQ,qBAAoB;AAElD,sGAAsG,GACtG,OAAO,SAASC,mBAAmBC,OAAwB;IACzD,OAAO;QACLC,SAAS,OAAOC;YACd,MAAMC,OAAOC,OAAOF,IAAIG,WAAW,EAAEF,QAAQ;YAC7C,MAAMG,OAAOH,OAAO,MAAML,cAAcI,IAAIK,OAAO,EAAEJ,MAAM;gBAAEK,QAASN,IAAIM,MAAM,IAAIC;gBAAqBP;YAAI,KAAK;YAClH,IAAI,CAACI,MAAM;gBACT,OAAOI,SAASC,IAAI,CAAC;oBAAEC,SAASZ,QAAQa,KAAK,CAACC,QAAQ;oBAAEC,IAAI;gBAAM,GAAG;oBAAEC,QAAQ;gBAAI;YACrF;YACA,OAAON,SAASC,IAAI,CAACL,MAAM;gBAAEU,QAAQ;YAAI;QAC3C;QACAC,QAAQ;QACRC,MAAM;IACR;AACF"}
@@ -0,0 +1,4 @@
1
+ import type { Endpoint } from 'payload';
2
+ import type { ResolvedOptions } from '../types.js';
3
+ /** `POST /api/<submissions>/:id/resend` — admins only: runs the failed outputs again. */
4
+ export declare function resendEndpoint(options: ResolvedOptions): Endpoint;
@@ -0,0 +1,58 @@
1
+ import { RESEND_CONTEXT } from '../hooks/resendTick.js';
2
+ import { resendFailedDeliveries } from '../server/resend.js';
3
+ /** `POST /api/<submissions>/:id/resend` — admins only: runs the failed outputs again. */ export function resendEndpoint(options) {
4
+ return {
5
+ handler: async (req)=>{
6
+ if (!req.user || !options.isAdmin(req)) {
7
+ return Response.json({
8
+ message: 'Forbidden',
9
+ ok: false
10
+ }, {
11
+ status: 403
12
+ });
13
+ }
14
+ const id = String(req.routeParams?.id || '');
15
+ let submission;
16
+ try {
17
+ submission = await req.payload.findByID({
18
+ id,
19
+ collection: options.slugs.submissions,
20
+ depth: 0,
21
+ overrideAccess: true
22
+ });
23
+ } catch {
24
+ return Response.json({
25
+ message: 'Not found',
26
+ ok: false
27
+ }, {
28
+ status: 404
29
+ });
30
+ }
31
+ const result = await resendFailedDeliveries(req.payload, submission, req);
32
+ if (result.added.length) {
33
+ await req.payload.update({
34
+ id,
35
+ collection: options.slugs.submissions,
36
+ context: {
37
+ [RESEND_CONTEXT]: true
38
+ },
39
+ data: {
40
+ deliveryLog: result.log,
41
+ status: result.status
42
+ },
43
+ overrideAccess: true
44
+ });
45
+ }
46
+ return Response.json({
47
+ deliveryLog: result.log,
48
+ ok: true,
49
+ resent: result.added.length,
50
+ status: result.status
51
+ });
52
+ },
53
+ method: 'post',
54
+ path: '/:id/resend'
55
+ };
56
+ }
57
+
58
+ //# sourceMappingURL=resend.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/endpoints/resend.ts"],"sourcesContent":["import type { Endpoint } from 'payload'\n\nimport type { ResolvedOptions } from '../types.js'\n\nimport { RESEND_CONTEXT } from '../hooks/resendTick.js'\nimport { resendFailedDeliveries, type SubmissionDoc } from '../server/resend.js'\n\n/** `POST /api/<submissions>/:id/resend` — admins only: runs the failed outputs again. */\nexport function resendEndpoint(options: ResolvedOptions): Endpoint {\n return {\n handler: async (req) => {\n if (!req.user || !options.isAdmin(req)) {\n return Response.json({ message: 'Forbidden', ok: false }, { status: 403 })\n }\n const id = String(req.routeParams?.id || '')\n let submission: SubmissionDoc\n try {\n submission = (await req.payload.findByID({\n id,\n collection: options.slugs.submissions as never,\n depth: 0,\n overrideAccess: true,\n })) as unknown as SubmissionDoc\n } catch {\n return Response.json({ message: 'Not found', ok: false }, { status: 404 })\n }\n const result = await resendFailedDeliveries(req.payload, submission, req)\n if (result.added.length) {\n await req.payload.update({\n id,\n collection: options.slugs.submissions as never,\n context: { [RESEND_CONTEXT]: true },\n data: { deliveryLog: result.log, status: result.status } as never,\n overrideAccess: true,\n })\n }\n return Response.json({ deliveryLog: result.log, ok: true, resent: result.added.length, status: result.status })\n },\n method: 'post',\n path: '/:id/resend',\n }\n}\n"],"names":["RESEND_CONTEXT","resendFailedDeliveries","resendEndpoint","options","handler","req","user","isAdmin","Response","json","message","ok","status","id","String","routeParams","submission","payload","findByID","collection","slugs","submissions","depth","overrideAccess","result","added","length","update","context","data","deliveryLog","log","resent","method","path"],"mappings":"AAIA,SAASA,cAAc,QAAQ,yBAAwB;AACvD,SAASC,sBAAsB,QAA4B,sBAAqB;AAEhF,uFAAuF,GACvF,OAAO,SAASC,eAAeC,OAAwB;IACrD,OAAO;QACLC,SAAS,OAAOC;YACd,IAAI,CAACA,IAAIC,IAAI,IAAI,CAACH,QAAQI,OAAO,CAACF,MAAM;gBACtC,OAAOG,SAASC,IAAI,CAAC;oBAAEC,SAAS;oBAAaC,IAAI;gBAAM,GAAG;oBAAEC,QAAQ;gBAAI;YAC1E;YACA,MAAMC,KAAKC,OAAOT,IAAIU,WAAW,EAAEF,MAAM;YACzC,IAAIG;YACJ,IAAI;gBACFA,aAAc,MAAMX,IAAIY,OAAO,CAACC,QAAQ,CAAC;oBACvCL;oBACAM,YAAYhB,QAAQiB,KAAK,CAACC,WAAW;oBACrCC,OAAO;oBACPC,gBAAgB;gBAClB;YACF,EAAE,OAAM;gBACN,OAAOf,SAASC,IAAI,CAAC;oBAAEC,SAAS;oBAAaC,IAAI;gBAAM,GAAG;oBAAEC,QAAQ;gBAAI;YAC1E;YACA,MAAMY,SAAS,MAAMvB,uBAAuBI,IAAIY,OAAO,EAAED,YAAYX;YACrE,IAAImB,OAAOC,KAAK,CAACC,MAAM,EAAE;gBACvB,MAAMrB,IAAIY,OAAO,CAACU,MAAM,CAAC;oBACvBd;oBACAM,YAAYhB,QAAQiB,KAAK,CAACC,WAAW;oBACrCO,SAAS;wBAAE,CAAC5B,eAAe,EAAE;oBAAK;oBAClC6B,MAAM;wBAAEC,aAAaN,OAAOO,GAAG;wBAAEnB,QAAQY,OAAOZ,MAAM;oBAAC;oBACvDW,gBAAgB;gBAClB;YACF;YACA,OAAOf,SAASC,IAAI,CAAC;gBAAEqB,aAAaN,OAAOO,GAAG;gBAAEpB,IAAI;gBAAMqB,QAAQR,OAAOC,KAAK,CAACC,MAAM;gBAAEd,QAAQY,OAAOZ,MAAM;YAAC;QAC/G;QACAqB,QAAQ;QACRC,MAAM;IACR;AACF"}
@@ -0,0 +1,4 @@
1
+ import type { Endpoint } from 'payload';
2
+ import type { ResolvedOptions } from '../types.js';
3
+ /** `POST /api/<forms>/submit/:slug` — every check runs here, whatever the browser did. */
4
+ export declare function submitEndpoint(options: ResolvedOptions): Endpoint;
@@ -0,0 +1,320 @@
1
+ import { Buffer } from 'node:buffer';
2
+ import { hasDeliveries, runDeliveries, statusFromLog } from '../delivery/run.js';
3
+ import { checkRateLimit } from '../security/rateLimit.js';
4
+ import { getRequestIP, isHoneypotFilled } from '../security/request.js';
5
+ import { checkTurnstile } from '../security/turnstile.js';
6
+ import { loadPublishedForm } from '../server/forms.js';
7
+ import { buildSnapshot, checkAnswers } from '../shared/answers.js';
8
+ import { getFieldLabel, getInputFields } from '../shared/rules.js';
9
+ import { buildWhatsAppMessage, whatsappUrl } from '../shared/whatsapp.js';
10
+ function respond(status, body) {
11
+ return Response.json(body, {
12
+ status
13
+ });
14
+ }
15
+ function isFile(value) {
16
+ return typeof value === 'object' && value !== null && typeof value.arrayBuffer === 'function' && 'name' in value;
17
+ }
18
+ async function parseBody(req) {
19
+ const type = req.headers.get('content-type') || '';
20
+ const raw = {};
21
+ const files = {};
22
+ if (type.includes('multipart/form-data') || type.includes('application/x-www-form-urlencoded')) {
23
+ const data = await req.formData();
24
+ for (const key of new Set(data.keys())){
25
+ const values = data.getAll(key);
26
+ const fileValues = values.filter(isFile).filter((f)=>f.size > 0);
27
+ if (fileValues.length) {
28
+ files[key] = fileValues;
29
+ continue;
30
+ }
31
+ const text = values.filter((v)=>typeof v === 'string');
32
+ raw[key] = text.length > 1 ? text : text[0];
33
+ }
34
+ return {
35
+ files,
36
+ raw
37
+ };
38
+ }
39
+ const json = await req.json();
40
+ if (json && typeof json === 'object' && !Array.isArray(json)) {
41
+ Object.assign(raw, json);
42
+ }
43
+ return {
44
+ files,
45
+ raw
46
+ };
47
+ }
48
+ /** `POST /api/<forms>/submit/:slug` — every check runs here, whatever the browser did. */ export function submitEndpoint(options) {
49
+ return {
50
+ handler: async (req)=>{
51
+ const payload = req.payload;
52
+ const slug = String(req.routeParams?.slug || '');
53
+ const locale = req.locale || undefined;
54
+ const loaded = slug ? await loadPublishedForm(payload, slug, {
55
+ locale,
56
+ req
57
+ }) : null;
58
+ if (!loaded) {
59
+ return respond(404, {
60
+ message: options.words.notFound,
61
+ ok: false
62
+ });
63
+ }
64
+ const { delivery, form } = loaded;
65
+ const words = form.wording;
66
+ if (form.intakeState === 'closed') {
67
+ return respond(409, {
68
+ message: words.closed,
69
+ ok: false
70
+ });
71
+ }
72
+ let body;
73
+ try {
74
+ body = await parseBody(req);
75
+ } catch {
76
+ return respond(400, {
77
+ message: words.genericError,
78
+ ok: false
79
+ });
80
+ }
81
+ const { files, raw } = body;
82
+ const ip = getRequestIP(req.headers);
83
+ if (form.spamChecks) {
84
+ if (isHoneypotFilled(raw, options.honeypotField)) {
85
+ return respond(400, {
86
+ message: words.genericError,
87
+ ok: false
88
+ });
89
+ }
90
+ const human = await checkTurnstile({
91
+ ip,
92
+ logger: payload.logger,
93
+ settings: options.turnstile,
94
+ token: raw[options.turnstile.fieldName]
95
+ });
96
+ if (!human) {
97
+ return respond(400, {
98
+ message: words.securityCheckFailed,
99
+ ok: false
100
+ });
101
+ }
102
+ if (options.rateLimit) {
103
+ const allowed = await checkRateLimit({
104
+ formId: form.id,
105
+ ip,
106
+ max: options.rateLimit.max,
107
+ payload,
108
+ windowSeconds: options.rateLimit.windowSeconds
109
+ });
110
+ if (!allowed) {
111
+ return respond(429, {
112
+ message: words.tooManyTries,
113
+ ok: false
114
+ });
115
+ }
116
+ }
117
+ }
118
+ const fileLikes = {};
119
+ for (const [key, list] of Object.entries(files)){
120
+ fileLikes[key] = list.map((f)=>({
121
+ name: f.name,
122
+ type: f.type,
123
+ size: f.size
124
+ }));
125
+ }
126
+ const checked = checkAnswers(form, raw, {
127
+ fieldTypes: options.fieldTypes,
128
+ files: fileLikes,
129
+ limits: options.attachments || undefined,
130
+ words
131
+ });
132
+ if (!checked.ok) {
133
+ return respond(400, {
134
+ fieldErrors: checked.fieldErrors,
135
+ message: words.errorPrefix,
136
+ ok: false
137
+ });
138
+ }
139
+ const answers = checked.answers;
140
+ let waUrl;
141
+ if (delivery.whatsapp.enabled) {
142
+ const number = form.whatsapp?.number;
143
+ if (!number) {
144
+ payload.logger.error(`payload-dynamic-forms: form "${form.slug}" has WhatsApp on but no number (form or whatsapp.number).`);
145
+ return respond(500, {
146
+ message: words.genericError,
147
+ ok: false
148
+ });
149
+ }
150
+ const message = buildWhatsAppMessage(form, answers, {
151
+ fieldTypes: options.fieldTypes,
152
+ rowTemplate: delivery.whatsapp.rowTemplate,
153
+ template: delivery.whatsapp.messageTemplate
154
+ });
155
+ waUrl = whatsappUrl(number, message);
156
+ }
157
+ const inputs = new Map(getInputFields(form).map((entry)=>[
158
+ entry.key,
159
+ entry
160
+ ]));
161
+ const visibleUploads = checked.fileKeys.map((key)=>inputs.get(key)).filter(Boolean);
162
+ let submissionId;
163
+ let snapshot = buildSnapshot(form, answers, options.fieldTypes);
164
+ const created = [];
165
+ const outputs = hasDeliveries(delivery);
166
+ if (delivery.store) {
167
+ const attachmentSlug = options.attachments?.collection;
168
+ try {
169
+ for (const { field, key } of visibleUploads){
170
+ for (const file of files[key]){
171
+ const doc = await payload.create({
172
+ collection: attachmentSlug,
173
+ data: {
174
+ fieldKey: key,
175
+ fieldLabel: getFieldLabel(field, key),
176
+ form: form.id
177
+ },
178
+ file: {
179
+ name: file.name || 'upload',
180
+ data: Buffer.from(await file.arrayBuffer()),
181
+ mimetype: file.type || 'application/octet-stream',
182
+ size: file.size
183
+ },
184
+ overrideAccess: true
185
+ });
186
+ const filename = String(doc.filename || file.name);
187
+ created.push({
188
+ id: doc.id,
189
+ fieldKey: key,
190
+ filename
191
+ });
192
+ const prev = answers[key];
193
+ answers[key] = {
194
+ attachmentIds: [
195
+ ...prev?.attachmentIds || [],
196
+ doc.id
197
+ ],
198
+ filenames: [
199
+ ...prev?.filenames || [],
200
+ filename
201
+ ]
202
+ };
203
+ }
204
+ }
205
+ snapshot = buildSnapshot(form, answers, options.fieldTypes);
206
+ const submission = await payload.create({
207
+ collection: options.slugs.submissions,
208
+ data: {
209
+ answers,
210
+ attachments: created.map((c)=>c.id),
211
+ deliveryLog: [],
212
+ form: form.id,
213
+ metadata: {
214
+ ip: options.metadata.ip ? ip || null : null,
215
+ locale: req.locale || null,
216
+ userAgent: options.metadata.userAgent ? req.headers.get('user-agent') || null : null
217
+ },
218
+ snapshot,
219
+ status: outputs ? 'received' : 'processed'
220
+ },
221
+ overrideAccess: true
222
+ });
223
+ submissionId = submission.id;
224
+ for (const file of created){
225
+ await payload.update({
226
+ id: file.id,
227
+ collection: attachmentSlug,
228
+ data: {
229
+ submission: submissionId
230
+ },
231
+ overrideAccess: true
232
+ });
233
+ }
234
+ } catch (error) {
235
+ payload.logger.error({
236
+ err: error,
237
+ msg: `payload-dynamic-forms: could not store a submission of "${form.slug}"`
238
+ });
239
+ for (const file of created){
240
+ try {
241
+ await payload.delete({
242
+ id: file.id,
243
+ collection: attachmentSlug,
244
+ overrideAccess: true
245
+ });
246
+ } catch (deleteError) {
247
+ payload.logger.error({
248
+ err: deleteError,
249
+ msg: 'payload-dynamic-forms: could not remove an attachment'
250
+ });
251
+ }
252
+ }
253
+ return respond(500, {
254
+ message: words.genericError,
255
+ ok: false
256
+ });
257
+ }
258
+ }
259
+ if (outputs) {
260
+ const task = async ()=>{
261
+ try {
262
+ const entries = await runDeliveries({
263
+ answers,
264
+ attachments: created,
265
+ delivery,
266
+ form,
267
+ options,
268
+ payload,
269
+ req,
270
+ snapshot,
271
+ submissionId
272
+ });
273
+ for (const entry of entries.filter((e)=>e.result === 'failed')){
274
+ payload.logger.error(`payload-dynamic-forms: ${entry.kind} "${entry.label}" of "${form.slug}" failed: ${entry.error}`);
275
+ }
276
+ if (submissionId !== undefined) {
277
+ await payload.update({
278
+ id: submissionId,
279
+ collection: options.slugs.submissions,
280
+ data: {
281
+ deliveryLog: entries,
282
+ status: statusFromLog(entries)
283
+ },
284
+ overrideAccess: true
285
+ });
286
+ }
287
+ } catch (error) {
288
+ payload.logger.error({
289
+ err: error,
290
+ msg: `payload-dynamic-forms: delivery of "${form.slug}" failed`
291
+ });
292
+ }
293
+ };
294
+ if (options.delivery?.schedule) {
295
+ options.delivery.schedule(task);
296
+ } else {
297
+ await task();
298
+ }
299
+ }
300
+ const result = {
301
+ message: words.success,
302
+ ok: true
303
+ };
304
+ if (form.successType === 'redirect' && form.successRedirectURL) {
305
+ result.redirectURL = form.successRedirectURL;
306
+ }
307
+ if (submissionId !== undefined) {
308
+ result.submissionId = submissionId;
309
+ }
310
+ if (waUrl) {
311
+ result.whatsappUrl = waUrl;
312
+ }
313
+ return respond(200, result);
314
+ },
315
+ method: 'post',
316
+ path: '/submit/:slug'
317
+ };
318
+ }
319
+
320
+ //# sourceMappingURL=submit.js.map