@10x-media/form-builder 0.1.0-beta.0 → 0.1.0-beta.1

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 (399) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/README.md +43 -1402
  3. package/dist/actions/buildActionBlocks.js +19 -0
  4. package/dist/actions/buildActionBlocks.js.map +1 -0
  5. package/dist/actions/builtin/confirmation.js +49 -0
  6. package/dist/actions/builtin/confirmation.js.map +1 -0
  7. package/dist/actions/builtin/emailTeam.js +46 -0
  8. package/dist/actions/builtin/emailTeam.js.map +1 -0
  9. package/dist/actions/builtin/index.d.ts +7 -0
  10. package/dist/actions/builtin/index.js +13 -0
  11. package/dist/actions/builtin/index.js.map +1 -0
  12. package/dist/actions/builtin/signedWebhook.js +49 -0
  13. package/dist/actions/builtin/signedWebhook.js.map +1 -0
  14. package/dist/actions/defineAction.d.ts +36 -0
  15. package/dist/actions/defineAction.js +6 -0
  16. package/dist/actions/defineAction.js.map +1 -0
  17. package/dist/actions/dispatch.js +48 -0
  18. package/dist/actions/dispatch.js.map +1 -0
  19. package/dist/actions/registry.d.ts +16 -0
  20. package/dist/actions/registry.js +16 -0
  21. package/dist/actions/registry.js.map +1 -0
  22. package/dist/actions/runActions.d.ts +9 -0
  23. package/dist/actions/runActions.js +34 -0
  24. package/dist/actions/runActions.js.map +1 -0
  25. package/dist/actions/sign.d.ts +7 -0
  26. package/dist/actions/sign.js +9 -0
  27. package/dist/actions/sign.js.map +1 -0
  28. package/dist/actions/task.js +82 -0
  29. package/dist/actions/task.js.map +1 -0
  30. package/dist/aggregation/aggregateResponses.d.ts +31 -0
  31. package/dist/aggregation/aggregateResponses.js +86 -0
  32. package/dist/aggregation/aggregateResponses.js.map +1 -0
  33. package/dist/aggregation/aggregateRows.d.ts +10 -0
  34. package/dist/aggregation/aggregateRows.js +64 -0
  35. package/dist/aggregation/aggregateRows.js.map +1 -0
  36. package/dist/aggregation/resolveResultsRequest.d.ts +32 -0
  37. package/dist/aggregation/resolveResultsRequest.js +56 -0
  38. package/dist/aggregation/resolveResultsRequest.js.map +1 -0
  39. package/dist/aggregation/types.d.ts +39 -0
  40. package/dist/calc/computeCalcFields.d.ts +15 -0
  41. package/dist/calc/computeCalcFields.js +27 -0
  42. package/dist/calc/computeCalcFields.js.map +1 -0
  43. package/dist/calc/evaluate.d.ts +8 -0
  44. package/dist/calc/evaluate.js +59 -0
  45. package/dist/calc/evaluate.js.map +1 -0
  46. package/dist/calc/normalizeCalc.d.ts +8 -0
  47. package/dist/calc/normalizeCalc.js +100 -0
  48. package/dist/calc/normalizeCalc.js.map +1 -0
  49. package/dist/calc/types.d.ts +28 -0
  50. package/dist/client/ConditionBuilder.js +121 -0
  51. package/dist/client/ConditionBuilder.js.map +1 -0
  52. package/dist/client/ConditionRow.js +150 -0
  53. package/dist/client/ConditionRow.js.map +1 -0
  54. package/dist/client/FlowBuilder.d.ts +15 -0
  55. package/dist/client/FlowBuilder.js +408 -0
  56. package/dist/client/FlowBuilder.js.map +1 -0
  57. package/dist/client/FormConditionField.d.ts +21 -0
  58. package/dist/client/FormConditionField.js +33 -0
  59. package/dist/client/FormConditionField.js.map +1 -0
  60. package/dist/client/synthesizeClientField.js +46 -0
  61. package/dist/client/synthesizeClientField.js.map +1 -0
  62. package/dist/collections/formSubmissions.js +138 -0
  63. package/dist/collections/formSubmissions.js.map +1 -0
  64. package/dist/collections/forms.js +187 -0
  65. package/dist/collections/forms.js.map +1 -0
  66. package/dist/collections/uploads.d.ts +29 -0
  67. package/dist/collections/uploads.js +46 -0
  68. package/dist/collections/uploads.js.map +1 -0
  69. package/dist/conditions/conditionType.js +14 -0
  70. package/dist/conditions/conditionType.js.map +1 -0
  71. package/dist/conditions/evaluate.d.ts +17 -0
  72. package/dist/conditions/evaluate.js +94 -0
  73. package/dist/conditions/evaluate.js.map +1 -0
  74. package/dist/conditions/fieldTypes.d.ts +9 -0
  75. package/dist/{fieldTypes-CzhhJXjg.js → conditions/fieldTypes.js} +2 -2
  76. package/dist/conditions/fieldTypes.js.map +1 -0
  77. package/dist/conditions/normalizeConditions.js +47 -0
  78. package/dist/conditions/normalizeConditions.js.map +1 -0
  79. package/dist/conditions/types.d.ts +8 -0
  80. package/dist/consent/buildConsentSourceConfig.js +49 -0
  81. package/dist/consent/buildConsentSourceConfig.js.map +1 -0
  82. package/dist/consent/builtin/index.d.ts +7 -0
  83. package/dist/consent/builtin/index.js +11 -0
  84. package/dist/consent/builtin/index.js.map +1 -0
  85. package/dist/consent/builtin/pageReference.js +75 -0
  86. package/dist/consent/builtin/pageReference.js.map +1 -0
  87. package/dist/consent/builtin/static.js +44 -0
  88. package/dist/consent/builtin/static.js.map +1 -0
  89. package/dist/consent/captureConsent.d.ts +28 -0
  90. package/dist/consent/captureConsent.js +26 -0
  91. package/dist/consent/captureConsent.js.map +1 -0
  92. package/dist/consent/defineConsentSource.d.ts +35 -0
  93. package/dist/consent/defineConsentSource.js +6 -0
  94. package/dist/consent/defineConsentSource.js.map +1 -0
  95. package/dist/consent/registry.d.ts +16 -0
  96. package/dist/consent/registry.js +16 -0
  97. package/dist/consent/registry.js.map +1 -0
  98. package/dist/consent/resolveConsentLinks.d.ts +19 -0
  99. package/dist/consent/resolveConsentLinks.js +26 -0
  100. package/dist/consent/resolveConsentLinks.js.map +1 -0
  101. package/dist/consent/resolvePublishedVersionRef.d.ts +17 -0
  102. package/dist/consent/resolvePublishedVersionRef.js +27 -0
  103. package/dist/consent/resolvePublishedVersionRef.js.map +1 -0
  104. package/dist/events/noopSink.js +6 -0
  105. package/dist/events/noopSink.js.map +1 -0
  106. package/dist/events/resolveEventSink.js +8 -0
  107. package/dist/events/resolveEventSink.js.map +1 -0
  108. package/dist/events/types.d.ts +31 -0
  109. package/dist/exports/client.d.ts +3 -21
  110. package/dist/exports/client.js +3 -319
  111. package/dist/exports/i18n.d.ts +3 -138
  112. package/dist/exports/i18n.js +2 -2
  113. package/dist/exports/react.d.ts +45 -550
  114. package/dist/exports/react.js +39 -1563
  115. package/dist/exports/rsc.d.ts +2 -16
  116. package/dist/exports/rsc.js +1 -54
  117. package/dist/exports/types.d.ts +27 -5
  118. package/dist/fields/buildFieldBlocks.js +46 -0
  119. package/dist/fields/buildFieldBlocks.js.map +1 -0
  120. package/dist/fields/builtin/calculation.js +24 -0
  121. package/dist/fields/builtin/calculation.js.map +1 -0
  122. package/dist/fields/builtin/checkbox.js +13 -0
  123. package/dist/fields/builtin/checkbox.js.map +1 -0
  124. package/dist/fields/builtin/consent.js +32 -0
  125. package/dist/fields/builtin/consent.js.map +1 -0
  126. package/dist/fields/builtin/email.js +18 -0
  127. package/dist/fields/builtin/email.js.map +1 -0
  128. package/dist/fields/builtin/file.js +40 -0
  129. package/dist/fields/builtin/file.js.map +1 -0
  130. package/dist/fields/builtin/index.js +25 -0
  131. package/dist/fields/builtin/index.js.map +1 -0
  132. package/dist/fields/builtin/number.js +17 -0
  133. package/dist/fields/builtin/number.js.map +1 -0
  134. package/dist/fields/builtin/select.js +44 -0
  135. package/dist/fields/builtin/select.js.map +1 -0
  136. package/dist/fields/builtin/text.js +13 -0
  137. package/dist/fields/builtin/text.js.map +1 -0
  138. package/dist/fields/builtin/textarea.js +13 -0
  139. package/dist/fields/builtin/textarea.js.map +1 -0
  140. package/dist/fields/defineFormField.d.ts +13 -0
  141. package/dist/fields/defineFormField.js +12 -0
  142. package/dist/fields/defineFormField.js.map +1 -0
  143. package/dist/fields/registry.d.ts +10 -0
  144. package/dist/fields/registry.js +24 -0
  145. package/dist/fields/registry.js.map +1 -0
  146. package/dist/fields/sharedConfig.js +89 -0
  147. package/dist/fields/sharedConfig.js.map +1 -0
  148. package/dist/fields/types.d.ts +88 -0
  149. package/dist/flow/engine.d.ts +17 -0
  150. package/dist/flow/engine.js +24 -0
  151. package/dist/flow/engine.js.map +1 -0
  152. package/dist/flow/normalizeFlow.js +35 -0
  153. package/dist/flow/normalizeFlow.js.map +1 -0
  154. package/dist/{types-DsJ_6kGJ.d.ts → flow/types.d.ts} +2 -2
  155. package/dist/index.d.ts +74 -4
  156. package/dist/index.js +40 -1889
  157. package/dist/index.js.map +1 -1
  158. package/dist/plugin/access.js +12 -0
  159. package/dist/plugin/access.js.map +1 -0
  160. package/dist/plugin/registerCollections.js +40 -0
  161. package/dist/plugin/registerCollections.js.map +1 -0
  162. package/dist/plugin/registerTranslations.js +19 -0
  163. package/dist/plugin/registerTranslations.js.map +1 -0
  164. package/dist/prefill/valuesFromSearchParams.d.ts +18 -0
  165. package/dist/prefill/valuesFromSearchParams.js +45 -0
  166. package/dist/prefill/valuesFromSearchParams.js.map +1 -0
  167. package/dist/presentations/defaults.d.ts +33 -0
  168. package/dist/presentations/defaults.js +35 -0
  169. package/dist/presentations/defaults.js.map +1 -0
  170. package/dist/presentations/registry.d.ts +15 -0
  171. package/dist/presentations/registry.js +15 -0
  172. package/dist/presentations/registry.js.map +1 -0
  173. package/dist/presentations/types.d.ts +18 -0
  174. package/dist/react/Form.d.ts +81 -0
  175. package/dist/react/Form.js +457 -0
  176. package/dist/react/Form.js.map +1 -0
  177. package/dist/react/FormContext.d.ts +16 -0
  178. package/dist/react/FormContext.js +14 -0
  179. package/dist/react/FormContext.js.map +1 -0
  180. package/dist/react/FormLayout.d.ts +20 -0
  181. package/dist/react/FormLayout.js +14 -0
  182. package/dist/react/FormLayout.js.map +1 -0
  183. package/dist/react/FormResults.d.ts +25 -0
  184. package/dist/react/FormResults.js +85 -0
  185. package/dist/react/FormResults.js.map +1 -0
  186. package/dist/react/Honeypot.d.ts +22 -0
  187. package/dist/react/Honeypot.js +38 -0
  188. package/dist/react/Honeypot.js.map +1 -0
  189. package/dist/react/Poll.d.ts +32 -0
  190. package/dist/react/Poll.js +77 -0
  191. package/dist/react/Poll.js.map +1 -0
  192. package/dist/react/cn.d.ts +6 -0
  193. package/dist/react/cn.js +8 -0
  194. package/dist/react/cn.js.map +1 -0
  195. package/dist/react/contract.d.ts +32 -0
  196. package/dist/react/contract.js +7 -0
  197. package/dist/react/contract.js.map +1 -0
  198. package/dist/react/events.js +14 -0
  199. package/dist/react/events.js.map +1 -0
  200. package/dist/react/fetchResults.d.ts +25 -0
  201. package/dist/react/fetchResults.js +31 -0
  202. package/dist/react/fetchResults.js.map +1 -0
  203. package/dist/react/presentation/Backdrop.d.ts +18 -0
  204. package/dist/react/presentation/Backdrop.js +14 -0
  205. package/dist/react/presentation/Backdrop.js.map +1 -0
  206. package/dist/react/presentation/DialogSurface.d.ts +35 -0
  207. package/dist/react/presentation/DialogSurface.js +56 -0
  208. package/dist/react/presentation/DialogSurface.js.map +1 -0
  209. package/dist/react/presentation/presentations.d.ts +7 -0
  210. package/dist/react/presentation/presentations.js +32 -0
  211. package/dist/react/presentation/presentations.js.map +1 -0
  212. package/dist/react/presentation/registry.d.ts +11 -0
  213. package/dist/react/presentation/registry.js +12 -0
  214. package/dist/react/presentation/registry.js.map +1 -0
  215. package/dist/react/presentation/types.d.ts +19 -0
  216. package/dist/react/presentation/useDismiss.d.ts +19 -0
  217. package/dist/react/presentation/useDismiss.js +33 -0
  218. package/dist/react/presentation/useDismiss.js.map +1 -0
  219. package/dist/react/presentation/useFocusTrap.d.ts +8 -0
  220. package/dist/react/presentation/useFocusTrap.js +35 -0
  221. package/dist/react/presentation/useFocusTrap.js.map +1 -0
  222. package/dist/react/presentation/useScrollLock.d.ts +6 -0
  223. package/dist/react/presentation/useScrollLock.js +18 -0
  224. package/dist/react/presentation/useScrollLock.js.map +1 -0
  225. package/dist/react/primitives/Checkbox.d.ts +26 -0
  226. package/dist/react/primitives/Checkbox.js +20 -0
  227. package/dist/react/primitives/Checkbox.js.map +1 -0
  228. package/dist/react/primitives/FieldShell.d.ts +31 -0
  229. package/dist/react/primitives/FieldShell.js +51 -0
  230. package/dist/react/primitives/FieldShell.js.map +1 -0
  231. package/dist/react/primitives/Input.d.ts +30 -0
  232. package/dist/react/primitives/Input.js +21 -0
  233. package/dist/react/primitives/Input.js.map +1 -0
  234. package/dist/react/primitives/Select.d.ts +34 -0
  235. package/dist/react/primitives/Select.js +26 -0
  236. package/dist/react/primitives/Select.js.map +1 -0
  237. package/dist/react/primitives/Textarea.d.ts +28 -0
  238. package/dist/react/primitives/Textarea.js +20 -0
  239. package/dist/react/primitives/Textarea.js.map +1 -0
  240. package/dist/react/recall.js +20 -0
  241. package/dist/react/recall.js.map +1 -0
  242. package/dist/react/registry.d.ts +16 -0
  243. package/dist/react/registry.js +16 -0
  244. package/dist/react/registry.js.map +1 -0
  245. package/dist/react/renderers/calculation.js +30 -0
  246. package/dist/react/renderers/calculation.js.map +1 -0
  247. package/dist/react/renderers/checkbox.js +35 -0
  248. package/dist/react/renderers/checkbox.js.map +1 -0
  249. package/dist/react/renderers/consent.js +54 -0
  250. package/dist/react/renderers/consent.js.map +1 -0
  251. package/dist/react/renderers/email.js +37 -0
  252. package/dist/react/renderers/email.js.map +1 -0
  253. package/dist/react/renderers/file.js +92 -0
  254. package/dist/react/renderers/file.js.map +1 -0
  255. package/dist/react/renderers/index.d.ts +8 -0
  256. package/dist/react/renderers/index.js +26 -0
  257. package/dist/react/renderers/index.js.map +1 -0
  258. package/dist/react/renderers/number.js +40 -0
  259. package/dist/react/renderers/number.js.map +1 -0
  260. package/dist/react/renderers/select.js +38 -0
  261. package/dist/react/renderers/select.js.map +1 -0
  262. package/dist/react/renderers/text.js +37 -0
  263. package/dist/react/renderers/text.js.map +1 -0
  264. package/dist/react/renderers/textarea.js +36 -0
  265. package/dist/react/renderers/textarea.js.map +1 -0
  266. package/dist/react/resolveForm.js +16 -0
  267. package/dist/react/resolveForm.js.map +1 -0
  268. package/dist/react/state.d.ts +15 -0
  269. package/dist/react/state.js +70 -0
  270. package/dist/react/state.js.map +1 -0
  271. package/dist/react/submitForm.d.ts +31 -0
  272. package/dist/react/submitForm.js +58 -0
  273. package/dist/react/submitForm.js.map +1 -0
  274. package/dist/react/toFormDocument.d.ts +24 -0
  275. package/dist/react/toFormDocument.js +22 -0
  276. package/dist/react/toFormDocument.js.map +1 -0
  277. package/dist/react/uploadFile.d.ts +23 -0
  278. package/dist/react/uploadFile.js +39 -0
  279. package/dist/react/uploadFile.js.map +1 -0
  280. package/dist/react/useField.d.ts +14 -0
  281. package/dist/react/useField.js +49 -0
  282. package/dist/react/useField.js.map +1 -0
  283. package/dist/react/useFormState.d.ts +8 -0
  284. package/dist/react/useFormState.js +9 -0
  285. package/dist/react/useFormState.js.map +1 -0
  286. package/dist/react/useFormStep.d.ts +8 -0
  287. package/dist/react/useFormStep.js +9 -0
  288. package/dist/react/useFormStep.js.map +1 -0
  289. package/dist/react/validateField.js +30 -0
  290. package/dist/react/validateField.js.map +1 -0
  291. package/dist/recall/interpolate.d.ts +10 -0
  292. package/dist/recall/interpolate.js +16 -0
  293. package/dist/recall/interpolate.js.map +1 -0
  294. package/dist/recall/resolver.d.ts +22 -0
  295. package/dist/recall/resolver.js +36 -0
  296. package/dist/recall/resolver.js.map +1 -0
  297. package/dist/spam/captcha.d.ts +12 -0
  298. package/dist/spam/captcha.js +11 -0
  299. package/dist/spam/captcha.js.map +1 -0
  300. package/dist/spam/constants.d.ts +12 -0
  301. package/dist/spam/constants.js +15 -0
  302. package/dist/spam/constants.js.map +1 -0
  303. package/dist/spam/identify.d.ts +12 -0
  304. package/dist/spam/identify.js +20 -0
  305. package/dist/spam/identify.js.map +1 -0
  306. package/dist/spam/rateLimiter.d.ts +18 -0
  307. package/dist/spam/rateLimiter.js +41 -0
  308. package/dist/spam/rateLimiter.js.map +1 -0
  309. package/dist/spam/reserved.js +30 -0
  310. package/dist/spam/reserved.js.map +1 -0
  311. package/dist/spam/resolveSpam.d.ts +12 -0
  312. package/dist/spam/resolveSpam.js +42 -0
  313. package/dist/spam/resolveSpam.js.map +1 -0
  314. package/dist/spam/spamGuard.js +68 -0
  315. package/dist/spam/spamGuard.js.map +1 -0
  316. package/dist/spam/types.d.ts +73 -0
  317. package/dist/spam/uploadHooks.js +33 -0
  318. package/dist/spam/uploadHooks.js.map +1 -0
  319. package/dist/submissions/SubmissionAnswers.d.ts +16 -0
  320. package/dist/submissions/SubmissionAnswers.js +57 -0
  321. package/dist/submissions/SubmissionAnswers.js.map +1 -0
  322. package/dist/submissions/runSubmission.js +205 -0
  323. package/dist/submissions/runSubmission.js.map +1 -0
  324. package/dist/submissions/types.d.ts +33 -0
  325. package/dist/submissions/validateSubmission.js +59 -0
  326. package/dist/submissions/validateSubmission.js.map +1 -0
  327. package/dist/translations/en.d.ts +12 -0
  328. package/dist/{translations-edMqq_2e.js → translations/en.js} +3 -22
  329. package/dist/translations/en.js.map +1 -0
  330. package/dist/translations/index.d.ts +14 -0
  331. package/dist/translations/index.js +26 -0
  332. package/dist/translations/index.js.map +1 -0
  333. package/dist/translations/keys.d.ts +132 -0
  334. package/dist/{keys-N5xGiUsh.js → translations/keys.js} +2 -2
  335. package/dist/translations/keys.js.map +1 -0
  336. package/dist/translations/makeTranslate.d.ts +11 -0
  337. package/dist/translations/makeTranslate.js +11 -0
  338. package/dist/translations/makeTranslate.js.map +1 -0
  339. package/dist/translations/server.js +25 -0
  340. package/dist/translations/server.js.map +1 -0
  341. package/dist/translations/useTranslation.js +12 -0
  342. package/dist/translations/useTranslation.js.map +1 -0
  343. package/dist/uploads/captureFileRef.d.ts +27 -0
  344. package/dist/uploads/captureFileRef.js +34 -0
  345. package/dist/uploads/captureFileRef.js.map +1 -0
  346. package/dist/uploads/resolveFileRef.d.ts +26 -0
  347. package/dist/uploads/resolveFileRef.js +44 -0
  348. package/dist/uploads/resolveFileRef.js.map +1 -0
  349. package/dist/uploads/types.d.ts +20 -0
  350. package/dist/validation/buildRuleBlocks.js +46 -0
  351. package/dist/validation/buildRuleBlocks.js.map +1 -0
  352. package/dist/validation/builtin/email.js +19 -0
  353. package/dist/validation/builtin/email.js.map +1 -0
  354. package/dist/validation/builtin/index.js +27 -0
  355. package/dist/validation/builtin/index.js.map +1 -0
  356. package/dist/validation/builtin/matchesField.js +20 -0
  357. package/dist/validation/builtin/matchesField.js.map +1 -0
  358. package/dist/validation/builtin/max.js +21 -0
  359. package/dist/validation/builtin/max.js.map +1 -0
  360. package/dist/validation/builtin/maxLength.js +26 -0
  361. package/dist/validation/builtin/maxLength.js.map +1 -0
  362. package/dist/validation/builtin/min.js +21 -0
  363. package/dist/validation/builtin/min.js.map +1 -0
  364. package/dist/validation/builtin/minLength.js +26 -0
  365. package/dist/validation/builtin/minLength.js.map +1 -0
  366. package/dist/validation/builtin/notAlreadySubmitted.js +32 -0
  367. package/dist/validation/builtin/notAlreadySubmitted.js.map +1 -0
  368. package/dist/validation/builtin/oneOf.js +34 -0
  369. package/dist/validation/builtin/oneOf.js.map +1 -0
  370. package/dist/validation/builtin/pattern.js +41 -0
  371. package/dist/validation/builtin/pattern.js.map +1 -0
  372. package/dist/validation/builtin/url.js +22 -0
  373. package/dist/validation/builtin/url.js.map +1 -0
  374. package/dist/validation/defineValidationRule.d.ts +12 -0
  375. package/dist/validation/defineValidationRule.js +11 -0
  376. package/dist/validation/defineValidationRule.js.map +1 -0
  377. package/dist/validation/message.js +14 -0
  378. package/dist/validation/message.js.map +1 -0
  379. package/dist/validation/registry.d.ts +10 -0
  380. package/dist/validation/registry.js +20 -0
  381. package/dist/validation/registry.js.map +1 -0
  382. package/dist/validation/runValidation.js +111 -0
  383. package/dist/validation/runValidation.js.map +1 -0
  384. package/dist/validation/types.d.ts +76 -0
  385. package/package.json +8 -5
  386. package/styles/form-builder.css +201 -11
  387. package/dist/constants-B-BUfetP.d.ts +0 -288
  388. package/dist/exports/client.js.map +0 -1
  389. package/dist/exports/react.js.map +0 -1
  390. package/dist/exports/rsc.js.map +0 -1
  391. package/dist/fieldTypes-B8jkNRob.d.ts +0 -188
  392. package/dist/fieldTypes-CzhhJXjg.js.map +0 -1
  393. package/dist/index-DfFYGbVF.d.ts +0 -481
  394. package/dist/keys-N5xGiUsh.js.map +0 -1
  395. package/dist/registry-CoCyhtvB.js +0 -282
  396. package/dist/registry-CoCyhtvB.js.map +0 -1
  397. package/dist/resolver-OeQyVH2N.js +0 -665
  398. package/dist/resolver-OeQyVH2N.js.map +0 -1
  399. package/dist/translations-edMqq_2e.js.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolver.js","names":[],"sources":["../../src/recall/resolver.ts"],"sourcesContent":["import type { AnyFormFieldDefinition } from '../fields/types'\nimport type { FormFieldInstance } from '../submissions/types'\n\nexport type RecallResolver = (name: string) => string\n\ntype Option = { label?: string; value?: string }\n\n/** Derive a value->label map from a select-like field instance's `options`. */\nexport const optionLabelsFor = (field: FormFieldInstance): Record<string, string> | undefined => {\n\tconst options = field.options\n\tif (!Array.isArray(options)) {\n\t\treturn undefined\n\t}\n\tconst map: Record<string, string> = {}\n\tfor (const option of options as Option[]) {\n\t\tif (option && typeof option.value === 'string') {\n\t\t\tmap[option.value] = option.label ?? option.value\n\t\t}\n\t}\n\treturn Object.keys(map).length > 0 ? map : undefined\n}\n\n/**\n * Build a recall resolver: `{{name}}` -> the field's current value, formatted via its type's `format`\n * (so a select shows its option label, a checkbox Yes/No, a date localized). Missing field or empty\n * value -> ''. Pure and isomorphic; the renderer and server templates share it.\n */\nexport const buildRecallResolver = (args: {\n\tfields: FormFieldInstance[]\n\tvalues: Record<string, unknown>\n\tregistry: Map<string, AnyFormFieldDefinition>\n\tlocale: string\n\tt: (key: string) => string\n}): RecallResolver => {\n\tconst byName = new Map(args.fields.map((field) => [field.name, field]))\n\treturn (name) => {\n\t\tconst field = byName.get(name)\n\t\tif (!field) {\n\t\t\treturn ''\n\t\t}\n\t\tconst value = args.values[name]\n\t\tif (value == null || value === '' || (Array.isArray(value) && value.length === 0)) {\n\t\t\treturn ''\n\t\t}\n\t\tconst definition = args.registry.get(field.blockType)\n\t\tif (definition?.format) {\n\t\t\treturn definition.format({\n\t\t\t\tvalue,\n\t\t\t\tconfig: {},\n\t\t\t\toptionLabels: optionLabelsFor(field),\n\t\t\t\tlocale: args.locale,\n\t\t\t\tt: args.t,\n\t\t\t})\n\t\t}\n\t\treturn Array.isArray(value) ? value.join(', ') : String(value)\n\t}\n}\n"],"mappings":";;AAQA,MAAa,mBAAmB,UAAiE;CAChG,MAAM,UAAU,MAAM;CACtB,IAAI,CAAC,MAAM,QAAQ,OAAO,GACzB;CAED,MAAM,MAA8B,CAAC;CACrC,KAAK,MAAM,UAAU,SACpB,IAAI,UAAU,OAAO,OAAO,UAAU,UACrC,IAAI,OAAO,SAAS,OAAO,SAAS,OAAO;CAG7C,OAAO,OAAO,KAAK,GAAG,EAAE,SAAS,IAAI,MAAM,KAAA;AAC5C;;;;;;AAOA,MAAa,uBAAuB,SAMd;CACrB,MAAM,SAAS,IAAI,IAAI,KAAK,OAAO,KAAK,UAAU,CAAC,MAAM,MAAM,KAAK,CAAC,CAAC;CACtE,QAAQ,SAAS;EAChB,MAAM,QAAQ,OAAO,IAAI,IAAI;EAC7B,IAAI,CAAC,OACJ,OAAO;EAER,MAAM,QAAQ,KAAK,OAAO;EAC1B,IAAI,SAAS,QAAQ,UAAU,MAAO,MAAM,QAAQ,KAAK,KAAK,MAAM,WAAW,GAC9E,OAAO;EAER,MAAM,aAAa,KAAK,SAAS,IAAI,MAAM,SAAS;EACpD,IAAI,YAAY,QACf,OAAO,WAAW,OAAO;GACxB;GACA,QAAQ,CAAC;GACT,cAAc,gBAAgB,KAAK;GACnC,QAAQ,KAAK;GACb,GAAG,KAAK;EACT,CAAC;EAEF,OAAO,MAAM,QAAQ,KAAK,IAAI,MAAM,KAAK,IAAI,IAAI,OAAO,KAAK;CAC9D;AACD"}
