@10x-media/form-builder 0.1.0-beta.0 → 0.1.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +68 -0
- package/README.md +45 -1402
- package/dist/actions/buildActionBlocks.js +19 -0
- package/dist/actions/buildActionBlocks.js.map +1 -0
- package/dist/actions/builtin/confirmation.js +49 -0
- package/dist/actions/builtin/confirmation.js.map +1 -0
- package/dist/actions/builtin/emailTeam.js +46 -0
- package/dist/actions/builtin/emailTeam.js.map +1 -0
- package/dist/actions/builtin/index.d.ts +7 -0
- package/dist/actions/builtin/index.js +13 -0
- package/dist/actions/builtin/index.js.map +1 -0
- package/dist/actions/builtin/signedWebhook.js +49 -0
- package/dist/actions/builtin/signedWebhook.js.map +1 -0
- package/dist/actions/defineAction.d.ts +36 -0
- package/dist/actions/defineAction.js +6 -0
- package/dist/actions/defineAction.js.map +1 -0
- package/dist/actions/dispatch.js +48 -0
- package/dist/actions/dispatch.js.map +1 -0
- package/dist/actions/registry.d.ts +16 -0
- package/dist/actions/registry.js +16 -0
- package/dist/actions/registry.js.map +1 -0
- package/dist/actions/runActions.d.ts +9 -0
- package/dist/actions/runActions.js +34 -0
- package/dist/actions/runActions.js.map +1 -0
- package/dist/actions/sign.d.ts +7 -0
- package/dist/actions/sign.js +9 -0
- package/dist/actions/sign.js.map +1 -0
- package/dist/actions/task.js +82 -0
- package/dist/actions/task.js.map +1 -0
- package/dist/aggregation/aggregateResponses.d.ts +31 -0
- package/dist/aggregation/aggregateResponses.js +86 -0
- package/dist/aggregation/aggregateResponses.js.map +1 -0
- package/dist/aggregation/aggregateRows.d.ts +10 -0
- package/dist/aggregation/aggregateRows.js +64 -0
- package/dist/aggregation/aggregateRows.js.map +1 -0
- package/dist/aggregation/resolveResultsRequest.d.ts +32 -0
- package/dist/aggregation/resolveResultsRequest.js +56 -0
- package/dist/aggregation/resolveResultsRequest.js.map +1 -0
- package/dist/aggregation/types.d.ts +39 -0
- package/dist/calc/computeCalcFields.d.ts +15 -0
- package/dist/calc/computeCalcFields.js +27 -0
- package/dist/calc/computeCalcFields.js.map +1 -0
- package/dist/calc/evaluate.d.ts +8 -0
- package/dist/calc/evaluate.js +59 -0
- package/dist/calc/evaluate.js.map +1 -0
- package/dist/calc/normalizeCalc.d.ts +8 -0
- package/dist/calc/normalizeCalc.js +100 -0
- package/dist/calc/normalizeCalc.js.map +1 -0
- package/dist/calc/types.d.ts +28 -0
- package/dist/client/ConditionBuilder.js +121 -0
- package/dist/client/ConditionBuilder.js.map +1 -0
- package/dist/client/ConditionRow.js +150 -0
- package/dist/client/ConditionRow.js.map +1 -0
- package/dist/client/FlowBuilder.d.ts +15 -0
- package/dist/client/FlowBuilder.js +408 -0
- package/dist/client/FlowBuilder.js.map +1 -0
- package/dist/client/FormConditionField.d.ts +21 -0
- package/dist/client/FormConditionField.js +33 -0
- package/dist/client/FormConditionField.js.map +1 -0
- package/dist/client/synthesizeClientField.js +46 -0
- package/dist/client/synthesizeClientField.js.map +1 -0
- package/dist/collections/formSubmissions.js +157 -0
- package/dist/collections/formSubmissions.js.map +1 -0
- package/dist/collections/forms.js +198 -0
- package/dist/collections/forms.js.map +1 -0
- package/dist/collections/uploads.d.ts +30 -0
- package/dist/collections/uploads.js +55 -0
- package/dist/collections/uploads.js.map +1 -0
- package/dist/conditions/conditionType.js +14 -0
- package/dist/conditions/conditionType.js.map +1 -0
- package/dist/conditions/evaluate.d.ts +17 -0
- package/dist/conditions/evaluate.js +94 -0
- package/dist/conditions/evaluate.js.map +1 -0
- package/dist/conditions/fieldTypes.d.ts +9 -0
- package/dist/{fieldTypes-CzhhJXjg.js → conditions/fieldTypes.js} +2 -2
- package/dist/conditions/fieldTypes.js.map +1 -0
- package/dist/conditions/normalizeConditions.js +47 -0
- package/dist/conditions/normalizeConditions.js.map +1 -0
- package/dist/conditions/types.d.ts +8 -0
- package/dist/consent/buildConsentSourceConfig.js +49 -0
- package/dist/consent/buildConsentSourceConfig.js.map +1 -0
- package/dist/consent/builtin/index.d.ts +7 -0
- package/dist/consent/builtin/index.js +11 -0
- package/dist/consent/builtin/index.js.map +1 -0
- package/dist/consent/builtin/pageReference.js +75 -0
- package/dist/consent/builtin/pageReference.js.map +1 -0
- package/dist/consent/builtin/static.js +44 -0
- package/dist/consent/builtin/static.js.map +1 -0
- package/dist/consent/captureConsent.d.ts +28 -0
- package/dist/consent/captureConsent.js +26 -0
- package/dist/consent/captureConsent.js.map +1 -0
- package/dist/consent/defineConsentSource.d.ts +35 -0
- package/dist/consent/defineConsentSource.js +6 -0
- package/dist/consent/defineConsentSource.js.map +1 -0
- package/dist/consent/registry.d.ts +16 -0
- package/dist/consent/registry.js +16 -0
- package/dist/consent/registry.js.map +1 -0
- package/dist/consent/resolveConsentLinks.d.ts +19 -0
- package/dist/consent/resolveConsentLinks.js +26 -0
- package/dist/consent/resolveConsentLinks.js.map +1 -0
- package/dist/consent/resolvePublishedVersionRef.d.ts +17 -0
- package/dist/consent/resolvePublishedVersionRef.js +27 -0
- package/dist/consent/resolvePublishedVersionRef.js.map +1 -0
- package/dist/events/noopSink.js +6 -0
- package/dist/events/noopSink.js.map +1 -0
- package/dist/events/resolveEventSink.js +8 -0
- package/dist/events/resolveEventSink.js.map +1 -0
- package/dist/events/types.d.ts +31 -0
- package/dist/exports/client.d.ts +3 -21
- package/dist/exports/client.js +3 -319
- package/dist/exports/i18n.d.ts +3 -138
- package/dist/exports/i18n.js +2 -2
- package/dist/exports/react.d.ts +45 -550
- package/dist/exports/react.js +39 -1563
- package/dist/exports/rsc.d.ts +2 -16
- package/dist/exports/rsc.js +1 -54
- package/dist/exports/types.d.ts +28 -5
- package/dist/fields/buildFieldBlocks.js +60 -0
- package/dist/fields/buildFieldBlocks.js.map +1 -0
- package/dist/fields/builtin/calculation.js +24 -0
- package/dist/fields/builtin/calculation.js.map +1 -0
- package/dist/fields/builtin/checkbox.js +13 -0
- package/dist/fields/builtin/checkbox.js.map +1 -0
- package/dist/fields/builtin/consent.js +32 -0
- package/dist/fields/builtin/consent.js.map +1 -0
- package/dist/fields/builtin/email.js +18 -0
- package/dist/fields/builtin/email.js.map +1 -0
- package/dist/fields/builtin/file.js +40 -0
- package/dist/fields/builtin/file.js.map +1 -0
- package/dist/fields/builtin/index.js +27 -0
- package/dist/fields/builtin/index.js.map +1 -0
- package/dist/fields/builtin/number.js +17 -0
- package/dist/fields/builtin/number.js.map +1 -0
- package/dist/fields/builtin/repeater.js +44 -0
- package/dist/fields/builtin/repeater.js.map +1 -0
- package/dist/fields/builtin/select.js +44 -0
- package/dist/fields/builtin/select.js.map +1 -0
- package/dist/fields/builtin/text.js +13 -0
- package/dist/fields/builtin/text.js.map +1 -0
- package/dist/fields/builtin/textarea.js +13 -0
- package/dist/fields/builtin/textarea.js.map +1 -0
- package/dist/fields/defineFormField.d.ts +13 -0
- package/dist/fields/defineFormField.js +12 -0
- package/dist/fields/defineFormField.js.map +1 -0
- package/dist/fields/registry.d.ts +10 -0
- package/dist/fields/registry.js +24 -0
- package/dist/fields/registry.js.map +1 -0
- package/dist/fields/sharedConfig.js +89 -0
- package/dist/fields/sharedConfig.js.map +1 -0
- package/dist/fields/types.d.ts +89 -0
- package/dist/flow/engine.d.ts +17 -0
- package/dist/flow/engine.js +24 -0
- package/dist/flow/engine.js.map +1 -0
- package/dist/flow/normalizeFlow.js +35 -0
- package/dist/flow/normalizeFlow.js.map +1 -0
- package/dist/{types-DsJ_6kGJ.d.ts → flow/types.d.ts} +2 -2
- package/dist/index.d.ts +93 -4
- package/dist/index.js +43 -1890
- package/dist/index.js.map +1 -1
- package/dist/plugin/access.js +12 -0
- package/dist/plugin/access.js.map +1 -0
- package/dist/plugin/collectionOverrides.d.ts +22 -0
- package/dist/plugin/registerCollections.js +68 -0
- package/dist/plugin/registerCollections.js.map +1 -0
- package/dist/plugin/registerTranslations.js +19 -0
- package/dist/plugin/registerTranslations.js.map +1 -0
- package/dist/prefill/valuesFromSearchParams.d.ts +18 -0
- package/dist/prefill/valuesFromSearchParams.js +45 -0
- package/dist/prefill/valuesFromSearchParams.js.map +1 -0
- package/dist/presentations/defaults.d.ts +33 -0
- package/dist/presentations/defaults.js +35 -0
- package/dist/presentations/defaults.js.map +1 -0
- package/dist/presentations/registry.d.ts +15 -0
- package/dist/presentations/registry.js +15 -0
- package/dist/presentations/registry.js.map +1 -0
- package/dist/presentations/types.d.ts +18 -0
- package/dist/react/Form.d.ts +110 -0
- package/dist/react/Form.js +499 -0
- package/dist/react/Form.js.map +1 -0
- package/dist/react/FormContext.d.ts +16 -0
- package/dist/react/FormContext.js +14 -0
- package/dist/react/FormContext.js.map +1 -0
- package/dist/react/FormLayout.d.ts +20 -0
- package/dist/react/FormLayout.js +14 -0
- package/dist/react/FormLayout.js.map +1 -0
- package/dist/react/FormResults.d.ts +25 -0
- package/dist/react/FormResults.js +85 -0
- package/dist/react/FormResults.js.map +1 -0
- package/dist/react/Honeypot.d.ts +22 -0
- package/dist/react/Honeypot.js +38 -0
- package/dist/react/Honeypot.js.map +1 -0
- package/dist/react/Poll.d.ts +32 -0
- package/dist/react/Poll.js +77 -0
- package/dist/react/Poll.js.map +1 -0
- package/dist/react/cn.d.ts +6 -0
- package/dist/react/cn.js +8 -0
- package/dist/react/cn.js.map +1 -0
- package/dist/react/contract.d.ts +32 -0
- package/dist/react/contract.js +7 -0
- package/dist/react/contract.js.map +1 -0
- package/dist/react/events.js +14 -0
- package/dist/react/events.js.map +1 -0
- package/dist/react/fetchResults.d.ts +25 -0
- package/dist/react/fetchResults.js +31 -0
- package/dist/react/fetchResults.js.map +1 -0
- package/dist/react/presentation/Backdrop.d.ts +18 -0
- package/dist/react/presentation/Backdrop.js +14 -0
- package/dist/react/presentation/Backdrop.js.map +1 -0
- package/dist/react/presentation/DialogSurface.d.ts +35 -0
- package/dist/react/presentation/DialogSurface.js +56 -0
- package/dist/react/presentation/DialogSurface.js.map +1 -0
- package/dist/react/presentation/presentations.d.ts +7 -0
- package/dist/react/presentation/presentations.js +32 -0
- package/dist/react/presentation/presentations.js.map +1 -0
- package/dist/react/presentation/registry.d.ts +11 -0
- package/dist/react/presentation/registry.js +12 -0
- package/dist/react/presentation/registry.js.map +1 -0
- package/dist/react/presentation/types.d.ts +19 -0
- package/dist/react/presentation/useDismiss.d.ts +19 -0
- package/dist/react/presentation/useDismiss.js +33 -0
- package/dist/react/presentation/useDismiss.js.map +1 -0
- package/dist/react/presentation/useFocusTrap.d.ts +8 -0
- package/dist/react/presentation/useFocusTrap.js +35 -0
- package/dist/react/presentation/useFocusTrap.js.map +1 -0
- package/dist/react/presentation/useScrollLock.d.ts +6 -0
- package/dist/react/presentation/useScrollLock.js +18 -0
- package/dist/react/presentation/useScrollLock.js.map +1 -0
- package/dist/react/primitives/Checkbox.d.ts +26 -0
- package/dist/react/primitives/Checkbox.js +20 -0
- package/dist/react/primitives/Checkbox.js.map +1 -0
- package/dist/react/primitives/FieldShell.d.ts +31 -0
- package/dist/react/primitives/FieldShell.js +51 -0
- package/dist/react/primitives/FieldShell.js.map +1 -0
- package/dist/react/primitives/Input.d.ts +30 -0
- package/dist/react/primitives/Input.js +21 -0
- package/dist/react/primitives/Input.js.map +1 -0
- package/dist/react/primitives/Select.d.ts +34 -0
- package/dist/react/primitives/Select.js +26 -0
- package/dist/react/primitives/Select.js.map +1 -0
- package/dist/react/primitives/Textarea.d.ts +28 -0
- package/dist/react/primitives/Textarea.js +20 -0
- package/dist/react/primitives/Textarea.js.map +1 -0
- package/dist/react/recall.js +20 -0
- package/dist/react/recall.js.map +1 -0
- package/dist/react/registry.d.ts +16 -0
- package/dist/react/registry.js +16 -0
- package/dist/react/registry.js.map +1 -0
- package/dist/react/renderers/calculation.js +30 -0
- package/dist/react/renderers/calculation.js.map +1 -0
- package/dist/react/renderers/checkbox.js +35 -0
- package/dist/react/renderers/checkbox.js.map +1 -0
- package/dist/react/renderers/consent.js +54 -0
- package/dist/react/renderers/consent.js.map +1 -0
- package/dist/react/renderers/email.js +37 -0
- package/dist/react/renderers/email.js.map +1 -0
- package/dist/react/renderers/file.js +92 -0
- package/dist/react/renderers/file.js.map +1 -0
- package/dist/react/renderers/index.d.ts +8 -0
- package/dist/react/renderers/index.js +28 -0
- package/dist/react/renderers/index.js.map +1 -0
- package/dist/react/renderers/number.js +40 -0
- package/dist/react/renderers/number.js.map +1 -0
- package/dist/react/renderers/repeater.js +113 -0
- package/dist/react/renderers/repeater.js.map +1 -0
- package/dist/react/renderers/select.js +38 -0
- package/dist/react/renderers/select.js.map +1 -0
- package/dist/react/renderers/text.js +37 -0
- package/dist/react/renderers/text.js.map +1 -0
- package/dist/react/renderers/textarea.js +36 -0
- package/dist/react/renderers/textarea.js.map +1 -0
- package/dist/react/resolveForm.js +16 -0
- package/dist/react/resolveForm.js.map +1 -0
- package/dist/react/state.d.ts +15 -0
- package/dist/react/state.js +70 -0
- package/dist/react/state.js.map +1 -0
- package/dist/react/submitForm.d.ts +31 -0
- package/dist/react/submitForm.js +58 -0
- package/dist/react/submitForm.js.map +1 -0
- package/dist/react/toFormDocument.d.ts +24 -0
- package/dist/react/toFormDocument.js +22 -0
- package/dist/react/toFormDocument.js.map +1 -0
- package/dist/react/uploadFile.d.ts +23 -0
- package/dist/react/uploadFile.js +39 -0
- package/dist/react/uploadFile.js.map +1 -0
- package/dist/react/useField.d.ts +14 -0
- package/dist/react/useField.js +49 -0
- package/dist/react/useField.js.map +1 -0
- package/dist/react/useFormState.d.ts +8 -0
- package/dist/react/useFormState.js +9 -0
- package/dist/react/useFormState.js.map +1 -0
- package/dist/react/useFormStep.d.ts +8 -0
- package/dist/react/useFormStep.js +9 -0
- package/dist/react/useFormStep.js.map +1 -0
- package/dist/react/validateField.js +30 -0
- package/dist/react/validateField.js.map +1 -0
- package/dist/recall/interpolate.d.ts +10 -0
- package/dist/recall/interpolate.js +16 -0
- package/dist/recall/interpolate.js.map +1 -0
- package/dist/recall/resolver.d.ts +22 -0
- package/dist/recall/resolver.js +36 -0
- package/dist/recall/resolver.js.map +1 -0
- package/dist/spam/captcha.d.ts +12 -0
- package/dist/spam/captcha.js +11 -0
- package/dist/spam/captcha.js.map +1 -0
- package/dist/spam/constants.d.ts +12 -0
- package/dist/spam/constants.js +15 -0
- package/dist/spam/constants.js.map +1 -0
- package/dist/spam/identify.d.ts +12 -0
- package/dist/spam/identify.js +20 -0
- package/dist/spam/identify.js.map +1 -0
- package/dist/spam/rateLimiter.d.ts +18 -0
- package/dist/spam/rateLimiter.js +41 -0
- package/dist/spam/rateLimiter.js.map +1 -0
- package/dist/spam/reserved.js +30 -0
- package/dist/spam/reserved.js.map +1 -0
- package/dist/spam/resolveSpam.d.ts +12 -0
- package/dist/spam/resolveSpam.js +42 -0
- package/dist/spam/resolveSpam.js.map +1 -0
- package/dist/spam/spamGuard.js +68 -0
- package/dist/spam/spamGuard.js.map +1 -0
- package/dist/spam/types.d.ts +73 -0
- package/dist/spam/uploadHooks.js +33 -0
- package/dist/spam/uploadHooks.js.map +1 -0
- package/dist/submissions/SubmissionAnswers.d.ts +14 -0
- package/dist/submissions/SubmissionAnswers.js +133 -0
- package/dist/submissions/SubmissionAnswers.js.map +1 -0
- package/dist/submissions/SubmissionAnswersClient.js +142 -0
- package/dist/submissions/SubmissionAnswersClient.js.map +1 -0
- package/dist/submissions/runSubmission.js +257 -0
- package/dist/submissions/runSubmission.js.map +1 -0
- package/dist/submissions/types.d.ts +34 -0
- package/dist/submissions/validateSubmission.js +59 -0
- package/dist/submissions/validateSubmission.js.map +1 -0
- package/dist/translations/en.d.ts +12 -0
- package/dist/{translations-edMqq_2e.js → translations/en.js} +29 -23
- package/dist/translations/en.js.map +1 -0
- package/dist/translations/index.d.ts +14 -0
- package/dist/translations/index.js +26 -0
- package/dist/translations/index.js.map +1 -0
- package/dist/translations/keys.d.ts +157 -0
- package/dist/{keys-N5xGiUsh.js → translations/keys.js} +28 -3
- package/dist/translations/keys.js.map +1 -0
- package/dist/translations/makeTranslate.d.ts +11 -0
- package/dist/translations/makeTranslate.js +11 -0
- package/dist/translations/makeTranslate.js.map +1 -0
- package/dist/translations/server.js +25 -0
- package/dist/translations/server.js.map +1 -0
- package/dist/translations/useTranslation.js +12 -0
- package/dist/translations/useTranslation.js.map +1 -0
- package/dist/uploads/captureFileRef.d.ts +27 -0
- package/dist/uploads/captureFileRef.js +34 -0
- package/dist/uploads/captureFileRef.js.map +1 -0
- package/dist/uploads/resolveFileRef.d.ts +26 -0
- package/dist/uploads/resolveFileRef.js +44 -0
- package/dist/uploads/resolveFileRef.js.map +1 -0
- package/dist/uploads/types.d.ts +20 -0
- package/dist/validation/buildRuleBlocks.js +46 -0
- package/dist/validation/buildRuleBlocks.js.map +1 -0
- package/dist/validation/builtin/email.js +19 -0
- package/dist/validation/builtin/email.js.map +1 -0
- package/dist/validation/builtin/index.js +27 -0
- package/dist/validation/builtin/index.js.map +1 -0
- package/dist/validation/builtin/matchesField.js +20 -0
- package/dist/validation/builtin/matchesField.js.map +1 -0
- package/dist/validation/builtin/max.js +21 -0
- package/dist/validation/builtin/max.js.map +1 -0
- package/dist/validation/builtin/maxLength.js +26 -0
- package/dist/validation/builtin/maxLength.js.map +1 -0
- package/dist/validation/builtin/min.js +21 -0
- package/dist/validation/builtin/min.js.map +1 -0
- package/dist/validation/builtin/minLength.js +26 -0
- package/dist/validation/builtin/minLength.js.map +1 -0
- package/dist/validation/builtin/notAlreadySubmitted.js +32 -0
- package/dist/validation/builtin/notAlreadySubmitted.js.map +1 -0
- package/dist/validation/builtin/oneOf.js +34 -0
- package/dist/validation/builtin/oneOf.js.map +1 -0
- package/dist/validation/builtin/pattern.js +41 -0
- package/dist/validation/builtin/pattern.js.map +1 -0
- package/dist/validation/builtin/url.js +22 -0
- package/dist/validation/builtin/url.js.map +1 -0
- package/dist/validation/defineValidationRule.d.ts +12 -0
- package/dist/validation/defineValidationRule.js +11 -0
- package/dist/validation/defineValidationRule.js.map +1 -0
- package/dist/validation/message.js +14 -0
- package/dist/validation/message.js.map +1 -0
- package/dist/validation/registry.d.ts +10 -0
- package/dist/validation/registry.js +20 -0
- package/dist/validation/registry.js.map +1 -0
- package/dist/validation/runValidation.js +111 -0
- package/dist/validation/runValidation.js.map +1 -0
- package/dist/validation/types.d.ts +76 -0
- package/package.json +8 -5
- package/styles/form-builder.css +201 -11
- package/dist/constants-B-BUfetP.d.ts +0 -288
- package/dist/exports/client.js.map +0 -1
- package/dist/exports/react.js.map +0 -1
- package/dist/exports/rsc.js.map +0 -1
- package/dist/fieldTypes-B8jkNRob.d.ts +0 -188
- package/dist/fieldTypes-CzhhJXjg.js.map +0 -1
- package/dist/index-DfFYGbVF.d.ts +0 -481
- package/dist/keys-N5xGiUsh.js.map +0 -1
- package/dist/registry-CoCyhtvB.js +0 -282
- package/dist/registry-CoCyhtvB.js.map +0 -1
- package/dist/resolver-OeQyVH2N.js +0 -665
- package/dist/resolver-OeQyVH2N.js.map +0 -1
- package/dist/translations-edMqq_2e.js.map +0 -1
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
//#region src/spam/captcha.ts
|
|
2
|
+
/**
|
|
3
|
+
* Identity helper for authoring a captcha provider (mirrors `defineAction`/`defineConsentSource`).
|
|
4
|
+
* v1 ships no built-in provider; a project supplies one (Turnstile/reCAPTCHA/hCaptcha) and the submit
|
|
5
|
+
* path verifies a token carried on the submission. Prebuilt providers are a v1.x addition.
|
|
6
|
+
*/
|
|
7
|
+
const defineCaptchaProvider = (provider) => provider;
|
|
8
|
+
//#endregion
|
|
9
|
+
export { defineCaptchaProvider };
|
|
10
|
+
|
|
11
|
+
//# sourceMappingURL=captcha.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"captcha.js","names":[],"sources":["../../src/spam/captcha.ts"],"sourcesContent":["import type { CaptchaProvider } from './types'\n\n/**\n * Identity helper for authoring a captcha provider (mirrors `defineAction`/`defineConsentSource`).\n * v1 ships no built-in provider; a project supplies one (Turnstile/reCAPTCHA/hCaptcha) and the submit\n * path verifies a token carried on the submission. Prebuilt providers are a v1.x addition.\n */\nexport const defineCaptchaProvider = (provider: CaptchaProvider): CaptchaProvider => provider\n"],"mappings":";;;;;;AAOA,MAAa,yBAAyB,aAA+C"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
//#region src/spam/constants.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Default honeypot decoy field name. "website" is plausible to bots but doesn't trigger Chrome's
|
|
4
|
+
* email-address autofill (unlike names containing "email"). Bots fill every input; real users
|
|
5
|
+
* never see the hidden field.
|
|
6
|
+
*/
|
|
7
|
+
declare const DEFAULT_HONEYPOT_FIELD = "website";
|
|
8
|
+
/** Reserved `values` entry carrying a captcha token from the client. */
|
|
9
|
+
declare const CAPTCHA_TOKEN_KEY = "__fb_captcha";
|
|
10
|
+
//#endregion
|
|
11
|
+
export { CAPTCHA_TOKEN_KEY, DEFAULT_HONEYPOT_FIELD };
|
|
12
|
+
//# sourceMappingURL=constants.d.ts.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
//#region src/spam/constants.ts
|
|
2
|
+
/**
|
|
3
|
+
* Default honeypot decoy field name. "website" is plausible to bots but doesn't trigger Chrome's
|
|
4
|
+
* email-address autofill (unlike names containing "email"). Bots fill every input; real users
|
|
5
|
+
* never see the hidden field.
|
|
6
|
+
*/
|
|
7
|
+
const DEFAULT_HONEYPOT_FIELD = "website";
|
|
8
|
+
/** Reserved `values` entry carrying a captcha token from the client. */
|
|
9
|
+
const CAPTCHA_TOKEN_KEY = "__fb_captcha";
|
|
10
|
+
/** `req.context` key under which the spam guard stashes the resolved identity for upload-ownership verification. */
|
|
11
|
+
const IDENTITY_CONTEXT_KEY = "formBuilderSpamIdentity";
|
|
12
|
+
//#endregion
|
|
13
|
+
export { CAPTCHA_TOKEN_KEY, DEFAULT_HONEYPOT_FIELD, IDENTITY_CONTEXT_KEY };
|
|
14
|
+
|
|
15
|
+
//# sourceMappingURL=constants.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"constants.js","names":[],"sources":["../../src/spam/constants.ts"],"sourcesContent":["/**\n * Default honeypot decoy field name. \"website\" is plausible to bots but doesn't trigger Chrome's\n * email-address autofill (unlike names containing \"email\"). Bots fill every input; real users\n * never see the hidden field.\n */\nexport const DEFAULT_HONEYPOT_FIELD = 'website'\n\n/** Reserved `values` entry carrying a captcha token from the client. */\nexport const CAPTCHA_TOKEN_KEY = '__fb_captcha'\n\n/** `req.context` key under which the spam guard stashes the resolved identity for upload-ownership verification. */\nexport const IDENTITY_CONTEXT_KEY = 'formBuilderSpamIdentity'\n"],"mappings":";;;;;;AAKA,MAAa,yBAAyB;;AAGtC,MAAa,oBAAoB;;AAGjC,MAAa,uBAAuB"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { IdentifyFn } from "./types.js";
|
|
2
|
+
|
|
3
|
+
//#region src/spam/identify.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Default identity resolution for rate-limiting + upload ownership. Prefers an authenticated user id
|
|
6
|
+
* (trustworthy); else the first hop of the configured trusted IP header (best-effort, proxy-dependent);
|
|
7
|
+
* else null, in which case the caller fails open (cannot fairly rate-limit an unidentifiable client).
|
|
8
|
+
*/
|
|
9
|
+
declare const defaultIdentify: (ipHeader: string) => IdentifyFn;
|
|
10
|
+
//#endregion
|
|
11
|
+
export { defaultIdentify };
|
|
12
|
+
//# sourceMappingURL=identify.d.ts.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
//#region src/spam/identify.ts
|
|
2
|
+
/**
|
|
3
|
+
* Default identity resolution for rate-limiting + upload ownership. Prefers an authenticated user id
|
|
4
|
+
* (trustworthy); else the first hop of the configured trusted IP header (best-effort, proxy-dependent);
|
|
5
|
+
* else null, in which case the caller fails open (cannot fairly rate-limit an unidentifiable client).
|
|
6
|
+
*/
|
|
7
|
+
const defaultIdentify = (ipHeader) => (req) => {
|
|
8
|
+
const userId = req.user?.id;
|
|
9
|
+
if (userId != null) return `user:${String(userId)}`;
|
|
10
|
+
const header = req.headers?.get(ipHeader);
|
|
11
|
+
if (header) {
|
|
12
|
+
const first = header.split(",")[0]?.trim();
|
|
13
|
+
if (first) return `ip:${first}`;
|
|
14
|
+
}
|
|
15
|
+
return null;
|
|
16
|
+
};
|
|
17
|
+
//#endregion
|
|
18
|
+
export { defaultIdentify };
|
|
19
|
+
|
|
20
|
+
//# sourceMappingURL=identify.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"identify.js","names":[],"sources":["../../src/spam/identify.ts"],"sourcesContent":["import type { IdentifyFn } from './types'\n\n/**\n * Default identity resolution for rate-limiting + upload ownership. Prefers an authenticated user id\n * (trustworthy); else the first hop of the configured trusted IP header (best-effort, proxy-dependent);\n * else null, in which case the caller fails open (cannot fairly rate-limit an unidentifiable client).\n */\nexport const defaultIdentify =\n\t(ipHeader: string): IdentifyFn =>\n\t(req) => {\n\t\tconst userId = req.user?.id\n\t\tif (userId != null) {\n\t\t\treturn `user:${String(userId)}`\n\t\t}\n\t\tconst header = req.headers?.get(ipHeader)\n\t\tif (header) {\n\t\t\tconst first = header.split(',')[0]?.trim()\n\t\t\tif (first) {\n\t\t\t\treturn `ip:${first}`\n\t\t\t}\n\t\t}\n\t\treturn null\n\t}\n"],"mappings":";;;;;;AAOA,MAAa,mBACX,cACA,QAAQ;CACR,MAAM,SAAS,IAAI,MAAM;CACzB,IAAI,UAAU,MACb,OAAO,QAAQ,OAAO,MAAM;CAE7B,MAAM,SAAS,IAAI,SAAS,IAAI,QAAQ;CACxC,IAAI,QAAQ;EACX,MAAM,QAAQ,OAAO,MAAM,GAAG,EAAE,IAAI,KAAK;EACzC,IAAI,OACH,OAAO,MAAM;CAEf;CACA,OAAO;AACR"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { RateLimiter } from "./types.js";
|
|
2
|
+
|
|
3
|
+
//#region src/spam/rateLimiter.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* The default rate limiter: a read-modify-write window counter over Payload's `payload.kv`
|
|
6
|
+
* (always present; default `DatabaseKVAdapter`, durable + cross-instance). Because the KV interface
|
|
7
|
+
* has no atomic increment, the get-then-set is NON-ATOMIC, so a concurrent burst can slightly undercount.
|
|
8
|
+
* This is a SOFT limit, acceptable for spam basics and complemented by edge/WAF limiting. The window
|
|
9
|
+
* expires lazily on read; stale keys for one-time identities linger (minor; the count self-resets).
|
|
10
|
+
* `now` is injectable for tests.
|
|
11
|
+
*/
|
|
12
|
+
declare const createKvRateLimiter: (options?: {
|
|
13
|
+
now?: () => number;
|
|
14
|
+
namespace?: string;
|
|
15
|
+
}) => RateLimiter;
|
|
16
|
+
//#endregion
|
|
17
|
+
export { createKvRateLimiter };
|
|
18
|
+
//# sourceMappingURL=rateLimiter.d.ts.map
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
//#region src/spam/rateLimiter.ts
|
|
2
|
+
/**
|
|
3
|
+
* The default rate limiter: a read-modify-write window counter over Payload's `payload.kv`
|
|
4
|
+
* (always present; default `DatabaseKVAdapter`, durable + cross-instance). Because the KV interface
|
|
5
|
+
* has no atomic increment, the get-then-set is NON-ATOMIC, so a concurrent burst can slightly undercount.
|
|
6
|
+
* This is a SOFT limit, acceptable for spam basics and complemented by edge/WAF limiting. The window
|
|
7
|
+
* expires lazily on read; stale keys for one-time identities linger (minor; the count self-resets).
|
|
8
|
+
* `now` is injectable for tests.
|
|
9
|
+
*/
|
|
10
|
+
const createKvRateLimiter = (options = {}) => {
|
|
11
|
+
const now = options.now ?? (() => Date.now());
|
|
12
|
+
const prefix = options.namespace ?? "fb:rl:";
|
|
13
|
+
return { async check({ key, max, window, req }) {
|
|
14
|
+
const kv = req.payload.kv;
|
|
15
|
+
const storeKey = `${prefix}${key}`;
|
|
16
|
+
const current = now();
|
|
17
|
+
const existing = await kv.get(storeKey).catch(() => null);
|
|
18
|
+
let count;
|
|
19
|
+
let windowStart;
|
|
20
|
+
if (!existing || current - existing.windowStart >= window) {
|
|
21
|
+
count = 1;
|
|
22
|
+
windowStart = current;
|
|
23
|
+
} else {
|
|
24
|
+
count = existing.count + 1;
|
|
25
|
+
windowStart = existing.windowStart;
|
|
26
|
+
}
|
|
27
|
+
await kv.set(storeKey, {
|
|
28
|
+
count,
|
|
29
|
+
windowStart
|
|
30
|
+
}).catch(() => void 0);
|
|
31
|
+
return {
|
|
32
|
+
ok: count <= max,
|
|
33
|
+
remaining: Math.max(0, max - count),
|
|
34
|
+
resetAt: windowStart + window
|
|
35
|
+
};
|
|
36
|
+
} };
|
|
37
|
+
};
|
|
38
|
+
//#endregion
|
|
39
|
+
export { createKvRateLimiter };
|
|
40
|
+
|
|
41
|
+
//# sourceMappingURL=rateLimiter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rateLimiter.js","names":[],"sources":["../../src/spam/rateLimiter.ts"],"sourcesContent":["import type { RateLimiter } from './types'\n\ntype WindowState = { count: number; windowStart: number }\n\n/**\n * The default rate limiter: a read-modify-write window counter over Payload's `payload.kv`\n * (always present; default `DatabaseKVAdapter`, durable + cross-instance). Because the KV interface\n * has no atomic increment, the get-then-set is NON-ATOMIC, so a concurrent burst can slightly undercount.\n * This is a SOFT limit, acceptable for spam basics and complemented by edge/WAF limiting. The window\n * expires lazily on read; stale keys for one-time identities linger (minor; the count self-resets).\n * `now` is injectable for tests.\n */\nexport const createKvRateLimiter = (\n\toptions: { now?: () => number; namespace?: string } = {}\n): RateLimiter => {\n\tconst now = options.now ?? (() => Date.now())\n\tconst prefix = options.namespace ?? 'fb:rl:'\n\treturn {\n\t\tasync check({ key, max, window, req }) {\n\t\t\tconst kv = req.payload.kv\n\t\t\tconst storeKey = `${prefix}${key}`\n\t\t\tconst current = now()\n\t\t\tconst existing = await kv.get<WindowState>(storeKey).catch(() => null)\n\t\t\tlet count: number\n\t\t\tlet windowStart: number\n\t\t\tif (!existing || current - existing.windowStart >= window) {\n\t\t\t\tcount = 1\n\t\t\t\twindowStart = current\n\t\t\t} else {\n\t\t\t\tcount = existing.count + 1\n\t\t\t\twindowStart = existing.windowStart\n\t\t\t}\n\t\t\tawait kv.set(storeKey, { count, windowStart }).catch(() => undefined)\n\t\t\treturn {\n\t\t\t\tok: count <= max,\n\t\t\t\tremaining: Math.max(0, max - count),\n\t\t\t\tresetAt: windowStart + window,\n\t\t\t}\n\t\t},\n\t}\n}\n"],"mappings":";;;;;;;;;AAYA,MAAa,uBACZ,UAAsD,CAAC,MACtC;CACjB,MAAM,MAAM,QAAQ,cAAc,KAAK,IAAI;CAC3C,MAAM,SAAS,QAAQ,aAAa;CACpC,OAAO,EACN,MAAM,MAAM,EAAE,KAAK,KAAK,QAAQ,OAAO;EACtC,MAAM,KAAK,IAAI,QAAQ;EACvB,MAAM,WAAW,GAAG,SAAS;EAC7B,MAAM,UAAU,IAAI;EACpB,MAAM,WAAW,MAAM,GAAG,IAAiB,QAAQ,EAAE,YAAY,IAAI;EACrE,IAAI;EACJ,IAAI;EACJ,IAAI,CAAC,YAAY,UAAU,SAAS,eAAe,QAAQ;GAC1D,QAAQ;GACR,cAAc;EACf,OAAO;GACN,QAAQ,SAAS,QAAQ;GACzB,cAAc,SAAS;EACxB;EACA,MAAM,GAAG,IAAI,UAAU;GAAE;GAAO;EAAY,CAAC,EAAE,YAAY,KAAA,CAAS;EACpE,OAAO;GACN,IAAI,SAAS;GACb,WAAW,KAAK,IAAI,GAAG,MAAM,KAAK;GAClC,SAAS,cAAc;EACxB;CACD,EACD;AACD"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import "./constants.js";
|
|
2
|
+
//#region src/spam/reserved.ts
|
|
3
|
+
/**
|
|
4
|
+
* Split reserved entries (honeypot decoy, captcha token) out of the submitted `values`. The honeypot
|
|
5
|
+
* rides a configurable, innocuous field name; the captcha token rides a fixed reserved key. Real field
|
|
6
|
+
* values pass through unchanged. `runSubmission` would already ignore unknown field names, but stripping
|
|
7
|
+
* here keeps them out of storage and out of the validation pass.
|
|
8
|
+
*/
|
|
9
|
+
const extractReservedValues = (values, honeypotField) => {
|
|
10
|
+
const cleaned = [];
|
|
11
|
+
const result = { cleaned };
|
|
12
|
+
for (const entry of values) {
|
|
13
|
+
if (entry.field === "__fb_captcha") {
|
|
14
|
+
if (typeof entry.value === "string") result.captchaToken = entry.value;
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
if (honeypotField !== null && entry.field === honeypotField) {
|
|
18
|
+
result.honeypot = entry.value;
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
cleaned.push(entry);
|
|
22
|
+
}
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
/** A honeypot is tripped when the decoy carries any non-empty value (a real user never fills it). */
|
|
26
|
+
const isHoneypotTripped = (value) => value != null && value !== "" && !(Array.isArray(value) && value.length === 0);
|
|
27
|
+
//#endregion
|
|
28
|
+
export { extractReservedValues, isHoneypotTripped };
|
|
29
|
+
|
|
30
|
+
//# sourceMappingURL=reserved.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reserved.js","names":[],"sources":["../../src/spam/reserved.ts"],"sourcesContent":["import type { SubmissionValue } from '../submissions/types'\nimport { CAPTCHA_TOKEN_KEY } from './constants'\n\nexport type ExtractedReserved = {\n\t/** Real field values with reserved entries removed. */\n\tcleaned: SubmissionValue[]\n\t/** The honeypot decoy value, if a honeypot field name was active and present. */\n\thoneypot?: unknown\n\t/** A captcha token carried by the client. */\n\tcaptchaToken?: string\n}\n\n/**\n * Split reserved entries (honeypot decoy, captcha token) out of the submitted `values`. The honeypot\n * rides a configurable, innocuous field name; the captcha token rides a fixed reserved key. Real field\n * values pass through unchanged. `runSubmission` would already ignore unknown field names, but stripping\n * here keeps them out of storage and out of the validation pass.\n */\nexport const extractReservedValues = (\n\tvalues: SubmissionValue[],\n\thoneypotField: string | null\n): ExtractedReserved => {\n\tconst cleaned: SubmissionValue[] = []\n\tconst result: ExtractedReserved = { cleaned }\n\tfor (const entry of values) {\n\t\tif (entry.field === CAPTCHA_TOKEN_KEY) {\n\t\t\tif (typeof entry.value === 'string') {\n\t\t\t\tresult.captchaToken = entry.value\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tif (honeypotField !== null && entry.field === honeypotField) {\n\t\t\tresult.honeypot = entry.value\n\t\t\tcontinue\n\t\t}\n\t\tcleaned.push(entry)\n\t}\n\treturn result\n}\n\n/** A honeypot is tripped when the decoy carries any non-empty value (a real user never fills it). */\nexport const isHoneypotTripped = (value: unknown): boolean =>\n\tvalue != null && value !== '' && !(Array.isArray(value) && value.length === 0)\n"],"mappings":";;;;;;;;AAkBA,MAAa,yBACZ,QACA,kBACuB;CACvB,MAAM,UAA6B,CAAC;CACpC,MAAM,SAA4B,EAAE,QAAQ;CAC5C,KAAK,MAAM,SAAS,QAAQ;EAC3B,IAAI,MAAM,UAAA,gBAA6B;GACtC,IAAI,OAAO,MAAM,UAAU,UAC1B,OAAO,eAAe,MAAM;GAE7B;EACD;EACA,IAAI,kBAAkB,QAAQ,MAAM,UAAU,eAAe;GAC5D,OAAO,WAAW,MAAM;GACxB;EACD;EACA,QAAQ,KAAK,KAAK;CACnB;CACA,OAAO;AACR;;AAGA,MAAa,qBAAqB,UACjC,SAAS,QAAQ,UAAU,MAAM,EAAE,MAAM,QAAQ,KAAK,KAAK,MAAM,WAAW"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ResolvedSpamConfig, SpamOption } from "./types.js";
|
|
2
|
+
|
|
3
|
+
//#region src/spam/resolveSpam.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Resolve the `spam` plugin option into a concrete config. `false` disables the whole subsystem.
|
|
6
|
+
* Otherwise honeypot + both rate limits default on, captcha + metadata capture default off, and the
|
|
7
|
+
* identity seam defaults to user id / trusted IP header.
|
|
8
|
+
*/
|
|
9
|
+
declare const resolveSpamConfig: (option: SpamOption | undefined) => ResolvedSpamConfig | false;
|
|
10
|
+
//#endregion
|
|
11
|
+
export { resolveSpamConfig };
|
|
12
|
+
//# sourceMappingURL=resolveSpam.d.ts.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import "./constants.js";
|
|
2
|
+
import { defaultIdentify } from "./identify.js";
|
|
3
|
+
import { createKvRateLimiter } from "./rateLimiter.js";
|
|
4
|
+
//#region src/spam/resolveSpam.ts
|
|
5
|
+
const DEFAULT_WINDOW = 6e4;
|
|
6
|
+
const DEFAULT_SUBMISSION_MAX = 5;
|
|
7
|
+
const DEFAULT_UPLOAD_MAX = 20;
|
|
8
|
+
const resolveRateLimit = (option, defaultMax) => {
|
|
9
|
+
if (option === false) return false;
|
|
10
|
+
const cfg = option ?? {};
|
|
11
|
+
return {
|
|
12
|
+
window: cfg.window ?? DEFAULT_WINDOW,
|
|
13
|
+
max: cfg.max ?? defaultMax,
|
|
14
|
+
limiter: cfg.limiter ?? createKvRateLimiter()
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Resolve the `spam` plugin option into a concrete config. `false` disables the whole subsystem.
|
|
19
|
+
* Otherwise honeypot + both rate limits default on, captcha + metadata capture default off, and the
|
|
20
|
+
* identity seam defaults to user id / trusted IP header.
|
|
21
|
+
*/
|
|
22
|
+
const resolveSpamConfig = (option) => {
|
|
23
|
+
if (option === false) return false;
|
|
24
|
+
const cfg = option ?? {};
|
|
25
|
+
const ipHeader = cfg.ipHeader ?? "x-forwarded-for";
|
|
26
|
+
return {
|
|
27
|
+
honeypot: cfg.honeypot === false ? false : { fieldName: cfg.honeypot?.fieldName ?? "website" },
|
|
28
|
+
rateLimit: resolveRateLimit(cfg.rateLimit, DEFAULT_SUBMISSION_MAX),
|
|
29
|
+
uploadRateLimit: resolveRateLimit(cfg.uploadRateLimit, DEFAULT_UPLOAD_MAX),
|
|
30
|
+
captcha: cfg.captcha,
|
|
31
|
+
identify: cfg.identify ?? defaultIdentify(ipHeader),
|
|
32
|
+
ipHeader,
|
|
33
|
+
metadata: {
|
|
34
|
+
ip: cfg.metadata?.ip ?? false,
|
|
35
|
+
ua: cfg.metadata?.ua ?? false
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
//#endregion
|
|
40
|
+
export { resolveSpamConfig };
|
|
41
|
+
|
|
42
|
+
//# sourceMappingURL=resolveSpam.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolveSpam.js","names":[],"sources":["../../src/spam/resolveSpam.ts"],"sourcesContent":["import { DEFAULT_HONEYPOT_FIELD } from './constants'\nimport { defaultIdentify } from './identify'\nimport { createKvRateLimiter } from './rateLimiter'\nimport type {\n\tRateLimitConfig,\n\tResolvedRateLimit,\n\tResolvedSpamConfig,\n\tSpamConfig,\n\tSpamOption,\n} from './types'\n\nconst DEFAULT_WINDOW = 60_000\nconst DEFAULT_SUBMISSION_MAX = 5\nconst DEFAULT_UPLOAD_MAX = 20\n\nconst resolveRateLimit = (\n\toption: false | RateLimitConfig | undefined,\n\tdefaultMax: number\n): false | ResolvedRateLimit => {\n\tif (option === false) {\n\t\treturn false\n\t}\n\tconst cfg = option ?? {}\n\treturn {\n\t\twindow: cfg.window ?? DEFAULT_WINDOW,\n\t\tmax: cfg.max ?? defaultMax,\n\t\tlimiter: cfg.limiter ?? createKvRateLimiter(),\n\t}\n}\n\n/**\n * Resolve the `spam` plugin option into a concrete config. `false` disables the whole subsystem.\n * Otherwise honeypot + both rate limits default on, captcha + metadata capture default off, and the\n * identity seam defaults to user id / trusted IP header.\n */\nexport const resolveSpamConfig = (option: SpamOption | undefined): ResolvedSpamConfig | false => {\n\tif (option === false) {\n\t\treturn false\n\t}\n\tconst cfg: SpamConfig = option ?? {}\n\tconst ipHeader = cfg.ipHeader ?? 'x-forwarded-for'\n\treturn {\n\t\thoneypot:\n\t\t\tcfg.honeypot === false\n\t\t\t\t? false\n\t\t\t\t: { fieldName: cfg.honeypot?.fieldName ?? DEFAULT_HONEYPOT_FIELD },\n\t\trateLimit: resolveRateLimit(cfg.rateLimit, DEFAULT_SUBMISSION_MAX),\n\t\tuploadRateLimit: resolveRateLimit(cfg.uploadRateLimit, DEFAULT_UPLOAD_MAX),\n\t\tcaptcha: cfg.captcha,\n\t\tidentify: cfg.identify ?? defaultIdentify(ipHeader),\n\t\tipHeader,\n\t\tmetadata: { ip: cfg.metadata?.ip ?? false, ua: cfg.metadata?.ua ?? false },\n\t}\n}\n"],"mappings":";;;;AAWA,MAAM,iBAAiB;AACvB,MAAM,yBAAyB;AAC/B,MAAM,qBAAqB;AAE3B,MAAM,oBACL,QACA,eAC+B;CAC/B,IAAI,WAAW,OACd,OAAO;CAER,MAAM,MAAM,UAAU,CAAC;CACvB,OAAO;EACN,QAAQ,IAAI,UAAU;EACtB,KAAK,IAAI,OAAO;EAChB,SAAS,IAAI,WAAW,oBAAoB;CAC7C;AACD;;;;;;AAOA,MAAa,qBAAqB,WAA+D;CAChG,IAAI,WAAW,OACd,OAAO;CAER,MAAM,MAAkB,UAAU,CAAC;CACnC,MAAM,WAAW,IAAI,YAAY;CACjC,OAAO;EACN,UACC,IAAI,aAAa,QACd,QACA,EAAE,WAAW,IAAI,UAAU,aAAA,UAAoC;EACnE,WAAW,iBAAiB,IAAI,WAAW,sBAAsB;EACjE,iBAAiB,iBAAiB,IAAI,iBAAiB,kBAAkB;EACzE,SAAS,IAAI;EACb,UAAU,IAAI,YAAY,gBAAgB,QAAQ;EAClD;EACA,UAAU;GAAE,IAAI,IAAI,UAAU,MAAM;GAAO,IAAI,IAAI,UAAU,MAAM;EAAM;CAC1E;AACD"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { keys } from "../translations/keys.js";
|
|
2
|
+
import { asTranslate } from "../translations/server.js";
|
|
3
|
+
import { IDENTITY_CONTEXT_KEY } from "./constants.js";
|
|
4
|
+
import { extractReservedValues, isHoneypotTripped } from "./reserved.js";
|
|
5
|
+
import { APIError } from "payload";
|
|
6
|
+
//#region src/spam/spamGuard.ts
|
|
7
|
+
const firstHop = (req, header) => {
|
|
8
|
+
const raw = req.headers?.get(header);
|
|
9
|
+
return raw ? raw.split(",")[0]?.trim() ?? void 0 : void 0;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Submissions spam guard, prepended before `validateSubmission` in `beforeValidate` so it rejects before
|
|
13
|
+
* the form load + field validation. On create it: resolves the request identity once (stashing it on
|
|
14
|
+
* `req.context` for upload-ownership verification downstream); rate-limits per form + identity (429,
|
|
15
|
+
* fail-open when identity is null); strips reserved `values` entries (honeypot, captcha token); rejects a
|
|
16
|
+
* filled honeypot with a generic error; verifies a configured captcha; and writes a server-authoritative
|
|
17
|
+
* `meta` (timestamp + spam signal, with opt-in ip/ua). App-level rate limiting is defense-in-depth that
|
|
18
|
+
* complements edge/CDN/WAF limiting, not a DoS replacement.
|
|
19
|
+
*/
|
|
20
|
+
const buildSpamGuard = (spam) => async ({ data, operation, req }) => {
|
|
21
|
+
if (operation !== "create" || !data) return data;
|
|
22
|
+
const t = asTranslate(req.i18n.t);
|
|
23
|
+
const authenticated = Boolean(req.user);
|
|
24
|
+
const identity = await spam.identify(req);
|
|
25
|
+
if (identity != null) req.context[IDENTITY_CONTEXT_KEY] = identity;
|
|
26
|
+
if (spam.rateLimit !== false && identity != null) {
|
|
27
|
+
const formKey = data.form != null ? String(data.form) : "unknown";
|
|
28
|
+
const { ok } = await spam.rateLimit.limiter.check({
|
|
29
|
+
key: `submissions:${formKey}:${identity}`,
|
|
30
|
+
max: spam.rateLimit.max,
|
|
31
|
+
window: spam.rateLimit.window,
|
|
32
|
+
req
|
|
33
|
+
});
|
|
34
|
+
if (!ok) throw new APIError(t(keys.spamRateLimited), 429);
|
|
35
|
+
}
|
|
36
|
+
const honeypotField = spam.honeypot === false ? null : spam.honeypot.fieldName;
|
|
37
|
+
const { cleaned, honeypot, captchaToken } = extractReservedValues(data.values ?? [], honeypotField);
|
|
38
|
+
data.values = cleaned;
|
|
39
|
+
if (!authenticated && honeypotField !== null && isHoneypotTripped(honeypot)) throw new APIError(t(keys.spamRejected), 400);
|
|
40
|
+
const captchaChecked = Boolean(spam.captcha) && !authenticated;
|
|
41
|
+
if (spam.captcha && !authenticated) {
|
|
42
|
+
if (!(typeof captchaToken === "string" && captchaToken.length > 0 ? await spam.captcha.verify({
|
|
43
|
+
token: captchaToken,
|
|
44
|
+
req
|
|
45
|
+
}).catch(() => false) : false)) throw new APIError(t(keys.spamCaptchaFailed), 400);
|
|
46
|
+
}
|
|
47
|
+
const serverMeta = {
|
|
48
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
49
|
+
spam: { captcha: captchaChecked ? "passed" : "skipped" }
|
|
50
|
+
};
|
|
51
|
+
if (spam.metadata.ip) {
|
|
52
|
+
const ip = firstHop(req, spam.ipHeader);
|
|
53
|
+
if (ip) serverMeta.ip = ip;
|
|
54
|
+
}
|
|
55
|
+
if (spam.metadata.ua) {
|
|
56
|
+
const ua = req.headers?.get("user-agent");
|
|
57
|
+
if (ua) serverMeta.ua = ua;
|
|
58
|
+
}
|
|
59
|
+
data.meta = {
|
|
60
|
+
...authenticated && data.meta != null && typeof data.meta === "object" ? data.meta : {},
|
|
61
|
+
...serverMeta
|
|
62
|
+
};
|
|
63
|
+
return data;
|
|
64
|
+
};
|
|
65
|
+
//#endregion
|
|
66
|
+
export { buildSpamGuard };
|
|
67
|
+
|
|
68
|
+
//# sourceMappingURL=spamGuard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spamGuard.js","names":[],"sources":["../../src/spam/spamGuard.ts"],"sourcesContent":["import { APIError, type CollectionBeforeValidateHook } from 'payload'\nimport type { SubmissionValue } from '../submissions/types'\nimport { keys } from '../translations/keys'\nimport { asTranslate } from '../translations/server'\nimport { IDENTITY_CONTEXT_KEY } from './constants'\nimport { extractReservedValues, isHoneypotTripped } from './reserved'\nimport type { ResolvedSpamConfig } from './types'\n\nconst firstHop = (req: { headers?: Headers }, header: string): string | undefined => {\n\tconst raw = req.headers?.get(header)\n\treturn raw ? (raw.split(',')[0]?.trim() ?? undefined) : undefined\n}\n\n/**\n * Submissions spam guard, prepended before `validateSubmission` in `beforeValidate` so it rejects before\n * the form load + field validation. On create it: resolves the request identity once (stashing it on\n * `req.context` for upload-ownership verification downstream); rate-limits per form + identity (429,\n * fail-open when identity is null); strips reserved `values` entries (honeypot, captcha token); rejects a\n * filled honeypot with a generic error; verifies a configured captcha; and writes a server-authoritative\n * `meta` (timestamp + spam signal, with opt-in ip/ua). App-level rate limiting is defense-in-depth that\n * complements edge/CDN/WAF limiting, not a DoS replacement.\n */\nexport const buildSpamGuard =\n\t(spam: ResolvedSpamConfig): CollectionBeforeValidateHook =>\n\tasync ({ data, operation, req }) => {\n\t\tif (operation !== 'create' || !data) {\n\t\t\treturn data\n\t\t}\n\t\tconst t = asTranslate(req.i18n.t)\n\t\t// Honeypot + captcha are anti-bot measures for the anonymous public path; an authenticated request\n\t\t// (admin panel, logged-in API client) is already gated by auth and never carries a decoy/token, so\n\t\t// checking it would falsely reject legitimate authed creates. Rate-limit + ownership still apply.\n\t\tconst authenticated = Boolean(req.user)\n\n\t\tconst identity = await spam.identify(req)\n\t\tif (identity != null) {\n\t\t\treq.context[IDENTITY_CONTEXT_KEY] = identity\n\t\t}\n\n\t\tif (spam.rateLimit !== false && identity != null) {\n\t\t\tconst formKey = data.form != null ? String(data.form) : 'unknown'\n\t\t\tconst { ok } = await spam.rateLimit.limiter.check({\n\t\t\t\tkey: `submissions:${formKey}:${identity}`,\n\t\t\t\tmax: spam.rateLimit.max,\n\t\t\t\twindow: spam.rateLimit.window,\n\t\t\t\treq,\n\t\t\t})\n\t\t\tif (!ok) {\n\t\t\t\tthrow new APIError(t(keys.spamRateLimited), 429)\n\t\t\t}\n\t\t}\n\n\t\tconst honeypotField = spam.honeypot === false ? null : spam.honeypot.fieldName\n\t\tconst values = (data.values as SubmissionValue[] | undefined) ?? []\n\t\tconst { cleaned, honeypot, captchaToken } = extractReservedValues(values, honeypotField)\n\t\tdata.values = cleaned\n\n\t\tif (!authenticated && honeypotField !== null && isHoneypotTripped(honeypot)) {\n\t\t\tthrow new APIError(t(keys.spamRejected), 400)\n\t\t}\n\n\t\tconst captchaChecked = Boolean(spam.captcha) && !authenticated\n\t\tif (spam.captcha && !authenticated) {\n\t\t\tconst passed =\n\t\t\t\ttypeof captchaToken === 'string' && captchaToken.length > 0\n\t\t\t\t\t? await spam.captcha.verify({ token: captchaToken, req }).catch(() => false)\n\t\t\t\t\t: false\n\t\t\tif (!passed) {\n\t\t\t\tthrow new APIError(t(keys.spamCaptchaFailed), 400)\n\t\t\t}\n\t\t}\n\n\t\tconst serverMeta: Record<string, unknown> = {\n\t\t\tat: new Date().toISOString(),\n\t\t\tspam: { captcha: captchaChecked ? 'passed' : 'skipped' },\n\t\t}\n\t\tif (spam.metadata.ip) {\n\t\t\tconst ip = firstHop(req, spam.ipHeader)\n\t\t\tif (ip) {\n\t\t\t\tserverMeta.ip = ip\n\t\t\t}\n\t\t}\n\t\tif (spam.metadata.ua) {\n\t\t\tconst ua = req.headers?.get('user-agent')\n\t\t\tif (ua) {\n\t\t\t\tserverMeta.ua = ua\n\t\t\t}\n\t\t}\n\t\t// Preserve an authenticated (trusted) caller's own `meta` keys, with server fields winning. Anonymous\n\t\t// `meta` is never trusted, so it is discarded.\n\t\tconst clientMeta =\n\t\t\tauthenticated && data.meta != null && typeof data.meta === 'object'\n\t\t\t\t? (data.meta as Record<string, unknown>)\n\t\t\t\t: {}\n\t\tdata.meta = { ...clientMeta, ...serverMeta }\n\n\t\treturn data\n\t}\n"],"mappings":";;;;;;AAQA,MAAM,YAAY,KAA4B,WAAuC;CACpF,MAAM,MAAM,IAAI,SAAS,IAAI,MAAM;CACnC,OAAO,MAAO,IAAI,MAAM,GAAG,EAAE,IAAI,KAAK,KAAK,KAAA,IAAa,KAAA;AACzD;;;;;;;;;;AAWA,MAAa,kBACX,SACD,OAAO,EAAE,MAAM,WAAW,UAAU;CACnC,IAAI,cAAc,YAAY,CAAC,MAC9B,OAAO;CAER,MAAM,IAAI,YAAY,IAAI,KAAK,CAAC;CAIhC,MAAM,gBAAgB,QAAQ,IAAI,IAAI;CAEtC,MAAM,WAAW,MAAM,KAAK,SAAS,GAAG;CACxC,IAAI,YAAY,MACf,IAAI,QAAQ,wBAAwB;CAGrC,IAAI,KAAK,cAAc,SAAS,YAAY,MAAM;EACjD,MAAM,UAAU,KAAK,QAAQ,OAAO,OAAO,KAAK,IAAI,IAAI;EACxD,MAAM,EAAE,OAAO,MAAM,KAAK,UAAU,QAAQ,MAAM;GACjD,KAAK,eAAe,QAAQ,GAAG;GAC/B,KAAK,KAAK,UAAU;GACpB,QAAQ,KAAK,UAAU;GACvB;EACD,CAAC;EACD,IAAI,CAAC,IACJ,MAAM,IAAI,SAAS,EAAE,KAAK,eAAe,GAAG,GAAG;CAEjD;CAEA,MAAM,gBAAgB,KAAK,aAAa,QAAQ,OAAO,KAAK,SAAS;CAErE,MAAM,EAAE,SAAS,UAAU,iBAAiB,sBAD5B,KAAK,UAA4C,CAAC,GACQ,aAAa;CACvF,KAAK,SAAS;CAEd,IAAI,CAAC,iBAAiB,kBAAkB,QAAQ,kBAAkB,QAAQ,GACzE,MAAM,IAAI,SAAS,EAAE,KAAK,YAAY,GAAG,GAAG;CAG7C,MAAM,iBAAiB,QAAQ,KAAK,OAAO,KAAK,CAAC;CACjD,IAAI,KAAK,WAAW,CAAC;MAKhB,EAHH,OAAO,iBAAiB,YAAY,aAAa,SAAS,IACvD,MAAM,KAAK,QAAQ,OAAO;GAAE,OAAO;GAAc;EAAI,CAAC,EAAE,YAAY,KAAK,IACzE,QAEH,MAAM,IAAI,SAAS,EAAE,KAAK,iBAAiB,GAAG,GAAG;CAAA;CAInD,MAAM,aAAsC;EAC3C,qBAAI,IAAI,KAAK,GAAE,YAAY;EAC3B,MAAM,EAAE,SAAS,iBAAiB,WAAW,UAAU;CACxD;CACA,IAAI,KAAK,SAAS,IAAI;EACrB,MAAM,KAAK,SAAS,KAAK,KAAK,QAAQ;EACtC,IAAI,IACH,WAAW,KAAK;CAElB;CACA,IAAI,KAAK,SAAS,IAAI;EACrB,MAAM,KAAK,IAAI,SAAS,IAAI,YAAY;EACxC,IAAI,IACH,WAAW,KAAK;CAElB;CAOA,KAAK,OAAO;EAAE,GAHb,iBAAiB,KAAK,QAAQ,QAAQ,OAAO,KAAK,SAAS,WACvD,KAAK,OACN,CAAC;EACwB,GAAG;CAAW;CAE3C,OAAO;AACR"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { PayloadRequest } from "payload";
|
|
2
|
+
|
|
3
|
+
//#region src/spam/types.d.ts
|
|
4
|
+
/** Stable identity key for a request, or null/undefined when the client cannot be identified (rate-limiting then skips: fail-open). */
|
|
5
|
+
type IdentifyFn = (req: PayloadRequest) => null | string | undefined | Promise<null | string | undefined>;
|
|
6
|
+
type RateLimitResult = {
|
|
7
|
+
ok: boolean; /** Remaining requests in the current window (>= 0). */
|
|
8
|
+
remaining: number; /** Epoch ms when the current window resets. */
|
|
9
|
+
resetAt: number;
|
|
10
|
+
};
|
|
11
|
+
type RateLimitCheckArgs = {
|
|
12
|
+
key: string; /** Max requests per window. */
|
|
13
|
+
max: number; /** Window length in ms. */
|
|
14
|
+
window: number;
|
|
15
|
+
req: PayloadRequest;
|
|
16
|
+
};
|
|
17
|
+
/** Pluggable limiter. The default is a window counter over `payload.kv`; swap for Redis/etc. */
|
|
18
|
+
type RateLimiter = {
|
|
19
|
+
check(args: RateLimitCheckArgs): Promise<RateLimitResult>;
|
|
20
|
+
};
|
|
21
|
+
type CaptchaVerifyArgs = {
|
|
22
|
+
token: string;
|
|
23
|
+
req: PayloadRequest;
|
|
24
|
+
};
|
|
25
|
+
/** A captcha adapter. v1 ships the seam only (no built-in provider). Build with `defineCaptchaProvider`. */
|
|
26
|
+
type CaptchaProvider = {
|
|
27
|
+
type: string;
|
|
28
|
+
verify(args: CaptchaVerifyArgs): Promise<boolean>;
|
|
29
|
+
};
|
|
30
|
+
type RateLimitConfig = {
|
|
31
|
+
/** Window length in ms. Default 60000. */window?: number; /** Max creates per identity per window. */
|
|
32
|
+
max?: number; /** Override the limiter (default: KV window counter). */
|
|
33
|
+
limiter?: RateLimiter;
|
|
34
|
+
};
|
|
35
|
+
type SpamMetadataConfig = {
|
|
36
|
+
/** Persist the client IP (from the trusted header) onto the submission `meta`. Default false (privacy). */ip?: boolean; /** Persist the user-agent onto the submission `meta`. Default false. */
|
|
37
|
+
ua?: boolean;
|
|
38
|
+
};
|
|
39
|
+
type SpamConfig = {
|
|
40
|
+
/** Honeypot decoy. Default on. `false` disables. */honeypot?: false | {
|
|
41
|
+
fieldName?: string;
|
|
42
|
+
}; /** Per-identity rate limit on submission create. Default on (60s window, max 5). `false` disables. */
|
|
43
|
+
rateLimit?: false | RateLimitConfig; /** Per-identity rate limit on upload create. Default on (60s window, max 20). `false` disables. */
|
|
44
|
+
uploadRateLimit?: false | RateLimitConfig; /** A captcha provider (none by default; seam only in v1). */
|
|
45
|
+
captcha?: CaptchaProvider; /** Identity resolution for rate-limiting, upload ownership, and (future) poll dedup. Default: user id, else trusted IP header. */
|
|
46
|
+
identify?: IdentifyFn; /** Header read for the client IP (proxy-dependent, best-effort). Default 'x-forwarded-for'. */
|
|
47
|
+
ipHeader?: string; /** Opt-in capture of client metadata onto the submission `meta`. Off by default. */
|
|
48
|
+
metadata?: SpamMetadataConfig;
|
|
49
|
+
};
|
|
50
|
+
/** `false` disables the whole subsystem; an object configures it (all controls default on except captcha + metadata). */
|
|
51
|
+
type SpamOption = false | SpamConfig;
|
|
52
|
+
type ResolvedRateLimit = {
|
|
53
|
+
window: number;
|
|
54
|
+
max: number;
|
|
55
|
+
limiter: RateLimiter;
|
|
56
|
+
};
|
|
57
|
+
type ResolvedSpamConfig = {
|
|
58
|
+
honeypot: false | {
|
|
59
|
+
fieldName: string;
|
|
60
|
+
};
|
|
61
|
+
rateLimit: false | ResolvedRateLimit;
|
|
62
|
+
uploadRateLimit: false | ResolvedRateLimit;
|
|
63
|
+
captcha?: CaptchaProvider;
|
|
64
|
+
identify: IdentifyFn;
|
|
65
|
+
ipHeader: string;
|
|
66
|
+
metadata: {
|
|
67
|
+
ip: boolean;
|
|
68
|
+
ua: boolean;
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
//#endregion
|
|
72
|
+
export { CaptchaProvider, CaptchaVerifyArgs, IdentifyFn, RateLimitCheckArgs, RateLimitConfig, RateLimitResult, RateLimiter, ResolvedSpamConfig, SpamConfig, SpamMetadataConfig, SpamOption };
|
|
73
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { keys } from "../translations/keys.js";
|
|
2
|
+
import { asTranslate } from "../translations/server.js";
|
|
3
|
+
import { APIError } from "payload";
|
|
4
|
+
//#region src/spam/uploadHooks.ts
|
|
5
|
+
/**
|
|
6
|
+
* Stamp the resolved request identity onto a new upload's `owner`. `captureFileRef` later requires the
|
|
7
|
+
* referencing submission's identity to match, so an anonymous submitter cannot capture another identity's
|
|
8
|
+
* upload. No identity (no user, no trusted IP header) means no stamp, so the upload stays unscoped (a
|
|
9
|
+
* deliberate fail-open; identity granularity is IP-level, so same-network clients share scope -- documented).
|
|
10
|
+
*/
|
|
11
|
+
const buildUploadOwnerStamp = (spam) => async ({ data, operation, req }) => {
|
|
12
|
+
if (operation !== "create" || !data) return data;
|
|
13
|
+
const identity = await spam.identify(req);
|
|
14
|
+
if (identity != null && data.owner == null) data.owner = identity;
|
|
15
|
+
return data;
|
|
16
|
+
};
|
|
17
|
+
/** Reject upload floods per identity before the file is written to `staticDir`. Fail-open without identity. */
|
|
18
|
+
const buildUploadRateLimit = (spam) => async ({ operation, req }) => {
|
|
19
|
+
if (operation !== "create" || spam.uploadRateLimit === false) return;
|
|
20
|
+
const identity = await spam.identify(req);
|
|
21
|
+
if (identity == null) return;
|
|
22
|
+
const { ok } = await spam.uploadRateLimit.limiter.check({
|
|
23
|
+
key: `uploads:${identity}`,
|
|
24
|
+
max: spam.uploadRateLimit.max,
|
|
25
|
+
window: spam.uploadRateLimit.window,
|
|
26
|
+
req
|
|
27
|
+
});
|
|
28
|
+
if (!ok) throw new APIError(asTranslate(req.i18n.t)(keys.spamRateLimited), 429);
|
|
29
|
+
};
|
|
30
|
+
//#endregion
|
|
31
|
+
export { buildUploadOwnerStamp, buildUploadRateLimit };
|
|
32
|
+
|
|
33
|
+
//# sourceMappingURL=uploadHooks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"uploadHooks.js","names":[],"sources":["../../src/spam/uploadHooks.ts"],"sourcesContent":["import {\n\tAPIError,\n\ttype CollectionBeforeOperationHook,\n\ttype CollectionBeforeValidateHook,\n} from 'payload'\nimport { keys } from '../translations/keys'\nimport { asTranslate } from '../translations/server'\nimport type { ResolvedSpamConfig } from './types'\n\n/**\n * Stamp the resolved request identity onto a new upload's `owner`. `captureFileRef` later requires the\n * referencing submission's identity to match, so an anonymous submitter cannot capture another identity's\n * upload. No identity (no user, no trusted IP header) means no stamp, so the upload stays unscoped (a\n * deliberate fail-open; identity granularity is IP-level, so same-network clients share scope -- documented).\n */\nexport const buildUploadOwnerStamp =\n\t(spam: ResolvedSpamConfig): CollectionBeforeValidateHook =>\n\tasync ({ data, operation, req }) => {\n\t\tif (operation !== 'create' || !data) {\n\t\t\treturn data\n\t\t}\n\t\tconst identity = await spam.identify(req)\n\t\tif (identity != null && (data as { owner?: unknown }).owner == null) {\n\t\t\t;(data as { owner?: string }).owner = identity\n\t\t}\n\t\treturn data\n\t}\n\n/** Reject upload floods per identity before the file is written to `staticDir`. Fail-open without identity. */\nexport const buildUploadRateLimit =\n\t(spam: ResolvedSpamConfig): CollectionBeforeOperationHook =>\n\tasync ({ operation, req }) => {\n\t\tif (operation !== 'create' || spam.uploadRateLimit === false) {\n\t\t\treturn\n\t\t}\n\t\tconst identity = await spam.identify(req)\n\t\tif (identity == null) {\n\t\t\treturn\n\t\t}\n\t\tconst { ok } = await spam.uploadRateLimit.limiter.check({\n\t\t\tkey: `uploads:${identity}`,\n\t\t\tmax: spam.uploadRateLimit.max,\n\t\t\twindow: spam.uploadRateLimit.window,\n\t\t\treq,\n\t\t})\n\t\tif (!ok) {\n\t\t\tthrow new APIError(asTranslate(req.i18n.t)(keys.spamRateLimited), 429)\n\t\t}\n\t}\n"],"mappings":";;;;;;;;;;AAeA,MAAa,yBACX,SACD,OAAO,EAAE,MAAM,WAAW,UAAU;CACnC,IAAI,cAAc,YAAY,CAAC,MAC9B,OAAO;CAER,MAAM,WAAW,MAAM,KAAK,SAAS,GAAG;CACxC,IAAI,YAAY,QAAS,KAA6B,SAAS,MAC7D,KAA6B,QAAQ;CAEvC,OAAO;AACR;;AAGD,MAAa,wBACX,SACD,OAAO,EAAE,WAAW,UAAU;CAC7B,IAAI,cAAc,YAAY,KAAK,oBAAoB,OACtD;CAED,MAAM,WAAW,MAAM,KAAK,SAAS,GAAG;CACxC,IAAI,YAAY,MACf;CAED,MAAM,EAAE,OAAO,MAAM,KAAK,gBAAgB,QAAQ,MAAM;EACvD,KAAK,WAAW;EAChB,KAAK,KAAK,gBAAgB;EAC1B,QAAQ,KAAK,gBAAgB;EAC7B;CACD,CAAC;CACD,IAAI,CAAC,IACJ,MAAM,IAAI,SAAS,YAAY,IAAI,KAAK,CAAC,EAAE,KAAK,eAAe,GAAG,GAAG;AAEvE"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { UIFieldServerProps } from "payload";
|
|
2
|
+
|
|
3
|
+
//#region src/submissions/SubmissionAnswers.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Server component. Formats submission data into serializable props and delegates
|
|
6
|
+
* rendering to the client component which uses Payload's UI components.
|
|
7
|
+
*/
|
|
8
|
+
declare const SubmissionAnswers: ({
|
|
9
|
+
data,
|
|
10
|
+
req
|
|
11
|
+
}: UIFieldServerProps) => import("react/jsx-runtime").JSX.Element;
|
|
12
|
+
//#endregion
|
|
13
|
+
export { SubmissionAnswers };
|
|
14
|
+
//# sourceMappingURL=SubmissionAnswers.d.ts.map
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { keys } from "../translations/keys.js";
|
|
2
|
+
import { asFieldTranslate } from "../translations/server.js";
|
|
3
|
+
import { defaultFieldDefinitions } from "../fields/builtin/index.js";
|
|
4
|
+
import { buildRegistry } from "../fields/registry.js";
|
|
5
|
+
import { SubmissionAnswersClient } from "./SubmissionAnswersClient.js";
|
|
6
|
+
import { jsx } from "react/jsx-runtime";
|
|
7
|
+
//#region src/submissions/SubmissionAnswers.tsx
|
|
8
|
+
const registry = buildRegistry(defaultFieldDefinitions);
|
|
9
|
+
const formatDate = (iso, locale) => {
|
|
10
|
+
try {
|
|
11
|
+
return new Intl.DateTimeFormat(locale, {
|
|
12
|
+
dateStyle: "medium",
|
|
13
|
+
timeStyle: "short"
|
|
14
|
+
}).format(new Date(iso));
|
|
15
|
+
} catch {
|
|
16
|
+
return iso;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Server component. Formats submission data into serializable props and delegates
|
|
21
|
+
* rendering to the client component which uses Payload's UI components.
|
|
22
|
+
*/
|
|
23
|
+
const SubmissionAnswers = ({ data, req }) => {
|
|
24
|
+
const doc = data ?? {};
|
|
25
|
+
const descriptors = doc.descriptors ?? [];
|
|
26
|
+
const values = doc.values ?? [];
|
|
27
|
+
const locale = doc.locale ?? req.locale ?? "en";
|
|
28
|
+
const t = asFieldTranslate(req.i18n.t);
|
|
29
|
+
const rawConsent = doc.consent ?? [];
|
|
30
|
+
const meta = doc.meta;
|
|
31
|
+
const valueByField = new Map(values.map((entry) => [entry.field, entry.value]));
|
|
32
|
+
const answers = [];
|
|
33
|
+
const repeaters = [];
|
|
34
|
+
for (const descriptor of descriptors) {
|
|
35
|
+
const raw = valueByField.get(descriptor.field);
|
|
36
|
+
if (descriptor.fieldType === "repeater") {
|
|
37
|
+
const rows = Array.isArray(raw) ? raw : [];
|
|
38
|
+
const subDescs = descriptor.subFieldDescriptors ?? [];
|
|
39
|
+
repeaters.push({
|
|
40
|
+
field: descriptor.field,
|
|
41
|
+
label: descriptor.label,
|
|
42
|
+
rows: rows.map((row, rowIndex) => ({
|
|
43
|
+
id: String(rowIndex),
|
|
44
|
+
subFields: subDescs.map((sub) => {
|
|
45
|
+
const subDef = registry.get(sub.fieldType);
|
|
46
|
+
const subRaw = row[sub.field];
|
|
47
|
+
const formatted = subDef?.format ? subDef.format({
|
|
48
|
+
value: subRaw,
|
|
49
|
+
config: {},
|
|
50
|
+
optionLabels: sub.optionLabels,
|
|
51
|
+
locale,
|
|
52
|
+
t
|
|
53
|
+
}) : subRaw == null ? "" : String(subRaw);
|
|
54
|
+
return {
|
|
55
|
+
label: sub.label,
|
|
56
|
+
value: formatted || "—"
|
|
57
|
+
};
|
|
58
|
+
})
|
|
59
|
+
}))
|
|
60
|
+
});
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const def = registry.get(descriptor.fieldType);
|
|
64
|
+
const formatted = def?.format ? def.format({
|
|
65
|
+
value: raw,
|
|
66
|
+
config: {},
|
|
67
|
+
optionLabels: descriptor.optionLabels,
|
|
68
|
+
locale,
|
|
69
|
+
t
|
|
70
|
+
}) : raw == null ? "" : String(raw);
|
|
71
|
+
if (descriptor.fieldType === "file") {
|
|
72
|
+
const ref = raw && typeof raw === "object" && "url" in raw ? raw : null;
|
|
73
|
+
const url = typeof ref?.url === "string" ? ref.url : void 0;
|
|
74
|
+
const filename = typeof ref?.filename === "string" ? ref.filename : url ?? "";
|
|
75
|
+
answers.push({
|
|
76
|
+
field: descriptor.field,
|
|
77
|
+
label: descriptor.label,
|
|
78
|
+
value: filename,
|
|
79
|
+
href: url
|
|
80
|
+
});
|
|
81
|
+
} else answers.push({
|
|
82
|
+
field: descriptor.field,
|
|
83
|
+
label: descriptor.label,
|
|
84
|
+
value: formatted || "—",
|
|
85
|
+
multiline: descriptor.fieldType === "textarea"
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
const consent = rawConsent.map((entry) => ({
|
|
89
|
+
...entry,
|
|
90
|
+
at: formatDate(entry.at, locale)
|
|
91
|
+
}));
|
|
92
|
+
const metaItems = [];
|
|
93
|
+
if (meta) {
|
|
94
|
+
if (meta.at) metaItems.push({
|
|
95
|
+
label: t(keys.submissionMetaReceivedAt),
|
|
96
|
+
value: formatDate(meta.at, locale)
|
|
97
|
+
});
|
|
98
|
+
if (meta.ip) metaItems.push({
|
|
99
|
+
label: t(keys.submissionMetaIp),
|
|
100
|
+
value: meta.ip
|
|
101
|
+
});
|
|
102
|
+
if (meta.ua) metaItems.push({
|
|
103
|
+
label: t(keys.submissionMetaUserAgent),
|
|
104
|
+
value: String(meta.ua)
|
|
105
|
+
});
|
|
106
|
+
if (meta.spam) {
|
|
107
|
+
const captcha = typeof meta.spam === "object" && "captcha" in meta.spam ? String(meta.spam.captcha) : "—";
|
|
108
|
+
metaItems.push({
|
|
109
|
+
label: t(keys.submissionMetaCaptcha),
|
|
110
|
+
value: captcha
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return /* @__PURE__ */ jsx(SubmissionAnswersClient, {
|
|
115
|
+
answers,
|
|
116
|
+
repeaters,
|
|
117
|
+
consent,
|
|
118
|
+
meta: metaItems,
|
|
119
|
+
labels: {
|
|
120
|
+
answers: t(keys.submissionAnswers),
|
|
121
|
+
consent: t(keys.submissionConsent),
|
|
122
|
+
submissionDetails: t(keys.submissionDetails),
|
|
123
|
+
agreed: t(keys.submissionConsentAgreed),
|
|
124
|
+
declined: t(keys.submissionConsentDeclined),
|
|
125
|
+
row: t(keys.repeaterRow),
|
|
126
|
+
empty: t(keys.submissionNoAnswers)
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
};
|
|
130
|
+
//#endregion
|
|
131
|
+
export { SubmissionAnswers };
|
|
132
|
+
|
|
133
|
+
//# sourceMappingURL=SubmissionAnswers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SubmissionAnswers.js","names":[],"sources":["../../src/submissions/SubmissionAnswers.tsx"],"sourcesContent":["import type { UIFieldServerProps } from 'payload'\nimport { defaultFieldDefinitions } from '../fields/builtin'\nimport { buildRegistry } from '../fields/registry'\nimport { keys } from '../translations/keys'\nimport { asFieldTranslate } from '../translations/server'\nimport type {\n\tAnswerItem,\n\tConsentItem,\n\tMetaItem,\n\tRepeaterItem,\n\tSubmissionAnswersLabels,\n} from './SubmissionAnswersClient'\nimport { SubmissionAnswersClient } from './SubmissionAnswersClient'\nimport type { SubmissionDescriptor, SubmissionValue } from './types'\n\nconst registry = buildRegistry(defaultFieldDefinitions)\n\ntype ConsentEntry = {\n\tfield: string\n\tagreed: boolean\n\tref?: string\n\tversionRef?: string\n\tat: string\n}\n\ntype MetaSpam = { captcha?: string }\n\ntype SubmissionMeta = {\n\tat?: string\n\tip?: string\n\tua?: string\n\tspam?: MetaSpam\n\t[key: string]: unknown\n}\n\ntype SubmissionDoc = {\n\tvalues?: SubmissionValue[]\n\tdescriptors?: SubmissionDescriptor[]\n\tlocale?: string\n\tconsent?: ConsentEntry[]\n\tmeta?: SubmissionMeta\n}\n\nconst formatDate = (iso: string, locale: string): string => {\n\ttry {\n\t\treturn new Intl.DateTimeFormat(locale, { dateStyle: 'medium', timeStyle: 'short' }).format(\n\t\t\tnew Date(iso)\n\t\t)\n\t} catch {\n\t\treturn iso\n\t}\n}\n\n/**\n * Server component. Formats submission data into serializable props and delegates\n * rendering to the client component which uses Payload's UI components.\n */\nexport const SubmissionAnswers = ({ data, req }: UIFieldServerProps) => {\n\tconst doc = (data ?? {}) as SubmissionDoc\n\tconst descriptors = doc.descriptors ?? []\n\tconst values = doc.values ?? []\n\tconst locale = doc.locale ?? req.locale ?? 'en'\n\tconst t = asFieldTranslate(req.i18n.t)\n\tconst rawConsent = doc.consent ?? []\n\tconst meta = doc.meta\n\n\tconst valueByField = new Map(values.map((entry) => [entry.field, entry.value]))\n\n\tconst answers: AnswerItem[] = []\n\tconst repeaters: RepeaterItem[] = []\n\n\tfor (const descriptor of descriptors) {\n\t\tconst raw = valueByField.get(descriptor.field)\n\n\t\tif (descriptor.fieldType === 'repeater') {\n\t\t\tconst rows = Array.isArray(raw) ? (raw as Array<Record<string, unknown>>) : []\n\t\t\tconst subDescs = descriptor.subFieldDescriptors ?? []\n\t\t\trepeaters.push({\n\t\t\t\tfield: descriptor.field,\n\t\t\t\tlabel: descriptor.label,\n\t\t\t\trows: rows.map((row, rowIndex) => ({\n\t\t\t\t\tid: String(rowIndex),\n\t\t\t\t\tsubFields: subDescs.map((sub) => {\n\t\t\t\t\t\tconst subDef = registry.get(sub.fieldType)\n\t\t\t\t\t\tconst subRaw = row[sub.field]\n\t\t\t\t\t\tconst formatted = subDef?.format\n\t\t\t\t\t\t\t? subDef.format({\n\t\t\t\t\t\t\t\t\tvalue: subRaw,\n\t\t\t\t\t\t\t\t\tconfig: {},\n\t\t\t\t\t\t\t\t\toptionLabels: sub.optionLabels,\n\t\t\t\t\t\t\t\t\tlocale,\n\t\t\t\t\t\t\t\t\tt,\n\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t: subRaw == null\n\t\t\t\t\t\t\t\t? ''\n\t\t\t\t\t\t\t\t: String(subRaw)\n\t\t\t\t\t\treturn { label: sub.label, value: formatted || '—' }\n\t\t\t\t\t}),\n\t\t\t\t})),\n\t\t\t})\n\t\t\tcontinue\n\t\t}\n\n\t\tconst def = registry.get(descriptor.fieldType)\n\t\tconst formatted = def?.format\n\t\t\t? def.format({ value: raw, config: {}, optionLabels: descriptor.optionLabels, locale, t })\n\t\t\t: raw == null\n\t\t\t\t? ''\n\t\t\t\t: String(raw)\n\n\t\tif (descriptor.fieldType === 'file') {\n\t\t\tconst ref =\n\t\t\t\traw && typeof raw === 'object' && 'url' in raw\n\t\t\t\t\t? (raw as { url?: string; filename?: string })\n\t\t\t\t\t: null\n\t\t\tconst url = typeof ref?.url === 'string' ? ref.url : undefined\n\t\t\tconst filename = typeof ref?.filename === 'string' ? ref.filename : (url ?? '')\n\t\t\tanswers.push({ field: descriptor.field, label: descriptor.label, value: filename, href: url })\n\t\t} else {\n\t\t\tanswers.push({\n\t\t\t\tfield: descriptor.field,\n\t\t\t\tlabel: descriptor.label,\n\t\t\t\tvalue: formatted || '—',\n\t\t\t\tmultiline: descriptor.fieldType === 'textarea',\n\t\t\t})\n\t\t}\n\t}\n\n\tconst consent: ConsentItem[] = rawConsent.map((entry) => ({\n\t\t...entry,\n\t\tat: formatDate(entry.at, locale),\n\t}))\n\n\tconst metaItems: MetaItem[] = []\n\tif (meta) {\n\t\tif (meta.at)\n\t\t\tmetaItems.push({\n\t\t\t\tlabel: t(keys.submissionMetaReceivedAt),\n\t\t\t\tvalue: formatDate(meta.at, locale),\n\t\t\t})\n\t\tif (meta.ip) metaItems.push({ label: t(keys.submissionMetaIp), value: meta.ip })\n\t\tif (meta.ua) metaItems.push({ label: t(keys.submissionMetaUserAgent), value: String(meta.ua) })\n\t\tif (meta.spam) {\n\t\t\tconst captcha =\n\t\t\t\ttypeof meta.spam === 'object' && 'captcha' in meta.spam\n\t\t\t\t\t? String((meta.spam as MetaSpam).captcha)\n\t\t\t\t\t: '—'\n\t\t\tmetaItems.push({ label: t(keys.submissionMetaCaptcha), value: captcha })\n\t\t}\n\t}\n\n\tconst labels: SubmissionAnswersLabels = {\n\t\tanswers: t(keys.submissionAnswers),\n\t\tconsent: t(keys.submissionConsent),\n\t\tsubmissionDetails: t(keys.submissionDetails),\n\t\tagreed: t(keys.submissionConsentAgreed),\n\t\tdeclined: t(keys.submissionConsentDeclined),\n\t\trow: t(keys.repeaterRow),\n\t\tempty: t(keys.submissionNoAnswers),\n\t}\n\n\treturn (\n\t\t<SubmissionAnswersClient\n\t\t\tanswers={answers}\n\t\t\trepeaters={repeaters}\n\t\t\tconsent={consent}\n\t\t\tmeta={metaItems}\n\t\t\tlabels={labels}\n\t\t/>\n\t)\n}\n"],"mappings":";;;;;;;AAeA,MAAM,WAAW,cAAc,uBAAuB;AA4BtD,MAAM,cAAc,KAAa,WAA2B;CAC3D,IAAI;EACH,OAAO,IAAI,KAAK,eAAe,QAAQ;GAAE,WAAW;GAAU,WAAW;EAAQ,CAAC,EAAE,OACnF,IAAI,KAAK,GAAG,CACb;CACD,QAAQ;EACP,OAAO;CACR;AACD;;;;;AAMA,MAAa,qBAAqB,EAAE,MAAM,UAA8B;CACvE,MAAM,MAAO,QAAQ,CAAC;CACtB,MAAM,cAAc,IAAI,eAAe,CAAC;CACxC,MAAM,SAAS,IAAI,UAAU,CAAC;CAC9B,MAAM,SAAS,IAAI,UAAU,IAAI,UAAU;CAC3C,MAAM,IAAI,iBAAiB,IAAI,KAAK,CAAC;CACrC,MAAM,aAAa,IAAI,WAAW,CAAC;CACnC,MAAM,OAAO,IAAI;CAEjB,MAAM,eAAe,IAAI,IAAI,OAAO,KAAK,UAAU,CAAC,MAAM,OAAO,MAAM,KAAK,CAAC,CAAC;CAE9E,MAAM,UAAwB,CAAC;CAC/B,MAAM,YAA4B,CAAC;CAEnC,KAAK,MAAM,cAAc,aAAa;EACrC,MAAM,MAAM,aAAa,IAAI,WAAW,KAAK;EAE7C,IAAI,WAAW,cAAc,YAAY;GACxC,MAAM,OAAO,MAAM,QAAQ,GAAG,IAAK,MAAyC,CAAC;GAC7E,MAAM,WAAW,WAAW,uBAAuB,CAAC;GACpD,UAAU,KAAK;IACd,OAAO,WAAW;IAClB,OAAO,WAAW;IAClB,MAAM,KAAK,KAAK,KAAK,cAAc;KAClC,IAAI,OAAO,QAAQ;KACnB,WAAW,SAAS,KAAK,QAAQ;MAChC,MAAM,SAAS,SAAS,IAAI,IAAI,SAAS;MACzC,MAAM,SAAS,IAAI,IAAI;MACvB,MAAM,YAAY,QAAQ,SACvB,OAAO,OAAO;OACd,OAAO;OACP,QAAQ,CAAC;OACT,cAAc,IAAI;OAClB;OACA;MACD,CAAC,IACA,UAAU,OACT,KACA,OAAO,MAAM;MACjB,OAAO;OAAE,OAAO,IAAI;OAAO,OAAO,aAAa;MAAI;KACpD,CAAC;IACF,EAAE;GACH,CAAC;GACD;EACD;EAEA,MAAM,MAAM,SAAS,IAAI,WAAW,SAAS;EAC7C,MAAM,YAAY,KAAK,SACpB,IAAI,OAAO;GAAE,OAAO;GAAK,QAAQ,CAAC;GAAG,cAAc,WAAW;GAAc;GAAQ;EAAE,CAAC,IACvF,OAAO,OACN,KACA,OAAO,GAAG;EAEd,IAAI,WAAW,cAAc,QAAQ;GACpC,MAAM,MACL,OAAO,OAAO,QAAQ,YAAY,SAAS,MACvC,MACD;GACJ,MAAM,MAAM,OAAO,KAAK,QAAQ,WAAW,IAAI,MAAM,KAAA;GACrD,MAAM,WAAW,OAAO,KAAK,aAAa,WAAW,IAAI,WAAY,OAAO;GAC5E,QAAQ,KAAK;IAAE,OAAO,WAAW;IAAO,OAAO,WAAW;IAAO,OAAO;IAAU,MAAM;GAAI,CAAC;EAC9F,OACC,QAAQ,KAAK;GACZ,OAAO,WAAW;GAClB,OAAO,WAAW;GAClB,OAAO,aAAa;GACpB,WAAW,WAAW,cAAc;EACrC,CAAC;CAEH;CAEA,MAAM,UAAyB,WAAW,KAAK,WAAW;EACzD,GAAG;EACH,IAAI,WAAW,MAAM,IAAI,MAAM;CAChC,EAAE;CAEF,MAAM,YAAwB,CAAC;CAC/B,IAAI,MAAM;EACT,IAAI,KAAK,IACR,UAAU,KAAK;GACd,OAAO,EAAE,KAAK,wBAAwB;GACtC,OAAO,WAAW,KAAK,IAAI,MAAM;EAClC,CAAC;EACF,IAAI,KAAK,IAAI,UAAU,KAAK;GAAE,OAAO,EAAE,KAAK,gBAAgB;GAAG,OAAO,KAAK;EAAG,CAAC;EAC/E,IAAI,KAAK,IAAI,UAAU,KAAK;GAAE,OAAO,EAAE,KAAK,uBAAuB;GAAG,OAAO,OAAO,KAAK,EAAE;EAAE,CAAC;EAC9F,IAAI,KAAK,MAAM;GACd,MAAM,UACL,OAAO,KAAK,SAAS,YAAY,aAAa,KAAK,OAChD,OAAQ,KAAK,KAAkB,OAAO,IACtC;GACJ,UAAU,KAAK;IAAE,OAAO,EAAE,KAAK,qBAAqB;IAAG,OAAO;GAAQ,CAAC;EACxE;CACD;CAYA,OACC,oBAAC,yBAAD;EACU;EACE;EACF;EACT,MAAM;EACN,QAAQ;GAfT,SAAS,EAAE,KAAK,iBAAiB;GACjC,SAAS,EAAE,KAAK,iBAAiB;GACjC,mBAAmB,EAAE,KAAK,iBAAiB;GAC3C,QAAQ,EAAE,KAAK,uBAAuB;GACtC,UAAU,EAAE,KAAK,yBAAyB;GAC1C,KAAK,EAAE,KAAK,WAAW;GACvB,OAAO,EAAE,KAAK,mBAAmB;EASnB;CACb,CAAA;AAEH"}
|