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.
- package/LICENSE +21 -0
- package/README.md +461 -2
- package/dist/access.d.ts +7 -0
- package/dist/access.js +10 -0
- package/dist/access.js.map +1 -0
- package/dist/collections/attachments.d.ts +6 -0
- package/dist/collections/attachments.js +67 -0
- package/dist/collections/attachments.js.map +1 -0
- package/dist/collections/blocks.d.ts +27 -0
- package/dist/collections/blocks.js +495 -0
- package/dist/collections/blocks.js.map +1 -0
- package/dist/collections/forms.d.ts +3 -0
- package/dist/collections/forms.js +558 -0
- package/dist/collections/forms.js.map +1 -0
- package/dist/collections/overrides.d.ts +7 -0
- package/dist/collections/overrides.js +49 -0
- package/dist/collections/overrides.js.map +1 -0
- package/dist/collections/ruleFields.d.ts +6 -0
- package/dist/collections/ruleFields.js +97 -0
- package/dist/collections/ruleFields.js.map +1 -0
- package/dist/collections/submissions.d.ts +7 -0
- package/dist/collections/submissions.js +225 -0
- package/dist/collections/submissions.js.map +1 -0
- package/dist/delivery/run.d.ts +37 -0
- package/dist/delivery/run.js +234 -0
- package/dist/delivery/run.js.map +1 -0
- package/dist/delivery/webhook.d.ts +41 -0
- package/dist/delivery/webhook.js +120 -0
- package/dist/delivery/webhook.js.map +1 -0
- package/dist/endpoints/public.d.ts +4 -0
- package/dist/endpoints/public.js +27 -0
- package/dist/endpoints/public.js.map +1 -0
- package/dist/endpoints/resend.d.ts +4 -0
- package/dist/endpoints/resend.js +58 -0
- package/dist/endpoints/resend.js.map +1 -0
- package/dist/endpoints/submit.d.ts +4 -0
- package/dist/endpoints/submit.js +320 -0
- package/dist/endpoints/submit.js.map +1 -0
- package/dist/exports/shared.d.ts +13 -0
- package/dist/exports/shared.js +13 -0
- package/dist/exports/shared.js.map +1 -0
- package/dist/formBlock.d.ts +13 -0
- package/dist/formBlock.js +25 -0
- package/dist/formBlock.js.map +1 -0
- package/dist/hooks/resendTick.d.ts +8 -0
- package/dist/hooks/resendTick.js +55 -0
- package/dist/hooks/resendTick.js.map +1 -0
- package/dist/hooks/validateForm.d.ts +8 -0
- package/dist/hooks/validateForm.js +251 -0
- package/dist/hooks/validateForm.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +130 -0
- package/dist/index.js.map +1 -0
- package/dist/security/crypto.d.ts +9 -0
- package/dist/security/crypto.js +54 -0
- package/dist/security/crypto.js.map +1 -0
- package/dist/security/rateLimit.d.ts +13 -0
- package/dist/security/rateLimit.js +28 -0
- package/dist/security/rateLimit.js.map +1 -0
- package/dist/security/request.d.ts +3 -0
- package/dist/security/request.js +13 -0
- package/dist/security/request.js.map +1 -0
- package/dist/security/turnstile.d.ts +28 -0
- package/dist/security/turnstile.js +56 -0
- package/dist/security/turnstile.js.map +1 -0
- package/dist/server/forms.d.ts +73 -0
- package/dist/server/forms.js +215 -0
- package/dist/server/forms.js.map +1 -0
- package/dist/server/resend.d.ts +27 -0
- package/dist/server/resend.js +84 -0
- package/dist/server/resend.js.map +1 -0
- package/dist/shared/answers.d.ts +47 -0
- package/dist/shared/answers.js +282 -0
- package/dist/shared/answers.js.map +1 -0
- package/dist/shared/operators.d.ts +41 -0
- package/dist/shared/operators.js +54 -0
- package/dist/shared/operators.js.map +1 -0
- package/dist/shared/rules.d.ts +58 -0
- package/dist/shared/rules.js +324 -0
- package/dist/shared/rules.js.map +1 -0
- package/dist/shared/steps.d.ts +28 -0
- package/dist/shared/steps.js +109 -0
- package/dist/shared/steps.js.map +1 -0
- package/dist/shared/tokens.d.ts +25 -0
- package/dist/shared/tokens.js +96 -0
- package/dist/shared/tokens.js.map +1 -0
- package/dist/shared/types.d.ts +181 -0
- package/dist/shared/types.js +5 -0
- package/dist/shared/types.js.map +1 -0
- package/dist/shared/whatsapp.d.ts +20 -0
- package/dist/shared/whatsapp.js +26 -0
- package/dist/shared/whatsapp.js.map +1 -0
- package/dist/shared/words.d.ts +9 -0
- package/dist/shared/words.js +41 -0
- package/dist/shared/words.js.map +1 -0
- package/dist/types.d.ts +182 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/package.json +114 -4
package/dist/index.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { attachmentLinkFields, attachmentsCollection } from './collections/attachments.js';
|
|
2
|
+
import { buildFieldBlocks } from './collections/blocks.js';
|
|
3
|
+
import { formsCollection } from './collections/forms.js';
|
|
4
|
+
import { applyOverrides } from './collections/overrides.js';
|
|
5
|
+
import { submissionsCollection } from './collections/submissions.js';
|
|
6
|
+
import { publicFormEndpoint } from './endpoints/public.js';
|
|
7
|
+
import { resendEndpoint } from './endpoints/resend.js';
|
|
8
|
+
import { submitEndpoint } from './endpoints/submit.js';
|
|
9
|
+
import { DEFAULT_FILE_LIMITS } from './shared/answers.js';
|
|
10
|
+
import { DEFAULT_WORDS } from './shared/words.js';
|
|
11
|
+
export const DEFAULT_SLUGS = {
|
|
12
|
+
attachments: 'custom-form-attachments',
|
|
13
|
+
forms: 'custom-forms',
|
|
14
|
+
submissions: 'custom-form-submissions'
|
|
15
|
+
};
|
|
16
|
+
export function resolveOptions(options = {}) {
|
|
17
|
+
const slugs = {
|
|
18
|
+
...DEFAULT_SLUGS,
|
|
19
|
+
...options.slugs
|
|
20
|
+
};
|
|
21
|
+
const attachments = options.attachments === false ? null : {
|
|
22
|
+
collection: options.attachments?.collection ? String(options.attachments.collection) : slugs.attachments,
|
|
23
|
+
createCollection: !options.attachments?.collection,
|
|
24
|
+
maxFileSize: options.attachments?.maxFileSize ?? DEFAULT_FILE_LIMITS.maxFileSize,
|
|
25
|
+
maxTotalSize: options.attachments?.maxTotalSize ?? DEFAULT_FILE_LIMITS.maxTotalSize,
|
|
26
|
+
mimeTypes: [
|
|
27
|
+
...options.attachments?.mimeTypes ?? DEFAULT_FILE_LIMITS.mimeTypes
|
|
28
|
+
]
|
|
29
|
+
};
|
|
30
|
+
if (attachments && !attachments.createCollection) {
|
|
31
|
+
slugs.attachments = attachments.collection;
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
attachments,
|
|
35
|
+
beforeEmail: options.beforeEmail,
|
|
36
|
+
delivery: options.delivery,
|
|
37
|
+
fieldTypes: {},
|
|
38
|
+
honeypotField: options.honeypotField || '_hp',
|
|
39
|
+
isAdmin: options.access?.isAdmin || ((req)=>Boolean(req.user)),
|
|
40
|
+
metadata: {
|
|
41
|
+
ip: options.metadata?.ip === true,
|
|
42
|
+
userAgent: options.metadata?.userAgent !== false
|
|
43
|
+
},
|
|
44
|
+
optionSources: options.optionSources,
|
|
45
|
+
rateLimit: options.rateLimit === false ? null : {
|
|
46
|
+
max: options.rateLimit?.max ?? 5,
|
|
47
|
+
windowSeconds: options.rateLimit?.windowSeconds ?? 600
|
|
48
|
+
},
|
|
49
|
+
slugs,
|
|
50
|
+
turnstile: {
|
|
51
|
+
enabled: options.turnstile ? options.turnstile.enabled !== false : false,
|
|
52
|
+
fieldName: options.turnstile?.fieldName || 'cf-turnstile-response',
|
|
53
|
+
secretKey: options.turnstile?.secretKey || ''
|
|
54
|
+
},
|
|
55
|
+
webhooks: {
|
|
56
|
+
headers: options.webhooks?.headers,
|
|
57
|
+
requireSignature: options.webhooks?.requireSignature === true,
|
|
58
|
+
secretsInAdmin: options.webhooks?.secretsInAdmin !== false,
|
|
59
|
+
signingSecret: options.webhooks?.signingSecret
|
|
60
|
+
},
|
|
61
|
+
whatsapp: options.whatsapp,
|
|
62
|
+
words: {
|
|
63
|
+
...DEFAULT_WORDS,
|
|
64
|
+
...options.words
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Adds forms, submissions and private attachments, with their endpoints, to a Payload config.
|
|
70
|
+
* `formsPlugin({ enabled: false })` returns the config untouched.
|
|
71
|
+
*/ export const formsPlugin = (pluginOptions = {})=>(config)=>{
|
|
72
|
+
if (pluginOptions.enabled === false) {
|
|
73
|
+
return config;
|
|
74
|
+
}
|
|
75
|
+
const options = resolveOptions(pluginOptions);
|
|
76
|
+
const collections = [
|
|
77
|
+
...config.collections || []
|
|
78
|
+
];
|
|
79
|
+
if (options.attachments && !options.attachments.createCollection) {
|
|
80
|
+
const index = collections.findIndex((c)=>c.slug === options.attachments.collection);
|
|
81
|
+
const target = collections[index];
|
|
82
|
+
if (!target || !target.upload) {
|
|
83
|
+
throw new Error(`formsPlugin: attachments.collection "${options.attachments.collection}" must be an upload collection in this config.`);
|
|
84
|
+
}
|
|
85
|
+
const names = new Set(target.fields.map((f)=>'name' in f ? f.name : ''));
|
|
86
|
+
collections[index] = {
|
|
87
|
+
...target,
|
|
88
|
+
fields: [
|
|
89
|
+
...target.fields,
|
|
90
|
+
...attachmentLinkFields(options).filter((f)=>!('name' in f) || !names.has(f.name))
|
|
91
|
+
]
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
const { blocks, fieldTypes } = buildFieldBlocks(pluginOptions.fields, {
|
|
95
|
+
attachmentsOn: Boolean(options.attachments),
|
|
96
|
+
mimeTypes: options.attachments?.mimeTypes || [
|
|
97
|
+
...DEFAULT_FILE_LIMITS.mimeTypes
|
|
98
|
+
],
|
|
99
|
+
sources: options.optionSources || {}
|
|
100
|
+
});
|
|
101
|
+
options.fieldTypes = fieldTypes;
|
|
102
|
+
const added = [
|
|
103
|
+
applyOverrides(formsCollection(options, blocks, [
|
|
104
|
+
submitEndpoint(options),
|
|
105
|
+
publicFormEndpoint(options)
|
|
106
|
+
]), pluginOptions.formOverrides),
|
|
107
|
+
applyOverrides(submissionsCollection(options, [
|
|
108
|
+
resendEndpoint(options)
|
|
109
|
+
]), pluginOptions.submissionOverrides)
|
|
110
|
+
];
|
|
111
|
+
if (options.attachments?.createCollection) {
|
|
112
|
+
added.push(applyOverrides(attachmentsCollection(options), pluginOptions.attachmentOverrides));
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
...config,
|
|
116
|
+
collections: [
|
|
117
|
+
...collections,
|
|
118
|
+
...added
|
|
119
|
+
],
|
|
120
|
+
custom: {
|
|
121
|
+
...config.custom,
|
|
122
|
+
payloadForms: options
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
};
|
|
126
|
+
export { signWebhookBody } from './delivery/webhook.js';
|
|
127
|
+
export { formBlock } from './formBlock.js';
|
|
128
|
+
export { getPublicForm, loadPublishedForm } from './server/forms.js';
|
|
129
|
+
|
|
130
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import type { CollectionConfig, Config, PayloadRequest } from 'payload'\n\nimport type { FormsPluginOptions, ResolvedOptions } from './types.js'\n\nimport { attachmentLinkFields, attachmentsCollection } from './collections/attachments.js'\nimport { buildFieldBlocks } from './collections/blocks.js'\nimport { formsCollection } from './collections/forms.js'\nimport { applyOverrides } from './collections/overrides.js'\nimport { submissionsCollection } from './collections/submissions.js'\nimport { publicFormEndpoint } from './endpoints/public.js'\nimport { resendEndpoint } from './endpoints/resend.js'\nimport { submitEndpoint } from './endpoints/submit.js'\nimport { DEFAULT_FILE_LIMITS } from './shared/answers.js'\nimport { DEFAULT_WORDS } from './shared/words.js'\n\nexport const DEFAULT_SLUGS = {\n attachments: 'custom-form-attachments',\n forms: 'custom-forms',\n submissions: 'custom-form-submissions',\n} as const\n\nexport function resolveOptions(options: FormsPluginOptions = {}): ResolvedOptions {\n const slugs = { ...DEFAULT_SLUGS, ...options.slugs }\n const attachments =\n options.attachments === false\n ? null\n : {\n collection: options.attachments?.collection ? String(options.attachments.collection) : slugs.attachments,\n createCollection: !options.attachments?.collection,\n maxFileSize: options.attachments?.maxFileSize ?? DEFAULT_FILE_LIMITS.maxFileSize,\n maxTotalSize: options.attachments?.maxTotalSize ?? DEFAULT_FILE_LIMITS.maxTotalSize,\n mimeTypes: [...(options.attachments?.mimeTypes ?? DEFAULT_FILE_LIMITS.mimeTypes)],\n }\n if (attachments && !attachments.createCollection) {\n slugs.attachments = attachments.collection\n }\n return {\n attachments,\n beforeEmail: options.beforeEmail,\n delivery: options.delivery,\n fieldTypes: {},\n honeypotField: options.honeypotField || '_hp',\n isAdmin: options.access?.isAdmin || ((req: PayloadRequest) => Boolean(req.user)),\n metadata: { ip: options.metadata?.ip === true, userAgent: options.metadata?.userAgent !== false },\n optionSources: options.optionSources,\n rateLimit:\n options.rateLimit === false\n ? null\n : { max: options.rateLimit?.max ?? 5, windowSeconds: options.rateLimit?.windowSeconds ?? 600 },\n slugs,\n turnstile: {\n enabled: options.turnstile ? options.turnstile.enabled !== false : false,\n fieldName: options.turnstile?.fieldName || 'cf-turnstile-response',\n secretKey: options.turnstile?.secretKey || '',\n },\n webhooks: {\n headers: options.webhooks?.headers,\n requireSignature: options.webhooks?.requireSignature === true,\n secretsInAdmin: options.webhooks?.secretsInAdmin !== false,\n signingSecret: options.webhooks?.signingSecret,\n },\n whatsapp: options.whatsapp,\n words: { ...DEFAULT_WORDS, ...options.words },\n }\n}\n\n/**\n * Adds forms, submissions and private attachments, with their endpoints, to a Payload config.\n * `formsPlugin({ enabled: false })` returns the config untouched.\n */\nexport const formsPlugin =\n (pluginOptions: FormsPluginOptions = {}) =>\n (config: Config): Config => {\n if (pluginOptions.enabled === false) {\n return config\n }\n const options = resolveOptions(pluginOptions)\n const collections = [...(config.collections || [])]\n\n if (options.attachments && !options.attachments.createCollection) {\n const index = collections.findIndex((c) => c.slug === options.attachments!.collection)\n const target = collections[index]\n if (!target || !target.upload) {\n throw new Error(\n `formsPlugin: attachments.collection \"${options.attachments.collection}\" must be an upload collection in this config.`,\n )\n }\n const names = new Set(target.fields.map((f) => ('name' in f ? f.name : '')))\n collections[index] = {\n ...target,\n fields: [...target.fields, ...attachmentLinkFields(options).filter((f) => !('name' in f) || !names.has(f.name))],\n }\n }\n\n const { blocks, fieldTypes } = buildFieldBlocks(pluginOptions.fields, {\n attachmentsOn: Boolean(options.attachments),\n mimeTypes: options.attachments?.mimeTypes || [...DEFAULT_FILE_LIMITS.mimeTypes],\n sources: options.optionSources || {},\n })\n options.fieldTypes = fieldTypes\n\n const added: CollectionConfig[] = [\n applyOverrides(formsCollection(options, blocks, [submitEndpoint(options), publicFormEndpoint(options)]), pluginOptions.formOverrides),\n applyOverrides(submissionsCollection(options, [resendEndpoint(options)]), pluginOptions.submissionOverrides),\n ]\n if (options.attachments?.createCollection) {\n added.push(applyOverrides(attachmentsCollection(options), pluginOptions.attachmentOverrides))\n }\n\n return {\n ...config,\n collections: [...collections, ...added],\n custom: { ...config.custom, payloadForms: options },\n }\n }\n\nexport { signWebhookBody } from './delivery/webhook.js'\nexport { formBlock, type FormBlockOptions } from './formBlock.js'\nexport { getPublicForm, loadPublishedForm } from './server/forms.js'\nexport type {\n Answers,\n AnswerValue,\n ChoiceOption,\n FieldBlock,\n FieldTypeChecks,\n FieldWords,\n FormSection,\n FormWords,\n InputBlock,\n PublicForm,\n RuleGroup,\n RuleRow,\n SnapshotRow,\n SubmissionStatus,\n SubmitResult,\n} from './shared/types.js'\nexport type { CollectionOverride, FieldTypeDefinition, FormsPluginOptions, OptionSource, ResolvedOptions } from './types.js'\n"],"names":["attachmentLinkFields","attachmentsCollection","buildFieldBlocks","formsCollection","applyOverrides","submissionsCollection","publicFormEndpoint","resendEndpoint","submitEndpoint","DEFAULT_FILE_LIMITS","DEFAULT_WORDS","DEFAULT_SLUGS","attachments","forms","submissions","resolveOptions","options","slugs","collection","String","createCollection","maxFileSize","maxTotalSize","mimeTypes","beforeEmail","delivery","fieldTypes","honeypotField","isAdmin","access","req","Boolean","user","metadata","ip","userAgent","optionSources","rateLimit","max","windowSeconds","turnstile","enabled","fieldName","secretKey","webhooks","headers","requireSignature","secretsInAdmin","signingSecret","whatsapp","words","formsPlugin","pluginOptions","config","collections","index","findIndex","c","slug","target","upload","Error","names","Set","fields","map","f","name","filter","has","blocks","attachmentsOn","sources","added","formOverrides","submissionOverrides","push","attachmentOverrides","custom","payloadForms","signWebhookBody","formBlock","getPublicForm","loadPublishedForm"],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare function sha256Hex(text: string): Promise<string>;
|
|
2
|
+
export declare function hmacSha256Hex(secret: string, text: string): Promise<string>;
|
|
3
|
+
/** Base64 HMAC-SHA256 of `text` keyed by raw bytes. */
|
|
4
|
+
export declare function hmacSha256Base64(key: Uint8Array, text: string): Promise<string>;
|
|
5
|
+
/**
|
|
6
|
+
* The Standard Webhooks HMAC key: the base64-decoded bytes after the prefix of a `whsec_` secret,
|
|
7
|
+
* else the secret's UTF-8 bytes.
|
|
8
|
+
*/
|
|
9
|
+
export declare function standardWebhooksKey(secret: string): Uint8Array;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/** Web Crypto only (works on Workers and Node); never node:crypto. */ function toHex(buffer) {
|
|
2
|
+
return Array.from(new Uint8Array(buffer)).map((b)=>b.toString(16).padStart(2, '0')).join('');
|
|
3
|
+
}
|
|
4
|
+
export async function sha256Hex(text) {
|
|
5
|
+
return toHex(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(text)));
|
|
6
|
+
}
|
|
7
|
+
export async function hmacSha256Hex(secret, text) {
|
|
8
|
+
const key = await crypto.subtle.importKey('raw', new TextEncoder().encode(secret), {
|
|
9
|
+
name: 'HMAC',
|
|
10
|
+
hash: 'SHA-256'
|
|
11
|
+
}, false, [
|
|
12
|
+
'sign'
|
|
13
|
+
]);
|
|
14
|
+
return toHex(await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(text)));
|
|
15
|
+
}
|
|
16
|
+
function bytesToBase64(bytes) {
|
|
17
|
+
let binary = '';
|
|
18
|
+
for (const byte of bytes){
|
|
19
|
+
binary += String.fromCharCode(byte);
|
|
20
|
+
}
|
|
21
|
+
return btoa(binary);
|
|
22
|
+
}
|
|
23
|
+
function base64ToBytes(base64) {
|
|
24
|
+
const binary = atob(base64);
|
|
25
|
+
const bytes = new Uint8Array(binary.length);
|
|
26
|
+
for(let i = 0; i < binary.length; i += 1){
|
|
27
|
+
bytes[i] = binary.charCodeAt(i);
|
|
28
|
+
}
|
|
29
|
+
return bytes;
|
|
30
|
+
}
|
|
31
|
+
/** Base64 HMAC-SHA256 of `text` keyed by raw bytes. */ export async function hmacSha256Base64(key, text) {
|
|
32
|
+
const cryptoKey = await crypto.subtle.importKey('raw', key, {
|
|
33
|
+
name: 'HMAC',
|
|
34
|
+
hash: 'SHA-256'
|
|
35
|
+
}, false, [
|
|
36
|
+
'sign'
|
|
37
|
+
]);
|
|
38
|
+
return bytesToBase64(new Uint8Array(await crypto.subtle.sign('HMAC', cryptoKey, new TextEncoder().encode(text))));
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The Standard Webhooks HMAC key: the base64-decoded bytes after the prefix of a `whsec_` secret,
|
|
42
|
+
* else the secret's UTF-8 bytes.
|
|
43
|
+
*/ export function standardWebhooksKey(secret) {
|
|
44
|
+
if (secret.startsWith('whsec_')) {
|
|
45
|
+
try {
|
|
46
|
+
return base64ToBytes(secret.slice('whsec_'.length));
|
|
47
|
+
} catch {
|
|
48
|
+
throw new Error('The signing secret starts with whsec_ but the rest is not valid base64.');
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return new TextEncoder().encode(secret);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
//# sourceMappingURL=crypto.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/security/crypto.ts"],"sourcesContent":["/** Web Crypto only (works on Workers and Node); never node:crypto. */\nfunction toHex(buffer: ArrayBuffer): string {\n return Array.from(new Uint8Array(buffer))\n .map((b) => b.toString(16).padStart(2, '0'))\n .join('')\n}\n\nexport async function sha256Hex(text: string): Promise<string> {\n return toHex(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(text)))\n}\n\nexport async function hmacSha256Hex(secret: string, text: string): Promise<string> {\n const key = await crypto.subtle.importKey(\n 'raw',\n new TextEncoder().encode(secret),\n { name: 'HMAC', hash: 'SHA-256' },\n false,\n ['sign'],\n )\n return toHex(await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(text)))\n}\n\nfunction bytesToBase64(bytes: Uint8Array): string {\n let binary = ''\n for (const byte of bytes) {\n binary += String.fromCharCode(byte)\n }\n return btoa(binary)\n}\n\nfunction base64ToBytes(base64: string): Uint8Array {\n const binary = atob(base64)\n const bytes = new Uint8Array(binary.length)\n for (let i = 0; i < binary.length; i += 1) {\n bytes[i] = binary.charCodeAt(i)\n }\n return bytes\n}\n\n/** Base64 HMAC-SHA256 of `text` keyed by raw bytes. */\nexport async function hmacSha256Base64(key: Uint8Array, text: string): Promise<string> {\n const cryptoKey = await crypto.subtle.importKey('raw', key, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'])\n return bytesToBase64(new Uint8Array(await crypto.subtle.sign('HMAC', cryptoKey, new TextEncoder().encode(text))))\n}\n\n/**\n * The Standard Webhooks HMAC key: the base64-decoded bytes after the prefix of a `whsec_` secret,\n * else the secret's UTF-8 bytes.\n */\nexport function standardWebhooksKey(secret: string): Uint8Array {\n if (secret.startsWith('whsec_')) {\n try {\n return base64ToBytes(secret.slice('whsec_'.length))\n } catch {\n throw new Error('The signing secret starts with whsec_ but the rest is not valid base64.')\n }\n }\n return new TextEncoder().encode(secret)\n}\n"],"names":["toHex","buffer","Array","from","Uint8Array","map","b","toString","padStart","join","sha256Hex","text","crypto","subtle","digest","TextEncoder","encode","hmacSha256Hex","secret","key","importKey","name","hash","sign","bytesToBase64","bytes","binary","byte","String","fromCharCode","btoa","base64ToBytes","base64","atob","length","i","charCodeAt","hmacSha256Base64","cryptoKey","standardWebhooksKey","startsWith","slice","Error"],"mappings":"AAAA,oEAAoE,GACpE,SAASA,MAAMC,MAAmB;IAChC,OAAOC,MAAMC,IAAI,CAAC,IAAIC,WAAWH,SAC9BI,GAAG,CAAC,CAACC,IAAMA,EAAEC,QAAQ,CAAC,IAAIC,QAAQ,CAAC,GAAG,MACtCC,IAAI,CAAC;AACV;AAEA,OAAO,eAAeC,UAAUC,IAAY;IAC1C,OAAOX,MAAM,MAAMY,OAAOC,MAAM,CAACC,MAAM,CAAC,WAAW,IAAIC,cAAcC,MAAM,CAACL;AAC9E;AAEA,OAAO,eAAeM,cAAcC,MAAc,EAAEP,IAAY;IAC9D,MAAMQ,MAAM,MAAMP,OAAOC,MAAM,CAACO,SAAS,CACvC,OACA,IAAIL,cAAcC,MAAM,CAACE,SACzB;QAAEG,MAAM;QAAQC,MAAM;IAAU,GAChC,OACA;QAAC;KAAO;IAEV,OAAOtB,MAAM,MAAMY,OAAOC,MAAM,CAACU,IAAI,CAAC,QAAQJ,KAAK,IAAIJ,cAAcC,MAAM,CAACL;AAC9E;AAEA,SAASa,cAAcC,KAAiB;IACtC,IAAIC,SAAS;IACb,KAAK,MAAMC,QAAQF,MAAO;QACxBC,UAAUE,OAAOC,YAAY,CAACF;IAChC;IACA,OAAOG,KAAKJ;AACd;AAEA,SAASK,cAAcC,MAAc;IACnC,MAAMN,SAASO,KAAKD;IACpB,MAAMP,QAAQ,IAAIrB,WAAWsB,OAAOQ,MAAM;IAC1C,IAAK,IAAIC,IAAI,GAAGA,IAAIT,OAAOQ,MAAM,EAAEC,KAAK,EAAG;QACzCV,KAAK,CAACU,EAAE,GAAGT,OAAOU,UAAU,CAACD;IAC/B;IACA,OAAOV;AACT;AAEA,qDAAqD,GACrD,OAAO,eAAeY,iBAAiBlB,GAAe,EAAER,IAAY;IAClE,MAAM2B,YAAY,MAAM1B,OAAOC,MAAM,CAACO,SAAS,CAAC,OAAOD,KAAK;QAAEE,MAAM;QAAQC,MAAM;IAAU,GAAG,OAAO;QAAC;KAAO;IAC9G,OAAOE,cAAc,IAAIpB,WAAW,MAAMQ,OAAOC,MAAM,CAACU,IAAI,CAAC,QAAQe,WAAW,IAAIvB,cAAcC,MAAM,CAACL;AAC3G;AAEA;;;CAGC,GACD,OAAO,SAAS4B,oBAAoBrB,MAAc;IAChD,IAAIA,OAAOsB,UAAU,CAAC,WAAW;QAC/B,IAAI;YACF,OAAOT,cAAcb,OAAOuB,KAAK,CAAC,SAASP,MAAM;QACnD,EAAE,OAAM;YACN,MAAM,IAAIQ,MAAM;QAClB;IACF;IACA,OAAO,IAAI3B,cAAcC,MAAM,CAACE;AAClC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Payload } from 'payload';
|
|
2
|
+
/**
|
|
3
|
+
* Soft per-form, per-visitor limit in Payload's KV store. The key holds a hash of the IP and the
|
|
4
|
+
* app secret, never the IP. Returns false when the visitor is over the limit.
|
|
5
|
+
*/
|
|
6
|
+
export declare function checkRateLimit(args: {
|
|
7
|
+
formId: number | string;
|
|
8
|
+
ip: string;
|
|
9
|
+
max: number;
|
|
10
|
+
now?: number;
|
|
11
|
+
payload: Payload;
|
|
12
|
+
windowSeconds: number;
|
|
13
|
+
}): Promise<boolean>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { sha256Hex } from './crypto.js';
|
|
2
|
+
/**
|
|
3
|
+
* Soft per-form, per-visitor limit in Payload's KV store. The key holds a hash of the IP and the
|
|
4
|
+
* app secret, never the IP. Returns false when the visitor is over the limit.
|
|
5
|
+
*/ export async function checkRateLimit(args) {
|
|
6
|
+
const { payload } = args;
|
|
7
|
+
const now = args.now ?? Date.now();
|
|
8
|
+
const hash = await sha256Hex(`${args.ip || 'unknown'}${payload.secret}`);
|
|
9
|
+
const key = `forms:rl:${args.formId}:${hash}`;
|
|
10
|
+
const entry = await payload.kv.get(key);
|
|
11
|
+
if (!entry || typeof entry.start !== 'number' || now - entry.start >= args.windowSeconds * 1000) {
|
|
12
|
+
await payload.kv.set(key, {
|
|
13
|
+
count: 1,
|
|
14
|
+
start: now
|
|
15
|
+
});
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
if (entry.count >= args.max) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
await payload.kv.set(key, {
|
|
22
|
+
count: entry.count + 1,
|
|
23
|
+
start: entry.start
|
|
24
|
+
});
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
//# sourceMappingURL=rateLimit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/security/rateLimit.ts"],"sourcesContent":["import type { Payload } from 'payload'\n\nimport { sha256Hex } from './crypto.js'\n\ntype Entry = { count: number; start: number }\n\n/**\n * Soft per-form, per-visitor limit in Payload's KV store. The key holds a hash of the IP and the\n * app secret, never the IP. Returns false when the visitor is over the limit.\n */\nexport async function checkRateLimit(args: {\n formId: number | string\n ip: string\n max: number\n now?: number\n payload: Payload\n windowSeconds: number\n}): Promise<boolean> {\n const { payload } = args\n const now = args.now ?? Date.now()\n const hash = await sha256Hex(`${args.ip || 'unknown'}${payload.secret}`)\n const key = `forms:rl:${args.formId}:${hash}`\n const entry = await payload.kv.get<Entry>(key)\n if (!entry || typeof entry.start !== 'number' || now - entry.start >= args.windowSeconds * 1000) {\n await payload.kv.set(key, { count: 1, start: now })\n return true\n }\n if (entry.count >= args.max) {\n return false\n }\n await payload.kv.set(key, { count: entry.count + 1, start: entry.start })\n return true\n}\n"],"names":["sha256Hex","checkRateLimit","args","payload","now","Date","hash","ip","secret","key","formId","entry","kv","get","start","windowSeconds","set","count","max"],"mappings":"AAEA,SAASA,SAAS,QAAQ,cAAa;AAIvC;;;CAGC,GACD,OAAO,eAAeC,eAAeC,IAOpC;IACC,MAAM,EAAEC,OAAO,EAAE,GAAGD;IACpB,MAAME,MAAMF,KAAKE,GAAG,IAAIC,KAAKD,GAAG;IAChC,MAAME,OAAO,MAAMN,UAAU,GAAGE,KAAKK,EAAE,IAAI,YAAYJ,QAAQK,MAAM,EAAE;IACvE,MAAMC,MAAM,CAAC,SAAS,EAAEP,KAAKQ,MAAM,CAAC,CAAC,EAAEJ,MAAM;IAC7C,MAAMK,QAAQ,MAAMR,QAAQS,EAAE,CAACC,GAAG,CAAQJ;IAC1C,IAAI,CAACE,SAAS,OAAOA,MAAMG,KAAK,KAAK,YAAYV,MAAMO,MAAMG,KAAK,IAAIZ,KAAKa,aAAa,GAAG,MAAM;QAC/F,MAAMZ,QAAQS,EAAE,CAACI,GAAG,CAACP,KAAK;YAAEQ,OAAO;YAAGH,OAAOV;QAAI;QACjD,OAAO;IACT;IACA,IAAIO,MAAMM,KAAK,IAAIf,KAAKgB,GAAG,EAAE;QAC3B,OAAO;IACT;IACA,MAAMf,QAAQS,EAAE,CAACI,GAAG,CAACP,KAAK;QAAEQ,OAAON,MAAMM,KAAK,GAAG;QAAGH,OAAOH,MAAMG,KAAK;IAAC;IACvE,OAAO;AACT"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** The visitor IP: `cf-connecting-ip`, else the first `x-forwarded-for`, else `x-real-ip`. */ export function getRequestIP(headers) {
|
|
2
|
+
const value = headers.get('cf-connecting-ip') || headers.get('x-forwarded-for') || headers.get('x-real-ip') || '';
|
|
3
|
+
return value.split(',')[0]?.trim() || '';
|
|
4
|
+
}
|
|
5
|
+
export function isHoneypotFilled(raw, fieldName) {
|
|
6
|
+
const value = raw[fieldName];
|
|
7
|
+
const list = Array.isArray(value) ? value : [
|
|
8
|
+
value
|
|
9
|
+
];
|
|
10
|
+
return list.some((v)=>typeof v === 'string' && v.trim() !== '');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
//# sourceMappingURL=request.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/security/request.ts"],"sourcesContent":["/** The visitor IP: `cf-connecting-ip`, else the first `x-forwarded-for`, else `x-real-ip`. */\nexport function getRequestIP(headers: Headers): string {\n const value =\n headers.get('cf-connecting-ip') || headers.get('x-forwarded-for') || headers.get('x-real-ip') || ''\n return value.split(',')[0]?.trim() || ''\n}\n\nexport function isHoneypotFilled(raw: Record<string, unknown>, fieldName: string): boolean {\n const value = raw[fieldName]\n const list = Array.isArray(value) ? value : [value]\n return list.some((v) => typeof v === 'string' && v.trim() !== '')\n}\n"],"names":["getRequestIP","headers","value","get","split","trim","isHoneypotFilled","raw","fieldName","list","Array","isArray","some","v"],"mappings":"AAAA,4FAA4F,GAC5F,OAAO,SAASA,aAAaC,OAAgB;IAC3C,MAAMC,QACJD,QAAQE,GAAG,CAAC,uBAAuBF,QAAQE,GAAG,CAAC,sBAAsBF,QAAQE,GAAG,CAAC,gBAAgB;IACnG,OAAOD,MAAME,KAAK,CAAC,IAAI,CAAC,EAAE,EAAEC,UAAU;AACxC;AAEA,OAAO,SAASC,iBAAiBC,GAA4B,EAAEC,SAAiB;IAC9E,MAAMN,QAAQK,GAAG,CAACC,UAAU;IAC5B,MAAMC,OAAOC,MAAMC,OAAO,CAACT,SAASA,QAAQ;QAACA;KAAM;IACnD,OAAOO,KAAKG,IAAI,CAAC,CAACC,IAAM,OAAOA,MAAM,YAAYA,EAAER,IAAI,OAAO;AAChE"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export declare const SITEVERIFY_URL = "https://challenges.cloudflare.com/turnstile/v0/siteverify";
|
|
2
|
+
type Logger = {
|
|
3
|
+
error: (msg: unknown, ...rest: unknown[]) => void;
|
|
4
|
+
};
|
|
5
|
+
/**
|
|
6
|
+
* Verifies a Turnstile token with Cloudflare's siteverify: secret, token, visitor IP and an
|
|
7
|
+
* idempotency key (safe retries). Any error counts as a failed check.
|
|
8
|
+
*/
|
|
9
|
+
export declare function verifyTurnstile(args: {
|
|
10
|
+
idempotencyKey?: string;
|
|
11
|
+
ip?: string;
|
|
12
|
+
secret: string;
|
|
13
|
+
token: string;
|
|
14
|
+
}): Promise<boolean>;
|
|
15
|
+
/**
|
|
16
|
+
* The Turnstile check for one plugin setup. Off: passes. On without a secret: refuses every
|
|
17
|
+
* submission and logs one error line. On: the token must verify.
|
|
18
|
+
*/
|
|
19
|
+
export declare function checkTurnstile(args: {
|
|
20
|
+
ip: string;
|
|
21
|
+
logger: Logger;
|
|
22
|
+
settings: {
|
|
23
|
+
enabled: boolean;
|
|
24
|
+
secretKey: string;
|
|
25
|
+
};
|
|
26
|
+
token: unknown;
|
|
27
|
+
}): Promise<boolean>;
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export const SITEVERIFY_URL = 'https://challenges.cloudflare.com/turnstile/v0/siteverify';
|
|
2
|
+
/**
|
|
3
|
+
* Verifies a Turnstile token with Cloudflare's siteverify: secret, token, visitor IP and an
|
|
4
|
+
* idempotency key (safe retries). Any error counts as a failed check.
|
|
5
|
+
*/ export async function verifyTurnstile(args) {
|
|
6
|
+
try {
|
|
7
|
+
const response = await fetch(SITEVERIFY_URL, {
|
|
8
|
+
body: JSON.stringify({
|
|
9
|
+
idempotency_key: args.idempotencyKey || crypto.randomUUID(),
|
|
10
|
+
remoteip: args.ip || undefined,
|
|
11
|
+
response: args.token,
|
|
12
|
+
secret: args.secret
|
|
13
|
+
}),
|
|
14
|
+
headers: {
|
|
15
|
+
'Content-Type': 'application/json'
|
|
16
|
+
},
|
|
17
|
+
method: 'POST'
|
|
18
|
+
});
|
|
19
|
+
if (!response.ok) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
const data = await response.json();
|
|
23
|
+
return data.success === true;
|
|
24
|
+
} catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const warned = new WeakSet();
|
|
29
|
+
/**
|
|
30
|
+
* The Turnstile check for one plugin setup. Off: passes. On without a secret: refuses every
|
|
31
|
+
* submission and logs one error line. On: the token must verify.
|
|
32
|
+
*/ export async function checkTurnstile(args) {
|
|
33
|
+
const { settings } = args;
|
|
34
|
+
if (!settings.enabled) {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
if (!settings.secretKey) {
|
|
38
|
+
if (!warned.has(settings)) {
|
|
39
|
+
warned.add(settings);
|
|
40
|
+
args.logger.error('payload-dynamic-forms: Turnstile is on but has no secret key; every submission is refused.');
|
|
41
|
+
}
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
const token = Array.isArray(args.token) ? args.token[0] : args.token;
|
|
45
|
+
if (typeof token !== 'string' || !token.trim()) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
return verifyTurnstile({
|
|
49
|
+
idempotencyKey: crypto.randomUUID(),
|
|
50
|
+
ip: args.ip,
|
|
51
|
+
secret: settings.secretKey,
|
|
52
|
+
token: token.trim()
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
//# sourceMappingURL=turnstile.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/security/turnstile.ts"],"sourcesContent":["export const SITEVERIFY_URL = 'https://challenges.cloudflare.com/turnstile/v0/siteverify'\n\ntype Logger = { error: (msg: unknown, ...rest: unknown[]) => void }\n\n/**\n * Verifies a Turnstile token with Cloudflare's siteverify: secret, token, visitor IP and an\n * idempotency key (safe retries). Any error counts as a failed check.\n */\nexport async function verifyTurnstile(args: {\n idempotencyKey?: string\n ip?: string\n secret: string\n token: string\n}): Promise<boolean> {\n try {\n const response = await fetch(SITEVERIFY_URL, {\n body: JSON.stringify({\n idempotency_key: args.idempotencyKey || crypto.randomUUID(),\n remoteip: args.ip || undefined,\n response: args.token,\n secret: args.secret,\n }),\n headers: { 'Content-Type': 'application/json' },\n method: 'POST',\n })\n if (!response.ok) {\n return false\n }\n const data = (await response.json()) as { success?: boolean }\n return data.success === true\n } catch {\n return false\n }\n}\n\nconst warned = new WeakSet<object>()\n\n/**\n * The Turnstile check for one plugin setup. Off: passes. On without a secret: refuses every\n * submission and logs one error line. On: the token must verify.\n */\nexport async function checkTurnstile(args: {\n ip: string\n logger: Logger\n settings: { enabled: boolean; secretKey: string }\n token: unknown\n}): Promise<boolean> {\n const { settings } = args\n if (!settings.enabled) {\n return true\n }\n if (!settings.secretKey) {\n if (!warned.has(settings)) {\n warned.add(settings)\n args.logger.error('payload-dynamic-forms: Turnstile is on but has no secret key; every submission is refused.')\n }\n return false\n }\n const token = Array.isArray(args.token) ? args.token[0] : args.token\n if (typeof token !== 'string' || !token.trim()) {\n return false\n }\n return verifyTurnstile({ idempotencyKey: crypto.randomUUID(), ip: args.ip, secret: settings.secretKey, token: token.trim() })\n}\n"],"names":["SITEVERIFY_URL","verifyTurnstile","args","response","fetch","body","JSON","stringify","idempotency_key","idempotencyKey","crypto","randomUUID","remoteip","ip","undefined","token","secret","headers","method","ok","data","json","success","warned","WeakSet","checkTurnstile","settings","enabled","secretKey","has","add","logger","error","Array","isArray","trim"],"mappings":"AAAA,OAAO,MAAMA,iBAAiB,4DAA2D;AAIzF;;;CAGC,GACD,OAAO,eAAeC,gBAAgBC,IAKrC;IACC,IAAI;QACF,MAAMC,WAAW,MAAMC,MAAMJ,gBAAgB;YAC3CK,MAAMC,KAAKC,SAAS,CAAC;gBACnBC,iBAAiBN,KAAKO,cAAc,IAAIC,OAAOC,UAAU;gBACzDC,UAAUV,KAAKW,EAAE,IAAIC;gBACrBX,UAAUD,KAAKa,KAAK;gBACpBC,QAAQd,KAAKc,MAAM;YACrB;YACAC,SAAS;gBAAE,gBAAgB;YAAmB;YAC9CC,QAAQ;QACV;QACA,IAAI,CAACf,SAASgB,EAAE,EAAE;YAChB,OAAO;QACT;QACA,MAAMC,OAAQ,MAAMjB,SAASkB,IAAI;QACjC,OAAOD,KAAKE,OAAO,KAAK;IAC1B,EAAE,OAAM;QACN,OAAO;IACT;AACF;AAEA,MAAMC,SAAS,IAAIC;AAEnB;;;CAGC,GACD,OAAO,eAAeC,eAAevB,IAKpC;IACC,MAAM,EAAEwB,QAAQ,EAAE,GAAGxB;IACrB,IAAI,CAACwB,SAASC,OAAO,EAAE;QACrB,OAAO;IACT;IACA,IAAI,CAACD,SAASE,SAAS,EAAE;QACvB,IAAI,CAACL,OAAOM,GAAG,CAACH,WAAW;YACzBH,OAAOO,GAAG,CAACJ;YACXxB,KAAK6B,MAAM,CAACC,KAAK,CAAC;QACpB;QACA,OAAO;IACT;IACA,MAAMjB,QAAQkB,MAAMC,OAAO,CAAChC,KAAKa,KAAK,IAAIb,KAAKa,KAAK,CAAC,EAAE,GAAGb,KAAKa,KAAK;IACpE,IAAI,OAAOA,UAAU,YAAY,CAACA,MAAMoB,IAAI,IAAI;QAC9C,OAAO;IACT;IACA,OAAOlC,gBAAgB;QAAEQ,gBAAgBC,OAAOC,UAAU;QAAIE,IAAIX,KAAKW,EAAE;QAAEG,QAAQU,SAASE,SAAS;QAAEb,OAAOA,MAAMoB,IAAI;IAAG;AAC7H"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { Payload, PayloadRequest, TypedLocale } from 'payload';
|
|
2
|
+
import type { PublicForm } from '../shared/types.js';
|
|
3
|
+
import type { ResolvedOptions } from '../types.js';
|
|
4
|
+
export type EmailOutput = {
|
|
5
|
+
bcc?: null | string;
|
|
6
|
+
body?: null | string;
|
|
7
|
+
cc?: null | string;
|
|
8
|
+
enabled?: boolean | null;
|
|
9
|
+
from?: null | string;
|
|
10
|
+
id?: null | string;
|
|
11
|
+
kind?: 'team' | 'visitor' | null;
|
|
12
|
+
label?: null | string;
|
|
13
|
+
replyToField?: null | string;
|
|
14
|
+
subject?: null | string;
|
|
15
|
+
to?: null | string;
|
|
16
|
+
visitorField?: null | string;
|
|
17
|
+
};
|
|
18
|
+
export type WebhookOutput = {
|
|
19
|
+
enabled?: boolean | null;
|
|
20
|
+
headers?: Array<{
|
|
21
|
+
key?: null | string;
|
|
22
|
+
value?: null | string;
|
|
23
|
+
}> | null;
|
|
24
|
+
id?: null | string;
|
|
25
|
+
label?: null | string;
|
|
26
|
+
method?: 'PATCH' | 'POST' | 'PUT' | null;
|
|
27
|
+
payloadMode?: 'answersOnly' | 'full' | null;
|
|
28
|
+
retries?: null | number;
|
|
29
|
+
signingSecret?: null | string;
|
|
30
|
+
timeoutMs?: null | number;
|
|
31
|
+
url?: null | string;
|
|
32
|
+
};
|
|
33
|
+
export type FormDelivery = {
|
|
34
|
+
emails: EmailOutput[];
|
|
35
|
+
store: boolean;
|
|
36
|
+
webhooks: WebhookOutput[];
|
|
37
|
+
whatsapp: {
|
|
38
|
+
enabled: boolean;
|
|
39
|
+
messageTemplate?: null | string;
|
|
40
|
+
number?: null | string;
|
|
41
|
+
rowTemplate?: null | string;
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
export type LoadedForm = {
|
|
45
|
+
delivery: FormDelivery;
|
|
46
|
+
form: PublicForm;
|
|
47
|
+
};
|
|
48
|
+
type Locale = 'all' | TypedLocale | undefined;
|
|
49
|
+
/** The plugin options of an app, kept on its config by `formsPlugin`. */
|
|
50
|
+
export declare function getPluginOptions(payload: Payload): ResolvedOptions;
|
|
51
|
+
/**
|
|
52
|
+
* Loads a published form by slug, as a visitor would see it, plus its delivery settings for the
|
|
53
|
+
* server. Null when the slug is unknown or the form is not published.
|
|
54
|
+
*/
|
|
55
|
+
export declare function loadPublishedForm(payload: Payload, slug: string, args?: {
|
|
56
|
+
locale?: Locale;
|
|
57
|
+
req?: PayloadRequest;
|
|
58
|
+
}): Promise<LoadedForm | null>;
|
|
59
|
+
/** Loads a form by id with its delivery settings (trusted server use: resend). */
|
|
60
|
+
export declare function loadFormById(payload: Payload, id: number | string, args?: {
|
|
61
|
+
locale?: Locale;
|
|
62
|
+
req?: PayloadRequest;
|
|
63
|
+
}): Promise<LoadedForm | null>;
|
|
64
|
+
/**
|
|
65
|
+
* A published form ready to render: sections, choices from sources resolved with their
|
|
66
|
+
* attributes, words filled from the defaults, the WhatsApp number when WhatsApp is on, and none of
|
|
67
|
+
* its other delivery settings. Null for unknown or unpublished slugs.
|
|
68
|
+
*/
|
|
69
|
+
export declare function getPublicForm(payload: Payload, slug: string, args?: {
|
|
70
|
+
locale?: Locale;
|
|
71
|
+
req?: PayloadRequest;
|
|
72
|
+
}): Promise<null | PublicForm>;
|
|
73
|
+
export {};
|