@10x-media/form-builder 0.1.0-beta.2 → 0.1.0-beta.3

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 (156) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +2 -2
  3. package/dist/actions/body/converters.d.ts +20 -0
  4. package/dist/actions/body/converters.js +57 -0
  5. package/dist/actions/body/converters.js.map +1 -0
  6. package/dist/actions/body/escapeHtml.d.ts +6 -0
  7. package/dist/actions/body/escapeHtml.js +14 -0
  8. package/dist/actions/body/escapeHtml.js.map +1 -0
  9. package/dist/actions/body/serializeBody.d.ts +35 -0
  10. package/dist/actions/body/serializeBody.js +76 -0
  11. package/dist/actions/body/serializeBody.js.map +1 -0
  12. package/dist/actions/body/serializeSlate.js +31 -0
  13. package/dist/actions/body/serializeSlate.js.map +1 -0
  14. package/dist/actions/body/wildcards.d.ts +10 -0
  15. package/dist/actions/body/wildcards.js +33 -0
  16. package/dist/actions/body/wildcards.js.map +1 -0
  17. package/dist/actions/builtin/confirmation.js +28 -12
  18. package/dist/actions/builtin/confirmation.js.map +1 -1
  19. package/dist/actions/builtin/emailTeam.js +7 -9
  20. package/dist/actions/builtin/emailTeam.js.map +1 -1
  21. package/dist/actions/builtin/signedWebhook.js +21 -3
  22. package/dist/actions/builtin/signedWebhook.js.map +1 -1
  23. package/dist/actions/defineAction.d.ts +2 -1
  24. package/dist/actions/defineAction.js.map +1 -1
  25. package/dist/actions/dispatch.js +2 -1
  26. package/dist/actions/dispatch.js.map +1 -1
  27. package/dist/actions/runActions.js +8 -1
  28. package/dist/actions/runActions.js.map +1 -1
  29. package/dist/actions/task.js +7 -5
  30. package/dist/actions/task.js.map +1 -1
  31. package/dist/calc/normalizeCalc.js +5 -17
  32. package/dist/calc/normalizeCalc.js.map +1 -1
  33. package/dist/calc/types.d.ts +8 -2
  34. package/dist/calc/types.js +22 -0
  35. package/dist/calc/types.js.map +1 -0
  36. package/dist/client/ByteSizeField.d.ts +21 -0
  37. package/dist/client/ByteSizeField.js +56 -0
  38. package/dist/client/ByteSizeField.js.map +1 -0
  39. package/dist/client/ConditionBuilder.js +2 -1
  40. package/dist/client/ConditionBuilder.js.map +1 -1
  41. package/dist/client/FieldNameSelect.d.ts +20 -0
  42. package/dist/client/FieldNameSelect.js +58 -0
  43. package/dist/client/FieldNameSelect.js.map +1 -0
  44. package/dist/client/FlowBuilder.d.ts +0 -1
  45. package/dist/client/FlowBuilder.js +4 -6
  46. package/dist/client/FlowBuilder.js.map +1 -1
  47. package/dist/client/FormConditionField.js +1 -4
  48. package/dist/client/FormConditionField.js.map +1 -1
  49. package/dist/client/admin.css +192 -0
  50. package/dist/client/toStaticLabel.js +13 -0
  51. package/dist/client/toStaticLabel.js.map +1 -0
  52. package/dist/collections/formSubmissions.js +5 -3
  53. package/dist/collections/formSubmissions.js.map +1 -1
  54. package/dist/collections/forms.js +74 -57
  55. package/dist/collections/forms.js.map +1 -1
  56. package/dist/exports/client.d.ts +3 -1
  57. package/dist/exports/client.js +3 -1
  58. package/dist/exports/react.d.ts +6 -1
  59. package/dist/exports/react.js +5 -1
  60. package/dist/fields/buildFieldBlocks.js +14 -18
  61. package/dist/fields/buildFieldBlocks.js.map +1 -1
  62. package/dist/fields/builtin/calculation.js +115 -2
  63. package/dist/fields/builtin/calculation.js.map +1 -1
  64. package/dist/fields/builtin/date.js +29 -0
  65. package/dist/fields/builtin/date.js.map +1 -0
  66. package/dist/fields/builtin/file.d.ts +15 -0
  67. package/dist/fields/builtin/file.js +79 -3
  68. package/dist/fields/builtin/file.js.map +1 -1
  69. package/dist/fields/builtin/index.d.ts +14 -0
  70. package/dist/fields/builtin/index.js +11 -2
  71. package/dist/fields/builtin/index.js.map +1 -1
  72. package/dist/fields/fieldNamesOfType.js +25 -0
  73. package/dist/fields/fieldNamesOfType.js.map +1 -0
  74. package/dist/fields/sharedConfig.js +30 -18
  75. package/dist/fields/sharedConfig.js.map +1 -1
  76. package/dist/index.d.ts +19 -2
  77. package/dist/index.js +14 -4
  78. package/dist/index.js.map +1 -1
  79. package/dist/plugin/registerCollections.js +3 -2
  80. package/dist/plugin/registerCollections.js.map +1 -1
  81. package/dist/react/Form.js +3 -3
  82. package/dist/react/Form.js.map +1 -1
  83. package/dist/react/Poll.js +1 -1
  84. package/dist/react/captcha/HcaptchaCaptcha.d.ts +28 -0
  85. package/dist/react/captcha/HcaptchaCaptcha.js +53 -0
  86. package/dist/react/captcha/HcaptchaCaptcha.js.map +1 -0
  87. package/dist/react/captcha/RecaptchaCaptcha.d.ts +41 -0
  88. package/dist/react/captcha/RecaptchaCaptcha.js +93 -0
  89. package/dist/react/captcha/RecaptchaCaptcha.js.map +1 -0
  90. package/dist/react/captcha/TurnstileCaptcha.d.ts +28 -0
  91. package/dist/react/captcha/TurnstileCaptcha.js +53 -0
  92. package/dist/react/captcha/TurnstileCaptcha.js.map +1 -0
  93. package/dist/react/captcha/types.d.ts +8 -0
  94. package/dist/react/captcha/useCaptchaScript.js +45 -0
  95. package/dist/react/captcha/useCaptchaScript.js.map +1 -0
  96. package/dist/react/captcha/vendors.js +8 -0
  97. package/dist/react/captcha/vendors.js.map +1 -0
  98. package/dist/react/primitives/Input.d.ts +1 -1
  99. package/dist/react/primitives/Input.js.map +1 -1
  100. package/dist/react/renderers/calculation.js +1 -1
  101. package/dist/react/renderers/checkbox.js +1 -1
  102. package/dist/react/renderers/consent.js +1 -1
  103. package/dist/react/renderers/date.js +37 -0
  104. package/dist/react/renderers/date.js.map +1 -0
  105. package/dist/react/renderers/email.js +1 -1
  106. package/dist/react/renderers/file.js +20 -7
  107. package/dist/react/renderers/file.js.map +1 -1
  108. package/dist/react/renderers/index.js +2 -0
  109. package/dist/react/renderers/index.js.map +1 -1
  110. package/dist/react/renderers/number.js +1 -1
  111. package/dist/react/renderers/repeater.js +1 -1
  112. package/dist/react/renderers/select.js +1 -1
  113. package/dist/react/renderers/text.js +1 -1
  114. package/dist/react/renderers/textarea.js +1 -1
  115. package/dist/spam/captcha.d.ts +3 -2
  116. package/dist/spam/captcha.js +3 -2
  117. package/dist/spam/captcha.js.map +1 -1
  118. package/dist/spam/clientIp.js +9 -0
  119. package/dist/spam/clientIp.js.map +1 -0
  120. package/dist/spam/identify.js +3 -6
  121. package/dist/spam/identify.js.map +1 -1
  122. package/dist/spam/providers/hcaptcha.d.ts +17 -0
  123. package/dist/spam/providers/hcaptcha.js +18 -0
  124. package/dist/spam/providers/hcaptcha.js.map +1 -0
  125. package/dist/spam/providers/recaptcha.d.ts +23 -0
  126. package/dist/spam/providers/recaptcha.js +23 -0
  127. package/dist/spam/providers/recaptcha.js.map +1 -0
  128. package/dist/spam/providers/siteverify.js +34 -0
  129. package/dist/spam/providers/siteverify.js.map +1 -0
  130. package/dist/spam/providers/turnstile.d.ts +17 -0
  131. package/dist/spam/providers/turnstile.js +18 -0
  132. package/dist/spam/providers/turnstile.js.map +1 -0
  133. package/dist/spam/spamGuard.js +2 -5
  134. package/dist/spam/spamGuard.js.map +1 -1
  135. package/dist/spam/types.d.ts +2 -2
  136. package/dist/translations/en.js +26 -4
  137. package/dist/translations/en.js.map +1 -1
  138. package/dist/translations/keys.d.ts +26 -4
  139. package/dist/translations/keys.js +26 -4
  140. package/dist/translations/keys.js.map +1 -1
  141. package/dist/uploads/formatBytes.d.ts +9 -0
  142. package/dist/uploads/formatBytes.js +25 -0
  143. package/dist/uploads/formatBytes.js.map +1 -0
  144. package/dist/uploads/resolveFileRef.js +1 -1
  145. package/dist/uploads/resolveFileRef.js.map +1 -1
  146. package/dist/validation/builtin/index.d.ts +14 -0
  147. package/dist/validation/builtin/index.js +12 -1
  148. package/dist/validation/builtin/index.js.map +1 -1
  149. package/dist/validation/builtin/maxDate.js +23 -0
  150. package/dist/validation/builtin/maxDate.js.map +1 -0
  151. package/dist/validation/builtin/minDate.js +23 -0
  152. package/dist/validation/builtin/minDate.js.map +1 -0
  153. package/dist/validation/builtin/validateDateBound.js +18 -0
  154. package/dist/validation/builtin/validateDateBound.js.map +1 -0
  155. package/package.json +3 -2
  156. package/styles/form-builder.css +0 -190
@@ -4,8 +4,8 @@ import { defineFieldRenderer } from "../contract.js";
4
4
  import { useFormContext } from "../FormContext.js";
5
5
  import { FieldShell } from "../primitives/FieldShell.js";
6
6
  import { useField } from "../useField.js";
7
- import { createElement, useId } from "react";
8
7
  import { jsx, jsxs } from "react/jsx-runtime";
8
+ import { createElement, useId } from "react";
9
9
  //#region src/react/renderers/repeater.tsx