@@ -0,0 +1,12 @@
1
+ import { CaptchaProvider } from "./types.js";
2
+
3
+ //#region src/spam/captcha.d.ts
4
+ /**
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.
8
+ */
9
+ declare const defineCaptchaProvider: (provider: CaptchaProvider) => CaptchaProvider;
10
+ //#endregion
11
+ export { defineCaptchaProvider };
12
+ //# sourceMappingURL=captcha.d.ts.map
@@ -0,0 +1,11 @@
1
+ //#region src/spam/captcha.ts
2
+ /**
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.
6
+ */
7
+ const defineCaptchaProvider = (provider) => provider;
8
+ //#endregion
9
+ export { defineCaptchaProvider };
10
+
11
+ //# sourceMappingURL=captcha.js.map
@@ -0,0 +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"}
@@ -0,0 +1,12 @@
1
+ //#region src/spam/constants.d.ts
2
+ /**
3
+ * Default honeypot decoy field name. "website" is plausible to bots but doesn't trigger Chrome's
4
+ * email-address autofill (unlike names containing "email"). Bots fill every input; real users
5
+ * never see the hidden field.
6
+ */
7
+ declare const DEFAULT_HONEYPOT_FIELD = "website";
8
+ /** Reserved `values` entry carrying a captcha token from the client. */
9
+ declare const CAPTCHA_TOKEN_KEY = "__fb_captcha";
10
+ //#endregion
11
+ export { CAPTCHA_TOKEN_KEY, DEFAULT_HONEYPOT_FIELD };
12
+ //# sourceMappingURL=constants.d.ts.map
@@ -0,0 +1,15 @@
1
+ //#region src/spam/constants.ts
2
+ /**
3
+ * Default honeypot decoy field name. "website" is plausible to bots but doesn't trigger Chrome's
4
+ * email-address autofill (unlike names containing "email"). Bots fill every input; real users
5
+ * never see the hidden field.
6
+ */
7
+ const DEFAULT_HONEYPOT_FIELD = "website";
8
+ /** Reserved `values` entry carrying a captcha token from the client. */
9
+ const CAPTCHA_TOKEN_KEY = "__fb_captcha";
10
+ /** `req.context` key under which the spam guard stashes the resolved identity for upload-ownership verification. */
11
+ const IDENTITY_CONTEXT_KEY = "formBuilderSpamIdentity";
12
+ //#endregion
13
+ export { CAPTCHA_TOKEN_KEY, DEFAULT_HONEYPOT_FIELD, IDENTITY_CONTEXT_KEY };
14
+
15
+ //# sourceMappingURL=constants.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.js","names":[],"sources":["../../src/spam/constants.ts"],"sourcesContent":["/**\n * Default honeypot decoy field name. \"website\" is plausible to bots but doesn't trigger Chrome's\n * email-address autofill (unlike names containing \"email\"). Bots fill every input; real users\n * never see the hidden field.\n */\nexport const DEFAULT_HONEYPOT_FIELD = 'website'\n\n/** Reserved `values` entry carrying a captcha token from the client. */\nexport const CAPTCHA_TOKEN_KEY = '__fb_captcha'\n\n/** `req.context` key under which the spam guard stashes the resolved identity for upload-ownership verification. */\nexport const IDENTITY_CONTEXT_KEY = 'formBuilderSpamIdentity'\n"],"mappings":";;;;;;AAKA,MAAa,yBAAyB;;AAGtC,MAAa,oBAAoB;;AAGjC,MAAa,uBAAuB"}
@@ -0,0 +1,12 @@
1
+ import { IdentifyFn } from "./types.js";
2
+
3
+ //#region src/spam/identify.d.ts
4
+ /**
5
+ * Default identity resolution for rate-limiting + upload ownership. Prefers an authenticated user id
6
+ * (trustworthy); else the first hop of the configured trusted IP header (best-effort, proxy-dependent);
7
+ * else null, in which case the caller fails open (cannot fairly rate-limit an unidentifiable client).
8
+ */
9
+ declare const defaultIdentify: (ipHeader: string) => IdentifyFn;
10
+ //#endregion
11
+ export { defaultIdentify };
12
+ //# sourceMappingURL=identify.d.ts.map
@@ -0,0 +1,20 @@
1
+ //#region src/spam/identify.ts
2
+ /**
3
+ * Default identity resolution for rate-limiting + upload ownership. Prefers an authenticated user id
4
+ * (trustworthy); else the first hop of the configured trusted IP header (best-effort, proxy-dependent);
5
+ * else null, in which case the caller fails open (cannot fairly rate-limit an unidentifiable client).
6
+ */
7
+ const defaultIdentify = (ipHeader) => (req) => {
8
+ const userId = req.user?.id;
9
+ 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;
16
+ };
17
+ //#endregion
18
+ export { defaultIdentify };
19
+
20
+ //# sourceMappingURL=identify.js.map
@@ -0,0 +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"}
@@ -0,0 +1,18 @@
1
+ import { RateLimiter } from "./types.js";
2
+
3
+ //#region src/spam/rateLimiter.d.ts
4
+ /**
5
+ * The default rate limiter: a read-modify-write window counter over Payload's `payload.kv`
6
+ * (always present; default `DatabaseKVAdapter`, durable + cross-instance). Because the KV interface
7
+ * has no atomic increment, the get-then-set is NON-ATOMIC, so a concurrent burst can slightly undercount.
8
+ * This is a SOFT limit, acceptable for spam basics and complemented by edge/WAF limiting. The window
9
+ * expires lazily on read; stale keys for one-time identities linger (minor; the count self-resets).
10
+ * `now` is injectable for tests.
11
+ */
12
+ declare const createKvRateLimiter: (options?: {
13
+ now?: () => number;
14
+ namespace?: string;
15
+ }) => RateLimiter;
16
+ //#endregion
17
+ export { createKvRateLimiter };
18
+ //# sourceMappingURL=rateLimiter.d.ts.map
@@ -0,0 +1,41 @@
1
+ //#region src/spam/rateLimiter.ts
2
+ /**
3
+ * The default rate limiter: a read-modify-write window counter over Payload's `payload.kv`
4
+ * (always present; default `DatabaseKVAdapter`, durable + cross-instance). Because the KV interface
5
+ * has no atomic increment, the get-then-set is NON-ATOMIC, so a concurrent burst can slightly undercount.
6
+ * This is a SOFT limit, acceptable for spam basics and complemented by edge/WAF limiting. The window
7
+ * expires lazily on read; stale keys for one-time identities linger (minor; the count self-resets).
8
+ * `now` is injectable for tests.
9
+ */
10
+ const createKvRateLimiter = (options = {}) => {
11
+ const now = options.now ?? (() => Date.now());
12
+ const prefix = options.namespace ?? "fb:rl:";
13
+ return { async check({ key, max, window, req }) {
14
+ const kv = req.payload.kv;
15
+ const storeKey = `${prefix}${key}`;
16
+ const current = now();
17
+ const existing = await kv.get(storeKey).catch(() => null);
18
+ let count;
19
+ let windowStart;
20
+ if (!existing || current - existing.windowStart >= window) {
21
+ count = 1;
22
+ windowStart = current;
23
+ } else {
24
+ count = existing.count + 1;
25
+ windowStart = existing.windowStart;
26
+ }
27
+ await kv.set(storeKey, {
28
+ count,
29
+ windowStart
30
+ }).catch(() => void 0);
31
+ return {
32
+ ok: count <= max,
33
+ remaining: Math.max(0, max - count),
34
+ resetAt: windowStart + window
35
+ };
36
+ } };
37
+ };
38
+ //#endregion
39
+ export { createKvRateLimiter };
40
+
41
+ //# sourceMappingURL=rateLimiter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rateLimiter.js","names":[],"sources":["../../src/spam/rateLimiter.ts"],"sourcesContent":["import type { RateLimiter } from './types'\n\ntype WindowState = { count: number; windowStart: number }\n\n/**\n * The default rate limiter: a read-modify-write window counter over Payload's `payload.kv`\n * (always present; default `DatabaseKVAdapter`, durable + cross-instance). Because the KV interface\n * has no atomic increment, the get-then-set is NON-ATOMIC, so a concurrent burst can slightly undercount.\n * This is a SOFT limit, acceptable for spam basics and complemented by edge/WAF limiting. The window\n * expires lazily on read; stale keys for one-time identities linger (minor; the count self-resets).\n * `now` is injectable for tests.\n */\nexport const createKvRateLimiter = (\n\toptions: { now?: () => number; namespace?: string } = {}\n): RateLimiter => {\n\tconst now = options.now ?? (() => Date.now())\n\tconst prefix = options.namespace ?? 'fb:rl:'\n\treturn {\n\t\tasync check({ key, max, window, req }) {\n\t\t\tconst kv = req.payload.kv\n\t\t\tconst storeKey = `${prefix}${key}`\n\t\t\tconst current = now()\n\t\t\tconst existing = await kv.get<WindowState>(storeKey).catch(() => null)\n\t\t\tlet count: number\n\t\t\tlet windowStart: number\n\t\t\tif (!existing || current - existing.windowStart >= window) {\n\t\t\t\tcount = 1\n\t\t\t\twindowStart = current\n\t\t\t} else {\n\t\t\t\tcount = existing.count + 1\n\t\t\t\twindowStart = existing.windowStart\n\t\t\t}\n\t\t\tawait kv.set(storeKey, { count, windowStart }).catch(() => undefined)\n\t\t\treturn {\n\t\t\t\tok: count <= max,\n\t\t\t\tremaining: Math.max(0, max - count),\n\t\t\t\tresetAt: windowStart + window,\n\t\t\t}\n\t\t},\n\t}\n}\n"],"mappings":";;;;;;;;;AAYA,MAAa,uBACZ,UAAsD,CAAC,MACtC;CACjB,MAAM,MAAM,QAAQ,cAAc,KAAK,IAAI;CAC3C,MAAM,SAAS,QAAQ,aAAa;CACpC,OAAO,EACN,MAAM,MAAM,EAAE,KAAK,KAAK,QAAQ,OAAO;EACtC,MAAM,KAAK,IAAI,QAAQ;EACvB,MAAM,WAAW,GAAG,SAAS;EAC7B,MAAM,UAAU,IAAI;EACpB,MAAM,WAAW,MAAM,GAAG,IAAiB,QAAQ,EAAE,YAAY,IAAI;EACrE,IAAI;EACJ,IAAI;EACJ,IAAI,CAAC,YAAY,UAAU,SAAS,eAAe,QAAQ;GAC1D,QAAQ;GACR,cAAc;EACf,OAAO;GACN,QAAQ,SAAS,QAAQ;GACzB,cAAc,SAAS;EACxB;EACA,MAAM,GAAG,IAAI,UAAU;GAAE;GAAO;EAAY,CAAC,EAAE,YAAY,KAAA,CAAS;EACpE,OAAO;GACN,IAAI,SAAS;GACb,WAAW,KAAK,IAAI,GAAG,MAAM,KAAK;GAClC,SAAS,cAAc;EACxB;CACD,EACD;AACD"}
@@ -0,0 +1,30 @@
1
+ import "./constants.js";
2
+ //#region src/spam/reserved.ts
3
+ /**
4
+ * Split reserved entries (honeypot decoy, captcha token) out of the submitted `values`. The honeypot
5
+ * rides a configurable, innocuous field name; the captcha token rides a fixed reserved key. Real field
6
+ * values pass through unchanged. `runSubmission` would already ignore unknown field names, but stripping
7
+ * here keeps them out of storage and out of the validation pass.
8
+ */
9
+ const extractReservedValues = (values, honeypotField) => {
10
+ const cleaned = [];
11
+ const result = { cleaned };
12
+ for (const entry of values) {
13
+ if (entry.field === "__fb_captcha") {
14
+ if (typeof entry.value === "string") result.captchaToken = entry.value;
15
+ continue;
16
+ }
17
+ if (honeypotField !== null && entry.field === honeypotField) {
18
+ result.honeypot = entry.value;
19
+ continue;
20
+ }
21
+ cleaned.push(entry);
22
+ }
23
+ return result;
24
+ };
25
+ /** A honeypot is tripped when the decoy carries any non-empty value (a real user never fills it). */
26
+ const isHoneypotTripped = (value) => value != null && value !== "" && !(Array.isArray(value) && value.length === 0);
27
+ //#endregion
28
+ export { extractReservedValues, isHoneypotTripped };
29
+
30
+ //# sourceMappingURL=reserved.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reserved.js","names":[],"sources":["../../src/spam/reserved.ts"],"sourcesContent":["import type { SubmissionValue } from '../submissions/types'\nimport { CAPTCHA_TOKEN_KEY } from './constants'\n\nexport type ExtractedReserved = {\n\t/** Real field values with reserved entries removed. */\n\tcleaned: SubmissionValue[]\n\t/** The honeypot decoy value, if a honeypot field name was active and present. */\n\thoneypot?: unknown\n\t/** A captcha token carried by the client. */\n\tcaptchaToken?: string\n}\n\n/**\n * Split reserved entries (honeypot decoy, captcha token) out of the submitted `values`. The honeypot\n * rides a configurable, innocuous field name; the captcha token rides a fixed reserved key. Real field\n * values pass through unchanged. `runSubmission` would already ignore unknown field names, but stripping\n * here keeps them out of storage and out of the validation pass.\n */\nexport const extractReservedValues = (\n\tvalues: SubmissionValue[],\n\thoneypotField: string | null\n): ExtractedReserved => {\n\tconst cleaned: SubmissionValue[] = []\n\tconst result: ExtractedReserved = { cleaned }\n\tfor (const entry of values) {\n\t\tif (entry.field === CAPTCHA_TOKEN_KEY) {\n\t\t\tif (typeof entry.value === 'string') {\n\t\t\t\tresult.captchaToken = entry.value\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tif (honeypotField !== null && entry.field === honeypotField) {\n\t\t\tresult.honeypot = entry.value\n\t\t\tcontinue\n\t\t}\n\t\tcleaned.push(entry)\n\t}\n\treturn result\n}\n\n/** A honeypot is tripped when the decoy carries any non-empty value (a real user never fills it). */\nexport const isHoneypotTripped = (value: unknown): boolean =>\n\tvalue != null && value !== '' && !(Array.isArray(value) && value.length === 0)\n"],"mappings":";;;;;;;;AAkBA,MAAa,yBACZ,QACA,kBACuB;CACvB,MAAM,UAA6B,CAAC;CACpC,MAAM,SAA4B,EAAE,QAAQ;CAC5C,KAAK,MAAM,SAAS,QAAQ;EAC3B,IAAI,MAAM,UAAA,gBAA6B;GACtC,IAAI,OAAO,MAAM,UAAU,UAC1B,OAAO,eAAe,MAAM;GAE7B;EACD;EACA,IAAI,kBAAkB,QAAQ,MAAM,UAAU,eAAe;GAC5D,OAAO,WAAW,MAAM;GACxB;EACD;EACA,QAAQ,KAAK,KAAK;CACnB;CACA,OAAO;AACR;;AAGA,MAAa,qBAAqB,UACjC,SAAS,QAAQ,UAAU,MAAM,EAAE,MAAM,QAAQ,KAAK,KAAK,MAAM,WAAW"}
@@ -0,0 +1,12 @@
1
+ import { ResolvedSpamConfig, SpamOption } from "./types.js";
2
+
3
+ //#region src/spam/resolveSpam.d.ts
4
+ /**
5
+ * Resolve the `spam` plugin option into a concrete config. `false` disables the whole subsystem.
6
+ * Otherwise honeypot + both rate limits default on, captcha + metadata capture default off, and the
7
+ * identity seam defaults to user id / trusted IP header.
8
+ */
9
+ declare const resolveSpamConfig: (option: SpamOption | undefined) => ResolvedSpamConfig | false;
10
+ //#endregion
11
+ export { resolveSpamConfig };
12
+ //# sourceMappingURL=resolveSpam.d.ts.map
@@ -0,0 +1,42 @@
1
+ import "./constants.js";
2
+ import { defaultIdentify } from "./identify.js";
3
+ import { createKvRateLimiter } from "./rateLimiter.js";
4
+ //#region src/spam/resolveSpam.ts
5
+ const DEFAULT_WINDOW = 6e4;
6
+ const DEFAULT_SUBMISSION_MAX = 5;
7
+ const DEFAULT_UPLOAD_MAX = 20;
8
+ const resolveRateLimit = (option, defaultMax) => {
9
+ if (option === false) return false;
10
+ const cfg = option ?? {};
11
+ return {
12
+ window: cfg.window ?? DEFAULT_WINDOW,
13
+ max: cfg.max ?? defaultMax,
14
+ limiter: cfg.limiter ?? createKvRateLimiter()
15
+ };
16
+ };
17
+ /**
18
+ * Resolve the `spam` plugin option into a concrete config. `false` disables the whole subsystem.
19
+ * Otherwise honeypot + both rate limits default on, captcha + metadata capture default off, and the
20
+ * identity seam defaults to user id / trusted IP header.
21
+ */
22
+ const resolveSpamConfig = (option) => {
23
+ if (option === false) return false;
24
+ const cfg = option ?? {};
25
+ const ipHeader = cfg.ipHeader ?? "x-forwarded-for";
26
+ return {
27
+ honeypot: cfg.honeypot === false ? false : { fieldName: cfg.honeypot?.fieldName ?? "website" },
28
+ rateLimit: resolveRateLimit(cfg.rateLimit, DEFAULT_SUBMISSION_MAX),
29
+ uploadRateLimit: resolveRateLimit(cfg.uploadRateLimit, DEFAULT_UPLOAD_MAX),
30
+ captcha: cfg.captcha,
31
+ identify: cfg.identify ?? defaultIdentify(ipHeader),
32
+ ipHeader,
33
+ metadata: {
34
+ ip: cfg.metadata?.ip ?? false,
35
+ ua: cfg.metadata?.ua ?? false
36
+ }
37
+ };
38
+ };
39
+ //#endregion
40
+ export { resolveSpamConfig };
41
+
42
+ //# sourceMappingURL=resolveSpam.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolveSpam.js","names":[],"sources":["../../src/spam/resolveSpam.ts"],"sourcesContent":["import { DEFAULT_HONEYPOT_FIELD } from './constants'\nimport { defaultIdentify } from './identify'\nimport { createKvRateLimiter } from './rateLimiter'\nimport type {\n\tRateLimitConfig,\n\tResolvedRateLimit,\n\tResolvedSpamConfig,\n\tSpamConfig,\n\tSpamOption,\n} from './types'\n\nconst DEFAULT_WINDOW = 60_000\nconst DEFAULT_SUBMISSION_MAX = 5\nconst DEFAULT_UPLOAD_MAX = 20\n\nconst resolveRateLimit = (\n\toption: false | RateLimitConfig | undefined,\n\tdefaultMax: number\n): false | ResolvedRateLimit => {\n\tif (option === false) {\n\t\treturn false\n\t}\n\tconst cfg = option ?? {}\n\treturn {\n\t\twindow: cfg.window ?? DEFAULT_WINDOW,\n\t\tmax: cfg.max ?? defaultMax,\n\t\tlimiter: cfg.limiter ?? createKvRateLimiter(),\n\t}\n}\n\n/**\n * Resolve the `spam` plugin option into a concrete config. `false` disables the whole subsystem.\n * Otherwise honeypot + both rate limits default on, captcha + metadata capture default off, and the\n * identity seam defaults to user id / trusted IP header.\n */\nexport const resolveSpamConfig = (option: SpamOption | undefined): ResolvedSpamConfig | false => {\n\tif (option === false) {\n\t\treturn false\n\t}\n\tconst cfg: SpamConfig = option ?? {}\n\tconst ipHeader = cfg.ipHeader ?? 'x-forwarded-for'\n\treturn {\n\t\thoneypot:\n\t\t\tcfg.honeypot === false\n\t\t\t\t? false\n\t\t\t\t: { fieldName: cfg.honeypot?.fieldName ?? DEFAULT_HONEYPOT_FIELD },\n\t\trateLimit: resolveRateLimit(cfg.rateLimit, DEFAULT_SUBMISSION_MAX),\n\t\tuploadRateLimit: resolveRateLimit(cfg.uploadRateLimit, DEFAULT_UPLOAD_MAX),\n\t\tcaptcha: cfg.captcha,\n\t\tidentify: cfg.identify ?? defaultIdentify(ipHeader),\n\t\tipHeader,\n\t\tmetadata: { ip: cfg.metadata?.ip ?? false, ua: cfg.metadata?.ua ?? false },\n\t}\n}\n"],"mappings":";;;;AAWA,MAAM,iBAAiB;AACvB,MAAM,yBAAyB;AAC/B,MAAM,qBAAqB;AAE3B,MAAM,oBACL,QACA,eAC+B;CAC/B,IAAI,WAAW,OACd,OAAO;CAER,MAAM,MAAM,UAAU,CAAC;CACvB,OAAO;EACN,QAAQ,IAAI,UAAU;EACtB,KAAK,IAAI,OAAO;EAChB,SAAS,IAAI,WAAW,oBAAoB;CAC7C;AACD;;;;;;AAOA,MAAa,qBAAqB,WAA+D;CAChG,IAAI,WAAW,OACd,OAAO;CAER,MAAM,MAAkB,UAAU,CAAC;CACnC,MAAM,WAAW,IAAI,YAAY;CACjC,OAAO;EACN,UACC,IAAI,aAAa,QACd,QACA,EAAE,WAAW,IAAI,UAAU,aAAA,UAAoC;EACnE,WAAW,iBAAiB,IAAI,WAAW,sBAAsB;EACjE,iBAAiB,iBAAiB,IAAI,iBAAiB,kBAAkB;EACzE,SAAS,IAAI;EACb,UAAU,IAAI,YAAY,gBAAgB,QAAQ;EAClD;EACA,UAAU;GAAE,IAAI,IAAI,UAAU,MAAM;GAAO,IAAI,IAAI,UAAU,MAAM;EAAM;CAC1E;AACD"}
@@ -0,0 +1,68 @@
1
+ import { keys } from "../translations/keys.js";
2
+ import { asTranslate } from "../translations/server.js";
3
+ import { IDENTITY_CONTEXT_KEY } from "./constants.js";
4
+ import { extractReservedValues, isHoneypotTripped } from "./reserved.js";
5
+ import { APIError } from "payload";
6
+ //#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
+ /**
12
+ * Submissions spam guard, prepended before `validateSubmission` in `beforeValidate` so it rejects before
13
+ * the form load + field validation. On create it: resolves the request identity once (stashing it on
14
+ * `req.context` for upload-ownership verification downstream); rate-limits per form + identity (429,
15
+ * fail-open when identity is null); strips reserved `values` entries (honeypot, captcha token); rejects a
16
+ * filled honeypot with a generic error; verifies a configured captcha; and writes a server-authoritative
17
+ * `meta` (timestamp + spam signal, with opt-in ip/ua). App-level rate limiting is defense-in-depth that
18
+ * complements edge/CDN/WAF limiting, not a DoS replacement.
19
+ */
20
+ const buildSpamGuard = (spam) => async ({ data, operation, req }) => {
21
+ if (operation !== "create" || !data) return data;
22
+ const t = asTranslate(req.i18n.t);
23
+ const authenticated = Boolean(req.user);
24
+ const identity = await spam.identify(req);
25
+ if (identity != null) req.context[IDENTITY_CONTEXT_KEY] = identity;
26
+ if (spam.rateLimit !== false && identity != null) {
27
+ const formKey = data.form != null ? String(data.form) : "unknown";
28
+ const { ok } = await spam.rateLimit.limiter.check({
29
+ key: `submissions:${formKey}:${identity}`,
30
+ max: spam.rateLimit.max,
31
+ window: spam.rateLimit.window,
32
+ req
33
+ });
34
+ if (!ok) throw new APIError(t(keys.spamRateLimited), 429);
35
+ }
36
+ const honeypotField = spam.honeypot === false ? null : spam.honeypot.fieldName;
37
+ const { cleaned, honeypot, captchaToken } = extractReservedValues(data.values ?? [], honeypotField);
38
+ data.values = cleaned;
39
+ if (!authenticated && honeypotField !== null && isHoneypotTripped(honeypot)) throw new APIError(t(keys.spamRejected), 400);
40
+ const captchaChecked = Boolean(spam.captcha) && !authenticated;
41
+ if (spam.captcha && !authenticated) {
42
+ if (!(typeof captchaToken === "string" && captchaToken.length > 0 ? await spam.captcha.verify({
43
+ token: captchaToken,
44
+ req
45
+ }).catch(() => false) : false)) throw new APIError(t(keys.spamCaptchaFailed), 400);
46
+ }
47
+ const serverMeta = {
48
+ at: (/* @__PURE__ */ new Date()).toISOString(),
49
+ spam: { captcha: captchaChecked ? "passed" : "skipped" }
50
+ };
51
+ if (spam.metadata.ip) {
52
+ const ip = firstHop(req, spam.ipHeader);
53
+ if (ip) serverMeta.ip = ip;
54
+ }
55
+ if (spam.metadata.ua) {
56
+ const ua = req.headers?.get("user-agent");
57
+ if (ua) serverMeta.ua = ua;
58
+ }
59
+ data.meta = {
60
+ ...authenticated && data.meta != null && typeof data.meta === "object" ? data.meta : {},
61
+ ...serverMeta
62
+ };
63
+ return data;
64
+ };
65
+ //#endregion
66
+ export { buildSpamGuard };
67
+
68
+ //# sourceMappingURL=spamGuard.js.map
@@ -0,0 +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"}
@@ -0,0 +1,73 @@
1
+ import { PayloadRequest } from "payload";
2
+
3
+ //#region src/spam/types.d.ts
4
+ /** Stable identity key for a request, or null/undefined when the client cannot be identified (rate-limiting then skips: fail-open). */
5
+ type IdentifyFn = (req: PayloadRequest) => null | string | undefined | Promise<null | string | undefined>;
6
+ type RateLimitResult = {
7
+ ok: boolean; /** Remaining requests in the current window (>= 0). */
8
+ remaining: number; /** Epoch ms when the current window resets. */
9
+ resetAt: number;
10
+ };
11
+ type RateLimitCheckArgs = {
12
+ key: string; /** Max requests per window. */
13
+ max: number; /** Window length in ms. */
14
+ window: number;
15
+ req: PayloadRequest;
16
+ };
17
+ /** Pluggable limiter. The default is a window counter over `payload.kv`; swap for Redis/etc. */
18
+ type RateLimiter = {
19
+ check(args: RateLimitCheckArgs): Promise<RateLimitResult>;
20
+ };
21
+ type CaptchaVerifyArgs = {
22
+ token: string;
23
+ req: PayloadRequest;
24
+ };
25
+ /** A captcha adapter. v1 ships the seam only (no built-in provider). Build with `defineCaptchaProvider`. */
26
+ type CaptchaProvider = {
27
+ type: string;
28
+ verify(args: CaptchaVerifyArgs): Promise<boolean>;
29
+ };
30
+ type RateLimitConfig = {
31
+ /** Window length in ms. Default 60000. */window?: number; /** Max creates per identity per window. */
32
+ max?: number; /** Override the limiter (default: KV window counter). */
33
+ limiter?: RateLimiter;
34
+ };
35
+ type SpamMetadataConfig = {
36
+ /** Persist the client IP (from the trusted header) onto the submission `meta`. Default false (privacy). */ip?: boolean; /** Persist the user-agent onto the submission `meta`. Default false. */
37
+ ua?: boolean;
38
+ };
39
+ type SpamConfig = {
40
+ /** Honeypot decoy. Default on. `false` disables. */honeypot?: false | {
41
+ fieldName?: string;
42
+ }; /** Per-identity rate limit on submission create. Default on (60s window, max 5). `false` disables. */
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). */
45
+ captcha?: CaptchaProvider; /** Identity resolution for rate-limiting, upload ownership, and (future) poll dedup. Default: user id, else trusted IP header. */
46
+ identify?: IdentifyFn; /** Header read for the client IP (proxy-dependent, best-effort). Default 'x-forwarded-for'. */
47
+ ipHeader?: string; /** Opt-in capture of client metadata onto the submission `meta`. Off by default. */
48
+ metadata?: SpamMetadataConfig;
49
+ };
50
+ /** `false` disables the whole subsystem; an object configures it (all controls default on except captcha + metadata). */
51
+ type SpamOption = false | SpamConfig;
52
+ type ResolvedRateLimit = {
53
+ window: number;
54
+ max: number;
55
+ limiter: RateLimiter;
56
+ };
57
+ type ResolvedSpamConfig = {
58
+ honeypot: false | {
59
+ fieldName: string;
60
+ };
61
+ rateLimit: false | ResolvedRateLimit;
62
+ uploadRateLimit: false | ResolvedRateLimit;
63
+ captcha?: CaptchaProvider;
64
+ identify: IdentifyFn;
65
+ ipHeader: string;
66
+ metadata: {
67
+ ip: boolean;
68
+ ua: boolean;
69
+ };
70
+ };
71
+ //#endregion
72
+ export { CaptchaProvider, CaptchaVerifyArgs, IdentifyFn, RateLimitCheckArgs, RateLimitConfig, RateLimitResult, RateLimiter, ResolvedSpamConfig, SpamConfig, SpamMetadataConfig, SpamOption };
73
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,33 @@
1
+ import { keys } from "../translations/keys.js";
2
+ import { asTranslate } from "../translations/server.js";
3
+ import { APIError } from "payload";
4
+ //#region src/spam/uploadHooks.ts
5
+ /**
6
+ * Stamp the resolved request identity onto a new upload's `owner`. `captureFileRef` later requires the
7
+ * referencing submission's identity to match, so an anonymous submitter cannot capture another identity's
8
+ * upload. No identity (no user, no trusted IP header) means no stamp, so the upload stays unscoped (a
9
+ * deliberate fail-open; identity granularity is IP-level, so same-network clients share scope -- documented).
10
+ */
11
+ const buildUploadOwnerStamp = (spam) => async ({ data, operation, req }) => {
12
+ if (operation !== "create" || !data) return data;
13
+ const identity = await spam.identify(req);
14
+ if (identity != null && data.owner == null) data.owner = identity;
15
+ return data;
16
+ };
17
+ /** Reject upload floods per identity before the file is written to `staticDir`. Fail-open without identity. */
18
+ const buildUploadRateLimit = (spam) => async ({ operation, req }) => {
19
+ if (operation !== "create" || spam.uploadRateLimit === false) return;
20
+ const identity = await spam.identify(req);
21
+ if (identity == null) return;
22
+ const { ok } = await spam.uploadRateLimit.limiter.check({
23
+ key: `uploads:${identity}`,
24
+ max: spam.uploadRateLimit.max,
25
+ window: spam.uploadRateLimit.window,
26
+ req
27
+ });
28
+ if (!ok) throw new APIError(asTranslate(req.i18n.t)(keys.spamRateLimited), 429);
29
+ };
30
+ //#endregion
31
+ export { buildUploadOwnerStamp, buildUploadRateLimit };
32
+
33
+ //# sourceMappingURL=uploadHooks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"uploadHooks.js","names":[],"sources":["../../src/spam/uploadHooks.ts"],"sourcesContent":["import {\n\tAPIError,\n\ttype CollectionBeforeOperationHook,\n\ttype CollectionBeforeValidateHook,\n} from 'payload'\nimport { keys } from '../translations/keys'\nimport { asTranslate } from '../translations/server'\nimport type { ResolvedSpamConfig } from './types'\n\n/**\n * Stamp the resolved request identity onto a new upload's `owner`. `captureFileRef` later requires the\n * referencing submission's identity to match, so an anonymous submitter cannot capture another identity's\n * upload. No identity (no user, no trusted IP header) means no stamp, so the upload stays unscoped (a\n * deliberate fail-open; identity granularity is IP-level, so same-network clients share scope -- documented).\n */\nexport const buildUploadOwnerStamp =\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 identity = await spam.identify(req)\n\t\tif (identity != null && (data as { owner?: unknown }).owner == null) {\n\t\t\t;(data as { owner?: string }).owner = identity\n\t\t}\n\t\treturn data\n\t}\n\n/** Reject upload floods per identity before the file is written to `staticDir`. Fail-open without identity. */\nexport const buildUploadRateLimit =\n\t(spam: ResolvedSpamConfig): CollectionBeforeOperationHook =>\n\tasync ({ operation, req }) => {\n\t\tif (operation !== 'create' || spam.uploadRateLimit === false) {\n\t\t\treturn\n\t\t}\n\t\tconst identity = await spam.identify(req)\n\t\tif (identity == null) {\n\t\t\treturn\n\t\t}\n\t\tconst { ok } = await spam.uploadRateLimit.limiter.check({\n\t\t\tkey: `uploads:${identity}`,\n\t\t\tmax: spam.uploadRateLimit.max,\n\t\t\twindow: spam.uploadRateLimit.window,\n\t\t\treq,\n\t\t})\n\t\tif (!ok) {\n\t\t\tthrow new APIError(asTranslate(req.i18n.t)(keys.spamRateLimited), 429)\n\t\t}\n\t}\n"],"mappings":";;;;;;;;;;AAeA,MAAa,yBACX,SACD,OAAO,EAAE,MAAM,WAAW,UAAU;CACnC,IAAI,cAAc,YAAY,CAAC,MAC9B,OAAO;CAER,MAAM,WAAW,MAAM,KAAK,SAAS,GAAG;CACxC,IAAI,YAAY,QAAS,KAA6B,SAAS,MAC7D,KAA6B,QAAQ;CAEvC,OAAO;AACR;;AAGD,MAAa,wBACX,SACD,OAAO,EAAE,WAAW,UAAU;CAC7B,IAAI,cAAc,YAAY,KAAK,oBAAoB,OACtD;CAED,MAAM,WAAW,MAAM,KAAK,SAAS,GAAG;CACxC,IAAI,YAAY,MACf;CAED,MAAM,EAAE,OAAO,MAAM,KAAK,gBAAgB,QAAQ,MAAM;EACvD,KAAK,WAAW;EAChB,KAAK,KAAK,gBAAgB;EAC1B,QAAQ,KAAK,gBAAgB;EAC7B;CACD,CAAC;CACD,IAAI,CAAC,IACJ,MAAM,IAAI,SAAS,YAAY,IAAI,KAAK,CAAC,EAAE,KAAK,eAAe,GAAG,GAAG;AAEvE"}
@@ -0,0 +1,16 @@
1
+ import { UIFieldServerProps } from "payload";
2
+
3
+ //#region src/submissions/SubmissionAnswers.d.ts
4
+ /**
5
+ * Read-only, localized submission view (server component). Renders each answered field's snapshot
6
+ * label next to its type-aware formatted value via the field type's `format`, using the request
7
+ * locale and i18n, with no client bundle. The built-in field types are formatted directly; threading
8
+ * the full resolved registry (custom types) to the view is a future enhancement.
9
+ */
10
+ declare const SubmissionAnswers: ({
11
+ data,
12
+ req
13
+ }: UIFieldServerProps) => import("react/jsx-runtime").JSX.Element;
14
+ //#endregion
15
+ export { SubmissionAnswers };
16
+ //# sourceMappingURL=SubmissionAnswers.d.ts.map
@@ -0,0 +1,57 @@
1
+ import { keys } from "../translations/keys.js";
2
+ import { asFieldTranslate } from "../translations/server.js";
3
+ import { defaultFieldDefinitions } from "../fields/builtin/index.js";
4
+ import { buildRegistry } from "../fields/registry.js";
5
+ import { jsx, jsxs } from "react/jsx-runtime";
6
+ //#region src/submissions/SubmissionAnswers.tsx
7
+ const registry = buildRegistry(defaultFieldDefinitions);
8
+ /** A stored file answer's link, when the captured `FileRef` carries a url; null otherwise. */
9
+ const fileHref = (raw) => {
10
+ if (raw && typeof raw === "object" && "url" in raw) {
11
+ const ref = raw;
12
+ if (typeof ref.url === "string" && ref.url.length > 0) return {
13
+ url: ref.url,
14
+ filename: typeof ref.filename === "string" ? ref.filename : ref.url
15
+ };
16
+ }
17
+ return null;
18
+ };
19
+ /**
20
+ * Read-only, localized submission view (server component). Renders each answered field's snapshot
21
+ * label next to its type-aware formatted value via the field type's `format`, using the request
22
+ * locale and i18n, with no client bundle. The built-in field types are formatted directly; threading
23
+ * the full resolved registry (custom types) to the view is a future enhancement.
24
+ */
25
+ const SubmissionAnswers = ({ data, req }) => {
26
+ const doc = data ?? {};
27
+ const descriptors = doc.descriptors ?? [];
28
+ const values = doc.values ?? [];
29
+ const locale = doc.locale ?? req.locale ?? "en";
30
+ const t = asFieldTranslate(req.i18n.t);
31
+ if (descriptors.length === 0) return /* @__PURE__ */ jsx("p", { children: t(keys.submissionNoAnswers) });
32
+ const valueByField = new Map(values.map((entry) => [entry.field, entry.value]));
33
+ return /* @__PURE__ */ jsxs("div", {
34
+ className: "form-builder-submission-answers",
35
+ children: [/* @__PURE__ */ jsx("h4", { children: t(keys.submissionAnswers) }), /* @__PURE__ */ jsx("dl", { children: descriptors.map((descriptor) => {
36
+ const definition = registry.get(descriptor.fieldType);
37
+ const raw = valueByField.get(descriptor.field);
38
+ const formatted = definition?.format ? definition.format({
39
+ value: raw,
40
+ config: {},
41
+ optionLabels: descriptor.optionLabels,
42
+ locale,
43
+ t
44
+ }) : raw == null ? "" : String(raw);
45
+ const href = descriptor.fieldType === "file" ? fileHref(raw) : null;
46
+ return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("dt", { children: descriptor.label }), /* @__PURE__ */ jsx("dd", { children: href ? /* @__PURE__ */ jsx("a", {
47
+ href: href.url,
48
+ rel: "noreferrer",
49
+ children: href.filename
50
+ }) : formatted })] }, descriptor.field);
51
+ }) })]
52
+ });
53
+ };
54
+ //#endregion
55
+ export { SubmissionAnswers };
56
+
57
+ //# sourceMappingURL=SubmissionAnswers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SubmissionAnswers.js","names":[],"sources":["../../src/submissions/SubmissionAnswers.tsx"],"sourcesContent":["import type { UIFieldServerProps } from 'payload'\nimport { defaultFieldDefinitions } from '../fields/builtin'\nimport { buildRegistry } from '../fields/registry'\nimport { keys } from '../translations/keys'\nimport { asFieldTranslate } from '../translations/server'\nimport type { SubmissionDescriptor, SubmissionValue } from './types'\n\nconst registry = buildRegistry(defaultFieldDefinitions)\n\ntype SubmissionDoc = {\n\tvalues?: SubmissionValue[]\n\tdescriptors?: SubmissionDescriptor[]\n\tlocale?: string\n}\n\n/** A stored file answer's link, when the captured `FileRef` carries a url; null otherwise. */\nconst fileHref = (raw: unknown): { url: string; filename: string } | null => {\n\tif (raw && typeof raw === 'object' && 'url' in raw) {\n\t\tconst ref = raw as { url?: unknown; filename?: unknown }\n\t\tif (typeof ref.url === 'string' && ref.url.length > 0) {\n\t\t\treturn { url: ref.url, filename: typeof ref.filename === 'string' ? ref.filename : ref.url }\n\t\t}\n\t}\n\treturn null\n}\n\n/**\n * Read-only, localized submission view (server component). Renders each answered field's snapshot\n * label next to its type-aware formatted value via the field type's `format`, using the request\n * locale and i18n, with no client bundle. The built-in field types are formatted directly; threading\n * the full resolved registry (custom types) to the view is a future enhancement.\n */\nexport const SubmissionAnswers = ({ data, req }: UIFieldServerProps) => {\n\tconst doc = (data ?? {}) as SubmissionDoc\n\tconst descriptors = doc.descriptors ?? []\n\tconst values = doc.values ?? []\n\tconst locale = doc.locale ?? req.locale ?? 'en'\n\tconst t = asFieldTranslate(req.i18n.t)\n\n\tif (descriptors.length === 0) {\n\t\treturn <p>{t(keys.submissionNoAnswers)}</p>\n\t}\n\n\tconst valueByField = new Map(values.map((entry) => [entry.field, entry.value]))\n\n\treturn (\n\t\t<div className=\"form-builder-submission-answers\">\n\t\t\t<h4>{t(keys.submissionAnswers)}</h4>\n\t\t\t<dl>\n\t\t\t\t{descriptors.map((descriptor) => {\n\t\t\t\t\tconst definition = registry.get(descriptor.fieldType)\n\t\t\t\t\tconst raw = valueByField.get(descriptor.field)\n\t\t\t\t\tconst formatted = definition?.format\n\t\t\t\t\t\t? definition.format({\n\t\t\t\t\t\t\t\tvalue: raw,\n\t\t\t\t\t\t\t\tconfig: {},\n\t\t\t\t\t\t\t\toptionLabels: descriptor.optionLabels,\n\t\t\t\t\t\t\t\tlocale,\n\t\t\t\t\t\t\t\tt,\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t: raw == null\n\t\t\t\t\t\t\t? ''\n\t\t\t\t\t\t\t: String(raw)\n\t\t\t\t\tconst href = descriptor.fieldType === 'file' ? fileHref(raw) : null\n\t\t\t\t\treturn (\n\t\t\t\t\t\t<div key={descriptor.field}>\n\t\t\t\t\t\t\t<dt>{descriptor.label}</dt>\n\t\t\t\t\t\t\t<dd>\n\t\t\t\t\t\t\t\t{href ? (\n\t\t\t\t\t\t\t\t\t<a href={href.url} rel=\"noreferrer\">\n\t\t\t\t\t\t\t\t\t\t{href.filename}\n\t\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t\tformatted\n\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t</dd>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t)\n\t\t\t\t})}\n\t\t\t</dl>\n\t\t</div>\n\t)\n}\n"],"mappings":";;;;;;AAOA,MAAM,WAAW,cAAc,uBAAuB;;AAStD,MAAM,YAAY,QAA2D;CAC5E,IAAI,OAAO,OAAO,QAAQ,YAAY,SAAS,KAAK;EACnD,MAAM,MAAM;EACZ,IAAI,OAAO,IAAI,QAAQ,YAAY,IAAI,IAAI,SAAS,GACnD,OAAO;GAAE,KAAK,IAAI;GAAK,UAAU,OAAO,IAAI,aAAa,WAAW,IAAI,WAAW,IAAI;EAAI;CAE7F;CACA,OAAO;AACR;;;;;;;AAQA,MAAa,qBAAqB,EAAE,MAAM,UAA8B;CACvE,MAAM,MAAO,QAAQ,CAAC;CACtB,MAAM,cAAc,IAAI,eAAe,CAAC;CACxC,MAAM,SAAS,IAAI,UAAU,CAAC;CAC9B,MAAM,SAAS,IAAI,UAAU,IAAI,UAAU;CAC3C,MAAM,IAAI,iBAAiB,IAAI,KAAK,CAAC;CAErC,IAAI,YAAY,WAAW,GAC1B,OAAO,oBAAC,KAAD,EAAA,UAAI,EAAE,KAAK,mBAAmB,EAAK,CAAA;CAG3C,MAAM,eAAe,IAAI,IAAI,OAAO,KAAK,UAAU,CAAC,MAAM,OAAO,MAAM,KAAK,CAAC,CAAC;CAE9E,OACC,qBAAC,OAAD;EAAK,WAAU;YAAf,CACC,oBAAC,MAAD,EAAA,UAAK,EAAE,KAAK,iBAAiB,EAAM,CAAA,GACnC,oBAAC,MAAD,EAAA,UACE,YAAY,KAAK,eAAe;GAChC,MAAM,aAAa,SAAS,IAAI,WAAW,SAAS;GACpD,MAAM,MAAM,aAAa,IAAI,WAAW,KAAK;GAC7C,MAAM,YAAY,YAAY,SAC3B,WAAW,OAAO;IAClB,OAAO;IACP,QAAQ,CAAC;IACT,cAAc,WAAW;IACzB;IACA;GACD,CAAC,IACA,OAAO,OACN,KACA,OAAO,GAAG;GACd,MAAM,OAAO,WAAW,cAAc,SAAS,SAAS,GAAG,IAAI;GAC/D,OACC,qBAAC,OAAD,EAAA,UAAA,CACC,oBAAC,MAAD,EAAA,UAAK,WAAW,MAAU,CAAA,GAC1B,oBAAC,MAAD,EAAA,UACE,OACA,oBAAC,KAAD;IAAG,MAAM,KAAK;IAAK,KAAI;cACrB,KAAK;GACJ,CAAA,IAEH,UAEE,CAAA,CACA,EAAA,GAXK,WAAW,KAWhB;EAEP,CAAC,EACE,CAAA,CACA;;AAEP"}