@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
package/dist/index.js CHANGED
@@ -1,1891 +1,42 @@
1
- import { _ as IDENTITY_CONTEXT_KEY, a as resolveValidationRules, c as runValidation, d as computeCalcFields, f as evaluateCalc, g as DEFAULT_HONEYPOT_FIELD, h as CAPTCHA_TOKEN_KEY, l as evaluateCondition, m as defaultPresentationDescriptors, n as optionLabelsFor, o as defaultValidationRules, p as DEFAULT_PRESENTATION_NAME, r as valuesFromSearchParams, s as defineValidationRule, t as buildRecallResolver, u as calcExpressionOf, v as noopEventSink, y as interpolate } from "./resolver-OeQyVH2N.js";
2
- import { t as keys } from "./keys-N5xGiUsh.js";
3
- import { a as asFieldTranslate, c as labelForKey, i as defineFormField, n as resolveFieldTypes, o as asTranslate, r as defaultFieldDefinitions, s as labelFor } from "./registry-CoCyhtvB.js";
4
- import { n as defaultConditionType, t as conditionOperators } from "./fieldTypes-CzhhJXjg.js";
5
- import { t as translations } from "./translations-edMqq_2e.js";
6
- import { APIError, ValidationError, definePlugin } from "payload";
7
- import { createHmac } from "node:crypto";
8
- import { deepMergeSimple, transformWhereQuery } from "payload/shared";
9
- //#region src/actions/defineAction.ts
10
- const defineAction = (definition) => definition;
11
- //#endregion
12
- //#region src/actions/builtin/confirmation.ts
13
- const confirmation = defineAction({
14
- type: "confirmation",
15
- label: keys.actionConfirmation,
16
- config: [
17
- {
18
- name: "toField",
19
- type: "text",
20
- label: labelFor(keys.actionConfigToField)
21
- },
22
- {
23
- name: "subject",
24
- type: "text",
25
- label: labelFor(keys.actionConfigSubject)
26
- },
27
- {
28
- name: "body",
29
- type: "textarea",
30
- label: labelFor(keys.actionConfigBody)
31
- }
32
- ],
33
- run: async (args) => {
34
- const { config, values, payload } = args;
35
- if (!config.toField) return;
36
- const entry = values.find((v) => v.field === config.toField);
37
- const to = entry == null ? "" : String(entry.value ?? "");
38
- if (!to) return;
39
- if (typeof payload.sendEmail !== "function") throw new Error("confirmation: no email adapter configured");
40
- const resolve = (name) => {
41
- const found = values.find((v) => v.field === name);
42
- return found == null ? "" : String(found.value ?? "");
43
- };
44
- const subject = interpolate(config.subject ?? "", resolve);
45
- const html = interpolate(config.body ?? "", resolve);
46
- await payload.sendEmail({
47
- to,
48
- subject,
49
- html
50
- });
51
- }
52
- });
53
- //#endregion
54
- //#region src/actions/builtin/emailTeam.ts
55
- const emailTeam = defineAction({
56
- type: "emailTeam",
57
- label: keys.actionEmailTeam,
58
- config: [
59
- {
60
- name: "to",
61
- type: "text",
62
- label: labelFor(keys.actionConfigTo)
63
- },
64
- {
65
- name: "subject",
66
- type: "text",
67
- label: labelFor(keys.actionConfigSubject)
68
- },
69
- {
70
- name: "body",
71
- type: "textarea",
72
- label: labelFor(keys.actionConfigBody)
73
- }
74
- ],
75
- run: async (args) => {
76
- const { config, values, payload } = args;
77
- if (!config.to) throw new Error("emailTeam: missing \"to\" address");
78
- if (typeof payload.sendEmail !== "function") throw new Error("emailTeam: no email adapter configured");
79
- const resolve = (name) => {
80
- const entry = values.find((v) => v.field === name);
81
- return entry == null ? "" : String(entry.value ?? "");
82
- };
83
- const subject = interpolate(config.subject ?? "", resolve);
84
- const html = interpolate(config.body ?? "", resolve);
85
- await payload.sendEmail({
86
- to: config.to,
87
- subject,
88
- html
89
- });
90
- }
91
- });
92
- //#endregion
93
- //#region src/actions/sign.ts
94
- const SIGNATURE_HEADER = "X-Form-Signature";
95
- /** HMAC-SHA256 over `body`, hex-encoded, wrapped in the versioned header value (`v1=<hex>`). */
96
- const signPayload = (body, secret) => `v1=${createHmac("sha256", secret).update(body).digest("hex")}`;
97
- //#endregion
98
- //#region src/actions/builtin/index.ts
99
- const defaultActionDefinitions = {
100
- emailTeam,
101
- confirmation,
102
- signedWebhook: defineAction({
103
- type: "signedWebhook",
104
- label: keys.actionSignedWebhook,
105
- config: [{
106
- name: "url",
107
- type: "text",
108
- label: labelFor(keys.actionConfigUrl)
109
- }, {
110
- name: "secret",
111
- type: "text",
112
- label: labelFor(keys.actionConfigSecret)
113
- }],
114
- run: async (args) => {
115
- const { config, form, submissionId, values } = args;
116
- if (!config.url) throw new Error("signedWebhook: missing \"url\"");
117
- if (!config.secret) throw new Error("signedWebhook: missing \"secret\"");
118
- const body = JSON.stringify({
119
- formId: form.id,
120
- submissionId,
121
- values
122
- });
123
- const controller = new AbortController();
124
- const timer = setTimeout(() => controller.abort(), 1e4);
125
- let response;
126
- try {
127
- response = await fetch(config.url, {
128
- method: "POST",
129
- headers: {
130
- "Content-Type": "application/json",
131
- [SIGNATURE_HEADER]: signPayload(body, config.secret)
132
- },
133
- body,
134
- signal: controller.signal
135
- });
136
- } finally {
137
- clearTimeout(timer);
138
- }
139
- if (!response.ok) throw new Error(`signedWebhook: server responded ${response.status}`);
140
- }
141
- })
142
- };
143
- //#endregion
144
- //#region src/actions/registry.ts
145
- /**
146
- * Resolve the active action registry from built-in defaults and a consumer override map. `false`
147
- * removes a type, `true` keeps the default (no-op when none exists), a definition adds or replaces.
148
- * Mirrors the field-type and validation-rule registry convention.
149
- */
150
- const resolveActions = (defaults, config = {}) => {
151
- const registry = new Map(Object.entries(defaults));
152
- for (const [type, option] of Object.entries(config)) if (option === false) registry.delete(type);
153
- else if (option === true) {} else registry.set(type, option);
154
- return registry;
155
- };
156
- //#endregion
157
- //#region src/collections/uploads.ts
158
- const FORM_UPLOADS_SLUG = "form-uploads";
159
- /**
160
- * The built-in upload collection backing the `file` field. Anonymous create (public forms upload here),
161
- * authed read/update/delete (only admins read stored files; submitters cannot enumerate others' uploads).
162
- * Per-field MIME/size is re-enforced at submit, so the collection stays permissive by default. No
163
- * `imageSizes`, so it needs no `sharp`. Projects with their own upload collection set `uploads: false` and
164
- * point the file field's `relationTo` at theirs.
165
- */
166
- const buildUploadsCollection = (config = {}) => ({
167
- slug: config.slug ?? "form-uploads",
168
- access: config.access ?? {
169
- create: () => true,
170
- read: ({ req }) => Boolean(req.user),
171
- update: ({ req }) => Boolean(req.user),
172
- delete: ({ req }) => Boolean(req.user)
173
- },
174
- admin: { group: "Forms" },
175
- upload: config.upload && config.upload !== true ? config.upload : true,
176
- fields: [{
177
- name: "owner",
178
- type: "text",
179
- admin: {
180
- readOnly: true,
181
- hidden: true
182
- }
183
- }, ...config.fields ?? []]
184
- });
185
- /** Resolve the `uploads` plugin option: `false` disables, `true`/object enables the built-in collection. */
186
- const resolveUploads = (option) => {
187
- if (option === false) return {
188
- enabled: false,
189
- slug: FORM_UPLOADS_SLUG
190
- };
191
- const collection = buildUploadsCollection(option && option !== true ? option : {});
192
- return {
193
- enabled: true,
194
- slug: collection.slug,
195
- collection
196
- };
197
- };
198
- //#endregion
199
- //#region src/consent/defineConsentSource.ts
200
- const defineConsentSource = (source) => source;
201
- //#endregion
202
- //#region src/consent/resolvePublishedVersionRef.ts
203
- /**
204
- * Returns the latest published version of a doc, or null when versions/drafts are off or nothing
205
- * is published. Never throws.
206
- */
207
- const resolvePublishedVersionRef = async (payload, args) => {
208
- try {
209
- const doc = (await payload.findVersions({
210
- collection: args.collection,
211
- where: { and: [{ parent: { equals: args.id } }, { "version._status": { equals: "published" } }] },
212
- sort: "-updatedAt",
213
- limit: 1,
214
- depth: 0
215
- }))?.docs?.[0];
216
- if (!doc) return null;
217
- return {
218
- versionId: String(doc.id),
219
- updatedAt: String(doc.updatedAt)
220
- };
221
- } catch {
222
- return null;
223
- }
224
- };
225
- //#endregion
226
- //#region src/consent/builtin/pageReference.ts
227
- const pageReferenceSource = defineConsentSource({
228
- type: "pageReference",
229
- label: keys.consentSourcePageReference,
230
- config: [
231
- {
232
- name: "relationTo",
233
- type: "text",
234
- label: labelFor(keys.consentConfigRelationTo)
235
- },
236
- {
237
- name: "docId",
238
- type: "text",
239
- label: labelFor(keys.consentConfigDocId)
240
- },
241
- {
242
- name: "urlField",
243
- type: "text",
244
- label: labelFor(keys.consentConfigUrlField),
245
- defaultValue: "slug"
246
- },
247
- {
248
- name: "captureVersion",
249
- type: "checkbox",
250
- label: labelFor(keys.consentConfigCaptureVersion)
251
- }
252
- ],
253
- resolve: async ({ config, payload, req, locale }) => {
254
- const relationTo = String(config.relationTo ?? "");
255
- const docId = String(config.docId ?? "");
256
- const urlField = String(config.urlField ?? "slug");
257
- const captureVersion = Boolean(config.captureVersion);
258
- if (!relationTo || !docId) return { links: [] };
259
- const doc = await payload.findByID({
260
- collection: relationTo,
261
- id: docId,
262
- depth: 0,
263
- locale,
264
- req
265
- }).catch(() => null);
266
- if (!doc) return { links: [{
267
- label: relationTo,
268
- url: ""
269
- }] };
270
- const docRecord = doc;
271
- const label = String(docRecord.title ?? docRecord[urlField] ?? relationTo);
272
- const url = String(docRecord[urlField] ?? "");
273
- if (!captureVersion) return { links: [{
274
- label,
275
- url
276
- }] };
277
- const v = await resolvePublishedVersionRef(payload, {
278
- collection: relationTo,
279
- id: docId
280
- });
281
- return {
282
- links: [{
283
- label,
284
- url
285
- }],
286
- ...v ? {
287
- versionRef: v.versionId,
288
- versionLabel: v.updatedAt
289
- } : {}
290
- };
291
- }
292
- });
293
- //#endregion
294
- //#region src/consent/builtin/index.ts
295
- const defaultConsentSources = {
296
- static: defineConsentSource({
297
- type: "static",
298
- label: keys.consentSourceStatic,
299
- config: [
300
- {
301
- name: "label",
302
- type: "text",
303
- label: labelFor(keys.consentConfigLabel)
304
- },
305
- {
306
- name: "url",
307
- type: "text",
308
- label: labelFor(keys.consentConfigUrl)
309
- },
310
- {
311
- name: "version",
312
- type: "text",
313
- label: labelFor(keys.consentConfigVersion)
314
- }
315
- ],
316
- resolve({ config }) {
317
- const label = String(config.label ?? "");
318
- const url = String(config.url ?? "");
319
- const version = config.version ? String(config.version) : void 0;
320
- return {
321
- links: [{
322
- label,
323
- url
324
- }],
325
- ...version ? {
326
- versionRef: version,
327
- versionLabel: version
328
- } : {}
329
- };
330
- }
331
- }),
332
- pageReference: pageReferenceSource
333
- };
334
- //#endregion
335
- //#region src/consent/registry.ts
336
- /**
337
- * Resolve the active consent-source registry from built-in defaults and a consumer override map.
338
- * `false` removes a type, `true` keeps the default (no-op when none exists), a definition adds or
339
- * replaces. Mirrors the action and field-type registry convention.
340
- */
341
- const resolveConsentSources = (defaults, config = {}) => {
342
- const registry = new Map(Object.entries(defaults));
343
- for (const [type, option] of Object.entries(config)) if (option === false) registry.delete(type);
344
- else if (option === true) {} else registry.set(type, option);
345
- return registry;
346
- };
347
- const canQueue = (payload) => typeof payload.jobs?.queue === "function";
348
- const deadline = (ms) => new Promise((resolve) => {
349
- setTimeout(resolve, ms).unref?.();
350
- });
351
- /**
352
- * Dispatch a submission's post-submit actions without throwing and without blocking the response on slow
353
- * action work. With no actions, returns immediately. When a job runner is present, enqueues the native
354
- * `form-builder-actions` task and returns (action work happens out of band). Otherwise runs the actions
355
- * inline but bounded by a deadline so a missing worker still delivers without hanging the request; any
356
- * error is swallowed (logged via `payload.logger`). Never rejects.
357
- */
358
- const dispatchActions = async (args) => {
359
- const { payload, registry, req, formId, submissionId } = args;
360
- if ((args.actions ?? []).length === 0) return;
361
- if (args.hasRunner && canQueue(payload)) {
362
- try {
363
- const input = {
364
- formId,
365
- submissionId
366
- };
367
- await payload.jobs.queue({
368
- task: ACTIONS_TASK_SLUG,
369
- input,
370
- req
371
- });
372
- } catch (error) {
373
- payload.logger?.error(`@10x-media/form-builder: failed to enqueue actions for submission ${String(submissionId)}: ${error instanceof Error ? error.message : String(error)}`);
374
- }
375
- return;
376
- }
377
- const ms = args.deadlineMs ?? 5e3;
378
- const work = runActionsForSubmission({
379
- input: {
380
- formId,
381
- submissionId
382
- },
383
- registry,
384
- payload,
385
- req
386
- }).catch((error) => {
387
- payload.logger?.error(`@10x-media/form-builder: inline action dispatch for submission ${String(submissionId)} threw: ${error instanceof Error ? error.message : String(error)}`);
388
- });
389
- await Promise.race([work, deadline(ms)]);
390
- };
391
- //#endregion
392
- //#region src/events/resolveEventSink.ts
393
- /** Resolves the configured event sink, falling back to the no-op sink. Consumed by the submission pipeline in a later phase. */
394
- const resolveEventSink = (sink) => sink ?? noopEventSink;
395
- //#endregion
396
- //#region src/spam/reserved.ts
397
- /**
398
- * Split reserved entries (honeypot decoy, captcha token) out of the submitted `values`. The honeypot
399
- * rides a configurable, innocuous field name; the captcha token rides a fixed reserved key. Real field
400
- * values pass through unchanged. `runSubmission` would already ignore unknown field names, but stripping
401
- * here keeps them out of storage and out of the validation pass.
402
- */
403
- const extractReservedValues = (values, honeypotField) => {
404
- const cleaned = [];
405
- const result = { cleaned };
406
- for (const entry of values) {
407
- if (entry.field === "__fb_captcha") {
408
- if (typeof entry.value === "string") result.captchaToken = entry.value;
409
- continue;
410
- }
411
- if (honeypotField !== null && entry.field === honeypotField) {
412
- result.honeypot = entry.value;
413
- continue;
414
- }
415
- cleaned.push(entry);
416
- }
417
- return result;
418
- };
419
- /** A honeypot is tripped when the decoy carries any non-empty value (a real user never fills it). */
420
- const isHoneypotTripped = (value) => value != null && value !== "" && !(Array.isArray(value) && value.length === 0);
421
- //#endregion
422
- //#region src/spam/spamGuard.ts
423
- const firstHop = (req, header) => {
424
- const raw = req.headers?.get(header);
425
- return raw ? raw.split(",")[0]?.trim() ?? void 0 : void 0;
426
- };
427
- /**
428
- * Submissions spam guard, prepended before `validateSubmission` in `beforeValidate` so it rejects before
429
- * the form load + field validation. On create it: resolves the request identity once (stashing it on
430
- * `req.context` for upload-ownership verification downstream); rate-limits per form + identity (429,
431
- * fail-open when identity is null); strips reserved `values` entries (honeypot, captcha token); rejects a
432
- * filled honeypot with a generic error; verifies a configured captcha; and writes a server-authoritative
433
- * `meta` (timestamp + spam signal, with opt-in ip/ua). App-level rate limiting is defense-in-depth that
434
- * complements edge/CDN/WAF limiting, not a DoS replacement.
435
- */
436
- const buildSpamGuard = (spam) => async ({ data, operation, req }) => {
437
- if (operation !== "create" || !data) return data;
438
- const t = asTranslate(req.i18n.t);
439
- const authenticated = Boolean(req.user);
440
- const identity = await spam.identify(req);
441
- if (identity != null) req.context[IDENTITY_CONTEXT_KEY] = identity;
442
- if (spam.rateLimit !== false && identity != null) {
443
- const formKey = data.form != null ? String(data.form) : "unknown";
444
- const { ok } = await spam.rateLimit.limiter.check({
445
- key: `submissions:${formKey}:${identity}`,
446
- max: spam.rateLimit.max,
447
- window: spam.rateLimit.window,
448
- req
449
- });
450
- if (!ok) throw new APIError(t(keys.spamRateLimited), 429);
451
- }
452
- const honeypotField = spam.honeypot === false ? null : spam.honeypot.fieldName;
453
- const { cleaned, honeypot, captchaToken } = extractReservedValues(data.values ?? [], honeypotField);
454
- data.values = cleaned;
455
- if (!authenticated && honeypotField !== null && isHoneypotTripped(honeypot)) throw new APIError(t(keys.spamRejected), 400);
456
- const captchaChecked = Boolean(spam.captcha) && !authenticated;
457
- if (spam.captcha && !authenticated) {
458
- if (!(typeof captchaToken === "string" && captchaToken.length > 0 ? await spam.captcha.verify({
459
- token: captchaToken,
460
- req
461
- }).catch(() => false) : false)) throw new APIError(t(keys.spamCaptchaFailed), 400);
462
- }
463
- const serverMeta = {
464
- at: (/* @__PURE__ */ new Date()).toISOString(),
465
- spam: { captcha: captchaChecked ? "passed" : "skipped" }
466
- };
467
- if (spam.metadata.ip) {
468
- const ip = firstHop(req, spam.ipHeader);
469
- if (ip) serverMeta.ip = ip;
470
- }
471
- if (spam.metadata.ua) {
472
- const ua = req.headers?.get("user-agent");
473
- if (ua) serverMeta.ua = ua;
474
- }
475
- data.meta = {
476
- ...authenticated && data.meta != null && typeof data.meta === "object" ? data.meta : {},
477
- ...serverMeta
478
- };
479
- return data;
480
- };
481
- //#endregion
482
- //#region src/actions/buildActionBlocks.ts
483
- /** One authoring block per registered action type: the action's own config fields. */
484
- const buildActionBlocks = (registry) => {
485
- const blocks = [];
486
- for (const definition of registry.values()) blocks.push({
487
- slug: definition.type,
488
- labels: {
489
- singular: labelFor(definition.label),
490
- plural: labelFor(definition.label)
491
- },
492
- fields: definition.config ?? []
493
- });
494
- return blocks;
495
- };
496
- //#endregion
497
- //#region src/aggregation/aggregateRows.ts
498
- const round1 = (n) => Math.round(n * 10) / 10;
499
- /** The grouping keys a single answer contributes: [] for empty, one per element for arrays, else one. */
500
- const valueKeys = (value) => {
501
- if (value == null || value === "") return [];
502
- if (Array.isArray(value)) return value.filter((entry) => entry != null && entry !== "").map((entry) => String(entry));
503
- return [String(value)];
504
- };
505
- /** Merge every row's descriptor snapshot for one field into a value -> label map (last write wins). */
506
- const snapshotLabels = (rows, field) => {
507
- const labels = /* @__PURE__ */ new Map();
508
- for (const row of rows) for (const descriptor of row.descriptors ?? []) if (descriptor.field === field && descriptor.optionLabels) for (const [value, label] of Object.entries(descriptor.optionLabels)) labels.set(value, label);
509
- return labels;
510
- };
511
- /** Aggregate one field across rows: respondent-denominated counts + percentages, option-ordered. */
512
- const aggregateRowForField = (rows, meta, truncated) => {
513
- const counts = /* @__PURE__ */ new Map();
514
- const order = [];
515
- for (const option of meta.options ?? []) {
516
- counts.set(option.value, 0);
517
- order.push(option.value);
518
- }
519
- const snapshots = snapshotLabels(rows, meta.field);
520
- let total = 0;
521
- for (const row of rows) {
522
- const keys = valueKeys((row.values ?? []).find((entry) => entry.field === meta.field)?.value);
523
- if (keys.length === 0) continue;
524
- total += 1;
525
- for (const key of keys) {
526
- if (!counts.has(key)) {
527
- counts.set(key, 0);
528
- order.push(key);
529
- }
530
- counts.set(key, (counts.get(key) ?? 0) + 1);
531
- }
532
- }
533
- const optionValues = new Set((meta.options ?? []).map((option) => option.value));
534
- const leftovers = order.filter((value) => !optionValues.has(value)).sort((a, b) => (counts.get(b) ?? 0) - (counts.get(a) ?? 0));
535
- const ordered = [...(meta.options ?? []).map((option) => option.value), ...leftovers];
536
- const optionLabelByValue = new Map((meta.options ?? []).map((option) => [option.value, option.label]));
537
- const buckets = ordered.map((value) => {
538
- const count = counts.get(value) ?? 0;
539
- return {
540
- value,
541
- label: optionLabelByValue.get(value) ?? snapshots.get(value) ?? value,
542
- count,
543
- percentage: total > 0 ? round1(count / total * 100) : 0
544
- };
545
- });
546
- return {
547
- field: meta.field,
548
- label: meta.label,
549
- fieldType: meta.fieldType,
550
- total,
551
- buckets,
552
- truncated
553
- };
554
- };
555
- /** Aggregate several fields across the same set of rows. */
556
- const aggregateRowsForFields = (rows, metas, truncated) => metas.map((meta) => aggregateRowForField(rows, meta, truncated));
557
- //#endregion
558
- //#region src/aggregation/aggregateResponses.ts
559
- const optionsOf = (field) => {
560
- if (!Array.isArray(field.options)) return;
561
- const options = field.options.filter((option) => typeof option?.value === "string").map((option) => ({
562
- value: String(option.value),
563
- label: option.label ?? String(option.value)
564
- }));
565
- return options.length > 0 ? options : void 0;
566
- };
567
- /** True when a field declares non-empty options (a choice field safe to aggregate publicly). */
568
- const fieldHasOptions = (field) => optionsOf(field) !== void 0;
569
- const metaFor = (field) => ({
570
- field: field.name,
571
- label: field.label ?? field.name,
572
- fieldType: field.blockType,
573
- options: optionsOf(field)
574
- });
575
- /**
576
- * Aggregate submission responses for a form. Loads the form for current labels + option order, then pages
577
- * `payload.find` over its submissions (JSON `values` cannot be GROUP BY'd portably across Mongo + Postgres,
578
- * so we scan + reduce in JS, cross-DB-identically). Bounded by `maxSubmissions`; beyond it the result is
579
- * `truncated`. Returns one `FieldAggregation` per requested field (or per enumerable field by default).
580
- */
581
- const aggregateFormResponses = async (args) => {
582
- const { payload, formId, fields, status = "complete", req, maxSubmissions = 1e4, pageSize = 500 } = args;
583
- const form = await payload.findByID({
584
- collection: FORMS_SLUG,
585
- id: formId,
586
- depth: 0,
587
- overrideAccess: true,
588
- req
589
- }).catch(() => null);
590
- if (!form || !Array.isArray(form.fields)) return [];
591
- const instances = form.fields;
592
- const selected = fields ? instances.filter((field) => fields.includes(field.name)) : instances.filter((field) => optionsOf(field) !== void 0);
593
- if (selected.length === 0) return [];
594
- const metas = selected.map(metaFor);
595
- const where = { and: [{ form: { equals: formId } }, ...status === "all" ? [] : [{ status: { equals: status } }]] };
596
- const rows = [];
597
- let page = 1;
598
- let truncated = false;
599
- for (;;) {
600
- const result = await payload.find({
601
- collection: FORM_SUBMISSIONS_SLUG,
602
- where,
603
- limit: pageSize,
604
- page,
605
- depth: 0,
606
- overrideAccess: true,
607
- req,
608
- select: {
609
- values: true,
610
- descriptors: true
611
- }
612
- });
613
- for (const doc of result.docs) {
614
- if (rows.length >= maxSubmissions) {
615
- truncated = true;
616
- break;
617
- }
618
- rows.push({
619
- values: doc.values,
620
- descriptors: doc.descriptors
621
- });
622
- }
623
- if (truncated || !result.hasNextPage) break;
624
- page += 1;
625
- }
626
- return aggregateRowsForFields(rows, metas, truncated);
627
- };
628
- /** Single-field convenience over `aggregateFormResponses`. Returns the field's aggregation, or null if absent. */
629
- const aggregateFieldResponses = async (args) => {
630
- const { field, ...rest } = args;
631
- const [result] = await aggregateFormResponses({
632
- ...rest,
633
- fields: [field]
634
- });
635
- return result ?? null;
636
- };
637
- //#endregion
638
- //#region src/aggregation/resolveResultsRequest.ts
639
- const forbidden = {
640
- status: 403,
641
- body: { errors: [{ message: "Forbidden" }] }
642
- };
643
- /**
644
- * Authorize and resolve a poll/survey results request. Authed callers may aggregate any field (or all
645
- * enumerable fields). Anonymous callers are allowed only when the form opted in (`showResults`), and then
646
- * only for the configured `resultsField`, and only if that field is enumerable (has options) so a
647
- * misconfigured `resultsField` pointing at a free-text or PII field can never be dumped publicly. Returns
648
- * only aggregate counts, never raw submissions.
649
- */
650
- const resolveFormResultsRequest = async (args) => {
651
- const { payload, formId, field, isAuthed, req } = args;
652
- if (formId == null) return {
653
- status: 400,
654
- body: { errors: [{ message: "Missing form id" }] }
655
- };
656
- const form = await payload.findByID({
657
- collection: FORMS_SLUG,
658
- id: formId,
659
- depth: 0,
660
- overrideAccess: true,
661
- req
662
- }).catch(() => null);
663
- if (!form) return {
664
- status: 404,
665
- body: { errors: [{ message: "Not found" }] }
666
- };
667
- let fields;
668
- if (isAuthed) fields = field ? [field] : void 0;
669
- else {
670
- if (form.showResults !== true) return forbidden;
671
- const publicField = typeof form.resultsField === "string" && form.resultsField.length > 0 ? form.resultsField : void 0;
672
- if (!publicField) return forbidden;
673
- if (field && field !== publicField) return forbidden;
674
- const instance = (Array.isArray(form.fields) ? form.fields : []).find((entry) => entry.name === publicField);
675
- if (!instance || !fieldHasOptions(instance)) return forbidden;
676
- fields = [publicField];
677
- }
678
- return {
679
- status: 200,
680
- body: { results: await aggregateFormResponses({
681
- payload,
682
- formId,
683
- fields,
684
- req
685
- }) }
686
- };
687
- };
688
- //#endregion
689
- //#region src/calc/normalizeCalc.ts
690
- const MAX_DEPTH = 64;
691
- const OP_SET = new Set([
692
- "+",
693
- "-",
694
- "*",
695
- "/",
696
- "%"
697
- ]);
698
- const FN_SET = new Set([
699
- "min",
700
- "max",
701
- "round",
702
- "abs",
703
- "ceil",
704
- "floor"
705
- ]);
706
- const isRecord$1 = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
707
- const normalizeNode = (value, depth) => {
708
- if (depth > MAX_DEPTH) return void 0;
709
- if (!isRecord$1(value)) return void 0;
710
- switch (value.type) {
711
- case "lit": {
712
- const { value: v } = value;
713
- if (typeof v !== "number" || !Number.isFinite(v)) return void 0;
714
- return {
715
- type: "lit",
716
- value: v
717
- };
718
- }
719
- case "ref": {
720
- const { field } = value;
721
- if (typeof field !== "string") return void 0;
722
- return {
723
- type: "ref",
724
- field
725
- };
726
- }
727
- case "op": {
728
- const { op, left, right } = value;
729
- if (typeof op !== "string" || !OP_SET.has(op)) return void 0;
730
- const l = normalizeNode(left, depth + 1);
731
- if (!l) return void 0;
732
- const r = normalizeNode(right, depth + 1);
733
- if (!r) return void 0;
734
- return {
735
- type: "op",
736
- op,
737
- left: l,
738
- right: r
739
- };
740
- }
741
- case "neg": {
742
- const operand = normalizeNode(value.operand, depth + 1);
743
- if (!operand) return void 0;
744
- return {
745
- type: "neg",
746
- operand
747
- };
748
- }
749
- case "fn": {
750
- const { fn, args } = value;
751
- if (typeof fn !== "string" || !FN_SET.has(fn)) return void 0;
752
- if (!Array.isArray(args)) return void 0;
753
- const normalizedArgs = [];
754
- for (const arg of args) {
755
- const a = normalizeNode(arg, depth + 1);
756
- if (!a) return void 0;
757
- normalizedArgs.push(a);
758
- }
759
- return {
760
- type: "fn",
761
- fn,
762
- args: normalizedArgs
763
- };
764
- }
765
- case "weight": {
766
- const { field, weights } = value;
767
- if (typeof field !== "string") return void 0;
768
- if (!isRecord$1(weights)) return void 0;
769
- const normalizedWeights = {};
770
- for (const [k, v] of Object.entries(weights)) {
771
- if (typeof v !== "number" || !Number.isFinite(v)) return void 0;
772
- normalizedWeights[k] = v;
773
- }
774
- return {
775
- type: "weight",
776
- field,
777
- weights: normalizedWeights
778
- };
779
- }
780
- default: return;
781
- }
782
- };
783
- /** Structural guard: returns a valid CalcExpression if `value` is structurally sound (depth-guarded at 64), otherwise undefined. Never throws. */
784
- const normalizeCalc = (value) => normalizeNode(value, 0);
785
- //#endregion
786
- //#region src/conditions/conditionType.ts
787
- /** The condition input a field type uses in the builder: its declared `conditionType` or the value-kind default. */
788
- const conditionTypeForDefinition = (definition) => definition.conditionType ?? defaultConditionType(definition.value);
789
- /** A serializable slug -> condition type map for every registered field type, handed to the client builder. */
790
- const buildConditionTypeMap = (registry) => {
791
- const map = {};
792
- for (const definition of registry.values()) map[definition.type] = conditionTypeForDefinition(definition);
793
- return map;
794
- };
795
- //#endregion
796
- //#region src/conditions/normalizeConditions.ts
797
- const isValidConstraint = (constraint, operandTypes) => {
798
- if (constraint == null || typeof constraint !== "object") return false;
799
- const field = Object.keys(constraint)[0];
800
- if (!field) return false;
801
- const type = operandTypes.get(field);
802
- if (!type) return false;
803
- const ops = constraint[field];
804
- if (ops == null || typeof ops !== "object") return false;
805
- const operator = Object.keys(ops)[0];
806
- return Boolean(operator && conditionOperators[type].includes(operator));
807
- };
808
- const normalizeOne = (raw, operandTypes) => {
809
- if (raw == null || typeof raw !== "object" || Object.keys(raw).length === 0) return;
810
- const canonical = transformWhereQuery(raw);
811
- const groups = (Array.isArray(canonical.or) ? canonical.or : Array.isArray(canonical.and) ? [{ and: canonical.and }] : []).map((group) => {
812
- return (Array.isArray(group.and) ? group.and : []).filter((constraint) => isValidConstraint(constraint, operandTypes));
813
- }).filter((group) => group.length > 0);
814
- return groups.length > 0 ? { or: groups.map((and) => ({ and })) } : void 0;
815
- };
816
- /**
817
- * Normalize every field's `visibleWhen`/`validateWhen` against the form's own field list. Canonicalizes
818
- * to OR-of-ANDs and strips constraints whose operand field is missing or whose operator is invalid for
819
- * that field's condition type, so stored conditions always match what `evaluateCondition` can run.
820
- */
821
- const normalizeFormConditions = (fields, conditionTypes) => {
822
- const operandTypes = /* @__PURE__ */ new Map();
823
- for (const row of fields) {
824
- const name = typeof row.name === "string" ? row.name.trim() : "";
825
- if (name.length > 0) operandTypes.set(name, conditionTypes[row.blockType] ?? "text");
826
- }
827
- return fields.map((row) => {
828
- const visibleWhen = normalizeOne(row.visibleWhen, operandTypes);
829
- const validateWhen = normalizeOne(row.validateWhen, operandTypes);
830
- return {
831
- ...row,
832
- visibleWhen,
833
- validateWhen
834
- };
835
- });
836
- };
837
- //#endregion
838
- //#region src/validation/buildRuleBlocks.ts
839
- /** Each rule block appends its own `message` and `severity` fields, so a rule param of either name would collide and be silently dropped by Payload. */
840
- const RESERVED_PARAM_NAMES = new Set(["message", "severity"]);
841
- /** One block per rule applicable to `fieldType` (gated by `appliesTo`): the rule params, then a message override and severity. */
842
- const buildRuleBlocks = (registry, fieldType) => {
843
- const blocks = [];
844
- for (const rule of registry.values()) {
845
- if (rule.appliesTo && !rule.appliesTo.includes(fieldType)) continue;
846
- for (const param of rule.params ?? []) if ("name" in param && RESERVED_PARAM_NAMES.has(param.name)) throw new Error(`form-builder: validation rule "${rule.type}" declares a reserved param name "${param.name}". The names "message" and "severity" are reserved for the constraint-list UI.`);
847
- blocks.push({
848
- slug: rule.type,
849
- labels: {
850
- singular: labelFor(rule.label),
851
- plural: labelFor(rule.label)
852
- },
853
- fields: [
854
- ...rule.params ?? [],
855
- {
856
- name: "message",
857
- type: "text",
858
- label: labelFor(keys.validationMessageLabel)
859
- },
860
- {
861
- name: "severity",
862
- type: "select",
863
- defaultValue: "error",
864
- label: labelFor(keys.validationSeverityLabel),
865
- options: [{
866
- label: labelFor(keys.validationSeverityError),
867
- value: "error"
868
- }, {
869
- label: labelFor(keys.validationSeverityWarning),
870
- value: "warning"
871
- }]
872
- }
873
- ]
874
- });
875
- }
876
- return blocks;
877
- };
878
- //#endregion
879
- //#region src/fields/sharedConfig.ts
880
- const CONDITION_FIELD_REF = "@10x-media/form-builder/client#FormConditionField";
881
- const conditionField = (name, labelKey, conditionTypes) => ({
882
- name,
883
- type: "json",
884
- label: labelFor(labelKey),
885
- admin: { components: { Field: {
886
- path: CONDITION_FIELD_REF,
887
- clientProps: { conditionTypes }
888
- } } }
889
- });
890
- /**
891
- * Config every field instance carries regardless of type. `name` is the machine key written into
892
- * submissions; `width` is stored for the layout grid. `visibleWhen`/`validateWhen` store a
893
- * canonical Payload `Where`, edited by the native condition builder. Field types add their own `config`
894
- * after these.
895
- */
896
- const sharedFieldConfig = (conditionTypes) => [
897
- {
898
- name: "name",
899
- type: "text",
900
- required: true,
901
- label: labelFor(keys.configName)
902
- },
903
- {
904
- name: "label",
905
- type: "text",
906
- label: labelFor(keys.configLabel)
907
- },
908
- {
909
- name: "required",
910
- type: "checkbox",
911
- label: labelFor(keys.configRequired)
912
- },
913
- {
914
- name: "width",
915
- type: "select",
916
- defaultValue: "full",
917
- label: labelFor(keys.configWidth),
918
- options: [
919
- {
920
- label: "Full",
921
- value: "full"
922
- },
923
- {
924
- label: "Half",
925
- value: "half"
926
- },
927
- {
928
- label: "Third",
929
- value: "third"
930
- },
931
- {
932
- label: "Two thirds",
933
- value: "twoThirds"
934
- }
935
- ]
936
- },
937
- {
938
- name: "placeholder",
939
- type: "text",
940
- label: labelFor(keys.configPlaceholder)
941
- },
942
- {
943
- name: "description",
944
- type: "textarea",
945
- label: labelFor(keys.configDescription)
946
- },
947
- {
948
- type: "collapsible",
949
- label: labelFor(keys.configAdvanced),
950
- admin: { initCollapsed: true },
951
- fields: [
952
- conditionField("visibleWhen", keys.configVisibleWhen, conditionTypes),
953
- conditionField("validateWhen", keys.configValidateWhen, conditionTypes),
954
- {
955
- name: "hidden",
956
- type: "checkbox",
957
- label: labelFor(keys.configHidden)
958
- }
959
- ]
960
- }
961
- ];
962
- //#endregion
963
- //#region src/fields/buildFieldBlocks.ts
964
- /** One add-field block per registered type: shared config, the type's own config, then its validations. */
965
- const buildFieldBlocks = (registry, ruleRegistry) => {
966
- const conditionTypes = buildConditionTypeMap(registry);
967
- const blocks = [];
968
- for (const definition of registry.values()) blocks.push({
969
- slug: definition.type,
970
- labels: {
971
- singular: labelFor(definition.label),
972
- plural: labelFor(definition.label)
973
- },
974
- fields: [
975
- ...sharedFieldConfig(conditionTypes),
976
- ...definition.config ?? [],
977
- {
978
- name: "validations",
979
- type: "blocks",
980
- label: labelFor(keys.validationsLabel),
981
- blocks: buildRuleBlocks(ruleRegistry, definition.type)
982
- }
983
- ]
984
- });
985
- return blocks;
986
- };
987
- //#endregion
988
- //#region src/flow/normalizeFlow.ts
989
- const isRecord = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
990
- /**
991
- * Pure guard that normalizes a raw (possibly untrusted) flow value against the
992
- * form's current field list. Returns `undefined` when the flow is absent, empty,
993
- * or resolves to fewer than two steps (a single step is an ordinary form).
994
- */
995
- const normalizeFlow = (raw, fieldNames) => {
996
- if (!isRecord(raw) || !Array.isArray(raw.steps)) return;
997
- const knownFields = new Set(fieldNames);
998
- const rawSteps = raw.steps.filter((s) => isRecord(s) && typeof s.id === "string");
999
- const knownIds = new Set(rawSteps.map((s) => s.id));
1000
- if (knownIds.size < 2) return;
1001
- return { steps: rawSteps.map((s) => {
1002
- const id = s.id;
1003
- const fields = Array.isArray(s.fields) ? s.fields.filter((f) => typeof f === "string" && knownFields.has(f)) : [];
1004
- const transitions = (Array.isArray(s.transitions) ? s.transitions : []).filter((t) => isRecord(t) && typeof t.to === "string" && knownIds.has(t.to) && isRecord(t.when)).map((t) => ({
1005
- when: t.when,
1006
- to: t.to
1007
- }));
1008
- const next = typeof s.next === "string" && knownIds.has(s.next) ? s.next : void 0;
1009
- const step = {
1010
- id,
1011
- fields
1012
- };
1013
- if (typeof s.title === "string") step.title = s.title;
1014
- if (transitions.length > 0) step.transitions = transitions;
1015
- if (next !== void 0) step.next = next;
1016
- return step;
1017
- }) };
1018
- };
1019
- //#endregion
1020
- //#region src/collections/forms.ts
1021
- const FORMS_SLUG = "forms";
1022
- const buildFormsCollection = ({ registry, ruleRegistry, presentationRegistry = new Map(Object.entries(defaultPresentationDescriptors)), actionRegistry = /* @__PURE__ */ new Map() }) => {
1023
- const conditionTypes = buildConditionTypeMap(registry);
1024
- const beforeValidate = ({ data }) => {
1025
- if (data && typeof data.defaultPresentation === "string" && !presentationRegistry.has(data.defaultPresentation)) data.defaultPresentation = DEFAULT_PRESENTATION_NAME;
1026
- if (data && Array.isArray(data.fields)) {
1027
- const normalized = normalizeFormConditions(data.fields, conditionTypes);
1028
- for (const field of normalized) if ("expression" in field) field.expression = normalizeCalc(field.expression);
1029
- data.fields = normalized;
1030
- const fieldNames = normalized.map((field) => typeof field.name === "string" ? field.name : void 0).filter((name) => name !== void 0);
1031
- data.flow = normalizeFlow(data.flow, fieldNames);
1032
- }
1033
- return data;
1034
- };
1035
- return {
1036
- slug: FORMS_SLUG,
1037
- labels: {
1038
- singular: "Form",
1039
- plural: "Forms"
1040
- },
1041
- admin: {
1042
- group: "Forms",
1043
- useAsTitle: "title"
1044
- },
1045
- access: { read: () => true },
1046
- hooks: { beforeValidate: [beforeValidate] },
1047
- fields: [
1048
- {
1049
- name: "title",
1050
- type: "text",
1051
- required: true,
1052
- label: labelForKey(keys.fieldTitle)
1053
- },
1054
- {
1055
- name: "fields",
1056
- type: "blocks",
1057
- blocks: buildFieldBlocks(registry, ruleRegistry)
1058
- },
1059
- {
1060
- name: "flow",
1061
- type: "json"
1062
- },
1063
- {
1064
- name: "actions",
1065
- type: "blocks",
1066
- blocks: buildActionBlocks(actionRegistry),
1067
- label: labelForKey(keys.configActions)
1068
- },
1069
- {
1070
- name: "defaultPresentation",
1071
- type: "select",
1072
- defaultValue: DEFAULT_PRESENTATION_NAME,
1073
- options: [...presentationRegistry.values()].map((descriptor) => ({
1074
- label: labelFor(descriptor.label),
1075
- value: descriptor.name
1076
- })),
1077
- label: labelForKey(keys.configDefaultPresentation),
1078
- admin: { position: "sidebar" }
1079
- },
1080
- {
1081
- name: "showResults",
1082
- type: "checkbox",
1083
- defaultValue: false,
1084
- label: labelForKey(keys.configShowResults),
1085
- admin: { position: "sidebar" }
1086
- },
1087
- {
1088
- name: "resultsField",
1089
- type: "text",
1090
- label: labelForKey(keys.configResultsField),
1091
- admin: {
1092
- position: "sidebar",
1093
- description: "Field whose aggregate results are public when \"Show results publicly\" is on. Use a choice field, never a free-text or PII field.",
1094
- condition: (data) => Boolean(data?.showResults)
1095
- }
1096
- }
1097
- ],
1098
- endpoints: [{
1099
- path: "/:id/results",
1100
- method: "get",
1101
- handler: async (req) => {
1102
- const field = typeof req.query?.field === "string" ? req.query.field : void 0;
1103
- const { status, body } = await resolveFormResultsRequest({
1104
- payload: req.payload,
1105
- formId: req.routeParams?.id,
1106
- field,
1107
- isAuthed: Boolean(req.user),
1108
- req
1109
- });
1110
- return Response.json(body, { status });
1111
- }
1112
- }]
1113
- };
1114
- };
1115
- //#endregion
1116
- //#region src/consent/resolveConsentLinks.ts
1117
- const EMPTY = { links: [] };
1118
- /**
1119
- * Resolve a consent field's policy links via its configured source (for display).
1120
- * Unknown/missing source returns empty links. Never throws.
1121
- */
1122
- const resolveConsentLinks = async (field, ctx) => {
1123
- const sourceType = typeof field.source === "string" ? field.source : "static";
1124
- const source = ctx.registry.get(sourceType);
1125
- if (!source) return EMPTY;
1126
- const sourceConfig = field.sourceConfig && typeof field.sourceConfig === "object" ? field.sourceConfig : {};
1127
- try {
1128
- return await source.resolve({
1129
- config: sourceConfig,
1130
- payload: ctx.payload,
1131
- req: ctx.req,
1132
- locale: ctx.locale
1133
- });
1134
- } catch {
1135
- return EMPTY;
1136
- }
1137
- };
1138
- //#endregion
1139
- //#region src/consent/captureConsent.ts
1140
- /**
1141
- * Build the authoritative consent proof at submit time: re-resolve the source server-side
1142
- * (ignores any client ref), capturing a reference url -- never the policy text.
1143
- * `now` is injected by the caller (`new Date().toISOString()`) for testability.
1144
- */
1145
- const captureConsent = async (args) => {
1146
- const resolved = await resolveConsentLinks(args.field, {
1147
- registry: args.registry,
1148
- payload: args.payload,
1149
- req: args.req,
1150
- locale: args.locale
1151
- });
1152
- const ref = resolved.links[0]?.url || void 0;
1153
- return {
1154
- agreed: args.agreed,
1155
- ...ref ? { ref } : {},
1156
- ...resolved.versionRef ? { versionRef: resolved.versionRef } : {},
1157
- at: args.now
1158
- };
1159
- };
1160
- //#endregion
1161
- //#region src/uploads/resolveFileRef.ts
1162
- const mimeAllowed = (mimeType, allow) => {
1163
- if (!allow || allow.length === 0) return true;
1164
- return allow.some((pattern) => {
1165
- if (pattern.endsWith("/*")) return mimeType.startsWith(pattern.slice(0, -1));
1166
- return mimeType === pattern;
1167
- });
1168
- };
1169
- /**
1170
- * Pure server trust-boundary check for an uploaded file: given the loaded upload doc (or null) and the
1171
- * field's constraints, either return an authoritative `FileRef` snapshot or a rejection code. Never trusts
1172
- * client metadata; the caller passes the doc it loaded from the upload collection.
1173
- */
1174
- const resolveFileRef = (doc, config) => {
1175
- if (doc == null || doc.id == null) return {
1176
- ok: false,
1177
- code: "missing"
1178
- };
1179
- const mimeType = typeof doc.mimeType === "string" ? doc.mimeType : "";
1180
- const filesize = typeof doc.filesize === "number" ? doc.filesize : 0;
1181
- if (!mimeAllowed(mimeType, config.mimeTypes)) return {
1182
- ok: false,
1183
- code: "mimeType"
1184
- };
1185
- if (typeof config.maxSize === "number" && filesize > config.maxSize) return {
1186
- ok: false,
1187
- code: "tooLarge"
1188
- };
1189
- const ref = {
1190
- id: doc.id,
1191
- filename: typeof doc.filename === "string" ? doc.filename : "",
1192
- mimeType,
1193
- filesize
1194
- };
1195
- if (typeof doc.url === "string" && doc.url.length > 0) ref.url = doc.url;
1196
- return {
1197
- ok: true,
1198
- ref
1199
- };
1200
- };
1201
- //#endregion
1202
- //#region src/uploads/captureFileRef.ts
1203
- /**
1204
- * Load the referenced upload doc and run the pure trust-boundary check. The client sends only the id; the
1205
- * filename/mimeType/filesize are read authoritatively from the stored doc, never from the client. When the
1206
- * upload carries an `owner` stamp AND the submitter is identifiable, the two must match, so an anonymous
1207
- * submitter cannot capture another identity's upload; a mismatch collapses to `missing`, indistinguishable
1208
- * from a deleted upload. When the submitter cannot be identified (no `expectedOwner`), ownership is not
1209
- * enforced (fail-open, consistent with rate-limiting): a proxy-configured deployment identifies every
1210
- * request, so this only relaxes scoping where it could not be applied fairly anyway. Unstamped uploads
1211
- * (no identity at upload time, or a BYO collection without the field) pass unchanged.
1212
- */
1213
- const captureFileRef = async (args) => {
1214
- const { payload, collectionSlug, uploadId, config, req, expectedOwner } = args;
1215
- const doc = await payload.findByID({
1216
- collection: collectionSlug,
1217
- id: uploadId,
1218
- depth: 0,
1219
- overrideAccess: true,
1220
- req
1221
- }).catch(() => null);
1222
- if (doc && expectedOwner != null) {
1223
- const owner = doc.owner;
1224
- if (typeof owner === "string" && owner.length > 0 && owner !== expectedOwner) return {
1225
- ok: false,
1226
- code: "missing"
1227
- };
1228
- }
1229
- return resolveFileRef(doc, config);
1230
- };
1231
- //#endregion
1232
- //#region src/submissions/runSubmission.ts
1233
- const errorKeyFor = (code) => {
1234
- if (code === "mimeType") return keys.validationFileMimeType;
1235
- if (code === "tooLarge") return keys.validationFileTooLarge;
1236
- return keys.validationFileMissing;
1237
- };
1238
- /** Normalize the authored `mimeTypes` (a `hasMany` text field) to a `string[]`. */
1239
- const mimeTypesOf = (raw) => {
1240
- if (!Array.isArray(raw)) return;
1241
- const out = raw.filter((entry) => typeof entry === "string");
1242
- return out.length > 0 ? out : void 0;
1243
- };
1244
- const fileFieldConfigOf = (instance) => ({
1245
- relationTo: typeof instance.relationTo === "string" ? instance.relationTo : void 0,
1246
- mimeTypes: mimeTypesOf(instance.mimeTypes),
1247
- maxSize: typeof instance.maxSize === "number" ? instance.maxSize : void 0
1248
- });
1249
- const isEmpty = (value) => value == null || value === "" || Array.isArray(value) && value.length === 0;
1250
- const coerce = (kind, value) => {
1251
- if (value == null) return value;
1252
- if (kind === "number") {
1253
- const next = typeof value === "number" ? value : Number(value);
1254
- return Number.isNaN(next) ? value : next;
1255
- }
1256
- if (kind === "boolean") {
1257
- if (typeof value === "string") {
1258
- const normalized = value.trim().toLowerCase();
1259
- return !(normalized === "" || normalized === "false" || normalized === "0" || normalized === "off" || normalized === "no");
1260
- }
1261
- return Boolean(value);
1262
- }
1263
- if (kind === "text") return typeof value === "string" ? value : String(value);
1264
- return value;
1265
- };
1266
- const optionLabelsFor$1 = (instance) => {
1267
- const options = instance.options;
1268
- if (!Array.isArray(options)) return;
1269
- const map = {};
1270
- for (const option of options) if (typeof option?.value === "string") map[option.value] = option.label ?? option.value;
1271
- return Object.keys(map).length > 0 ? map : void 0;
1272
- };
1273
- /**
1274
- * Pure submission core, two-pass: first coerce every answered field to its typed kind (so cross-field
1275
- * rules see coerced siblings), then validate each field through `runValidation` (required, intrinsic
1276
- * facet, declarative rules), snapshotting a localized descriptor per answered field. Calc fields are
1277
- * the trust boundary: their client-sent values are never seeded, their values are derived from their
1278
- * expressions over the coerced answers, and those derived values are authoritative everywhere downstream
1279
- * (conditions, validation, storage). Conditions gate the second pass against these effective answers: a
1280
- * field whose `visibleWhen` is false is skipped entirely (never validated, never stored, so a client-sent
1281
- * value for it is ignored), and a visible field whose `validateWhen` is false stores its value but skips
1282
- * validation. A visible calc field stores its derived value and is never validated. Only `error` severity
1283
- * blocks; warnings are computed but not surfaced server-side (the renderer surfaces them).
1284
- */
1285
- const runSubmission = async (input) => {
1286
- const { fields, values, registry, ruleRegistry, consentRegistry, locale, t, operation, req, payload, formId, uploadSlug, expectedOwner } = input;
1287
- const incoming = new Map(values.map((entry) => [entry.field, entry.value]));
1288
- const coercedAnswers = {};
1289
- const coercedByName = /* @__PURE__ */ new Map();
1290
- for (const instance of fields) {
1291
- const definition = registry.get(instance.blockType);
1292
- const raw = incoming.get(instance.name);
1293
- if (!definition || calcExpressionOf(instance)) continue;
1294
- const effectiveRaw = instance.blockType === "consent" && isEmpty(raw) ? false : raw;
1295
- if (isEmpty(effectiveRaw)) continue;
1296
- const value = coerce(definition.value, effectiveRaw);
1297
- coercedAnswers[instance.name] = value;
1298
- coercedByName.set(instance.name, value);
1299
- }
1300
- const effective = computeCalcFields(fields, coercedAnswers);
1301
- const errors = [];
1302
- const outValues = [];
1303
- const descriptors = [];
1304
- const consentProofs = [];
1305
- const now = (/* @__PURE__ */ new Date()).toISOString();
1306
- for (const instance of fields) {
1307
- const definition = registry.get(instance.blockType);
1308
- if (!definition) continue;
1309
- const raw = incoming.get(instance.name);
1310
- const value = coercedByName.has(instance.name) ? coercedByName.get(instance.name) : raw;
1311
- if (!evaluateCondition(instance.visibleWhen, effective)) continue;
1312
- if (calcExpressionOf(instance)) {
1313
- outValues.push({
1314
- field: instance.name,
1315
- value: effective[instance.name]
1316
- });
1317
- descriptors.push({
1318
- field: instance.name,
1319
- label: instance.label ?? instance.name,
1320
- fieldType: instance.blockType
1321
- });
1322
- continue;
1323
- }
1324
- if (evaluateCondition(instance.validateWhen, effective)) {
1325
- const { errors: issues } = await runValidation({
1326
- field: instance,
1327
- fieldDefinition: definition,
1328
- value,
1329
- fieldType: instance.blockType,
1330
- ruleRegistry,
1331
- answers: effective,
1332
- locale,
1333
- t,
1334
- operation,
1335
- event: "submit",
1336
- mode: "server",
1337
- req,
1338
- payload,
1339
- formId
1340
- });
1341
- const blocking = issues.filter((issue) => issue.severity === "error");
1342
- if (blocking.length > 0) {
1343
- for (const issue of blocking) errors.push({
1344
- path: instance.name,
1345
- message: issue.message
1346
- });
1347
- continue;
1348
- }
1349
- }
1350
- if (instance.blockType === "file") {
1351
- if (isEmpty(value)) continue;
1352
- if (payload) {
1353
- const fileConfig = fileFieldConfigOf(instance);
1354
- const captured = await captureFileRef({
1355
- payload,
1356
- collectionSlug: fileConfig.relationTo ?? uploadSlug ?? "form-uploads",
1357
- uploadId: value,
1358
- config: fileConfig,
1359
- req,
1360
- expectedOwner
1361
- });
1362
- if (!captured.ok) {
1363
- errors.push({
1364
- path: instance.name,
1365
- message: t(errorKeyFor(captured.code))
1366
- });
1367
- continue;
1368
- }
1369
- outValues.push({
1370
- field: instance.name,
1371
- value: captured.ref
1372
- });
1373
- descriptors.push({
1374
- field: instance.name,
1375
- label: instance.label ?? instance.name,
1376
- fieldType: instance.blockType
1377
- });
1378
- continue;
1379
- }
1380
- outValues.push({
1381
- field: instance.name,
1382
- value
1383
- });
1384
- descriptors.push({
1385
- field: instance.name,
1386
- label: instance.label ?? instance.name,
1387
- fieldType: instance.blockType
1388
- });
1389
- continue;
1390
- }
1391
- if (instance.blockType === "consent" && payload) {
1392
- const proof = await captureConsent({
1393
- field: instance,
1394
- agreed: value === true,
1395
- registry: consentRegistry,
1396
- payload,
1397
- req,
1398
- locale,
1399
- now
1400
- });
1401
- consentProofs.push({
1402
- field: instance.name,
1403
- ...proof
1404
- });
1405
- continue;
1406
- }
1407
- if (isEmpty(raw)) continue;
1408
- outValues.push({
1409
- field: instance.name,
1410
- value
1411
- });
1412
- const optionLabels = optionLabelsFor$1(instance);
1413
- descriptors.push({
1414
- field: instance.name,
1415
- label: instance.label ?? instance.name,
1416
- fieldType: instance.blockType,
1417
- ...optionLabels ? { optionLabels } : {}
1418
- });
1419
- }
1420
- return {
1421
- errors,
1422
- values: outValues,
1423
- descriptors,
1424
- consent: consentProofs
1425
- };
1426
- };
1427
- //#endregion
1428
- //#region src/submissions/validateSubmission.ts
1429
- /**
1430
- * Server-authoritative submission validation. On create it loads the referenced form, re-runs every
1431
- * field's required check, intrinsic validator, and declarative rules through `runSubmission`, threading
1432
- * `req`/`payload` so server-only async rules can hit the DB, and throws a Payload `ValidationError` with
1433
- * per-field paths on any error-severity failure. The client is never trusted.
1434
- * Consent fields are captured into `result.consent` (array of proofs, one per visible consent field).
1435
- */
1436
- const validateSubmission = ({ registry, ruleRegistry, consentRegistry, uploadSlug }) => async ({ data, operation, req }) => {
1437
- if (operation !== "create" || !data) return data;
1438
- const formId = data.form;
1439
- if (formId == null) return data;
1440
- const fields = (await req.payload.findByID({
1441
- collection: "forms",
1442
- id: formId,
1443
- depth: 0,
1444
- locale: req.locale,
1445
- req
1446
- })).fields ?? [];
1447
- const incoming = data.values ?? [];
1448
- const locale = req.locale ?? "en";
1449
- const t = asFieldTranslate(req.i18n.t);
1450
- const expectedOwner = typeof req.context?.["formBuilderSpamIdentity"] === "string" ? req.context[IDENTITY_CONTEXT_KEY] : void 0;
1451
- const result = await runSubmission({
1452
- fields,
1453
- values: incoming,
1454
- registry,
1455
- ruleRegistry,
1456
- consentRegistry,
1457
- locale,
1458
- t,
1459
- operation: "create",
1460
- req,
1461
- payload: req.payload,
1462
- formId,
1463
- uploadSlug,
1464
- expectedOwner
1465
- });
1466
- if (result.errors.length > 0) throw new ValidationError({
1467
- collection: FORM_SUBMISSIONS_SLUG,
1468
- errors: result.errors
1469
- }, req.t);
1470
- data.values = result.values;
1471
- data.descriptors = result.descriptors;
1472
- data.consent = result.consent.length > 0 ? result.consent : void 0;
1473
- data.locale = locale;
1474
- return data;
1475
- };
1476
- //#endregion
1477
- //#region src/collections/formSubmissions.ts
1478
- const FORM_SUBMISSIONS_SLUG = "form-submissions";
1479
- const formIdOf = (form) => {
1480
- if (typeof form === "number" || typeof form === "string") return form;
1481
- if (form && typeof form === "object" && "id" in form) {
1482
- const id = form.id;
1483
- if (typeof id === "number" || typeof id === "string") return id;
1484
- }
1485
- };
1486
- /**
1487
- * On a completed submission create, dispatch the form's post-submit actions and emit `submission.created`.
1488
- * Both are side effects on an already-written row, so the whole body is wrapped: nothing it does can throw
1489
- * past the hook (a failed action or sink must never fail the submission write). Dispatch is itself bounded
1490
- * and non-throwing; the inline fallback is awaited so the bound caps the added latency, while the queued
1491
- * path returns immediately. The form's `actions` are null-guarded for legacy rows created before the field
1492
- * existed.
1493
- */
1494
- const makeAfterChange = (args) => async ({ doc, operation, req }) => {
1495
- if (operation !== "create" || doc.status != null && doc.status !== "complete") return doc;
1496
- const { payload } = req;
1497
- try {
1498
- const formId = formIdOf(doc.form);
1499
- if (formId == null) return doc;
1500
- await dispatchActions({
1501
- actions: (await payload.findByID({
1502
- collection: "forms",
1503
- id: formId,
1504
- depth: 0,
1505
- overrideAccess: true,
1506
- req
1507
- }).catch(() => null))?.actions ?? null,
1508
- formId,
1509
- submissionId: doc.id,
1510
- registry: args.actionRegistry,
1511
- payload,
1512
- req,
1513
- hasRunner: args.hasRunner
1514
- });
1515
- try {
1516
- await resolveEventSink(args.events).emit({
1517
- type: "submission.created",
1518
- formId: String(formId),
1519
- submissionId: String(doc.id),
1520
- at: (/* @__PURE__ */ new Date()).toISOString()
1521
- });
1522
- } catch (error) {
1523
- payload.logger?.error(`@10x-media/form-builder: submission.created sink threw: ${error instanceof Error ? error.message : String(error)}`);
1524
- }
1525
- } catch (error) {
1526
- payload.logger?.error(`@10x-media/form-builder: afterChange dispatch failed for submission ${String(doc.id)}: ${error instanceof Error ? error.message : String(error)}`);
1527
- }
1528
- return doc;
1529
- };
1530
- const buildSubmissionsCollection = ({ registry, ruleRegistry, consentRegistry, actionRegistry = /* @__PURE__ */ new Map(), events, hasRunner = false, uploadSlug, spam }) => ({
1531
- slug: FORM_SUBMISSIONS_SLUG,
1532
- labels: {
1533
- singular: "Submission",
1534
- plural: "Submissions"
1535
- },
1536
- admin: { group: "Forms" },
1537
- access: {
1538
- create: () => true,
1539
- read: ({ req }) => Boolean(req.user),
1540
- update: () => false
1541
- },
1542
- hooks: {
1543
- beforeValidate: [...spam ? [buildSpamGuard(spam)] : [], validateSubmission({
1544
- registry,
1545
- ruleRegistry,
1546
- consentRegistry,
1547
- uploadSlug
1548
- })],
1549
- afterChange: [makeAfterChange({
1550
- actionRegistry,
1551
- events,
1552
- hasRunner
1553
- })]
1554
- },
1555
- fields: [
1556
- {
1557
- name: "form",
1558
- type: "relationship",
1559
- relationTo: FORMS_SLUG,
1560
- required: true
1561
- },
1562
- {
1563
- name: "status",
1564
- type: "select",
1565
- defaultValue: "complete",
1566
- options: [{
1567
- label: "Complete",
1568
- value: "complete"
1569
- }, {
1570
- label: "Partial",
1571
- value: "partial"
1572
- }]
1573
- },
1574
- {
1575
- name: "locale",
1576
- type: "text"
1577
- },
1578
- {
1579
- name: "values",
1580
- type: "json"
1581
- },
1582
- {
1583
- name: "descriptors",
1584
- type: "json"
1585
- },
1586
- {
1587
- name: "consent",
1588
- type: "json"
1589
- },
1590
- {
1591
- name: "meta",
1592
- type: "json"
1593
- },
1594
- {
1595
- name: "answers",
1596
- type: "ui",
1597
- admin: { components: { Field: "@10x-media/form-builder/rsc#SubmissionAnswers" } }
1598
- }
1599
- ]
1600
- });
1601
- //#endregion
1602
- //#region src/actions/runActions.ts
1603
- /**
1604
- * Run each configured action, isolating failures: a throwing action is captured as a failed result
1605
- * and never breaks the others or the caller.
1606
- */
1607
- const runActions = async (args) => {
1608
- const { actions, registry, ...ctx } = args;
1609
- const results = [];
1610
- for (const instance of actions) {
1611
- const definition = registry.get(instance.blockType);
1612
- if (!definition) continue;
1613
- try {
1614
- await definition.run({
1615
- ...ctx,
1616
- config: instance
1617
- });
1618
- results.push({
1619
- type: instance.blockType,
1620
- ok: true
1621
- });
1622
- } catch (error) {
1623
- results.push({
1624
- type: instance.blockType,
1625
- ok: false,
1626
- error: error instanceof Error ? error.message : String(error)
1627
- });
1628
- }
1629
- }
1630
- return results;
1631
- };
1632
- //#endregion
1633
- //#region src/actions/task.ts
1634
- const ACTIONS_TASK_SLUG = "form-builder-actions";
1635
- const asActions = (value) => Array.isArray(value) ? value : [];
1636
- const asValues = (value) => Array.isArray(value) ? value : [];
1637
- const asDescriptors = (value) => Array.isArray(value) ? value : [];
1638
- /**
1639
- * Load the form and submission by id and run the form's actions through the shared, failure-isolating
1640
- * `runActions`. Tolerates a missing form or submission (the row may have been deleted between enqueue and
1641
- * run) by returning early. Used by both the queued task handler and the inline fallback.
1642
- */
1643
- const runActionsForSubmission = async (args) => {
1644
- const { input, registry, payload, req } = args;
1645
- const form = await payload.findByID({
1646
- collection: FORMS_SLUG,
1647
- id: input.formId,
1648
- depth: 0,
1649
- overrideAccess: true,
1650
- req
1651
- }).catch(() => null);
1652
- if (!form) return;
1653
- const submission = await payload.findByID({
1654
- collection: FORM_SUBMISSIONS_SLUG,
1655
- id: input.submissionId,
1656
- depth: 0,
1657
- overrideAccess: true,
1658
- req
1659
- }).catch(() => null);
1660
- if (!submission) return;
1661
- const locale = typeof submission.locale === "string" ? submission.locale : req?.locale ?? "en";
1662
- const t = asFieldTranslate(req?.i18n?.t ?? ((key) => key));
1663
- await runActions({
1664
- actions: asActions(form.actions),
1665
- registry,
1666
- form: {
1667
- id: form.id,
1668
- title: typeof form.title === "string" ? form.title : void 0
1669
- },
1670
- submissionId: submission.id,
1671
- values: asValues(submission.values),
1672
- descriptors: asDescriptors(submission.descriptors),
1673
- payload,
1674
- req,
1675
- locale,
1676
- t
1677
- });
1678
- };
1679
- /** Native Payload jobs task that runs a submission's post-submit actions out of band. */
1680
- const buildActionsTask = (registry) => ({
1681
- slug: ACTIONS_TASK_SLUG,
1682
- inputSchema: [{
1683
- name: "formId",
1684
- type: "text",
1685
- required: true
1686
- }, {
1687
- name: "submissionId",
1688
- type: "text",
1689
- required: true
1690
- }],
1691
- handler: async ({ input, req }) => {
1692
- await runActionsForSubmission({
1693
- input,
1694
- registry,
1695
- payload: req.payload,
1696
- req
1697
- });
1698
- return { output: {} };
1699
- }
1700
- });
1701
- /** Register the actions task on `config.jobs.tasks`, creating the jobs config if absent. */
1702
- const registerActionsTask = (config, registry) => {
1703
- config.jobs ??= {};
1704
- config.jobs.tasks ??= [];
1705
- config.jobs.tasks.push(buildActionsTask(registry));
1706
- };
1707
- //#endregion
1708
- //#region src/spam/uploadHooks.ts
1709
- /**
1710
- * Stamp the resolved request identity onto a new upload's `owner`. `captureFileRef` later requires the
1711
- * referencing submission's identity to match, so an anonymous submitter cannot capture another identity's
1712
- * upload. No identity (no user, no trusted IP header) means no stamp, so the upload stays unscoped (a
1713
- * deliberate fail-open; identity granularity is IP-level, so same-network clients share scope -- documented).
1714
- */
1715
- const buildUploadOwnerStamp = (spam) => async ({ data, operation, req }) => {
1716
- if (operation !== "create" || !data) return data;
1717
- const identity = await spam.identify(req);
1718
- if (identity != null && data.owner == null) data.owner = identity;
1719
- return data;
1720
- };
1721
- /** Reject upload floods per identity before the file is written to `staticDir`. Fail-open without identity. */
1722
- const buildUploadRateLimit = (spam) => async ({ operation, req }) => {
1723
- if (operation !== "create" || spam.uploadRateLimit === false) return;
1724
- const identity = await spam.identify(req);
1725
- if (identity == null) return;
1726
- const { ok } = await spam.uploadRateLimit.limiter.check({
1727
- key: `uploads:${identity}`,
1728
- max: spam.uploadRateLimit.max,
1729
- window: spam.uploadRateLimit.window,
1730
- req
1731
- });
1732
- if (!ok) throw new APIError(asTranslate(req.i18n.t)(keys.spamRateLimited), 429);
1733
- };
1734
- //#endregion
1735
- //#region src/plugin/registerCollections.ts
1736
- const registerCollections = ({ config, registry, ruleRegistry, consentRegistry, presentationRegistry, actionRegistry, hasJobsPlugin, events, uploads, spam }) => {
1737
- registerActionsTask(config, actionRegistry);
1738
- const hasRunner = Boolean(config.jobs?.autoRun) || hasJobsPlugin;
1739
- if (spam && uploads.enabled && uploads.collection) {
1740
- const collection = uploads.collection;
1741
- collection.hooks = collection.hooks ?? {};
1742
- collection.hooks.beforeOperation = [...collection.hooks.beforeOperation ?? [], buildUploadRateLimit(spam)];
1743
- collection.hooks.beforeValidate = [...collection.hooks.beforeValidate ?? [], buildUploadOwnerStamp(spam)];
1744
- }
1745
- config.collections = [
1746
- ...config.collections ?? [],
1747
- ...uploads.enabled && uploads.collection ? [uploads.collection] : [],
1748
- buildFormsCollection({
1749
- registry,
1750
- ruleRegistry,
1751
- presentationRegistry,
1752
- actionRegistry
1753
- }),
1754
- buildSubmissionsCollection({
1755
- registry,
1756
- ruleRegistry,
1757
- consentRegistry,
1758
- actionRegistry,
1759
- events,
1760
- hasRunner,
1761
- uploadSlug: uploads.slug,
1762
- spam
1763
- })
1764
- ];
1765
- };
1766
- //#endregion
1767
- //#region src/plugin/registerTranslations.ts
1768
- /**
1769
- * Merge this plugin's translations into the host config. A host value wins on
1770
- * conflict (`deepMergeSimple` lets the second argument override), so projects can
1771
- * override any string.
1772
- */
1773
- const registerTranslations = (config) => {
1774
- config.i18n ??= {};
1775
- config.i18n.translations = deepMergeSimple(translations, config.i18n.translations ?? {});
1776
- };
1777
- //#endregion
1778
- //#region src/presentations/registry.ts
1779
- /**
1780
- * Resolve the active presentation descriptors from the defaults and a consumer override map.
1781
- * Mirrors the field-type, validation-rule, and renderer registry convention.
1782
- */
1783
- const resolvePresentationDescriptors = (defaults, config = {}) => {
1784
- const registry = new Map(Object.entries(defaults));
1785
- for (const [name, option] of Object.entries(config)) if (option === false) registry.delete(name);
1786
- else if (option === true) {} else registry.set(name, option);
1787
- return registry;
1788
- };
1789
- //#endregion
1790
- //#region src/spam/identify.ts
1791
- /**
1792
- * Default identity resolution for rate-limiting + upload ownership. Prefers an authenticated user id
1793
- * (trustworthy); else the first hop of the configured trusted IP header (best-effort, proxy-dependent);
1794
- * else null, in which case the caller fails open (cannot fairly rate-limit an unidentifiable client).
1795
- */
1796
- const defaultIdentify = (ipHeader) => (req) => {
1797
- const userId = req.user?.id;
1798
- if (userId != null) return `user:${String(userId)}`;
1799
- const header = req.headers?.get(ipHeader);
1800
- if (header) {
1801
- const first = header.split(",")[0]?.trim();
1802
- if (first) return `ip:${first}`;
1803
- }
1804
- return null;
1805
- };
1806
- //#endregion
1807
- //#region src/spam/rateLimiter.ts
1808
- /**
1809
- * The default rate limiter: a read-modify-write window counter over Payload's `payload.kv`
1810
- * (always present; default `DatabaseKVAdapter`, durable + cross-instance). Because the KV interface
1811
- * has no atomic increment, the get-then-set is NON-ATOMIC, so a concurrent burst can slightly undercount.
1812
- * This is a SOFT limit, acceptable for spam basics and complemented by edge/WAF limiting. The window
1813
- * expires lazily on read; stale keys for one-time identities linger (minor; the count self-resets).
1814
- * `now` is injectable for tests.
1815
- */
1816
- const createKvRateLimiter = (options = {}) => {
1817
- const now = options.now ?? (() => Date.now());
1818
- const prefix = options.namespace ?? "fb:rl:";
1819
- return { async check({ key, max, window, req }) {
1820
- const kv = req.payload.kv;
1821
- const storeKey = `${prefix}${key}`;
1822
- const current = now();
1823
- const existing = await kv.get(storeKey).catch(() => null);
1824
- let count;
1825
- let windowStart;
1826
- if (!existing || current - existing.windowStart >= window) {
1827
- count = 1;
1828
- windowStart = current;
1829
- } else {
1830
- count = existing.count + 1;
1831
- windowStart = existing.windowStart;
1832
- }
1833
- await kv.set(storeKey, {
1834
- count,
1835
- windowStart
1836
- }).catch(() => void 0);
1837
- return {
1838
- ok: count <= max,
1839
- remaining: Math.max(0, max - count),
1840
- resetAt: windowStart + window
1841
- };
1842
- } };
1843
- };
1844
- //#endregion
1845
- //#region src/spam/resolveSpam.ts
1846
- const DEFAULT_WINDOW = 6e4;
1847
- const DEFAULT_SUBMISSION_MAX = 5;
1848
- const DEFAULT_UPLOAD_MAX = 20;
1849
- const resolveRateLimit = (option, defaultMax) => {
1850
- if (option === false) return false;
1851
- const cfg = option ?? {};
1852
- return {
1853
- window: cfg.window ?? DEFAULT_WINDOW,
1854
- max: cfg.max ?? defaultMax,
1855
- limiter: cfg.limiter ?? createKvRateLimiter()
1856
- };
1857
- };
1858
- /**
1859
- * Resolve the `spam` plugin option into a concrete config. `false` disables the whole subsystem.
1860
- * Otherwise honeypot + both rate limits default on, captcha + metadata capture default off, and the
1861
- * identity seam defaults to user id / trusted IP header.
1862
- */
1863
- const resolveSpamConfig = (option) => {
1864
- if (option === false) return false;
1865
- const cfg = option ?? {};
1866
- const ipHeader = cfg.ipHeader ?? "x-forwarded-for";
1867
- return {
1868
- honeypot: cfg.honeypot === false ? false : { fieldName: cfg.honeypot?.fieldName ?? "confirm_email" },
1869
- rateLimit: resolveRateLimit(cfg.rateLimit, DEFAULT_SUBMISSION_MAX),
1870
- uploadRateLimit: resolveRateLimit(cfg.uploadRateLimit, DEFAULT_UPLOAD_MAX),
1871
- captcha: cfg.captcha,
1872
- identify: cfg.identify ?? defaultIdentify(ipHeader),
1873
- ipHeader,
1874
- metadata: {
1875
- ip: cfg.metadata?.ip ?? false,
1876
- ua: cfg.metadata?.ua ?? false
1877
- }
1878
- };
1879
- };
1880
- //#endregion
1881
- //#region src/spam/captcha.ts
1882
- /**
1883
- * Identity helper for authoring a captcha provider (mirrors `defineAction`/`defineConsentSource`).
1884
- * v1 ships no built-in provider; a project supplies one (Turnstile/reCAPTCHA/hCaptcha) and the submit
1885
- * path verifies a token carried on the submission. Prebuilt providers are a v1.x addition.
1886
- */
1887
- const defineCaptchaProvider = (provider) => provider;
1888
- //#endregion
1
+ import { interpolate } from "./recall/interpolate.js";
2
+ import { defineAction } from "./actions/defineAction.js";
3
+ import { SIGNATURE_HEADER, signPayload } from "./actions/sign.js";
4
+ import { defaultActionDefinitions } from "./actions/builtin/index.js";
5
+ import { resolveActions } from "./actions/registry.js";
6
+ import { FORM_UPLOADS_SLUG, buildUploadsCollection, resolveUploads } from "./collections/uploads.js";
7
+ import { defineConsentSource } from "./consent/defineConsentSource.js";
8
+ import { resolvePublishedVersionRef } from "./consent/resolvePublishedVersionRef.js";
9
+ import { defaultConsentSources } from "./consent/builtin/index.js";
10
+ import { resolveConsentSources } from "./consent/registry.js";
11
+ import { defineFormField } from "./fields/defineFormField.js";
12
+ import { defaultFieldDefinitions } from "./fields/builtin/index.js";
13
+ import { resolveFieldTypes } from "./fields/registry.js";
14
+ import { CAPTCHA_TOKEN_KEY, DEFAULT_HONEYPOT_FIELD } from "./spam/constants.js";
15
+ import { aggregateRowForField, aggregateRowsForFields } from "./aggregation/aggregateRows.js";
16
+ import { aggregateFieldResponses, aggregateFormResponses, fieldHasOptions } from "./aggregation/aggregateResponses.js";
17
+ import { resolveFormResultsRequest } from "./aggregation/resolveResultsRequest.js";
18
+ import { normalizeCalc } from "./calc/normalizeCalc.js";
19
+ import { DEFAULT_PRESENTATION_NAME, defaultPresentationDescriptors } from "./presentations/defaults.js";
20
+ import { evaluateCalc } from "./calc/evaluate.js";
21
+ import { calcExpressionOf, computeCalcFields } from "./calc/computeCalcFields.js";
22
+ import { evaluateCondition } from "./conditions/evaluate.js";
23
+ import { resolveConsentLinks } from "./consent/resolveConsentLinks.js";
24
+ import { captureConsent } from "./consent/captureConsent.js";
25
+ import { resolveFileRef } from "./uploads/resolveFileRef.js";
26
+ import { captureFileRef } from "./uploads/captureFileRef.js";
27
+ import { registerCollections } from "./plugin/registerCollections.js";
28
+ import { registerTranslations } from "./plugin/registerTranslations.js";
29
+ import { resolvePresentationDescriptors } from "./presentations/registry.js";
30
+ import { defaultIdentify } from "./spam/identify.js";
31
+ import { createKvRateLimiter } from "./spam/rateLimiter.js";
32
+ import { resolveSpamConfig } from "./spam/resolveSpam.js";
33
+ import { defineValidationRule } from "./validation/defineValidationRule.js";
34
+ import { defaultValidationRules } from "./validation/builtin/index.js";
35
+ import { resolveValidationRules } from "./validation/registry.js";
36
+ import { valuesFromSearchParams } from "./prefill/valuesFromSearchParams.js";
37
+ import { buildRecallResolver, optionLabelsFor } from "./recall/resolver.js";
38
+ import { defineCaptchaProvider } from "./spam/captcha.js";
39
+ import { definePlugin } from "payload";
1889
40
  //#region src/index.ts
1890
41
  const formBuilder = definePlugin({
1891
42
  slug: "@10x-media/form-builder",
@@ -1899,7 +50,7 @@ const formBuilder = definePlugin({
1899
50
  const actionRegistry = resolveActions(defaultActionDefinitions, options.actions);
1900
51
  const uploads = resolveUploads(options.uploads);
1901
52
  const spam = resolveSpamConfig(options.spam);
1902
- registerTranslations(config);
53
+ registerTranslations(config, options.translations);
1903
54
  registerCollections({
1904
55
  config,
1905
56
  registry,