10
10
  /**
11
11
  * Thin wrapper so each sub-field renderer is a proper React component (has stable hook identity
@@ -2,8 +2,8 @@
2
2
  import { defineFieldRenderer } from "../contract.js";
3
3
  import { FieldShell } from "../primitives/FieldShell.js";
4
4
  import { Select } from "../primitives/Select.js";
5
- import { useId } from "react";
6
5
  import { jsx } from "react/jsx-runtime";
6
+ import { useId } from "react";
7
7
  //#region src/react/renderers/select.tsx
8
8
  const selectRenderer = defineFieldRenderer(({ field, name, value, onChange, onBlur, errors, warnings, required, disabled }) => {
9
9
  const id = useId();
@@ -2,8 +2,8 @@
2
2
  import { defineFieldRenderer } from "../contract.js";
3
3
  import { FieldShell } from "../primitives/FieldShell.js";
4
4
  import { Input } from "../primitives/Input.js";
5
- import { useId } from "react";
6
5
  import { jsx } from "react/jsx-runtime";
6
+ import { useId } from "react";
7
7
  //#region src/react/renderers/text.tsx
8
8
  const textRenderer = defineFieldRenderer(({ field, name, value, onChange, onBlur, errors, warnings, required, disabled }) => {
9
9
  const id = useId();
@@ -2,8 +2,8 @@
2
2
  import { defineFieldRenderer } from "../contract.js";
3
3
  import { FieldShell } from "../primitives/FieldShell.js";
4
4
  import { Textarea } from "../primitives/Textarea.js";
5
- import { useId } from "react";
6
5
  import { jsx } from "react/jsx-runtime";
6
+ import { useId } from "react";
7
7
  //#region src/react/renderers/textarea.tsx
8
8
  const textareaRenderer = defineFieldRenderer(({ field, name, value, onChange, onBlur, errors, warnings, required, disabled }) => {
9
9
  const id = useId();
@@ -3,8 +3,9 @@ import { CaptchaProvider } from "./types.js";
3
3
  //#region src/spam/captcha.d.ts
4
4
  /**
5
5
  * Identity helper for authoring a captcha provider (mirrors `defineAction`/`defineConsentSource`).
6
- * v1 ships no built-in provider; a project supplies one (Turnstile/reCAPTCHA/hCaptcha) and the submit
7
- * path verifies a token carried on the submission. Prebuilt providers are a v1.x addition.
6
+ * Bundled adapters (`turnstileProvider`, `recaptchaProvider`, `hcaptchaProvider`) are built with it;
7
+ * use it directly for any other verification service. The submit path verifies a token carried on
8
+ * the submission.
8
9
  */
9
10
  declare const defineCaptchaProvider: (provider: CaptchaProvider) => CaptchaProvider;
10
11
  //#endregion
@@ -1,8 +1,9 @@
1
1
  //#region src/spam/captcha.ts
2
2
  /**
3
3
  * Identity helper for authoring a captcha provider (mirrors `defineAction`/`defineConsentSource`).
4
- * v1 ships no built-in provider; a project supplies one (Turnstile/reCAPTCHA/hCaptcha) and the submit
5
- * path verifies a token carried on the submission. Prebuilt providers are a v1.x addition.
4
+ * Bundled adapters (`turnstileProvider`, `recaptchaProvider`, `hcaptchaProvider`) are built with it;
5
+ * use it directly for any other verification service. The submit path verifies a token carried on
6
+ * the submission.
6
7
  */
7
8
  const defineCaptchaProvider = (provider) => provider;
8
9
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"captcha.js","names":[],"sources":["../../src/spam/captcha.ts"],"sourcesContent":["import type { CaptchaProvider } from './types'\n\n/**\n * Identity helper for authoring a captcha provider (mirrors `defineAction`/`defineConsentSource`).\n * v1 ships no built-in provider; a project supplies one (Turnstile/reCAPTCHA/hCaptcha) and the submit\n * path verifies a token carried on the submission. Prebuilt providers are a v1.x addition.\n */\nexport const defineCaptchaProvider = (provider: CaptchaProvider): CaptchaProvider => provider\n"],"mappings":";;;;;;AAOA,MAAa,yBAAyB,aAA+C"}
1
+ {"version":3,"file":"captcha.js","names":[],"sources":["../../src/spam/captcha.ts"],"sourcesContent":["import type { CaptchaProvider } from './types'\n\n/**\n * Identity helper for authoring a captcha provider (mirrors `defineAction`/`defineConsentSource`).\n * Bundled adapters (`turnstileProvider`, `recaptchaProvider`, `hcaptchaProvider`) are built with it;\n * use it directly for any other verification service. The submit path verifies a token carried on\n * the submission.\n */\nexport const defineCaptchaProvider = (provider: CaptchaProvider): CaptchaProvider => provider\n"],"mappings":";;;;;;;AAQA,MAAa,yBAAyB,aAA+C"}
@@ -0,0 +1,9 @@
1
+ //#region src/spam/clientIp.ts
2
+ /** First hop of a comma-separated proxy header (the client, per X-Forwarded-For convention), or undefined. */
3
+ const firstHop = (headers, header) => {
4
+ return headers?.get(header)?.split(",")[0]?.trim() || void 0;
5
+ };
6
+ //#endregion
7
+ export { firstHop };
8
+
9
+ //# sourceMappingURL=clientIp.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clientIp.js","names":[],"sources":["../../src/spam/clientIp.ts"],"sourcesContent":["/** First hop of a comma-separated proxy header (the client, per X-Forwarded-For convention), or undefined. */\nexport const firstHop = (headers: Headers | undefined, header: string): string | undefined => {\n\tconst first = headers?.get(header)?.split(',')[0]?.trim()\n\treturn first || undefined\n}\n"],"mappings":";;AACA,MAAa,YAAY,SAA8B,WAAuC;CAE7F,OADc,SAAS,IAAI,MAAM,GAAG,MAAM,GAAG,EAAE,IAAI,KAAK,KACxC,KAAA;AACjB"}
@@ -1,3 +1,4 @@
1
+ import { firstHop } from "./clientIp.js";
1
2
  //#region src/spam/identify.ts
