@10x-media/form-builder 0.1.0-beta.0 → 0.1.0-beta.10
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 +345 -0
- package/README.md +45 -1402
- package/dist/actions/body/converters.d.ts +20 -0
- package/dist/actions/body/converters.js +57 -0
- package/dist/actions/body/converters.js.map +1 -0
- package/dist/actions/body/escapeHtml.d.ts +6 -0
- package/dist/actions/body/escapeHtml.js +14 -0
- package/dist/actions/body/escapeHtml.js.map +1 -0
- package/dist/actions/body/serializeBody.d.ts +55 -0
- package/dist/actions/body/serializeBody.js +78 -0
- package/dist/actions/body/serializeBody.js.map +1 -0
- package/dist/actions/body/serializeSlate.js +31 -0
- package/dist/actions/body/serializeSlate.js.map +1 -0
- package/dist/actions/body/textOfBody.d.ts +13 -0
- package/dist/actions/body/textOfBody.js +35 -0
- package/dist/actions/body/textOfBody.js.map +1 -0
- package/dist/actions/body/wildcards.d.ts +10 -0
- package/dist/actions/body/wildcards.js +33 -0
- package/dist/actions/body/wildcards.js.map +1 -0
- package/dist/actions/buildActionBlocks.js +22 -0
- package/dist/actions/buildActionBlocks.js.map +1 -0
- package/dist/actions/builtin/confirmation.js +52 -0
- package/dist/actions/builtin/confirmation.js.map +1 -0
- package/dist/actions/builtin/emailAction.d.ts +17 -0
- package/dist/actions/builtin/emailAction.js +83 -0
- package/dist/actions/builtin/emailAction.js.map +1 -0
- package/dist/actions/builtin/emailTeam.js +22 -0
- package/dist/actions/builtin/emailTeam.js.map +1 -0
- package/dist/actions/builtin/index.d.ts +22 -0
- package/dist/actions/builtin/index.js +27 -0
- package/dist/actions/builtin/index.js.map +1 -0
- package/dist/actions/builtin/signedWebhook.js +53 -0
- package/dist/actions/builtin/signedWebhook.js.map +1 -0
- package/dist/actions/defineAction.d.ts +37 -0
- package/dist/actions/defineAction.js +6 -0
- package/dist/actions/defineAction.js.map +1 -0
- package/dist/actions/dispatch.js +49 -0
- package/dist/actions/dispatch.js.map +1 -0
- package/dist/actions/emailRecipients.d.ts +12 -0
- package/dist/actions/emailRecipients.js +116 -0
- package/dist/actions/emailRecipients.js.map +1 -0
- package/dist/actions/fromAddresses.d.ts +21 -0
- package/dist/actions/fromAddresses.js +74 -0
- package/dist/actions/fromAddresses.js.map +1 -0
- package/dist/actions/registry.d.ts +17 -0
- package/dist/actions/registry.js +12 -0
- package/dist/actions/registry.js.map +1 -0
- package/dist/actions/runActions.d.ts +9 -0
- package/dist/actions/runActions.js +43 -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 +87 -0
- package/dist/actions/task.js.map +1 -0
- package/dist/aggregation/aggregateResponses.d.ts +40 -0
- package/dist/aggregation/aggregateResponses.js +80 -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 +61 -0
- package/dist/aggregation/resolveResultsRequest.js +113 -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 +28 -0
- package/dist/calc/computeCalcFields.js.map +1 -0
- package/dist/calc/evaluate.d.ts +8 -0
- package/dist/calc/evaluate.js +58 -0
- package/dist/calc/evaluate.js.map +1 -0
- package/dist/calc/normalizeCalc.d.ts +8 -0
- package/dist/calc/normalizeCalc.js +87 -0
- package/dist/calc/normalizeCalc.js.map +1 -0
- package/dist/calc/types.d.ts +34 -0
- package/dist/calc/types.js +22 -0
- package/dist/calc/types.js.map +1 -0
- package/dist/client/ByteSizeField.d.ts +29 -0
- package/dist/client/ByteSizeField.js +58 -0
- package/dist/client/ByteSizeField.js.map +1 -0
- package/dist/client/ClosePollButton.d.ts +15 -0
- package/dist/client/ClosePollButton.js +80 -0
- package/dist/client/ClosePollButton.js.map +1 -0
- package/dist/client/CondensedArray.d.ts +15 -0
- package/dist/client/CondensedArray.js +97 -0
- package/dist/client/CondensedArray.js.map +1 -0
- package/dist/client/ConditionBuilder.js +122 -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/ConsentBlockLabel.d.ts +15 -0
- package/dist/client/ConsentBlockLabel.js +71 -0
- package/dist/client/ConsentBlockLabel.js.map +1 -0
- package/dist/client/ConsentSourceRowLabel.d.ts +9 -0
- package/dist/client/ConsentSourceRowLabel.js +21 -0
- package/dist/client/ConsentSourceRowLabel.js.map +1 -0
- package/dist/client/EndpointOptionsSelect.d.ts +46 -0
- package/dist/client/EndpointOptionsSelect.js +106 -0
- package/dist/client/EndpointOptionsSelect.js.map +1 -0
- package/dist/client/FieldBlockLabel.d.ts +18 -0
- package/dist/client/FieldBlockLabel.js +34 -0
- package/dist/client/FieldBlockLabel.js.map +1 -0
- package/dist/client/FieldNameSelect.d.ts +37 -0
- package/dist/client/FieldNameSelect.js +72 -0
- package/dist/client/FieldNameSelect.js.map +1 -0
- package/dist/client/FlowBuilder.d.ts +15 -0
- package/dist/client/FlowBuilder.js +457 -0
- package/dist/client/FlowBuilder.js.map +1 -0
- package/dist/client/FormConditionField.d.ts +21 -0
- package/dist/client/FormConditionField.js +30 -0
- package/dist/client/FormConditionField.js.map +1 -0
- package/dist/client/RecipientsSelect.d.ts +31 -0
- package/dist/client/RecipientsSelect.js +174 -0
- package/dist/client/RecipientsSelect.js.map +1 -0
- package/dist/client/RuleDescription.d.ts +21 -0
- package/dist/client/RuleDescription.js +19 -0
- package/dist/client/RuleDescription.js.map +1 -0
- package/dist/client/admin.css +275 -0
- package/dist/client/endpointOptions.js +29 -0
- package/dist/client/endpointOptions.js.map +1 -0
- package/dist/client/flowAuthoring.js +142 -0
- package/dist/client/flowAuthoring.js.map +1 -0
- package/dist/client/synthesizeClientField.js +57 -0
- package/dist/client/synthesizeClientField.js.map +1 -0
- package/dist/client/toStaticLabel.js +13 -0
- package/dist/client/toStaticLabel.js.map +1 -0
- package/dist/collections/buttonFields.d.ts +34 -0
- package/dist/collections/buttonFields.js +35 -0
- package/dist/collections/buttonFields.js.map +1 -0
- package/dist/collections/cells/FieldCountCell.d.ts +15 -0
- package/dist/collections/cells/FieldCountCell.js +18 -0
- package/dist/collections/cells/FieldCountCell.js.map +1 -0
- package/dist/collections/cells/FlowStepsCell.d.ts +14 -0
- package/dist/collections/cells/FlowStepsCell.js +17 -0
- package/dist/collections/cells/FlowStepsCell.js.map +1 -0
- package/dist/collections/formSubmissions.js +204 -0
- package/dist/collections/formSubmissions.js.map +1 -0
- package/dist/collections/forms.js +482 -0
- package/dist/collections/forms.js.map +1 -0
- package/dist/collections/formsEndpoints.js +101 -0
- package/dist/collections/formsEndpoints.js.map +1 -0
- package/dist/collections/redirectFields.d.ts +29 -0
- package/dist/conditions/conditionType.js +21 -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 +11 -0
- package/dist/{fieldTypes-CzhhJXjg.js → conditions/fieldTypes.js} +6 -3
- package/dist/conditions/fieldTypes.js.map +1 -0
- package/dist/conditions/normalizeConditions.js +61 -0
- package/dist/conditions/normalizeConditions.js.map +1 -0
- package/dist/conditions/types.d.ts +8 -0
- package/dist/consent/applyConsentStatements.d.ts +14 -0
- package/dist/consent/applyConsentStatements.js +17 -0
- package/dist/consent/applyConsentStatements.js.map +1 -0
- package/dist/consent/captureConsent.d.ts +55 -0
- package/dist/consent/captureConsent.js +73 -0
- package/dist/consent/captureConsent.js.map +1 -0
- package/dist/consent/consentSourcesField.d.ts +66 -0
- package/dist/consent/consentSourcesField.js +90 -0
- package/dist/consent/consentSourcesField.js.map +1 -0
- package/dist/consent/resolveConsentEntries.d.ts +24 -0
- package/dist/consent/resolveConsentEntries.js +40 -0
- package/dist/consent/resolveConsentEntries.js.map +1 -0
- package/dist/consent/resolveConsentSourcesRequest.d.ts +37 -0
- package/dist/consent/resolveConsentSourcesRequest.js +54 -0
- package/dist/consent/resolveConsentSourcesRequest.js.map +1 -0
- package/dist/consent/resolveConsentStatements.d.ts +45 -0
- package/dist/consent/resolveConsentStatements.js +44 -0
- package/dist/consent/resolveConsentStatements.js.map +1 -0
- package/dist/consent/resolvePublishedVersionRef.d.ts +23 -0
- package/dist/consent/resolvePublishedVersionRef.js +30 -0
- package/dist/consent/resolvePublishedVersionRef.js.map +1 -0
- package/dist/consent/types.d.ts +61 -0
- package/dist/email/departments.d.ts +59 -0
- package/dist/email/departments.js +87 -0
- package/dist/email/departments.js.map +1 -0
- package/dist/email/departmentsField.d.ts +53 -0
- package/dist/email/departmentsField.js +73 -0
- package/dist/email/departmentsField.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 +15 -21
- package/dist/exports/client.js +15 -319
- package/dist/exports/i18n.d.ts +6 -138
- package/dist/exports/i18n.js +6 -3
- package/dist/exports/react.d.ts +63 -550
- package/dist/exports/react.js +55 -1563
- package/dist/exports/rsc.d.ts +9 -16
- package/dist/exports/rsc.js +9 -55
- package/dist/exports/types.d.ts +36 -5
- package/dist/fields/buildFieldBlocks.js +80 -0
- package/dist/fields/buildFieldBlocks.js.map +1 -0
- package/dist/fields/builtin/calculation.js +144 -0
- package/dist/fields/builtin/calculation.js.map +1 -0
- package/dist/fields/builtin/checkbox.js +21 -0
- package/dist/fields/builtin/checkbox.js.map +1 -0
- package/dist/fields/builtin/consent.js +47 -0
- package/dist/fields/builtin/consent.js.map +1 -0
- package/dist/fields/builtin/country.d.ts +13 -0
- package/dist/fields/builtin/country.js +30 -0
- package/dist/fields/builtin/country.js.map +1 -0
- package/dist/fields/builtin/date.js +31 -0
- package/dist/fields/builtin/date.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.d.ts +15 -0
- package/dist/fields/builtin/file.js +127 -0
- package/dist/fields/builtin/file.js.map +1 -0
- package/dist/fields/builtin/index.d.ts +26 -0
- package/dist/fields/builtin/index.js +49 -0
- package/dist/fields/builtin/index.js.map +1 -0
- package/dist/fields/builtin/message.js +30 -0
- package/dist/fields/builtin/message.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 +49 -0
- package/dist/fields/builtin/repeater.js.map +1 -0
- package/dist/fields/builtin/select.js +49 -0
- package/dist/fields/builtin/select.js.map +1 -0
- package/dist/fields/builtin/state.d.ts +13 -0
- package/dist/fields/builtin/state.js +30 -0
- package/dist/fields/builtin/state.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/data/regions.d.ts +21 -0
- package/dist/fields/data/regions.js +1238 -0
- package/dist/fields/data/regions.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/fieldKey.d.ts +15 -0
- package/dist/fields/fieldKey.js +16 -0
- package/dist/fields/fieldKey.js.map +1 -0
- package/dist/fields/fieldNamesOfType.js +30 -0
- package/dist/fields/fieldNamesOfType.js.map +1 -0
- package/dist/fields/instanceOptions.js +20 -0
- package/dist/fields/instanceOptions.js.map +1 -0
- package/dist/fields/localizedIf.d.ts +13 -0
- package/dist/fields/localizedIf.js +12 -0
- package/dist/fields/localizedIf.js.map +1 -0
- package/dist/fields/registry.d.ts +12 -0
- package/dist/fields/registry.js +39 -0
- package/dist/fields/registry.js.map +1 -0
- package/dist/fields/sharedConfig.js +151 -0
- package/dist/fields/sharedConfig.js.map +1 -0
- package/dist/fields/types.d.ts +152 -0
- package/dist/flow/engine.d.ts +19 -0
- package/dist/flow/engine.js +29 -0
- package/dist/flow/engine.js.map +1 -0
- package/dist/flow/normalizeFlow.js +54 -0
- package/dist/flow/normalizeFlow.js.map +1 -0
- package/dist/flow/stepLabel.js +15 -0
- package/dist/flow/stepLabel.js.map +1 -0
- package/dist/flow/types.d.ts +32 -0
- package/dist/flow/types.js +11 -0
- package/dist/flow/types.js.map +1 -0
- package/dist/form/pollState.d.ts +22 -0
- package/dist/form/pollState.js +17 -0
- package/dist/form/pollState.js.map +1 -0
- package/dist/form/toFormDocument.d.ts +102 -0
- package/dist/form/toFormDocument.js +87 -0
- package/dist/form/toFormDocument.js.map +1 -0
- package/dist/form/types.d.ts +65 -0
- package/dist/index.d.ts +88 -4
- package/dist/index.js +111 -1898
- package/dist/index.js.map +1 -1
- package/dist/options.d.ts +229 -0
- package/dist/plugin/access.js +12 -0
- package/dist/plugin/access.js.map +1 -0
- package/dist/plugin/applyRegistryConfig.d.ts +11 -0
- package/dist/plugin/applyRegistryConfig.js +20 -0
- package/dist/plugin/applyRegistryConfig.js.map +1 -0
- package/dist/plugin/collectionOverrides.d.ts +22 -0
- package/dist/plugin/customState.js +26 -0
- package/dist/plugin/customState.js.map +1 -0
- package/dist/plugin/registerCollections.js +60 -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/plugin/uploadsCollection.d.ts +16 -0
- package/dist/plugin/uploadsCollection.js +53 -0
- package/dist/plugin/uploadsCollection.js.map +1 -0
- package/dist/poll/applyPollOptions.js +18 -0
- package/dist/poll/applyPollOptions.js.map +1 -0
- package/dist/poll/buildPollOptionSourceFields.js +50 -0
- package/dist/poll/buildPollOptionSourceFields.js.map +1 -0
- package/dist/poll/closeJob.d.ts +52 -0
- package/dist/poll/closeJob.js +119 -0
- package/dist/poll/closeJob.js.map +1 -0
- package/dist/poll/definePollOptionSource.d.ts +44 -0
- package/dist/poll/definePollOptionSource.js +6 -0
- package/dist/poll/definePollOptionSource.js.map +1 -0
- package/dist/poll/definePollType.d.ts +34 -0
- package/dist/poll/definePollType.js +6 -0
- package/dist/poll/definePollType.js.map +1 -0
- package/dist/poll/effectivePollOptions.d.ts +38 -0
- package/dist/poll/effectivePollOptions.js +64 -0
- package/dist/poll/effectivePollOptions.js.map +1 -0
- package/dist/poll/mostVoted.d.ts +21 -0
- package/dist/poll/mostVoted.js +32 -0
- package/dist/poll/mostVoted.js.map +1 -0
- package/dist/poll/outcomeBeforeChange.js +91 -0
- package/dist/poll/outcomeBeforeChange.js.map +1 -0
- package/dist/poll/outcomeFields.d.ts +38 -0
- package/dist/poll/outcomeFields.js +52 -0
- package/dist/poll/outcomeFields.js.map +1 -0
- package/dist/poll/pollTypeRegistry.d.ts +34 -0
- package/dist/poll/pollTypeRegistry.js +69 -0
- package/dist/poll/pollTypeRegistry.js.map +1 -0
- package/dist/poll/registry.d.ts +17 -0
- package/dist/poll/registry.js +12 -0
- package/dist/poll/registry.js.map +1 -0
- package/dist/poll/resolvePollCloseRequest.js +105 -0
- package/dist/poll/resolvePollCloseRequest.js.map +1 -0
- package/dist/poll/resolvePollOptions.d.ts +29 -0
- package/dist/poll/resolvePollOptions.js +52 -0
- package/dist/poll/resolvePollOptions.js.map +1 -0
- package/dist/poll/resolvePollOptionsRequest.js +50 -0
- package/dist/poll/resolvePollOptionsRequest.js.map +1 -0
- package/dist/poll/resolvePollOutcome.d.ts +34 -0
- package/dist/poll/resolvePollOutcome.js +76 -0
- package/dist/poll/resolvePollOutcome.js.map +1 -0
- package/dist/poll/resultsField.js +21 -0
- package/dist/poll/resultsField.js.map +1 -0
- package/dist/prefill/valuesFromSearchParams.d.ts +18 -0
- package/dist/prefill/valuesFromSearchParams.js +41 -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/types.d.ts +19 -0
- package/dist/react/Form.d.ts +128 -0
- package/dist/react/Form.js +619 -0
- package/dist/react/Form.js.map +1 -0
- package/dist/react/FormContext.d.ts +59 -0
- package/dist/react/FormContext.js +14 -0
- package/dist/react/FormContext.js.map +1 -0
- package/dist/react/FormControls.d.ts +55 -0
- package/dist/react/FormControls.js +56 -0
- package/dist/react/FormControls.js.map +1 -0
- package/dist/react/FormFields.d.ts +18 -0
- package/dist/react/FormFields.js +114 -0
- package/dist/react/FormFields.js.map +1 -0
- package/dist/react/FormLayout.d.ts +22 -0
- package/dist/react/FormLayout.js +15 -0
- package/dist/react/FormLayout.js.map +1 -0
- package/dist/react/FormResults.d.ts +28 -0
- package/dist/react/FormResults.js +93 -0
- package/dist/react/FormResults.js.map +1 -0
- package/dist/react/FormSteps.d.ts +17 -0
- package/dist/react/FormSteps.js +35 -0
- package/dist/react/FormSteps.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 +43 -0
- package/dist/react/Poll.js +135 -0
- package/dist/react/Poll.js.map +1 -0
- package/dist/react/captcha/HcaptchaCaptcha.d.ts +28 -0
- package/dist/react/captcha/HcaptchaCaptcha.js +53 -0
- package/dist/react/captcha/HcaptchaCaptcha.js.map +1 -0
- package/dist/react/captcha/RecaptchaCaptcha.d.ts +41 -0
- package/dist/react/captcha/RecaptchaCaptcha.js +93 -0
- package/dist/react/captcha/RecaptchaCaptcha.js.map +1 -0
- package/dist/react/captcha/TurnstileCaptcha.d.ts +28 -0
- package/dist/react/captcha/TurnstileCaptcha.js +53 -0
- package/dist/react/captcha/TurnstileCaptcha.js.map +1 -0
- package/dist/react/captcha/types.d.ts +8 -0
- package/dist/react/captcha/useCaptchaScript.js +45 -0
- package/dist/react/captcha/useCaptchaScript.js.map +1 -0
- package/dist/react/captcha/vendors.js +8 -0
- package/dist/react/captcha/vendors.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 +31 -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 +53 -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 +28 -0
- package/dist/react/primitives/Checkbox.js +21 -0
- package/dist/react/primitives/Checkbox.js.map +1 -0
- package/dist/react/primitives/FieldShell.d.ts +29 -0
- package/dist/react/primitives/FieldShell.js +44 -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 +27 -0
- package/dist/react/renderers/calculation.js.map +1 -0
- package/dist/react/renderers/checkbox.js +32 -0
- package/dist/react/renderers/checkbox.js.map +1 -0
- package/dist/react/renderers/consent.js +97 -0
- package/dist/react/renderers/consent.js.map +1 -0
- package/dist/react/renderers/date.js +33 -0
- package/dist/react/renderers/date.js.map +1 -0
- package/dist/react/renderers/email.js +34 -0
- package/dist/react/renderers/email.js.map +1 -0
- package/dist/react/renderers/file.js +107 -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 +35 -0
- package/dist/react/renderers/index.js.map +1 -0
- package/dist/react/renderers/message.js +41 -0
- package/dist/react/renderers/message.js.map +1 -0
- package/dist/react/renderers/number.js +37 -0
- package/dist/react/renderers/number.js.map +1 -0
- package/dist/react/renderers/region.js +43 -0
- package/dist/react/renderers/region.js.map +1 -0
- package/dist/react/renderers/repeater.js +131 -0
- package/dist/react/renderers/repeater.js.map +1 -0
- package/dist/react/renderers/select.js +35 -0
- package/dist/react/renderers/select.js.map +1 -0
- package/dist/react/renderers/text.js +34 -0
- package/dist/react/renderers/text.js.map +1 -0
- package/dist/react/renderers/textarea.js +33 -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 +52 -0
- package/dist/react/state.js +125 -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/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 +13 -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 +27 -0
- package/dist/react/validateField.js.map +1 -0
- package/dist/recall/interpolate.d.ts +11 -0
- package/dist/recall/interpolate.js +17 -0
- package/dist/recall/interpolate.js.map +1 -0
- package/dist/recall/resolver.d.ts +22 -0
- package/dist/recall/resolver.js +48 -0
- package/dist/recall/resolver.js.map +1 -0
- package/dist/spam/captcha.d.ts +13 -0
- package/dist/spam/captcha.js +12 -0
- package/dist/spam/captcha.js.map +1 -0
- package/dist/spam/clientIp.js +9 -0
- package/dist/spam/clientIp.js.map +1 -0
- package/dist/spam/constants.d.ts +12 -0
- package/dist/spam/constants.js +21 -0
- package/dist/spam/constants.js.map +1 -0
- package/dist/spam/identify.d.ts +12 -0
- package/dist/spam/identify.js +17 -0
- package/dist/spam/identify.js.map +1 -0
- package/dist/spam/providers/hcaptcha.d.ts +17 -0
- package/dist/spam/providers/hcaptcha.js +18 -0
- package/dist/spam/providers/hcaptcha.js.map +1 -0
- package/dist/spam/providers/recaptcha.d.ts +23 -0
- package/dist/spam/providers/recaptcha.js +23 -0
- package/dist/spam/providers/recaptcha.js.map +1 -0
- package/dist/spam/providers/siteverify.js +34 -0
- package/dist/spam/providers/siteverify.js.map +1 -0
- package/dist/spam/providers/turnstile.d.ts +17 -0
- package/dist/spam/providers/turnstile.js +18 -0
- package/dist/spam/providers/turnstile.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 +43 -0
- package/dist/spam/resolveSpam.js.map +1 -0
- package/dist/spam/spamGuard.js +81 -0
- package/dist/spam/spamGuard.js.map +1 -0
- package/dist/spam/types.d.ts +88 -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 +137 -0
- package/dist/submissions/SubmissionAnswers.js.map +1 -0
- package/dist/submissions/SubmissionAnswersClient.js +178 -0
- package/dist/submissions/SubmissionAnswersClient.js.map +1 -0
- package/dist/submissions/coerceBoolean.js +14 -0
- package/dist/submissions/coerceBoolean.js.map +1 -0
- package/dist/submissions/createSubmission.d.ts +36 -0
- package/dist/submissions/createSubmission.js +26 -0
- package/dist/submissions/createSubmission.js.map +1 -0
- package/dist/submissions/runSubmission.d.ts +10 -0
- package/dist/submissions/runSubmission.js +306 -0
- package/dist/submissions/runSubmission.js.map +1 -0
- package/dist/submissions/types.d.ts +41 -0
- package/dist/submissions/validateSubmission.js +110 -0
- package/dist/submissions/validateSubmission.js.map +1 -0
- package/dist/submissions/votedCookie.d.ts +13 -0
- package/dist/submissions/votedCookie.js +27 -0
- package/dist/submissions/votedCookie.js.map +1 -0
- package/dist/translations/clientRuntimeKeys.d.ts +17 -0
- package/dist/translations/clientRuntimeKeys.js +46 -0
- package/dist/translations/clientRuntimeKeys.js.map +1 -0
- package/dist/translations/de.d.ts +12 -0
- package/dist/translations/de.js +309 -0
- package/dist/translations/de.js.map +1 -0
- package/dist/translations/en.d.ts +12 -0
- package/dist/translations/en.js +309 -0
- package/dist/translations/en.js.map +1 -0
- package/dist/translations/index.d.ts +23 -0
- package/dist/translations/index.js +37 -0
- package/dist/translations/index.js.map +1 -0
- package/dist/translations/keys.d.ts +308 -0
- package/dist/translations/keys.js +308 -0
- package/dist/translations/keys.js.map +1 -0
- package/dist/translations/makeTranslate.d.ts +15 -0
- package/dist/translations/makeTranslate.js +20 -0
- package/dist/translations/makeTranslate.js.map +1 -0
- package/dist/translations/server.js +32 -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 +34 -0
- package/dist/uploads/captureFileRef.js +43 -0
- package/dist/uploads/captureFileRef.js.map +1 -0
- package/dist/uploads/formatBytes.d.ts +9 -0
- package/dist/uploads/formatBytes.js +25 -0
- package/dist/uploads/formatBytes.js.map +1 -0
- package/dist/uploads/resolveFileRef.d.ts +26 -0
- package/dist/uploads/resolveFileRef.js +45 -0
- package/dist/uploads/resolveFileRef.js.map +1 -0
- package/dist/uploads/types.d.ts +19 -0
- package/dist/validation/buildRuleBlocks.js +47 -0
- package/dist/validation/buildRuleBlocks.js.map +1 -0
- package/dist/validation/builtin/email.js +20 -0
- package/dist/validation/builtin/email.js.map +1 -0
- package/dist/validation/builtin/index.d.ts +14 -0
- package/dist/validation/builtin/index.js +38 -0
- package/dist/validation/builtin/index.js.map +1 -0
- package/dist/validation/builtin/matchesField.js +16 -0
- package/dist/validation/builtin/matchesField.js.map +1 -0
- package/dist/validation/builtin/max.js +22 -0
- package/dist/validation/builtin/max.js.map +1 -0
- package/dist/validation/builtin/maxDate.js +24 -0
- package/dist/validation/builtin/maxDate.js.map +1 -0
- package/dist/validation/builtin/maxLength.js +27 -0
- package/dist/validation/builtin/maxLength.js.map +1 -0
- package/dist/validation/builtin/min.js +22 -0
- package/dist/validation/builtin/min.js.map +1 -0
- package/dist/validation/builtin/minDate.js +24 -0
- package/dist/validation/builtin/minDate.js.map +1 -0
- package/dist/validation/builtin/minLength.js +27 -0
- package/dist/validation/builtin/minLength.js.map +1 -0
- package/dist/validation/builtin/notAlreadySubmitted.js +34 -0
- package/dist/validation/builtin/notAlreadySubmitted.js.map +1 -0
- package/dist/validation/builtin/oneOf.js +35 -0
- package/dist/validation/builtin/oneOf.js.map +1 -0
- package/dist/validation/builtin/pattern.js +50 -0
- package/dist/validation/builtin/pattern.js.map +1 -0
- package/dist/validation/builtin/url.js +23 -0
- package/dist/validation/builtin/url.js.map +1 -0
- package/dist/validation/builtin/validateDateBound.js +18 -0
- package/dist/validation/builtin/validateDateBound.js.map +1 -0
- package/dist/validation/builtin/validateRegexParams.js +44 -0
- package/dist/validation/builtin/validateRegexParams.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/emailPattern.js +10 -0
- package/dist/validation/emailPattern.js.map +1 -0
- package/dist/validation/fieldTargetParam.d.ts +22 -0
- package/dist/validation/fieldTargetParam.js +38 -0
- package/dist/validation/fieldTargetParam.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 +11 -0
- package/dist/validation/registry.js +13 -0
- package/dist/validation/registry.js.map +1 -0
- package/dist/validation/runValidation.js +90 -0
- package/dist/validation/runValidation.js.map +1 -0
- package/dist/validation/types.d.ts +70 -0
- package/dist/validation/validateUrl.js +22 -0
- package/dist/validation/validateUrl.js.map +1 -0
- package/package.json +12 -7
- package/styles/form-builder.css +21 -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 +0 -132
- 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 +0 -152
- package/dist/translations-edMqq_2e.js.map +0 -1
- package/dist/types-DsJ_6kGJ.d.ts +0 -23
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { defineCaptchaProvider } from "../captcha.js";
|
|
2
|
+
import { postSiteverify, siteverifyParams } from "./siteverify.js";
|
|
3
|
+
//#region src/spam/providers/turnstile.ts
|
|
4
|
+
const TURNSTILE_VERIFY_URL = "https://challenges.cloudflare.com/turnstile/v0/siteverify";
|
|
5
|
+
/**
|
|
6
|
+
* Cloudflare Turnstile verification adapter. Network errors, timeouts, and non-2xx responses
|
|
7
|
+
* fail closed (the submission is rejected).
|
|
8
|
+
*/
|
|
9
|
+
const turnstileProvider = (options) => defineCaptchaProvider({
|
|
10
|
+
type: "turnstile",
|
|
11
|
+
verify: async ({ token, req }) => {
|
|
12
|
+
return (await postSiteverify(options.verifyUrl ?? TURNSTILE_VERIFY_URL, siteverifyParams(options, token, req), options.timeoutMs ?? 5e3))?.success === true;
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
//#endregion
|
|
16
|
+
export { turnstileProvider };
|
|
17
|
+
|
|
18
|
+
//# sourceMappingURL=turnstile.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"turnstile.js","names":[],"sources":["../../../src/spam/providers/turnstile.ts"],"sourcesContent":["import { defineCaptchaProvider } from '../captcha'\nimport type { CaptchaProvider } from '../types'\nimport { DEFAULT_SITEVERIFY_TIMEOUT, postSiteverify, siteverifyParams } from './siteverify'\n\nconst TURNSTILE_VERIFY_URL = 'https://challenges.cloudflare.com/turnstile/v0/siteverify'\n\nexport type TurnstileProviderOptions = {\n\t/** Turnstile secret key (server-side, never the site key). */\n\tsecretKey: string\n\t/** Override the siteverify endpoint. */\n\tverifyUrl?: string\n\t/** Header read for the client IP forwarded as `remoteip`. Default 'x-forwarded-for'. */\n\tipHeader?: string\n\t/** Verification request timeout in ms. Default 5000. */\n\ttimeoutMs?: number\n}\n\n/**\n * Cloudflare Turnstile verification adapter. Network errors, timeouts, and non-2xx responses\n * fail closed (the submission is rejected).\n */\nexport const turnstileProvider = (options: TurnstileProviderOptions): CaptchaProvider =>\n\tdefineCaptchaProvider({\n\t\ttype: 'turnstile',\n\t\tverify: async ({ token, req }) => {\n\t\t\tconst result = await postSiteverify(\n\t\t\t\toptions.verifyUrl ?? TURNSTILE_VERIFY_URL,\n\t\t\t\tsiteverifyParams(options, token, req),\n\t\t\t\toptions.timeoutMs ?? DEFAULT_SITEVERIFY_TIMEOUT\n\t\t\t)\n\t\t\treturn result?.success === true\n\t\t},\n\t})\n"],"mappings":";;;AAIA,MAAM,uBAAuB;;;;;AAiB7B,MAAa,qBAAqB,YACjC,sBAAsB;CACrB,MAAM;CACN,QAAQ,OAAO,EAAE,OAAO,UAAU;EAMjC,QAAO,MALc,eACpB,QAAQ,aAAa,sBACrB,iBAAiB,SAAS,OAAO,GAAG,GACpC,QAAQ,aAAA,GACT,IACe,YAAY;CAC5B;AACD,CAAC"}
|
|
@@ -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,43 @@
|
|
|
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
|
+
uploadOwnership: cfg.uploadOwnership ?? "lenient"
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
//#endregion
|
|
41
|
+
export { resolveSpamConfig };
|
|
42
|
+
|
|
43
|
+
//# 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\tuploadOwnership: cfg.uploadOwnership ?? 'lenient',\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;EACzE,iBAAiB,IAAI,mBAAmB;CACzC;AACD"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { keys } from "../translations/keys.js";
|
|
2
|
+
import { asTranslate } from "../translations/server.js";
|
|
3
|
+
import { firstHop } from "./clientIp.js";
|
|
4
|
+
import { HONEYPOT_VALUE_KEY, IDENTITY_CONTEXT_KEY } from "./constants.js";
|
|
5
|
+
import { extractReservedValues, isHoneypotTripped } from "./reserved.js";
|
|
6
|
+
import { APIError } from "payload";
|
|
7
|
+
//#region src/spam/spamGuard.ts
|
|
8
|
+
let warnedRateLimitSkipped = false;
|
|
9
|
+
/** Warn once per process when rate limiting is configured but skipped for lack of a request identity. */
|
|
10
|
+
const warnRateLimitSkippedOnce = (req) => {
|
|
11
|
+
if (warnedRateLimitSkipped) return;
|
|
12
|
+
warnedRateLimitSkipped = true;
|
|
13
|
+
req.payload.logger?.warn("@10x-media/form-builder: submission rate limiting is configured but skipped for a request with no resolvable identity (no user and no client IP). Set spam.ipHeader for your proxy or provide a custom spam.identify.");
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Submissions spam guard, prepended before `validateSubmission` in `beforeValidate` so it rejects before
|
|
17
|
+
* the form load + field validation. On create it: resolves the request identity once (stashing it on
|
|
18
|
+
* `req.context` for upload-ownership verification downstream); rate-limits per form + identity (429,
|
|
19
|
+
* fail-open when identity is null); strips reserved `values` entries (honeypot, captcha token); rejects a
|
|
20
|
+
* filled honeypot with a generic error; verifies a configured captcha; and writes a server-authoritative
|
|
21
|
+
* `meta` (timestamp + spam signal, with opt-in ip/ua). App-level rate limiting is defense-in-depth that
|
|
22
|
+
* complements edge/CDN/WAF limiting, not a DoS replacement.
|
|
23
|
+
*/
|
|
24
|
+
const buildSpamGuard = (spam) => async ({ data, operation, req }) => {
|
|
25
|
+
if (operation !== "create" || !data) return data;
|
|
26
|
+
const t = asTranslate(req.i18n.t);
|
|
27
|
+
const authenticated = Boolean(req.user);
|
|
28
|
+
const identity = await spam.identify(req);
|
|
29
|
+
if (identity != null) req.context[IDENTITY_CONTEXT_KEY] = identity;
|
|
30
|
+
let rateLimitState;
|
|
31
|
+
if (spam.rateLimit === false) rateLimitState = "disabled";
|
|
32
|
+
else if (identity == null) {
|
|
33
|
+
rateLimitState = "skipped-no-identity";
|
|
34
|
+
warnRateLimitSkippedOnce(req);
|
|
35
|
+
} else {
|
|
36
|
+
const formKey = data.form != null ? String(data.form) : "unknown";
|
|
37
|
+
const { ok } = await spam.rateLimit.limiter.check({
|
|
38
|
+
key: `submissions:${formKey}:${identity}`,
|
|
39
|
+
max: spam.rateLimit.max,
|
|
40
|
+
window: spam.rateLimit.window,
|
|
41
|
+
req
|
|
42
|
+
});
|
|
43
|
+
if (!ok) throw new APIError(t(keys.spamRateLimited), 429);
|
|
44
|
+
rateLimitState = "enforced";
|
|
45
|
+
}
|
|
46
|
+
const honeypotKey = spam.honeypot === false ? null : HONEYPOT_VALUE_KEY;
|
|
47
|
+
const { cleaned, honeypot, captchaToken } = extractReservedValues(data.values ?? [], honeypotKey);
|
|
48
|
+
data.values = cleaned;
|
|
49
|
+
if (!authenticated && honeypotKey !== null && isHoneypotTripped(honeypot)) throw new APIError(t(keys.spamRejected), 400);
|
|
50
|
+
const captchaChecked = Boolean(spam.captcha) && !authenticated;
|
|
51
|
+
if (spam.captcha && !authenticated) {
|
|
52
|
+
if (!(typeof captchaToken === "string" && captchaToken.length > 0 ? await spam.captcha.verify({
|
|
53
|
+
token: captchaToken,
|
|
54
|
+
req
|
|
55
|
+
}).catch(() => false) : false)) throw new APIError(t(keys.spamCaptchaFailed), 400);
|
|
56
|
+
}
|
|
57
|
+
const serverMeta = {
|
|
58
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
59
|
+
spam: {
|
|
60
|
+
captcha: captchaChecked ? "passed" : "skipped",
|
|
61
|
+
rateLimit: rateLimitState
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
if (spam.metadata.ip) {
|
|
65
|
+
const ip = firstHop(req.headers, spam.ipHeader);
|
|
66
|
+
if (ip) serverMeta.ip = ip;
|
|
67
|
+
}
|
|
68
|
+
if (spam.metadata.ua) {
|
|
69
|
+
const ua = req.headers?.get("user-agent");
|
|
70
|
+
if (ua) serverMeta.ua = ua;
|
|
71
|
+
}
|
|
72
|
+
data.meta = {
|
|
73
|
+
...authenticated && data.meta != null && typeof data.meta === "object" ? data.meta : {},
|
|
74
|
+
...serverMeta
|
|
75
|
+
};
|
|
76
|
+
return data;
|
|
77
|
+
};
|
|
78
|
+
//#endregion
|
|
79
|
+
export { buildSpamGuard };
|
|
80
|
+
|
|
81
|
+
//# sourceMappingURL=spamGuard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spamGuard.js","names":[],"sources":["../../src/spam/spamGuard.ts"],"sourcesContent":["import { APIError, type CollectionBeforeValidateHook, type PayloadRequest } from 'payload'\nimport type { SubmissionValue } from '../submissions/types'\nimport { keys } from '../translations/keys'\nimport { asTranslate } from '../translations/server'\nimport { firstHop } from './clientIp'\nimport { HONEYPOT_VALUE_KEY, IDENTITY_CONTEXT_KEY } from './constants'\nimport { extractReservedValues, isHoneypotTripped } from './reserved'\nimport type { ResolvedSpamConfig } from './types'\n\nlet warnedRateLimitSkipped = false\n\n/** Warn once per process when rate limiting is configured but skipped for lack of a request identity. */\nconst warnRateLimitSkippedOnce = (req: PayloadRequest): void => {\n\tif (warnedRateLimitSkipped) {\n\t\treturn\n\t}\n\twarnedRateLimitSkipped = true\n\treq.payload.logger?.warn(\n\t\t'@10x-media/form-builder: submission rate limiting is configured but skipped for a request with no resolvable identity (no user and no client IP). Set spam.ipHeader for your proxy or provide a custom spam.identify.'\n\t)\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\tlet rateLimitState: 'enforced' | 'skipped-no-identity' | 'disabled'\n\t\tif (spam.rateLimit === false) {\n\t\t\trateLimitState = 'disabled'\n\t\t} else if (identity == null) {\n\t\t\t// Fail open (an unidentifiable submitter cannot be keyed) but never silently: record it on\n\t\t\t// meta.spam and warn once, so a deployment with no proxy or a misconfigured ipHeader is visible.\n\t\t\trateLimitState = 'skipped-no-identity'\n\t\t\twarnRateLimitSkippedOnce(req)\n\t\t} else {\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\trateLimitState = 'enforced'\n\t\t}\n\n\t\t// The decoy rides a fixed reserved key (not its cosmetic DOM name), so a real field sharing that\n\t\t// name is never stripped or mistaken for the honeypot. `spam.honeypot.fieldName` is now only the\n\t\t// client's DOM-input name hint and no longer participates in server matching.\n\t\tconst honeypotKey = spam.honeypot === false ? null : HONEYPOT_VALUE_KEY\n\t\tconst values = (data.values as SubmissionValue[] | undefined) ?? []\n\t\tconst { cleaned, honeypot, captchaToken } = extractReservedValues(values, honeypotKey)\n\t\tdata.values = cleaned\n\n\t\tif (!authenticated && honeypotKey !== 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', rateLimit: rateLimitState },\n\t\t}\n\t\tif (spam.metadata.ip) {\n\t\t\tconst ip = firstHop(req.headers, spam.ipHeader)\n\t\t\tif (ip) {\n\t\t\t\tserverMeta.ip = ip\n\t\t\t}\n\t\t}\n\t\tif (spam.metadata.ua) {\n\t\t\tconst ua = req.headers?.get('user-agent')\n\t\t\tif (ua) {\n\t\t\t\tserverMeta.ua = ua\n\t\t\t}\n\t\t}\n\t\t// Preserve an authenticated (trusted) caller's own `meta` keys, with server fields winning. Anonymous\n\t\t// `meta` is never trusted, so it is discarded.\n\t\tconst clientMeta =\n\t\t\tauthenticated && data.meta != null && typeof data.meta === 'object'\n\t\t\t\t? (data.meta as Record<string, unknown>)\n\t\t\t\t: {}\n\t\tdata.meta = { ...clientMeta, ...serverMeta }\n\n\t\treturn data\n\t}\n"],"mappings":";;;;;;;AASA,IAAI,yBAAyB;;AAG7B,MAAM,4BAA4B,QAA8B;CAC/D,IAAI,wBACH;CAED,yBAAyB;CACzB,IAAI,QAAQ,QAAQ,KACnB,uNACD;AACD;;;;;;;;;;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;CACJ,IAAI,KAAK,cAAc,OACtB,iBAAiB;MACX,IAAI,YAAY,MAAM;EAG5B,iBAAiB;EACjB,yBAAyB,GAAG;CAC7B,OAAO;EACN,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;EAEhD,iBAAiB;CAClB;CAKA,MAAM,cAAc,KAAK,aAAa,QAAQ,OAAO;CAErD,MAAM,EAAE,SAAS,UAAU,iBAAiB,sBAD5B,KAAK,UAA4C,CAAC,GACQ,WAAW;CACrF,KAAK,SAAS;CAEd,IAAI,CAAC,iBAAiB,gBAAgB,QAAQ,kBAAkB,QAAQ,GACvE,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;GAAE,SAAS,iBAAiB,WAAW;GAAW,WAAW;EAAe;CACnF;CACA,IAAI,KAAK,SAAS,IAAI;EACrB,MAAM,KAAK,SAAS,IAAI,SAAS,KAAK,QAAQ;EAC9C,IAAI,IACH,WAAW,KAAK;CAElB;CACA,IAAI,KAAK,SAAS,IAAI;EACrB,MAAM,KAAK,IAAI,SAAS,IAAI,YAAY;EACxC,IAAI,IACH,WAAW,KAAK;CAElB;CAOA,KAAK,OAAO;EAAE,GAHb,iBAAiB,KAAK,QAAQ,QAAQ,OAAO,KAAK,SAAS,WACvD,KAAK,OACN,CAAC;EACwB,GAAG;CAAW;CAE3C,OAAO;AACR"}
|
|
@@ -0,0 +1,88 @@
|
|
|
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. Use a bundled provider (Turnstile/reCAPTCHA/hCaptcha) or build one 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
|
+
/**
|
|
41
|
+
* Honeypot decoy. Default on. `false` disables. `fieldName` is accepted for backward compatibility
|
|
42
|
+
* but no longer participates in server matching: the decoy is submitted under a fixed reserved key,
|
|
43
|
+
* so it never collides with a real field, and its DOM input name is set on the client via `<Form honeypot>`.
|
|
44
|
+
*/
|
|
45
|
+
honeypot?: false | {
|
|
46
|
+
fieldName?: string;
|
|
47
|
+
}; /** Per-identity rate limit on submission create. Default on (60s window, max 5). `false` disables. */
|
|
48
|
+
rateLimit?: false | RateLimitConfig; /** Per-identity rate limit on upload create. Default on (60s window, max 20). `false` disables. */
|
|
49
|
+
uploadRateLimit?: false | RateLimitConfig; /** A captcha provider (none by default): `turnstileProvider`/`recaptchaProvider`/`hcaptchaProvider` or a custom adapter. */
|
|
50
|
+
captcha?: CaptchaProvider; /** Identity resolution for rate-limiting, upload ownership, and (future) poll dedup. Default: user id, else trusted IP header. */
|
|
51
|
+
identify?: IdentifyFn;
|
|
52
|
+
/**
|
|
53
|
+
* How strictly a submitted file's upload-ownership is enforced. `'lenient'` (default) only rejects a
|
|
54
|
+
* stamped upload when the submitter is identified and does not match, and passes an owned upload
|
|
55
|
+
* through when the submitter cannot be identified (fail-open, consistent with rate-limiting).
|
|
56
|
+
* `'strict'` also rejects a stamped upload whenever the submitter cannot be identified, so a
|
|
57
|
+
* deployment that always identifies requests (a proxy setting the trusted IP header, or a custom
|
|
58
|
+
* `identify`) never lets an unverifiable claim capture an owned upload.
|
|
59
|
+
*/
|
|
60
|
+
uploadOwnership?: 'lenient' | 'strict'; /** Header read for the client IP (proxy-dependent, best-effort). Default 'x-forwarded-for'. */
|
|
61
|
+
ipHeader?: string; /** Opt-in capture of client metadata onto the submission `meta`. Off by default. */
|
|
62
|
+
metadata?: SpamMetadataConfig;
|
|
63
|
+
};
|
|
64
|
+
/** `false` disables the whole subsystem; an object configures it (all controls default on except captcha + metadata). */
|
|
65
|
+
type SpamOption = false | SpamConfig;
|
|
66
|
+
type ResolvedRateLimit = {
|
|
67
|
+
window: number;
|
|
68
|
+
max: number;
|
|
69
|
+
limiter: RateLimiter;
|
|
70
|
+
};
|
|
71
|
+
type ResolvedSpamConfig = {
|
|
72
|
+
honeypot: false | {
|
|
73
|
+
fieldName: string;
|
|
74
|
+
};
|
|
75
|
+
rateLimit: false | ResolvedRateLimit;
|
|
76
|
+
uploadRateLimit: false | ResolvedRateLimit;
|
|
77
|
+
captcha?: CaptchaProvider;
|
|
78
|
+
identify: IdentifyFn;
|
|
79
|
+
ipHeader: string;
|
|
80
|
+
metadata: {
|
|
81
|
+
ip: boolean;
|
|
82
|
+
ua: boolean;
|
|
83
|
+
};
|
|
84
|
+
uploadOwnership: 'lenient' | 'strict';
|
|
85
|
+
};
|
|
86
|
+
//#endregion
|
|
87
|
+
export { CaptchaProvider, CaptchaVerifyArgs, IdentifyFn, RateLimitCheckArgs, RateLimitConfig, RateLimitResult, RateLimiter, ResolvedSpamConfig, SpamConfig, SpamMetadataConfig, SpamOption };
|
|
88
|
+
//# 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,137 @@
|
|
|
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
|
+
...descriptor.width ? { width: descriptor.width } : {}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
const consent = rawConsent.map((entry) => ({
|
|
90
|
+
...entry,
|
|
91
|
+
at: formatDate(entry.at, locale)
|
|
92
|
+
}));
|
|
93
|
+
const metaItems = [{
|
|
94
|
+
label: t(keys.submissionMetaLocale),
|
|
95
|
+
value: locale
|
|
96
|
+
}];
|
|
97
|
+
if (meta) {
|
|
98
|
+
if (meta.at) metaItems.push({
|
|
99
|
+
label: t(keys.submissionMetaReceivedAt),
|
|
100
|
+
value: formatDate(meta.at, locale)
|
|
101
|
+
});
|
|
102
|
+
if (meta.ip) metaItems.push({
|
|
103
|
+
label: t(keys.submissionMetaIp),
|
|
104
|
+
value: meta.ip
|
|
105
|
+
});
|
|
106
|
+
if (meta.ua) metaItems.push({
|
|
107
|
+
label: t(keys.submissionMetaUserAgent),
|
|
108
|
+
value: String(meta.ua)
|
|
109
|
+
});
|
|
110
|
+
if (meta.spam) {
|
|
111
|
+
const captcha = typeof meta.spam === "object" && "captcha" in meta.spam ? String(meta.spam.captcha) : "—";
|
|
112
|
+
metaItems.push({
|
|
113
|
+
label: t(keys.submissionMetaCaptcha),
|
|
114
|
+
value: captcha
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return /* @__PURE__ */ jsx(SubmissionAnswersClient, {
|
|
119
|
+
answers,
|
|
120
|
+
repeaters,
|
|
121
|
+
consent,
|
|
122
|
+
meta: metaItems,
|
|
123
|
+
labels: {
|
|
124
|
+
answers: t(keys.submissionAnswers),
|
|
125
|
+
consent: t(keys.submissionConsent),
|
|
126
|
+
submissionDetails: t(keys.submissionDetails),
|
|
127
|
+
agreed: t(keys.submissionConsentAgreed),
|
|
128
|
+
declined: t(keys.submissionConsentDeclined),
|
|
129
|
+
row: t(keys.repeaterRow),
|
|
130
|
+
empty: t(keys.submissionNoAnswers)
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
};
|
|
134
|
+
//#endregion
|
|
135
|
+
export { SubmissionAnswers };
|
|
136
|
+
|
|
137
|
+
//# 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\t/** The consent source's name at submit time (see `ConsentProof.name`). */\n\tname?: string\n\t/** The plain-text snapshot of the wording agreed to (see `ConsentProof.statementText`). */\n\tstatementText?: 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\t...(descriptor.width ? { width: descriptor.width } : {}),\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[] = [{ label: t(keys.submissionMetaLocale), value: locale }]\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;AA+BtD,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;GACpC,GAAI,WAAW,QAAQ,EAAE,OAAO,WAAW,MAAM,IAAI,CAAC;EACvD,CAAC;CAEH;CAEA,MAAM,UAAyB,WAAW,KAAK,WAAW;EACzD,GAAG;EACH,IAAI,WAAW,MAAM,IAAI,MAAM;CAChC,EAAE;CAEF,MAAM,YAAwB,CAAC;EAAE,OAAO,EAAE,KAAK,oBAAoB;EAAG,OAAO;CAAO,CAAC;CACrF,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"}
|