2
3
  /**
3
4
  * Default identity resolution for rate-limiting + upload ownership. Prefers an authenticated user id
@@ -7,12 +8,8 @@
7
8
  const defaultIdentify = (ipHeader) => (req) => {
8
9
  const userId = req.user?.id;
9
10
  if (userId != null) return `user:${String(userId)}`;
10
- const header = req.headers?.get(ipHeader);
11
- if (header) {
12
- const first = header.split(",")[0]?.trim();
13
- if (first) return `ip:${first}`;
14
- }
15
- return null;
11
+ const ip = firstHop(req.headers, ipHeader);
12
+ return ip ? `ip:${ip}` : null;
16
13
  };
17
14
  //#endregion
18
15
  export { defaultIdentify };
@@ -1 +1 @@
1
- {"version":3,"file":"identify.js","names":[],"sources":["../../src/spam/identify.ts"],"sourcesContent":["import type { IdentifyFn } from './types'\n\n/**\n * Default identity resolution for rate-limiting + upload ownership. Prefers an authenticated user id\n * (trustworthy); else the first hop of the configured trusted IP header (best-effort, proxy-dependent);\n * else null, in which case the caller fails open (cannot fairly rate-limit an unidentifiable client).\n */\nexport const defaultIdentify =\n\t(ipHeader: string): IdentifyFn =>\n\t(req) => {\n\t\tconst userId = req.user?.id\n\t\tif (userId != null) {\n\t\t\treturn `user:${String(userId)}`\n\t\t}\n\t\tconst header = req.headers?.get(ipHeader)\n\t\tif (header) {\n\t\t\tconst first = header.split(',')[0]?.trim()\n\t\t\tif (first) {\n\t\t\t\treturn `ip:${first}`\n\t\t\t}\n\t\t}\n\t\treturn null\n\t}\n"],"mappings":";;;;;;AAOA,MAAa,mBACX,cACA,QAAQ;CACR,MAAM,SAAS,IAAI,MAAM;CACzB,IAAI,UAAU,MACb,OAAO,QAAQ,OAAO,MAAM;CAE7B,MAAM,SAAS,IAAI,SAAS,IAAI,QAAQ;CACxC,IAAI,QAAQ;EACX,MAAM,QAAQ,OAAO,MAAM,GAAG,EAAE,IAAI,KAAK;EACzC,IAAI,OACH,OAAO,MAAM;CAEf;CACA,OAAO;AACR"}
1
+ {"version":3,"file":"identify.js","names":[],"sources":["../../src/spam/identify.ts"],"sourcesContent":["import { firstHop } from './clientIp'\nimport type { IdentifyFn } from './types'\n\n/**\n * Default identity resolution for rate-limiting + upload ownership. Prefers an authenticated user id\n * (trustworthy); else the first hop of the configured trusted IP header (best-effort, proxy-dependent);\n * else null, in which case the caller fails open (cannot fairly rate-limit an unidentifiable client).\n */\nexport const defaultIdentify =\n\t(ipHeader: string): IdentifyFn =>\n\t(req) => {\n\t\tconst userId = req.user?.id\n\t\tif (userId != null) {\n\t\t\treturn `user:${String(userId)}`\n\t\t}\n\t\tconst ip = firstHop(req.headers, ipHeader)\n\t\treturn ip ? `ip:${ip}` : null\n\t}\n"],"mappings":";;;;;;;AAQA,MAAa,mBACX,cACA,QAAQ;CACR,MAAM,SAAS,IAAI,MAAM;CACzB,IAAI,UAAU,MACb,OAAO,QAAQ,OAAO,MAAM;CAE7B,MAAM,KAAK,SAAS,IAAI,SAAS,QAAQ;CACzC,OAAO,KAAK,MAAM,OAAO;AAC1B"}
@@ -0,0 +1,17 @@
1
+ import { CaptchaProvider } from "../types.js";
2
+
3
+ //#region src/spam/providers/hcaptcha.d.ts
4
+ type HcaptchaProviderOptions = {
5
+ /** hCaptcha secret key (server-side, never the site key). */secretKey: string; /** Override the siteverify endpoint. */
6
+ verifyUrl?: string; /** Header read for the client IP forwarded as `remoteip`. Default 'x-forwarded-for'. */
7
+ ipHeader?: string; /** Verification request timeout in ms. Default 5000. */
8
+ timeoutMs?: number;
9
+ };
10
+ /**
11
+ * hCaptcha verification adapter. Network errors, timeouts, and non-2xx responses fail closed
12
+ * (the submission is rejected).
13
+ */
14
+ declare const hcaptchaProvider: (options: HcaptchaProviderOptions) => CaptchaProvider;
15
+ //#endregion
16
+ export { HcaptchaProviderOptions, hcaptchaProvider };
17
+ //# sourceMappingURL=hcaptcha.d.ts.map
@@ -0,0 +1,18 @@
1
+ import { defineCaptchaProvider } from "../captcha.js";
2
+ import { postSiteverify, siteverifyParams } from "./siteverify.js";
3
+ //#region src/spam/providers/hcaptcha.ts
4
+ const HCAPTCHA_VERIFY_URL = "https://api.hcaptcha.com/siteverify";
5
+ /**
6
+ * hCaptcha verification adapter. Network errors, timeouts, and non-2xx responses fail closed
7
+ * (the submission is rejected).
8
+ */
9
+ const hcaptchaProvider = (options) => defineCaptchaProvider({
10
+ type: "hcaptcha",
11
+ verify: async ({ token, req }) => {
12
+ return (await postSiteverify(options.verifyUrl ?? HCAPTCHA_VERIFY_URL, siteverifyParams(options, token, req), options.timeoutMs ?? 5e3))?.success === true;
13
+ }
14
+ });
15
+ //#endregion
16
+ export { hcaptchaProvider };
17
+
18
+ //# sourceMappingURL=hcaptcha.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hcaptcha.js","names":[],"sources":["../../../src/spam/providers/hcaptcha.ts"],"sourcesContent":["import { defineCaptchaProvider } from '../captcha'\nimport type { CaptchaProvider } from '../types'\nimport { DEFAULT_SITEVERIFY_TIMEOUT, postSiteverify, siteverifyParams } from './siteverify'\n\nconst HCAPTCHA_VERIFY_URL = 'https://api.hcaptcha.com/siteverify'\n\nexport type HcaptchaProviderOptions = {\n\t/** hCaptcha secret key (server-side, never the site key). */\n\tsecretKey: string\n\t/** Override the siteverify endpoint. */\n\tverifyUrl?: string\n\t/** Header read for the client IP forwarded as `remoteip`. Default 'x-forwarded-for'. */\n\tipHeader?: string\n\t/** Verification request timeout in ms. Default 5000. */\n\ttimeoutMs?: number\n}\n\n/**\n * hCaptcha verification adapter. Network errors, timeouts, and non-2xx responses fail closed\n * (the submission is rejected).\n */\nexport const hcaptchaProvider = (options: HcaptchaProviderOptions): CaptchaProvider =>\n\tdefineCaptchaProvider({\n\t\ttype: 'hcaptcha',\n\t\tverify: async ({ token, req }) => {\n\t\t\tconst result = await postSiteverify(\n\t\t\t\toptions.verifyUrl ?? HCAPTCHA_VERIFY_URL,\n\t\t\t\tsiteverifyParams(options, token, req),\n\t\t\t\toptions.timeoutMs ?? DEFAULT_SITEVERIFY_TIMEOUT\n\t\t\t)\n\t\t\treturn result?.success === true\n\t\t},\n\t})\n"],"mappings":";;;AAIA,MAAM,sBAAsB;;;;;AAiB5B,MAAa,oBAAoB,YAChC,sBAAsB;CACrB,MAAM;CACN,QAAQ,OAAO,EAAE,OAAO,UAAU;EAMjC,QAAO,MALc,eACpB,QAAQ,aAAa,qBACrB,iBAAiB,SAAS,OAAO,GAAG,GACpC,QAAQ,aAAA,GACT,IACe,YAAY;CAC5B;AACD,CAAC"}
@@ -0,0 +1,23 @@
1
+ import { CaptchaProvider } from "../types.js";
2
+
3
+ //#region src/spam/providers/recaptcha.d.ts
4
+ type RecaptchaProviderOptions = {
5
+ /** reCAPTCHA secret key (server-side, never the site key). */secretKey: string;
6
+ /**
7
+ * Minimum acceptable v3 score (0 to 1). Applied only when the response carries a score,
8
+ * defaulting to 0.5; v2 responses have no score and pass on `success` alone.
9
+ */
10
+ minScore?: number; /** Override the siteverify endpoint. */
11
+ verifyUrl?: string; /** Header read for the client IP forwarded as `remoteip`. Default 'x-forwarded-for'. */
12
+ ipHeader?: string; /** Verification request timeout in ms. Default 5000. */
13
+ timeoutMs?: number;
14
+ };
15
+ /**
16
+ * Google reCAPTCHA verification adapter for both v2 (checkbox) and v3 (score) tokens. A v3
17
+ * response must also meet `minScore`. Network errors, timeouts, and non-2xx responses fail
18
+ * closed (the submission is rejected).
19
+ */
20
+ declare const recaptchaProvider: (options: RecaptchaProviderOptions) => CaptchaProvider;
21
+ //#endregion
22
+ export { RecaptchaProviderOptions, recaptchaProvider };
23
+ //# sourceMappingURL=recaptcha.d.ts.map
@@ -0,0 +1,23 @@
1
+ import { defineCaptchaProvider } from "../captcha.js";
2
+ import { postSiteverify, siteverifyParams } from "./siteverify.js";
3
+ //#region src/spam/providers/recaptcha.ts
4
+ const RECAPTCHA_VERIFY_URL = "https://www.google.com/recaptcha/api/siteverify";
5
+ const DEFAULT_MIN_SCORE = .5;
6
+ /**
7
+ * Google reCAPTCHA verification adapter for both v2 (checkbox) and v3 (score) tokens. A v3
8
+ * response must also meet `minScore`. Network errors, timeouts, and non-2xx responses fail
9
+ * closed (the submission is rejected).
10
+ */
11
+ const recaptchaProvider = (options) => defineCaptchaProvider({
12
+ type: "recaptcha",
13
+ verify: async ({ token, req }) => {
14
+ const result = await postSiteverify(options.verifyUrl ?? RECAPTCHA_VERIFY_URL, siteverifyParams(options, token, req), options.timeoutMs ?? 5e3);
15
+ if (result?.success !== true) return false;
16
+ if (typeof result.score === "number") return result.score >= (options.minScore ?? DEFAULT_MIN_SCORE);
17
+ return true;
18
+ }
19
+ });
20
+ //#endregion
21
+ export { recaptchaProvider };
22
+
23
+ //# sourceMappingURL=recaptcha.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recaptcha.js","names":[],"sources":["../../../src/spam/providers/recaptcha.ts"],"sourcesContent":["import { defineCaptchaProvider } from '../captcha'\nimport type { CaptchaProvider } from '../types'\nimport { DEFAULT_SITEVERIFY_TIMEOUT, postSiteverify, siteverifyParams } from './siteverify'\n\nconst RECAPTCHA_VERIFY_URL = 'https://www.google.com/recaptcha/api/siteverify'\nconst DEFAULT_MIN_SCORE = 0.5\n\nexport type RecaptchaProviderOptions = {\n\t/** reCAPTCHA secret key (server-side, never the site key). */\n\tsecretKey: string\n\t/**\n\t * Minimum acceptable v3 score (0 to 1). Applied only when the response carries a score,\n\t * defaulting to 0.5; v2 responses have no score and pass on `success` alone.\n\t */\n\tminScore?: number\n\t/** Override the siteverify endpoint. */\n\tverifyUrl?: string\n\t/** Header read for the client IP forwarded as `remoteip`. Default 'x-forwarded-for'. */\n\tipHeader?: string\n\t/** Verification request timeout in ms. Default 5000. */\n\ttimeoutMs?: number\n}\n\n/**\n * Google reCAPTCHA verification adapter for both v2 (checkbox) and v3 (score) tokens. A v3\n * response must also meet `minScore`. Network errors, timeouts, and non-2xx responses fail\n * closed (the submission is rejected).\n */\nexport const recaptchaProvider = (options: RecaptchaProviderOptions): CaptchaProvider =>\n\tdefineCaptchaProvider({\n\t\ttype: 'recaptcha',\n\t\tverify: async ({ token, req }) => {\n\t\t\tconst result = await postSiteverify(\n\t\t\t\toptions.verifyUrl ?? RECAPTCHA_VERIFY_URL,\n\t\t\t\tsiteverifyParams(options, token, req),\n\t\t\t\toptions.timeoutMs ?? DEFAULT_SITEVERIFY_TIMEOUT\n\t\t\t)\n\t\t\tif (result?.success !== true) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tif (typeof result.score === 'number') {\n\t\t\t\treturn result.score >= (options.minScore ?? DEFAULT_MIN_SCORE)\n\t\t\t}\n\t\t\treturn true\n\t\t},\n\t})\n"],"mappings":";;;AAIA,MAAM,uBAAuB;AAC7B,MAAM,oBAAoB;;;;;;AAuB1B,MAAa,qBAAqB,YACjC,sBAAsB;CACrB,MAAM;CACN,QAAQ,OAAO,EAAE,OAAO,UAAU;EACjC,MAAM,SAAS,MAAM,eACpB,QAAQ,aAAa,sBACrB,iBAAiB,SAAS,OAAO,GAAG,GACpC,QAAQ,aAAA,GACT;EACA,IAAI,QAAQ,YAAY,MACvB,OAAO;EAER,IAAI,OAAO,OAAO,UAAU,UAC3B,OAAO,OAAO,UAAU,QAAQ,YAAY;EAE7C,OAAO;CACR;AACD,CAAC"}
@@ -0,0 +1,34 @@
1
+ import { firstHop } from "../clientIp.js";
2
+ //#region src/spam/providers/siteverify.ts
3
+ const DEFAULT_SITEVERIFY_TIMEOUT = 5e3;
4
+ /** Form params for a siteverify POST: secret + response, plus `remoteip` when the IP header resolves. */
5
+ const siteverifyParams = (args, token, req) => {
6
+ const params = new URLSearchParams({
7
+ secret: args.secretKey,
8
+ response: token
9
+ });
10
+ const remoteip = firstHop(req.headers, args.ipHeader ?? "x-forwarded-for");
11
+ if (remoteip) params.set("remoteip", remoteip);
12
+ return params;
13
+ };
14
+ /**
15
+ * POST a form-encoded siteverify request. Network errors, timeouts, and non-2xx responses all
16
+ * resolve to null so adapters fail closed instead of throwing into the submission path.
17
+ */
18
+ const postSiteverify = async (url, params, timeoutMs) => {
19
+ try {
20
+ const res = await fetch(url, {
21
+ method: "POST",
22
+ body: params,
23
+ signal: AbortSignal.timeout(timeoutMs)
24
+ });
25
+ if (!res.ok) return null;
26
+ return await res.json();
27
+ } catch {
28
+ return null;
29
+ }
30
+ };
31
+ //#endregion
32
+ export { DEFAULT_SITEVERIFY_TIMEOUT, postSiteverify, siteverifyParams };
33
+
34
+ //# sourceMappingURL=siteverify.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"siteverify.js","names":[],"sources":["../../../src/spam/providers/siteverify.ts"],"sourcesContent":["import type { PayloadRequest } from 'payload'\nimport { firstHop } from '../clientIp'\n\n/** The response shape shared by the Turnstile, reCAPTCHA, and hCaptcha siteverify endpoints. */\nexport type SiteverifyResult = {\n\tsuccess?: boolean\n\tscore?: number\n\t'error-codes'?: string[]\n}\n\nexport const DEFAULT_SITEVERIFY_TIMEOUT = 5_000\n\nexport type SiteverifyParamsArgs = {\n\tsecretKey: string\n\t/** Header read for the client IP forwarded as `remoteip`. Default 'x-forwarded-for'. */\n\tipHeader?: string\n}\n\n/** Form params for a siteverify POST: secret + response, plus `remoteip` when the IP header resolves. */\nexport const siteverifyParams = (\n\targs: SiteverifyParamsArgs,\n\ttoken: string,\n\treq: PayloadRequest\n): URLSearchParams => {\n\tconst params = new URLSearchParams({ secret: args.secretKey, response: token })\n\tconst remoteip = firstHop(req.headers, args.ipHeader ?? 'x-forwarded-for')\n\tif (remoteip) {\n\t\tparams.set('remoteip', remoteip)\n\t}\n\treturn params\n}\n\n/**\n * POST a form-encoded siteverify request. Network errors, timeouts, and non-2xx responses all\n * resolve to null so adapters fail closed instead of throwing into the submission path.\n */\nexport const postSiteverify = async (\n\turl: string,\n\tparams: URLSearchParams,\n\ttimeoutMs: number\n): Promise<SiteverifyResult | null> => {\n\ttry {\n\t\tconst res = await fetch(url, {\n\t\t\tmethod: 'POST',\n\t\t\tbody: params,\n\t\t\tsignal: AbortSignal.timeout(timeoutMs),\n\t\t})\n\t\tif (!res.ok) {\n\t\t\treturn null\n\t\t}\n\t\treturn (await res.json()) as SiteverifyResult\n\t} catch {\n\t\treturn null\n\t}\n}\n"],"mappings":";;AAUA,MAAa,6BAA6B;;AAS1C,MAAa,oBACZ,MACA,OACA,QACqB;CACrB,MAAM,SAAS,IAAI,gBAAgB;EAAE,QAAQ,KAAK;EAAW,UAAU;CAAM,CAAC;CAC9E,MAAM,WAAW,SAAS,IAAI,SAAS,KAAK,YAAY,iBAAiB;CACzE,IAAI,UACH,OAAO,IAAI,YAAY,QAAQ;CAEhC,OAAO;AACR;;;;;AAMA,MAAa,iBAAiB,OAC7B,KACA,QACA,cACsC;CACtC,IAAI;EACH,MAAM,MAAM,MAAM,MAAM,KAAK;GAC5B,QAAQ;GACR,MAAM;GACN,QAAQ,YAAY,QAAQ,SAAS;EACtC,CAAC;EACD,IAAI,CAAC,IAAI,IACR,OAAO;EAER,OAAQ,MAAM,IAAI,KAAK;CACxB,QAAQ;EACP,OAAO;CACR;AACD"}
@@ -0,0 +1,17 @@
1
+ import { CaptchaProvider } from "../types.js";
2
+
3
+ //#region src/spam/providers/turnstile.d.ts
4
+ type TurnstileProviderOptions = {
5
+ /** Turnstile secret key (server-side, never the site key). */secretKey: string; /** Override the siteverify endpoint. */
6
+ verifyUrl?: string; /** Header read for the client IP forwarded as `remoteip`. Default 'x-forwarded-for'. */
7
+ ipHeader?: string; /** Verification request timeout in ms. Default 5000. */
8
+ timeoutMs?: number;
9
+ };
10
+ /**
11
+ * Cloudflare Turnstile verification adapter. Network errors, timeouts, and non-2xx responses
12
+ * fail closed (the submission is rejected).
13
+ */
14
+ declare const turnstileProvider: (options: TurnstileProviderOptions) => CaptchaProvider;
15
+ //#endregion
16
+ export { TurnstileProviderOptions, turnstileProvider };
17
+ //# sourceMappingURL=turnstile.d.ts.map
@@ -0,0 +1,18 @@
1
+ import { defineCaptchaProvider } from "../captcha.js";
2
+ import { postSiteverify, siteverifyParams } from "./siteverify.js";
3
+ //#region src/spam/providers/turnstile.ts
4
+ const TURNSTILE_VERIFY_URL = "https://challenges.cloudflare.com/turnstile/v0/siteverify";
5
+ /**
6
+ * Cloudflare Turnstile verification adapter. Network errors, timeouts, and non-2xx responses
7
+ * fail closed (the submission is rejected).
8
+ */
9
+ const turnstileProvider = (options) => defineCaptchaProvider({
10
+ type: "turnstile",
11
+ verify: async ({ token, req }) => {
12
+ return (await postSiteverify(options.verifyUrl ?? TURNSTILE_VERIFY_URL, siteverifyParams(options, token, req), options.timeoutMs ?? 5e3))?.success === true;
13
+ }
14
+ });
15
+ //#endregion
16
+ export { turnstileProvider };
17
+
18
+ //# sourceMappingURL=turnstile.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"turnstile.js","names":[],"sources":["../../../src/spam/providers/turnstile.ts"],"sourcesContent":["import { defineCaptchaProvider } from '../captcha'\nimport type { CaptchaProvider } from '../types'\nimport { DEFAULT_SITEVERIFY_TIMEOUT, postSiteverify, siteverifyParams } from './siteverify'\n\nconst TURNSTILE_VERIFY_URL = 'https://challenges.cloudflare.com/turnstile/v0/siteverify'\n\nexport type TurnstileProviderOptions = {\n\t/** Turnstile secret key (server-side, never the site key). */\n\tsecretKey: string\n\t/** Override the siteverify endpoint. */\n\tverifyUrl?: string\n\t/** Header read for the client IP forwarded as `remoteip`. Default 'x-forwarded-for'. */\n\tipHeader?: string\n\t/** Verification request timeout in ms. Default 5000. */\n\ttimeoutMs?: number\n}\n\n/**\n * Cloudflare Turnstile verification adapter. Network errors, timeouts, and non-2xx responses\n * fail closed (the submission is rejected).\n */\nexport const turnstileProvider = (options: TurnstileProviderOptions): CaptchaProvider =>\n\tdefineCaptchaProvider({\n\t\ttype: 'turnstile',\n\t\tverify: async ({ token, req }) => {\n\t\t\tconst result = await postSiteverify(\n\t\t\t\toptions.verifyUrl ?? TURNSTILE_VERIFY_URL,\n\t\t\t\tsiteverifyParams(options, token, req),\n\t\t\t\toptions.timeoutMs ?? DEFAULT_SITEVERIFY_TIMEOUT\n\t\t\t)\n\t\t\treturn result?.success === true\n\t\t},\n\t})\n"],"mappings":";;;AAIA,MAAM,uBAAuB;;;;;AAiB7B,MAAa,qBAAqB,YACjC,sBAAsB;CACrB,MAAM;CACN,QAAQ,OAAO,EAAE,OAAO,UAAU;EAMjC,QAAO,MALc,eACpB,QAAQ,aAAa,sBACrB,iBAAiB,SAAS,OAAO,GAAG,GACpC,QAAQ,aAAA,GACT,IACe,YAAY;CAC5B;AACD,CAAC"}
@@ -1,13 +1,10 @@
1
1
  import { keys } from "../translations/keys.js";
2
2
  import { asTranslate } from "../translations/server.js";
3
+ import { firstHop } from "./clientIp.js";
3
4
  import { IDENTITY_CONTEXT_KEY } from "./constants.js";
4
5
  import { extractReservedValues, isHoneypotTripped } from "./reserved.js";
5
6
  import { APIError } from "payload";
6
7
  //#region src/spam/spamGuard.ts
7
- const firstHop = (req, header) => {
8
- const raw = req.headers?.get(header);
9
- return raw ? raw.split(",")[0]?.trim() ?? void 0 : void 0;
10
- };
11
8
  /**
12
9
  * Submissions spam guard, prepended before `validateSubmission` in `beforeValidate` so it rejects before
13
10
  * the form load + field validation. On create it: resolves the request identity once (stashing it on
@@ -49,7 +46,7 @@ const buildSpamGuard = (spam) => async ({ data, operation, req }) => {
49
46
  spam: { captcha: captchaChecked ? "passed" : "skipped" }
50
47
  };
51
48
  if (spam.metadata.ip) {
52
- const ip = firstHop(req, spam.ipHeader);
49
+ const ip = firstHop(req.headers, spam.ipHeader);
53
50
  if (ip) serverMeta.ip = ip;
54
51
  }
55
52
  if (spam.metadata.ua) {
@@ -1 +1 @@
1
- {"version":3,"file":"spamGuard.js","names":[],"sources":["../../src/spam/spamGuard.ts"],"sourcesContent":["import { APIError, type CollectionBeforeValidateHook } from 'payload'\nimport type { SubmissionValue } from '../submissions/types'\nimport { keys } from '../translations/keys'\nimport { asTranslate } from '../translations/server'\nimport { IDENTITY_CONTEXT_KEY } from './constants'\nimport { extractReservedValues, isHoneypotTripped } from './reserved'\nimport type { ResolvedSpamConfig } from './types'\n\nconst firstHop = (req: { headers?: Headers }, header: string): string | undefined => {\n\tconst raw = req.headers?.get(header)\n\treturn raw ? (raw.split(',')[0]?.trim() ?? undefined) : undefined\n}\n\n/**\n * Submissions spam guard, prepended before `validateSubmission` in `beforeValidate` so it rejects before\n * the form load + field validation. On create it: resolves the request identity once (stashing it on\n * `req.context` for upload-ownership verification downstream); rate-limits per form + identity (429,\n * fail-open when identity is null); strips reserved `values` entries (honeypot, captcha token); rejects a\n * filled honeypot with a generic error; verifies a configured captcha; and writes a server-authoritative\n * `meta` (timestamp + spam signal, with opt-in ip/ua). App-level rate limiting is defense-in-depth that\n * complements edge/CDN/WAF limiting, not a DoS replacement.\n */\nexport const buildSpamGuard =\n\t(spam: ResolvedSpamConfig): CollectionBeforeValidateHook =>\n\tasync ({ data, operation, req }) => {\n\t\tif (operation !== 'create' || !data) {\n\t\t\treturn data\n\t\t}\n\t\tconst t = asTranslate(req.i18n.t)\n\t\t// Honeypot + captcha are anti-bot measures for the anonymous public path; an authenticated request\n\t\t// (admin panel, logged-in API client) is already gated by auth and never carries a decoy/token, so\n\t\t// checking it would falsely reject legitimate authed creates. Rate-limit + ownership still apply.\n\t\tconst authenticated = Boolean(req.user)\n\n\t\tconst identity = await spam.identify(req)\n\t\tif (identity != null) {\n\t\t\treq.context[IDENTITY_CONTEXT_KEY] = identity\n\t\t}\n\n\t\tif (spam.rateLimit !== false && identity != null) {\n\t\t\tconst formKey = data.form != null ? String(data.form) : 'unknown'\n\t\t\tconst { ok } = await spam.rateLimit.limiter.check({\n\t\t\t\tkey: `submissions:${formKey}:${identity}`,\n\t\t\t\tmax: spam.rateLimit.max,\n\t\t\t\twindow: spam.rateLimit.window,\n\t\t\t\treq,\n\t\t\t})\n\t\t\tif (!ok) {\n\t\t\t\tthrow new APIError(t(keys.spamRateLimited), 429)\n\t\t\t}\n\t\t}\n\n\t\tconst honeypotField = spam.honeypot === false ? null : spam.honeypot.fieldName\n\t\tconst values = (data.values as SubmissionValue[] | undefined) ?? []\n\t\tconst { cleaned, honeypot, captchaToken } = extractReservedValues(values, honeypotField)\n\t\tdata.values = cleaned\n\n\t\tif (!authenticated && honeypotField !== null && isHoneypotTripped(honeypot)) {\n\t\t\tthrow new APIError(t(keys.spamRejected), 400)\n\t\t}\n\n\t\tconst captchaChecked = Boolean(spam.captcha) && !authenticated\n\t\tif (spam.captcha && !authenticated) {\n\t\t\tconst passed =\n\t\t\t\ttypeof captchaToken === 'string' && captchaToken.length > 0\n\t\t\t\t\t? await spam.captcha.verify({ token: captchaToken, req }).catch(() => false)\n\t\t\t\t\t: false\n\t\t\tif (!passed) {\n\t\t\t\tthrow new APIError(t(keys.spamCaptchaFailed), 400)\n\t\t\t}\n\t\t}\n\n\t\tconst serverMeta: Record<string, unknown> = {\n\t\t\tat: new Date().toISOString(),\n\t\t\tspam: { captcha: captchaChecked ? 'passed' : 'skipped' },\n\t\t}\n\t\tif (spam.metadata.ip) {\n\t\t\tconst ip = firstHop(req, spam.ipHeader)\n\t\t\tif (ip) {\n\t\t\t\tserverMeta.ip = ip\n\t\t\t}\n\t\t}\n\t\tif (spam.metadata.ua) {\n\t\t\tconst ua = req.headers?.get('user-agent')\n\t\t\tif (ua) {\n\t\t\t\tserverMeta.ua = ua\n\t\t\t}\n\t\t}\n\t\t// Preserve an authenticated (trusted) caller's own `meta` keys, with server fields winning. Anonymous\n\t\t// `meta` is never trusted, so it is discarded.\n\t\tconst clientMeta =\n\t\t\tauthenticated && data.meta != null && typeof data.meta === 'object'\n\t\t\t\t? (data.meta as Record<string, unknown>)\n\t\t\t\t: {}\n\t\tdata.meta = { ...clientMeta, ...serverMeta }\n\n\t\treturn data\n\t}\n"],"mappings":";;;;;;AAQA,MAAM,YAAY,KAA4B,WAAuC;CACpF,MAAM,MAAM,IAAI,SAAS,IAAI,MAAM;CACnC,OAAO,MAAO,IAAI,MAAM,GAAG,EAAE,IAAI,KAAK,KAAK,KAAA,IAAa,KAAA;AACzD;;;;;;;;;;AAWA,MAAa,kBACX,SACD,OAAO,EAAE,MAAM,WAAW,UAAU;CACnC,IAAI,cAAc,YAAY,CAAC,MAC9B,OAAO;CAER,MAAM,IAAI,YAAY,IAAI,KAAK,CAAC;CAIhC,MAAM,gBAAgB,QAAQ,IAAI,IAAI;CAEtC,MAAM,WAAW,MAAM,KAAK,SAAS,GAAG;CACxC,IAAI,YAAY,MACf,IAAI,QAAQ,wBAAwB;CAGrC,IAAI,KAAK,cAAc,SAAS,YAAY,MAAM;EACjD,MAAM,UAAU,KAAK,QAAQ,OAAO,OAAO,KAAK,IAAI,IAAI;EACxD,MAAM,EAAE,OAAO,MAAM,KAAK,UAAU,QAAQ,MAAM;GACjD,KAAK,eAAe,QAAQ,GAAG;GAC/B,KAAK,KAAK,UAAU;GACpB,QAAQ,KAAK,UAAU;GACvB;EACD,CAAC;EACD,IAAI,CAAC,IACJ,MAAM,IAAI,SAAS,EAAE,KAAK,eAAe,GAAG,GAAG;CAEjD;CAEA,MAAM,gBAAgB,KAAK,aAAa,QAAQ,OAAO,KAAK,SAAS;CAErE,MAAM,EAAE,SAAS,UAAU,iBAAiB,sBAD5B,KAAK,UAA4C,CAAC,GACQ,aAAa;CACvF,KAAK,SAAS;CAEd,IAAI,CAAC,iBAAiB,kBAAkB,QAAQ,kBAAkB,QAAQ,GACzE,MAAM,IAAI,SAAS,EAAE,KAAK,YAAY,GAAG,GAAG;CAG7C,MAAM,iBAAiB,QAAQ,KAAK,OAAO,KAAK,CAAC;CACjD,IAAI,KAAK,WAAW,CAAC;MAKhB,EAHH,OAAO,iBAAiB,YAAY,aAAa,SAAS,IACvD,MAAM,KAAK,QAAQ,OAAO;GAAE,OAAO;GAAc;EAAI,CAAC,EAAE,YAAY,KAAK,IACzE,QAEH,MAAM,IAAI,SAAS,EAAE,KAAK,iBAAiB,GAAG,GAAG;CAAA;CAInD,MAAM,aAAsC;EAC3C,qBAAI,IAAI,KAAK,GAAE,YAAY;EAC3B,MAAM,EAAE,SAAS,iBAAiB,WAAW,UAAU;CACxD;CACA,IAAI,KAAK,SAAS,IAAI;EACrB,MAAM,KAAK,SAAS,KAAK,KAAK,QAAQ;EACtC,IAAI,IACH,WAAW,KAAK;CAElB;CACA,IAAI,KAAK,SAAS,IAAI;EACrB,MAAM,KAAK,IAAI,SAAS,IAAI,YAAY;EACxC,IAAI,IACH,WAAW,KAAK;CAElB;CAOA,KAAK,OAAO;EAAE,GAHb,iBAAiB,KAAK,QAAQ,QAAQ,OAAO,KAAK,SAAS,WACvD,KAAK,OACN,CAAC;EACwB,GAAG;CAAW;CAE3C,OAAO;AACR"}
1
+ {"version":3,"file":"spamGuard.js","names":[],"sources":["../../src/spam/spamGuard.ts"],"sourcesContent":["import { APIError, type CollectionBeforeValidateHook } from 'payload'\nimport type { SubmissionValue } from '../submissions/types'\nimport { keys } from '../translations/keys'\nimport { asTranslate } from '../translations/server'\nimport { firstHop } from './clientIp'\nimport { IDENTITY_CONTEXT_KEY } from './constants'\nimport { extractReservedValues, isHoneypotTripped } from './reserved'\nimport type { ResolvedSpamConfig } from './types'\n\n/**\n * Submissions spam guard, prepended before `validateSubmission` in `beforeValidate` so it rejects before\n * the form load + field validation. On create it: resolves the request identity once (stashing it on\n * `req.context` for upload-ownership verification downstream); rate-limits per form + identity (429,\n * fail-open when identity is null); strips reserved `values` entries (honeypot, captcha token); rejects a\n * filled honeypot with a generic error; verifies a configured captcha; and writes a server-authoritative\n * `meta` (timestamp + spam signal, with opt-in ip/ua). App-level rate limiting is defense-in-depth that\n * complements edge/CDN/WAF limiting, not a DoS replacement.\n */\nexport const buildSpamGuard =\n\t(spam: ResolvedSpamConfig): CollectionBeforeValidateHook =>\n\tasync ({ data, operation, req }) => {\n\t\tif (operation !== 'create' || !data) {\n\t\t\treturn data\n\t\t}\n\t\tconst t = asTranslate(req.i18n.t)\n\t\t// Honeypot + captcha are anti-bot measures for the anonymous public path; an authenticated request\n\t\t// (admin panel, logged-in API client) is already gated by auth and never carries a decoy/token, so\n\t\t// checking it would falsely reject legitimate authed creates. Rate-limit + ownership still apply.\n\t\tconst authenticated = Boolean(req.user)\n\n\t\tconst identity = await spam.identify(req)\n\t\tif (identity != null) {\n\t\t\treq.context[IDENTITY_CONTEXT_KEY] = identity\n\t\t}\n\n\t\tif (spam.rateLimit !== false && identity != null) {\n\t\t\tconst formKey = data.form != null ? String(data.form) : 'unknown'\n\t\t\tconst { ok } = await spam.rateLimit.limiter.check({\n\t\t\t\tkey: `submissions:${formKey}:${identity}`,\n\t\t\t\tmax: spam.rateLimit.max,\n\t\t\t\twindow: spam.rateLimit.window,\n\t\t\t\treq,\n\t\t\t})\n\t\t\tif (!ok) {\n\t\t\t\tthrow new APIError(t(keys.spamRateLimited), 429)\n\t\t\t}\n\t\t}\n\n\t\tconst honeypotField = spam.honeypot === false ? null : spam.honeypot.fieldName\n\t\tconst values = (data.values as SubmissionValue[] | undefined) ?? []\n\t\tconst { cleaned, honeypot, captchaToken } = extractReservedValues(values, honeypotField)\n\t\tdata.values = cleaned\n\n\t\tif (!authenticated && honeypotField !== null && isHoneypotTripped(honeypot)) {\n\t\t\tthrow new APIError(t(keys.spamRejected), 400)\n\t\t}\n\n\t\tconst captchaChecked = Boolean(spam.captcha) && !authenticated\n\t\tif (spam.captcha && !authenticated) {\n\t\t\tconst passed =\n\t\t\t\ttypeof captchaToken === 'string' && captchaToken.length > 0\n\t\t\t\t\t? await spam.captcha.verify({ token: captchaToken, req }).catch(() => false)\n\t\t\t\t\t: false\n\t\t\tif (!passed) {\n\t\t\t\tthrow new APIError(t(keys.spamCaptchaFailed), 400)\n\t\t\t}\n\t\t}\n\n\t\tconst serverMeta: Record<string, unknown> = {\n\t\t\tat: new Date().toISOString(),\n\t\t\tspam: { captcha: captchaChecked ? 'passed' : 'skipped' },\n\t\t}\n\t\tif (spam.metadata.ip) {\n\t\t\tconst ip = firstHop(req.headers, spam.ipHeader)\n\t\t\tif (ip) {\n\t\t\t\tserverMeta.ip = ip\n\t\t\t}\n\t\t}\n\t\tif (spam.metadata.ua) {\n\t\t\tconst ua = req.headers?.get('user-agent')\n\t\t\tif (ua) {\n\t\t\t\tserverMeta.ua = ua\n\t\t\t}\n\t\t}\n\t\t// Preserve an authenticated (trusted) caller's own `meta` keys, with server fields winning. Anonymous\n\t\t// `meta` is never trusted, so it is discarded.\n\t\tconst clientMeta =\n\t\t\tauthenticated && data.meta != null && typeof data.meta === 'object'\n\t\t\t\t? (data.meta as Record<string, unknown>)\n\t\t\t\t: {}\n\t\tdata.meta = { ...clientMeta, ...serverMeta }\n\n\t\treturn data\n\t}\n"],"mappings":";;;;;;;;;;;;;;;;AAkBA,MAAa,kBACX,SACD,OAAO,EAAE,MAAM,WAAW,UAAU;CACnC,IAAI,cAAc,YAAY,CAAC,MAC9B,OAAO;CAER,MAAM,IAAI,YAAY,IAAI,KAAK,CAAC;CAIhC,MAAM,gBAAgB,QAAQ,IAAI,IAAI;CAEtC,MAAM,WAAW,MAAM,KAAK,SAAS,GAAG;CACxC,IAAI,YAAY,MACf,IAAI,QAAQ,wBAAwB;CAGrC,IAAI,KAAK,cAAc,SAAS,YAAY,MAAM;EACjD,MAAM,UAAU,KAAK,QAAQ,OAAO,OAAO,KAAK,IAAI,IAAI;EACxD,MAAM,EAAE,OAAO,MAAM,KAAK,UAAU,QAAQ,MAAM;GACjD,KAAK,eAAe,QAAQ,GAAG;GAC/B,KAAK,KAAK,UAAU;GACpB,QAAQ,KAAK,UAAU;GACvB;EACD,CAAC;EACD,IAAI,CAAC,IACJ,MAAM,IAAI,SAAS,EAAE,KAAK,eAAe,GAAG,GAAG;CAEjD;CAEA,MAAM,gBAAgB,KAAK,aAAa,QAAQ,OAAO,KAAK,SAAS;CAErE,MAAM,EAAE,SAAS,UAAU,iBAAiB,sBAD5B,KAAK,UAA4C,CAAC,GACQ,aAAa;CACvF,KAAK,SAAS;CAEd,IAAI,CAAC,iBAAiB,kBAAkB,QAAQ,kBAAkB,QAAQ,GACzE,MAAM,IAAI,SAAS,EAAE,KAAK,YAAY,GAAG,GAAG;CAG7C,MAAM,iBAAiB,QAAQ,KAAK,OAAO,KAAK,CAAC;CACjD,IAAI,KAAK,WAAW,CAAC;MAKhB,EAHH,OAAO,iBAAiB,YAAY,aAAa,SAAS,IACvD,MAAM,KAAK,QAAQ,OAAO;GAAE,OAAO;GAAc;EAAI,CAAC,EAAE,YAAY,KAAK,IACzE,QAEH,MAAM,IAAI,SAAS,EAAE,KAAK,iBAAiB,GAAG,GAAG;CAAA;CAInD,MAAM,aAAsC;EAC3C,qBAAI,IAAI,KAAK,GAAE,YAAY;EAC3B,MAAM,EAAE,SAAS,iBAAiB,WAAW,UAAU;CACxD;CACA,IAAI,KAAK,SAAS,IAAI;EACrB,MAAM,KAAK,SAAS,IAAI,SAAS,KAAK,QAAQ;EAC9C,IAAI,IACH,WAAW,KAAK;CAElB;CACA,IAAI,KAAK,SAAS,IAAI;EACrB,MAAM,KAAK,IAAI,SAAS,IAAI,YAAY;EACxC,IAAI,IACH,WAAW,KAAK;CAElB;CAOA,KAAK,OAAO;EAAE,GAHb,iBAAiB,KAAK,QAAQ,QAAQ,OAAO,KAAK,SAAS,WACvD,KAAK,OACN,CAAC;EACwB,GAAG;CAAW;CAE3C,OAAO;AACR"}
@@ -22,7 +22,7 @@ type CaptchaVerifyArgs = {
22
22
  token: string;
23
23
  req: PayloadRequest;
24
24
  };
25
- /** A captcha adapter. v1 ships the seam only (no built-in provider). Build with `defineCaptchaProvider`. */
25
+ /** A captcha adapter. Use a bundled provider (Turnstile/reCAPTCHA/hCaptcha) or build one with `defineCaptchaProvider`. */
26
26
  type CaptchaProvider = {
27
27
  type: string;
28
28
  verify(args: CaptchaVerifyArgs): Promise<boolean>;
@@ -41,7 +41,7 @@ type SpamConfig = {
41
41
  fieldName?: string;
42
42
  }; /** Per-identity rate limit on submission create. Default on (60s window, max 5). `false` disables. */
43
43
  rateLimit?: false | RateLimitConfig; /** Per-identity rate limit on upload create. Default on (60s window, max 20). `false` disables. */
44
- uploadRateLimit?: false | RateLimitConfig; /** A captcha provider (none by default; seam only in v1). */
44
+ uploadRateLimit?: false | RateLimitConfig; /** A captcha provider (none by default): `turnstileProvider`/`recaptchaProvider`/`hcaptchaProvider` or a custom adapter. */
45
45
  captcha?: CaptchaProvider; /** Identity resolution for rate-limiting, upload ownership, and (future) poll dedup. Default: user id, else trusted IP header. */
46
46
  identify?: IdentifyFn; /** Header read for the client IP (proxy-dependent, best-effort). Default 'x-forwarded-for'. */
47
47
  ipHeader?: string; /** Opt-in capture of client metadata onto the submission `meta`. Off by default. */
@@ -6,7 +6,6 @@ import { keys } from "./keys.js";
6
6
  * unknown key a type error. `translations/index.ts` nests these for Payload.
7
7
  */
8
8
  const en = {
9
- [keys.pluginName]: "Form Builder",
10
9
  [keys.fieldTitle]: "Title",
11
10
  [keys.fieldTypeText]: "Text",
12
11
  [keys.fieldTypeTextarea]: "Long text",
@@ -14,6 +13,7 @@ const en = {
14
13
  [keys.fieldTypeNumber]: "Number",
15
14
  [keys.fieldTypeSelect]: "Select",
16
15
  [keys.fieldTypeCheckbox]: "Checkbox",
16
+ [keys.fieldTypeDate]: "Date",
17
17
  [keys.configOptions]: "Options",
18
18
  [keys.configOption]: "Option",
19
19
  [keys.configOptionLabel]: "Option label",
@@ -21,7 +21,9 @@ const en = {
21
21
  [keys.validationRequired]: "This field is required",
22
22
  [keys.validationEmail]: "Enter a valid email address",
23
23
  [keys.validationNumber]: "Enter a valid number",
24
+ [keys.validationDate]: "Enter a valid date",
24
25
  [keys.validationSelect]: "Choose a valid option",
26
+ [keys.validationEmailFieldUnknown]: "Choose an existing email field on this form",
25
27
  [keys.formatYes]: "Yes",
26
28
  [keys.formatNo]: "No",
27
29
  [keys.configName]: "Name",
@@ -38,6 +40,8 @@ const en = {
38
40
  [keys.ruleMaxLength]: "Maximum length",
39
41
  [keys.ruleMin]: "Minimum",
40
42
  [keys.ruleMax]: "Maximum",
43
+ [keys.ruleMinDate]: "Earliest date",
44
+ [keys.ruleMaxDate]: "Latest date",
41
45
  [keys.rulePattern]: "Pattern",
42
46
  [keys.ruleEmail]: "Email",
43
47
  [keys.ruleUrl]: "URL",
@@ -48,6 +52,8 @@ const en = {
48
52
  [keys.ruleMaxLengthMessage]: "Must be at most {max} characters",
49
53
  [keys.ruleMinMessage]: "Must be at least {min}",
50
54
  [keys.ruleMaxMessage]: "Must be at most {max}",
55
+ [keys.ruleMinDateMessage]: "Must be on or after {min}",
56
+ [keys.ruleMaxDateMessage]: "Must be on or before {max}",
51
57
  [keys.rulePatternMessage]: "Invalid format",
52
58
  [keys.ruleEmailMessage]: "Enter a valid email address",
53
59
  [keys.ruleUrlMessage]: "Enter a valid URL",
@@ -56,6 +62,8 @@ const en = {
56
62
  [keys.ruleNotAlreadySubmittedMessage]: "This value was already submitted",
57
63
  [keys.ruleParamMin]: "Minimum",
58
64
  [keys.ruleParamMax]: "Maximum",
65
+ [keys.ruleParamMinDate]: "Earliest date (YYYY-MM-DD)",
66
+ [keys.ruleParamMaxDate]: "Latest date (YYYY-MM-DD)",
59
67
  [keys.ruleParamPattern]: "Pattern",
60
68
  [keys.ruleParamFlags]: "Flags",
61
69
  [keys.ruleParamValues]: "Allowed values",
@@ -73,14 +81,20 @@ const en = {
73
81
  [keys.conditionNoFields]: "Add named fields to this form to build a condition.",
74
82
  [keys.conditionEmpty]: "No conditions. This field is always shown.",
75
83
  [keys.conditionSelectField]: "Select a field",
76
- [keys.conditionValuePlaceholder]: "Value",
77
84
  [keys.conditionTrue]: "True",
78
85
  [keys.conditionFalse]: "False",
79
- [keys.configAdvanced]: "Advanced",
80
86
  [keys.configHidden]: "Hidden (capture without showing)",
87
+ [keys.tabFields]: "Fields",
88
+ [keys.tabFlow]: "Flow",
89
+ [keys.tabActions]: "Actions",
90
+ [keys.tabField]: "Field",
91
+ [keys.tabValidation]: "Validation",
92
+ [keys.tabAdvanced]: "Advanced",
81
93
  [keys.fieldTypeCalculation]: "Calculation",
82
94
  [keys.configExpression]: "Expression (JSON)",
95
+ [keys.configExpressionDescription]: "Expression tree of nodes {\"type\":\"lit\"|\"ref\"|\"op\"|\"neg\"|\"fn\"|\"weight\"}. Example: {\"type\":\"op\",\"op\":\"+\",\"left\":{\"type\":\"ref\",\"field\":\"qty\"},\"right\":{\"type\":\"lit\",\"value\":10}}",
83
96
  [keys.configCalcDisplay]: "Show computed value",
97
+ [keys.validationCalcExpressionInvalid]: "Enter a valid calculation expression",
84
98
  [keys.configDefaultPresentation]: "Presentation",
85
99
  [keys.presentationPage]: "Page",
86
100
  [keys.presentationModal]: "Modal",
@@ -92,9 +106,14 @@ const en = {
92
106
  [keys.actionConfigTo]: "To",
93
107
  [keys.actionConfigSubject]: "Subject",
94
108
  [keys.actionConfigBody]: "Body",
109
+ [keys.actionConfigBodyDescription]: "Supports {{ fieldName|fallback }} tokens, {{*}} for all answers as lines, and {{*:table}} for all answers as a table.",
95
110
  [keys.actionConfigToField]: "Email field name",
111
+ [keys.actionConfigToFieldDescription]: "The email field on this form the confirmation is sent to.",
96
112
  [keys.actionConfigUrl]: "URL",
113
+ [keys.actionConfigUrlDescription]: "The endpoint that receives a signed JSON POST for each submission.",
97
114
  [keys.actionConfigSecret]: "Secret",
115
+ [keys.actionConfigSecretDescription]: "HMAC key used for the X-Form-Signature header, shared with the receiver.",
116
+ [keys.validationUrlInvalid]: "Enter a valid http or https URL",
98
117
  [keys.configActions]: "Actions",
99
118
  [keys.consentSourceStatic]: "Static text",
100
119
  [keys.consentSourcePageReference]: "Page reference",
@@ -110,7 +129,6 @@ const en = {
110
129
  [keys.consentConfigSource]: "Source",
111
130
  [keys.consentConfigSourceConfig]: "Source settings",
112
131
  [keys.consentConfigOptional]: "Optional (e.g. marketing)",
113
- [keys.resultsTitle]: "Results",
114
132
  [keys.resultsResponses]: "responses",
115
133
  [keys.resultsNoResponses]: "No responses yet",
116
134
  [keys.resultsTruncated]: "Showing a sample of responses",
@@ -123,6 +141,10 @@ const en = {
123
141
  [keys.fileConfigRelationTo]: "Upload collection",
124
142
  [keys.fileConfigMimeTypes]: "Allowed file types",
125
143
  [keys.fileConfigMaxSize]: "Maximum size (bytes)",
144
+ [keys.fileConfigMaxSizeDescription]: "Files larger than this are rejected.",
145
+ [keys.fileTooLarge]: "File is too large (max {max})",
146
+ [keys.fileHintAccepted]: "Accepted: {types}",
147
+ [keys.fileHintMaxSize]: "Max size: {max}",
126
148
  [keys.spamRateLimited]: "You have sent too many requests. Please try again later.",
127
149
  [keys.spamRejected]: "Your submission could not be processed.",
128
150
  [keys.spamCaptchaFailed]: "Captcha verification failed. Please try again.",
@@ -1 +1 @@
1
- {"version":3,"file":"en.js","names":[],"sources":["../../src/translations/en.ts"],"sourcesContent":["import { keys, type TranslationKey } from './keys'\n\n/**\n * English values, keyed by the typed constants in `keys.ts` so the two stay in\n * lockstep. The `Record<TranslationKey, string>` annotation makes a missing or\n * unknown key a type error. `translations/index.ts` nests these for Payload.\n */\nexport const en: Record<TranslationKey, string> = {\n\t[keys.pluginName]: 'Form Builder',\n\t[keys.fieldTitle]: 'Title',\n\t[keys.fieldTypeText]: 'Text',\n\t[keys.fieldTypeTextarea]: 'Long text',\n\t[keys.fieldTypeEmail]: 'Email',\n\t[keys.fieldTypeNumber]: 'Number',\n\t[keys.fieldTypeSelect]: 'Select',\n\t[keys.fieldTypeCheckbox]: 'Checkbox',\n\t[keys.configOptions]: 'Options',\n\t[keys.configOption]: 'Option',\n\t[keys.configOptionLabel]: 'Option label',\n\t[keys.configOptionValue]: 'Option value',\n\t[keys.validationRequired]: 'This field is required',\n\t[keys.validationEmail]: 'Enter a valid email address',\n\t[keys.validationNumber]: 'Enter a valid number',\n\t[keys.validationSelect]: 'Choose a valid option',\n\t[keys.formatYes]: 'Yes',\n\t[keys.formatNo]: 'No',\n\t[keys.configName]: 'Name',\n\t[keys.configLabel]: 'Label',\n\t[keys.configRequired]: 'Required',\n\t[keys.configWidth]: 'Width',\n\t[keys.configPlaceholder]: 'Placeholder',\n\t[keys.configDescription]: 'Description',\n\t[keys.configVisibleWhen]: 'Show this field when',\n\t[keys.configValidateWhen]: 'Validate this field only when',\n\t[keys.submissionAnswers]: 'Answers',\n\t[keys.submissionNoAnswers]: 'No answers',\n\t[keys.ruleMinLength]: 'Minimum length',\n\t[keys.ruleMaxLength]: 'Maximum length',\n\t[keys.ruleMin]: 'Minimum',\n\t[keys.ruleMax]: 'Maximum',\n\t[keys.rulePattern]: 'Pattern',\n\t[keys.ruleEmail]: 'Email',\n\t[keys.ruleUrl]: 'URL',\n\t[keys.ruleOneOf]: 'One of',\n\t[keys.ruleMatchesField]: 'Matches field',\n\t[keys.ruleNotAlreadySubmitted]: 'Not already submitted',\n\t[keys.ruleMinLengthMessage]: 'Must be at least {min} characters',\n\t[keys.ruleMaxLengthMessage]: 'Must be at most {max} characters',\n\t[keys.ruleMinMessage]: 'Must be at least {min}',\n\t[keys.ruleMaxMessage]: 'Must be at most {max}',\n\t[keys.rulePatternMessage]: 'Invalid format',\n\t[keys.ruleEmailMessage]: 'Enter a valid email address',\n\t[keys.ruleUrlMessage]: 'Enter a valid URL',\n\t[keys.ruleOneOfMessage]: 'Choose an allowed value',\n\t[keys.ruleMatchesFieldMessage]: 'Does not match',\n\t[keys.ruleNotAlreadySubmittedMessage]: 'This value was already submitted',\n\t[keys.ruleParamMin]: 'Minimum',\n\t[keys.ruleParamMax]: 'Maximum',\n\t[keys.ruleParamPattern]: 'Pattern',\n\t[keys.ruleParamFlags]: 'Flags',\n\t[keys.ruleParamValues]: 'Allowed values',\n\t[keys.ruleParamField]: 'Field name',\n\t[keys.validationsLabel]: 'Validation rules',\n\t[keys.validationMessageLabel]: 'Custom message',\n\t[keys.validationSeverityLabel]: 'Severity',\n\t[keys.validationSeverityError]: 'Error',\n\t[keys.validationSeverityWarning]: 'Warning',\n\t[keys.conditionAddCondition]: 'Add condition',\n\t[keys.conditionAddOr]: 'Add \"or\" group',\n\t[keys.conditionAnd]: 'And',\n\t[keys.conditionOr]: 'Or',\n\t[keys.conditionRemove]: 'Remove',\n\t[keys.conditionNoFields]: 'Add named fields to this form to build a condition.',\n\t[keys.conditionEmpty]: 'No conditions. This field is always shown.',\n\t[keys.conditionSelectField]: 'Select a field',\n\t[keys.conditionValuePlaceholder]: 'Value',\n\t[keys.conditionTrue]: 'True',\n\t[keys.conditionFalse]: 'False',\n\t[keys.configAdvanced]: 'Advanced',\n\t[keys.configHidden]: 'Hidden (capture without showing)',\n\t[keys.fieldTypeCalculation]: 'Calculation',\n\t[keys.configExpression]: 'Expression (JSON)',\n\t[keys.configCalcDisplay]: 'Show computed value',\n\t[keys.configDefaultPresentation]: 'Presentation',\n\t[keys.presentationPage]: 'Page',\n\t[keys.presentationModal]: 'Modal',\n\t[keys.presentationDrawer]: 'Drawer',\n\t[keys.presentationInline]: 'Inline',\n\t[keys.actionEmailTeam]: 'Email team',\n\t[keys.actionConfirmation]: 'Confirmation email',\n\t[keys.actionSignedWebhook]: 'Signed webhook',\n\t[keys.actionConfigTo]: 'To',\n\t[keys.actionConfigSubject]: 'Subject',\n\t[keys.actionConfigBody]: 'Body',\n\t[keys.actionConfigToField]: 'Email field name',\n\t[keys.actionConfigUrl]: 'URL',\n\t[keys.actionConfigSecret]: 'Secret',\n\t[keys.configActions]: 'Actions',\n\t[keys.consentSourceStatic]: 'Static text',\n\t[keys.consentSourcePageReference]: 'Page reference',\n\t[keys.consentConfigLabel]: 'Label',\n\t[keys.consentConfigUrl]: 'URL',\n\t[keys.consentConfigVersion]: 'Version',\n\t[keys.consentConfigRelationTo]: 'Collection',\n\t[keys.consentConfigDocId]: 'Document ID',\n\t[keys.consentConfigUrlField]: 'URL field',\n\t[keys.consentConfigCaptureVersion]: 'Capture published version',\n\t[keys.fieldTypeConsent]: 'Consent',\n\t[keys.consentConfigStatement]: 'Statement',\n\t[keys.consentConfigSource]: 'Source',\n\t[keys.consentConfigSourceConfig]: 'Source settings',\n\t[keys.consentConfigOptional]: 'Optional (e.g. marketing)',\n\t[keys.resultsTitle]: 'Results',\n\t[keys.resultsResponses]: 'responses',\n\t[keys.resultsNoResponses]: 'No responses yet',\n\t[keys.resultsTruncated]: 'Showing a sample of responses',\n\t[keys.configShowResults]: 'Show results publicly',\n\t[keys.configResultsField]: 'Results field (machine name)',\n\t[keys.validationFileMissing]: 'Upload a file',\n\t[keys.validationFileMimeType]: 'File type not allowed',\n\t[keys.validationFileTooLarge]: 'File is too large',\n\t[keys.fieldTypeFile]: 'File upload',\n\t[keys.fileConfigRelationTo]: 'Upload collection',\n\t[keys.fileConfigMimeTypes]: 'Allowed file types',\n\t[keys.fileConfigMaxSize]: 'Maximum size (bytes)',\n\t[keys.spamRateLimited]: 'You have sent too many requests. Please try again later.',\n\t[keys.spamRejected]: 'Your submission could not be processed.',\n\t[keys.spamCaptchaFailed]: 'Captcha verification failed. Please try again.',\n\t[keys.collectionFormSingular]: 'Form',\n\t[keys.collectionFormPlural]: 'Forms',\n\t[keys.collectionSubmissionSingular]: 'Submission',\n\t[keys.collectionSubmissionPlural]: 'Submissions',\n\t[keys.statusComplete]: 'Complete',\n\t[keys.statusPartial]: 'Partial',\n\t[keys.fieldTypeRepeater]: 'Repeater',\n\t[keys.configMinRows]: 'Minimum rows',\n\t[keys.configMaxRows]: 'Maximum rows',\n\t[keys.configAddLabel]: 'Add button label',\n\t[keys.configSubFields]: 'Sub-fields',\n\t[keys.validationRepeaterMin]: 'Add at least {min} row(s)',\n\t[keys.validationRepeaterMax]: 'Remove rows to stay within {max}',\n\t[keys.repeaterAddRow]: 'Add row',\n\t[keys.repeaterRemoveRow]: 'Remove',\n\t[keys.repeaterRow]: 'Row {n}',\n\t[keys.repeaterRowCount]: '{count} row(s)',\n\t[keys.submissionConsent]: 'Consent',\n\t[keys.submissionDetails]: 'Submission details',\n\t[keys.submissionConsentAgreed]: 'Agreed',\n\t[keys.submissionConsentDeclined]: 'Declined',\n\t[keys.submissionMetaReceivedAt]: 'Received at',\n\t[keys.submissionMetaIp]: 'IP address',\n\t[keys.submissionMetaUserAgent]: 'User agent',\n\t[keys.submissionMetaCaptcha]: 'Captcha',\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"en.js","names":[],"sources":["../../src/translations/en.ts"],"sourcesContent":["import { keys, type TranslationKey } from './keys'\n\n/**\n * English values, keyed by the typed constants in `keys.ts` so the two stay in\n * lockstep. The `Record<TranslationKey, string>` annotation makes a missing or\n * unknown key a type error. `translations/index.ts` nests these for Payload.\n */\nexport const en: Record<TranslationKey, string> = {\n\t[keys.fieldTitle]: 'Title',\n\t[keys.fieldTypeText]: 'Text',\n\t[keys.fieldTypeTextarea]: 'Long text',\n\t[keys.fieldTypeEmail]: 'Email',\n\t[keys.fieldTypeNumber]: 'Number',\n\t[keys.fieldTypeSelect]: 'Select',\n\t[keys.fieldTypeCheckbox]: 'Checkbox',\n\t[keys.fieldTypeDate]: 'Date',\n\t[keys.configOptions]: 'Options',\n\t[keys.configOption]: 'Option',\n\t[keys.configOptionLabel]: 'Option label',\n\t[keys.configOptionValue]: 'Option value',\n\t[keys.validationRequired]: 'This field is required',\n\t[keys.validationEmail]: 'Enter a valid email address',\n\t[keys.validationNumber]: 'Enter a valid number',\n\t[keys.validationDate]: 'Enter a valid date',\n\t[keys.validationSelect]: 'Choose a valid option',\n\t[keys.validationEmailFieldUnknown]: 'Choose an existing email field on this form',\n\t[keys.formatYes]: 'Yes',\n\t[keys.formatNo]: 'No',\n\t[keys.configName]: 'Name',\n\t[keys.configLabel]: 'Label',\n\t[keys.configRequired]: 'Required',\n\t[keys.configWidth]: 'Width',\n\t[keys.configPlaceholder]: 'Placeholder',\n\t[keys.configDescription]: 'Description',\n\t[keys.configVisibleWhen]: 'Show this field when',\n\t[keys.configValidateWhen]: 'Validate this field only when',\n\t[keys.submissionAnswers]: 'Answers',\n\t[keys.submissionNoAnswers]: 'No answers',\n\t[keys.ruleMinLength]: 'Minimum length',\n\t[keys.ruleMaxLength]: 'Maximum length',\n\t[keys.ruleMin]: 'Minimum',\n\t[keys.ruleMax]: 'Maximum',\n\t[keys.ruleMinDate]: 'Earliest date',\n\t[keys.ruleMaxDate]: 'Latest date',\n\t[keys.rulePattern]: 'Pattern',\n\t[keys.ruleEmail]: 'Email',\n\t[keys.ruleUrl]: 'URL',\n\t[keys.ruleOneOf]: 'One of',\n\t[keys.ruleMatchesField]: 'Matches field',\n\t[keys.ruleNotAlreadySubmitted]: 'Not already submitted',\n\t[keys.ruleMinLengthMessage]: 'Must be at least {min} characters',\n\t[keys.ruleMaxLengthMessage]: 'Must be at most {max} characters',\n\t[keys.ruleMinMessage]: 'Must be at least {min}',\n\t[keys.ruleMaxMessage]: 'Must be at most {max}',\n\t[keys.ruleMinDateMessage]: 'Must be on or after {min}',\n\t[keys.ruleMaxDateMessage]: 'Must be on or before {max}',\n\t[keys.rulePatternMessage]: 'Invalid format',\n\t[keys.ruleEmailMessage]: 'Enter a valid email address',\n\t[keys.ruleUrlMessage]: 'Enter a valid URL',\n\t[keys.ruleOneOfMessage]: 'Choose an allowed value',\n\t[keys.ruleMatchesFieldMessage]: 'Does not match',\n\t[keys.ruleNotAlreadySubmittedMessage]: 'This value was already submitted',\n\t[keys.ruleParamMin]: 'Minimum',\n\t[keys.ruleParamMax]: 'Maximum',\n\t[keys.ruleParamMinDate]: 'Earliest date (YYYY-MM-DD)',\n\t[keys.ruleParamMaxDate]: 'Latest date (YYYY-MM-DD)',\n\t[keys.ruleParamPattern]: 'Pattern',\n\t[keys.ruleParamFlags]: 'Flags',\n\t[keys.ruleParamValues]: 'Allowed values',\n\t[keys.ruleParamField]: 'Field name',\n\t[keys.validationsLabel]: 'Validation rules',\n\t[keys.validationMessageLabel]: 'Custom message',\n\t[keys.validationSeverityLabel]: 'Severity',\n\t[keys.validationSeverityError]: 'Error',\n\t[keys.validationSeverityWarning]: 'Warning',\n\t[keys.conditionAddCondition]: 'Add condition',\n\t[keys.conditionAddOr]: 'Add \"or\" group',\n\t[keys.conditionAnd]: 'And',\n\t[keys.conditionOr]: 'Or',\n\t[keys.conditionRemove]: 'Remove',\n\t[keys.conditionNoFields]: 'Add named fields to this form to build a condition.',\n\t[keys.conditionEmpty]: 'No conditions. This field is always shown.',\n\t[keys.conditionSelectField]: 'Select a field',\n\t[keys.conditionTrue]: 'True',\n\t[keys.conditionFalse]: 'False',\n\t[keys.configHidden]: 'Hidden (capture without showing)',\n\t[keys.tabFields]: 'Fields',\n\t[keys.tabFlow]: 'Flow',\n\t[keys.tabActions]: 'Actions',\n\t[keys.tabField]: 'Field',\n\t[keys.tabValidation]: 'Validation',\n\t[keys.tabAdvanced]: 'Advanced',\n\t[keys.fieldTypeCalculation]: 'Calculation',\n\t[keys.configExpression]: 'Expression (JSON)',\n\t[keys.configExpressionDescription]:\n\t\t'Expression tree of nodes {\"type\":\"lit\"|\"ref\"|\"op\"|\"neg\"|\"fn\"|\"weight\"}. Example: {\"type\":\"op\",\"op\":\"+\",\"left\":{\"type\":\"ref\",\"field\":\"qty\"},\"right\":{\"type\":\"lit\",\"value\":10}}',\n\t[keys.configCalcDisplay]: 'Show computed value',\n\t[keys.validationCalcExpressionInvalid]: 'Enter a valid calculation expression',\n\t[keys.configDefaultPresentation]: 'Presentation',\n\t[keys.presentationPage]: 'Page',\n\t[keys.presentationModal]: 'Modal',\n\t[keys.presentationDrawer]: 'Drawer',\n\t[keys.presentationInline]: 'Inline',\n\t[keys.actionEmailTeam]: 'Email team',\n\t[keys.actionConfirmation]: 'Confirmation email',\n\t[keys.actionSignedWebhook]: 'Signed webhook',\n\t[keys.actionConfigTo]: 'To',\n\t[keys.actionConfigSubject]: 'Subject',\n\t[keys.actionConfigBody]: 'Body',\n\t[keys.actionConfigBodyDescription]:\n\t\t'Supports {{ fieldName|fallback }} tokens, {{*}} for all answers as lines, and {{*:table}} for all answers as a table.',\n\t[keys.actionConfigToField]: 'Email field name',\n\t[keys.actionConfigToFieldDescription]:\n\t\t'The email field on this form the confirmation is sent to.',\n\t[keys.actionConfigUrl]: 'URL',\n\t[keys.actionConfigUrlDescription]:\n\t\t'The endpoint that receives a signed JSON POST for each submission.',\n\t[keys.actionConfigSecret]: 'Secret',\n\t[keys.actionConfigSecretDescription]:\n\t\t'HMAC key used for the X-Form-Signature header, shared with the receiver.',\n\t[keys.validationUrlInvalid]: 'Enter a valid http or https URL',\n\t[keys.configActions]: 'Actions',\n\t[keys.consentSourceStatic]: 'Static text',\n\t[keys.consentSourcePageReference]: 'Page reference',\n\t[keys.consentConfigLabel]: 'Label',\n\t[keys.consentConfigUrl]: 'URL',\n\t[keys.consentConfigVersion]: 'Version',\n\t[keys.consentConfigRelationTo]: 'Collection',\n\t[keys.consentConfigDocId]: 'Document ID',\n\t[keys.consentConfigUrlField]: 'URL field',\n\t[keys.consentConfigCaptureVersion]: 'Capture published version',\n\t[keys.fieldTypeConsent]: 'Consent',\n\t[keys.consentConfigStatement]: 'Statement',\n\t[keys.consentConfigSource]: 'Source',\n\t[keys.consentConfigSourceConfig]: 'Source settings',\n\t[keys.consentConfigOptional]: 'Optional (e.g. marketing)',\n\t[keys.resultsResponses]: 'responses',\n\t[keys.resultsNoResponses]: 'No responses yet',\n\t[keys.resultsTruncated]: 'Showing a sample of responses',\n\t[keys.configShowResults]: 'Show results publicly',\n\t[keys.configResultsField]: 'Results field (machine name)',\n\t[keys.validationFileMissing]: 'Upload a file',\n\t[keys.validationFileMimeType]: 'File type not allowed',\n\t[keys.validationFileTooLarge]: 'File is too large',\n\t[keys.fieldTypeFile]: 'File upload',\n\t[keys.fileConfigRelationTo]: 'Upload collection',\n\t[keys.fileConfigMimeTypes]: 'Allowed file types',\n\t[keys.fileConfigMaxSize]: 'Maximum size (bytes)',\n\t[keys.fileConfigMaxSizeDescription]: 'Files larger than this are rejected.',\n\t[keys.fileTooLarge]: 'File is too large (max {max})',\n\t[keys.fileHintAccepted]: 'Accepted: {types}',\n\t[keys.fileHintMaxSize]: 'Max size: {max}',\n\t[keys.spamRateLimited]: 'You have sent too many requests. Please try again later.',\n\t[keys.spamRejected]: 'Your submission could not be processed.',\n\t[keys.spamCaptchaFailed]: 'Captcha verification failed. Please try again.',\n\t[keys.collectionFormSingular]: 'Form',\n\t[keys.collectionFormPlural]: 'Forms',\n\t[keys.collectionSubmissionSingular]: 'Submission',\n\t[keys.collectionSubmissionPlural]: 'Submissions',\n\t[keys.statusComplete]: 'Complete',\n\t[keys.statusPartial]: 'Partial',\n\t[keys.fieldTypeRepeater]: 'Repeater',\n\t[keys.configMinRows]: 'Minimum rows',\n\t[keys.configMaxRows]: 'Maximum rows',\n\t[keys.configAddLabel]: 'Add button label',\n\t[keys.configSubFields]: 'Sub-fields',\n\t[keys.validationRepeaterMin]: 'Add at least {min} row(s)',\n\t[keys.validationRepeaterMax]: 'Remove rows to stay within {max}',\n\t[keys.repeaterAddRow]: 'Add row',\n\t[keys.repeaterRemoveRow]: 'Remove',\n\t[keys.repeaterRow]: 'Row {n}',\n\t[keys.repeaterRowCount]: '{count} row(s)',\n\t[keys.submissionConsent]: 'Consent',\n\t[keys.submissionDetails]: 'Submission details',\n\t[keys.submissionConsentAgreed]: 'Agreed',\n\t[keys.submissionConsentDeclined]: 'Declined',\n\t[keys.submissionMetaReceivedAt]: 'Received at',\n\t[keys.submissionMetaIp]: 'IP address',\n\t[keys.submissionMetaUserAgent]: 'User agent',\n\t[keys.submissionMetaCaptcha]: 'Captcha',\n}\n"],"mappings":"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"}