@10x-media/form-builder 0.1.0-beta.1 → 0.1.0-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -0
- package/README.md +4 -2
- 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 +35 -0
- package/dist/actions/body/serializeBody.js +76 -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/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/builtin/confirmation.js +28 -12
- package/dist/actions/builtin/confirmation.js.map +1 -1
- package/dist/actions/builtin/emailTeam.js +7 -9
- package/dist/actions/builtin/emailTeam.js.map +1 -1
- package/dist/actions/builtin/signedWebhook.js +21 -3
- package/dist/actions/builtin/signedWebhook.js.map +1 -1
- package/dist/actions/defineAction.d.ts +2 -1
- package/dist/actions/defineAction.js.map +1 -1
- package/dist/actions/dispatch.js +2 -1
- package/dist/actions/dispatch.js.map +1 -1
- package/dist/actions/runActions.js +8 -1
- package/dist/actions/runActions.js.map +1 -1
- package/dist/actions/task.js +7 -5
- package/dist/actions/task.js.map +1 -1
- package/dist/calc/normalizeCalc.js +5 -17
- package/dist/calc/normalizeCalc.js.map +1 -1
- package/dist/calc/types.d.ts +8 -2
- package/dist/calc/types.js +22 -0
- package/dist/calc/types.js.map +1 -0
- package/dist/client/ByteSizeField.d.ts +21 -0
- package/dist/client/ByteSizeField.js +56 -0
- package/dist/client/ByteSizeField.js.map +1 -0
- package/dist/client/ConditionBuilder.js +2 -1
- package/dist/client/ConditionBuilder.js.map +1 -1
- package/dist/client/FieldNameSelect.d.ts +20 -0
- package/dist/client/FieldNameSelect.js +58 -0
- package/dist/client/FieldNameSelect.js.map +1 -0
- package/dist/client/FlowBuilder.d.ts +0 -1
- package/dist/client/FlowBuilder.js +4 -6
- package/dist/client/FlowBuilder.js.map +1 -1
- package/dist/client/FormConditionField.js +1 -4
- package/dist/client/FormConditionField.js.map +1 -1
- package/dist/client/admin.css +192 -0
- package/dist/client/toStaticLabel.js +13 -0
- package/dist/client/toStaticLabel.js.map +1 -0
- package/dist/collections/formSubmissions.js +60 -39
- package/dist/collections/formSubmissions.js.map +1 -1
- package/dist/collections/forms.js +143 -115
- package/dist/collections/forms.js.map +1 -1
- package/dist/collections/uploads.d.ts +2 -1
- package/dist/collections/uploads.js +22 -13
- package/dist/collections/uploads.js.map +1 -1
- package/dist/exports/client.d.ts +3 -1
- package/dist/exports/client.js +3 -1
- package/dist/exports/react.d.ts +7 -2
- package/dist/exports/react.js +6 -2
- package/dist/exports/types.d.ts +2 -1
- package/dist/fields/buildFieldBlocks.js +22 -12
- package/dist/fields/buildFieldBlocks.js.map +1 -1
- package/dist/fields/builtin/calculation.js +115 -2
- package/dist/fields/builtin/calculation.js.map +1 -1
- package/dist/fields/builtin/date.js +29 -0
- package/dist/fields/builtin/date.js.map +1 -0
- package/dist/fields/builtin/file.d.ts +15 -0
- package/dist/fields/builtin/file.js +79 -3
- package/dist/fields/builtin/file.js.map +1 -1
- package/dist/fields/builtin/index.d.ts +14 -0
- package/dist/fields/builtin/index.js +13 -2
- package/dist/fields/builtin/index.js.map +1 -1
- package/dist/fields/builtin/repeater.js +44 -0
- package/dist/fields/builtin/repeater.js.map +1 -0
- package/dist/fields/fieldNamesOfType.js +25 -0
- package/dist/fields/fieldNamesOfType.js.map +1 -0
- package/dist/fields/sharedConfig.js +30 -18
- package/dist/fields/sharedConfig.js.map +1 -1
- package/dist/fields/types.d.ts +2 -1
- package/dist/index.d.ts +38 -2
- package/dist/index.js +17 -5
- package/dist/index.js.map +1 -1
- package/dist/plugin/collectionOverrides.d.ts +22 -0
- package/dist/plugin/registerCollections.js +39 -10
- package/dist/plugin/registerCollections.js.map +1 -1
- package/dist/react/Form.d.ts +32 -3
- package/dist/react/Form.js +50 -8
- package/dist/react/Form.js.map +1 -1
- package/dist/react/FormContext.js.map +1 -1
- package/dist/react/Poll.js +1 -1
- 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/primitives/Input.d.ts +1 -1
- package/dist/react/primitives/Input.js.map +1 -1
- package/dist/react/renderers/calculation.js +1 -1
- package/dist/react/renderers/checkbox.js +1 -1
- package/dist/react/renderers/consent.js +1 -1
- package/dist/react/renderers/date.js +37 -0
- package/dist/react/renderers/date.js.map +1 -0
- package/dist/react/renderers/email.js +1 -1
- package/dist/react/renderers/file.js +20 -7
- package/dist/react/renderers/file.js.map +1 -1
- package/dist/react/renderers/index.js +4 -0
- package/dist/react/renderers/index.js.map +1 -1
- package/dist/react/renderers/number.js +1 -1
- package/dist/react/renderers/repeater.js +113 -0
- package/dist/react/renderers/repeater.js.map +1 -0
- package/dist/react/renderers/select.js +1 -1
- package/dist/react/renderers/text.js +1 -1
- package/dist/react/renderers/textarea.js +1 -1
- package/dist/spam/captcha.d.ts +3 -2
- package/dist/spam/captcha.js +3 -2
- package/dist/spam/captcha.js.map +1 -1
- package/dist/spam/clientIp.js +9 -0
- package/dist/spam/clientIp.js.map +1 -0
- package/dist/spam/identify.js +3 -6
- package/dist/spam/identify.js.map +1 -1
- 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/spamGuard.js +2 -5
- package/dist/spam/spamGuard.js.map +1 -1
- package/dist/spam/types.d.ts +2 -2
- package/dist/submissions/SubmissionAnswers.d.ts +2 -4
- package/dist/submissions/SubmissionAnswers.js +110 -34
- package/dist/submissions/SubmissionAnswers.js.map +1 -1
- package/dist/submissions/SubmissionAnswersClient.js +142 -0
- package/dist/submissions/SubmissionAnswersClient.js.map +1 -0
- package/dist/submissions/runSubmission.js +54 -2
- package/dist/submissions/runSubmission.js.map +1 -1
- package/dist/submissions/types.d.ts +2 -1
- package/dist/translations/en.js +52 -5
- package/dist/translations/en.js.map +1 -1
- package/dist/translations/keys.d.ts +51 -4
- package/dist/translations/keys.js +52 -5
- package/dist/translations/keys.js.map +1 -1
- 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.js +1 -1
- package/dist/uploads/resolveFileRef.js.map +1 -1
- package/dist/validation/builtin/index.d.ts +14 -0
- package/dist/validation/builtin/index.js +12 -1
- package/dist/validation/builtin/index.js.map +1 -1
- package/dist/validation/builtin/maxDate.js +23 -0
- package/dist/validation/builtin/maxDate.js.map +1 -0
- package/dist/validation/builtin/minDate.js +23 -0
- package/dist/validation/builtin/minDate.js.map +1 -0
- package/dist/validation/builtin/validateDateBound.js +18 -0
- package/dist/validation/builtin/validateDateBound.js.map +1 -0
- package/dist/validation/runValidation.js +1 -1
- package/dist/validation/runValidation.js.map +1 -1
- package/package.json +4 -3
- package/styles/form-builder.css +0 -190
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import { defineFieldRenderer } from "../contract.js";
|
|
3
3
|
import { FieldShell } from "../primitives/FieldShell.js";
|
|
4
4
|
import { Checkbox } from "../primitives/Checkbox.js";
|
|
5
|
-
import { useId } from "react";
|
|
6
5
|
import { jsx } from "react/jsx-runtime";
|
|
6
|
+
import { useId } from "react";
|
|
7
7
|
//#region src/react/renderers/checkbox.tsx
|
|
8
8
|
const checkboxRenderer = defineFieldRenderer(({ field, name, value, onChange, onBlur, errors, warnings, required, disabled }) => {
|
|
9
9
|
const id = useId();
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import { defineFieldRenderer } from "../contract.js";
|
|
3
3
|
import { FieldShell } from "../primitives/FieldShell.js";
|
|
4
4
|
import { Checkbox } from "../primitives/Checkbox.js";
|
|
5
|
-
import { useId } from "react";
|
|
6
5
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
import { useId } from "react";
|
|
7
7
|
//#region src/react/renderers/consent.tsx
|
|
8
8
|
const consentRenderer = defineFieldRenderer(({ field, name, value, onChange, onBlur, errors, warnings, required, disabled }) => {
|
|
9
9
|
const id = useId();
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { defineFieldRenderer } from "../contract.js";
|
|
3
|
+
import { FieldShell } from "../primitives/FieldShell.js";
|
|
4
|
+
import { Input } from "../primitives/Input.js";
|
|
5
|
+
import { jsx } from "react/jsx-runtime";
|
|
6
|
+
import { useId } from "react";
|
|
7
|
+
//#region src/react/renderers/date.tsx
|
|
8
|
+
const dateRenderer = defineFieldRenderer(({ field, name, value, onChange, onBlur, errors, warnings, required, disabled }) => {
|
|
9
|
+
const id = useId();
|
|
10
|
+
const describedById = `${id}-desc`;
|
|
11
|
+
return /* @__PURE__ */ jsx(FieldShell, {
|
|
12
|
+
id,
|
|
13
|
+
label: field.label,
|
|
14
|
+
description: typeof field.description === "string" ? field.description : void 0,
|
|
15
|
+
required,
|
|
16
|
+
errors,
|
|
17
|
+
warnings,
|
|
18
|
+
describedById,
|
|
19
|
+
children: /* @__PURE__ */ jsx(Input, {
|
|
20
|
+
id,
|
|
21
|
+
name,
|
|
22
|
+
type: "date",
|
|
23
|
+
value: value ?? "",
|
|
24
|
+
onChange,
|
|
25
|
+
onBlur,
|
|
26
|
+
placeholder: typeof field.placeholder === "string" ? field.placeholder : void 0,
|
|
27
|
+
required,
|
|
28
|
+
disabled,
|
|
29
|
+
invalid: errors.length > 0,
|
|
30
|
+
describedById
|
|
31
|
+
})
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
//#endregion
|
|
35
|
+
export { dateRenderer };
|
|
36
|
+
|
|
37
|
+
//# sourceMappingURL=date.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"date.js","names":[],"sources":["../../../src/react/renderers/date.tsx"],"sourcesContent":["'use client'\n\nimport { useId } from 'react'\nimport { defineFieldRenderer } from '../contract'\nimport { FieldShell } from '../primitives/FieldShell'\nimport { Input } from '../primitives/Input'\n\nexport const dateRenderer = defineFieldRenderer<string>(\n\t({ field, name, value, onChange, onBlur, errors, warnings, required, disabled }) => {\n\t\tconst id = useId()\n\t\tconst describedById = `${id}-desc`\n\t\treturn (\n\t\t\t<FieldShell\n\t\t\t\tid={id}\n\t\t\t\tlabel={field.label}\n\t\t\t\tdescription={typeof field.description === 'string' ? field.description : undefined}\n\t\t\t\trequired={required}\n\t\t\t\terrors={errors}\n\t\t\t\twarnings={warnings}\n\t\t\t\tdescribedById={describedById}\n\t\t\t>\n\t\t\t\t<Input\n\t\t\t\t\tid={id}\n\t\t\t\t\tname={name}\n\t\t\t\t\ttype=\"date\"\n\t\t\t\t\tvalue={value ?? ''}\n\t\t\t\t\tonChange={onChange}\n\t\t\t\t\tonBlur={onBlur}\n\t\t\t\t\tplaceholder={typeof field.placeholder === 'string' ? field.placeholder : undefined}\n\t\t\t\t\trequired={required}\n\t\t\t\t\tdisabled={disabled}\n\t\t\t\t\tinvalid={errors.length > 0}\n\t\t\t\t\tdescribedById={describedById}\n\t\t\t\t/>\n\t\t\t</FieldShell>\n\t\t)\n\t}\n)\n"],"mappings":";;;;;;;AAOA,MAAa,eAAe,qBAC1B,EAAE,OAAO,MAAM,OAAO,UAAU,QAAQ,QAAQ,UAAU,UAAU,eAAe;CACnF,MAAM,KAAK,MAAM;CACjB,MAAM,gBAAgB,GAAG,GAAG;CAC5B,OACC,oBAAC,YAAD;EACK;EACJ,OAAO,MAAM;EACb,aAAa,OAAO,MAAM,gBAAgB,WAAW,MAAM,cAAc,KAAA;EAC/D;EACF;EACE;EACK;YAEf,oBAAC,OAAD;GACK;GACE;GACN,MAAK;GACL,OAAO,SAAS;GACN;GACF;GACR,aAAa,OAAO,MAAM,gBAAgB,WAAW,MAAM,cAAc,KAAA;GAC/D;GACA;GACV,SAAS,OAAO,SAAS;GACV;EACf,CAAA;CACU,CAAA;AAEd,CACD"}
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import { defineFieldRenderer } from "../contract.js";
|
|
3
3
|
import { FieldShell } from "../primitives/FieldShell.js";
|
|
4
4
|
import { Input } from "../primitives/Input.js";
|
|
5
|
-
import { useId } from "react";
|
|
6
5
|
import { jsx } from "react/jsx-runtime";
|
|
6
|
+
import { useId } from "react";
|
|
7
7
|
//#region src/react/renderers/email.tsx
|
|
8
8
|
const emailRenderer = defineFieldRenderer(({ field, name, value, onChange, onBlur, errors, warnings, required, disabled }) => {
|
|
9
9
|
const id = useId();
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
"use client";
|
|
2
|
+
import { keys } from "../../translations/keys.js";
|
|
3
|
+
import { resolveMessage } from "../../validation/message.js";
|
|
4
|
+
import { formatBytes } from "../../uploads/formatBytes.js";
|
|
2
5
|
import { defineFieldRenderer } from "../contract.js";
|
|
3
6
|
import { FieldShell } from "../primitives/FieldShell.js";
|
|
4
7
|
import { uploadFile } from "../uploadFile.js";
|
|
5
|
-
import { useId, useState } from "react";
|
|
6
8
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
9
|
+
import { useId, useState } from "react";
|
|
7
10
|
//#region src/react/renderers/file.tsx
|
|
8
|
-
const
|
|
9
|
-
if (!Array.isArray(mimeTypes)) return;
|
|
10
|
-
|
|
11
|
-
return types.length > 0 ? types.join(",") : void 0;
|
|
11
|
+
const typesOf = (mimeTypes) => {
|
|
12
|
+
if (!Array.isArray(mimeTypes)) return [];
|
|
13
|
+
return mimeTypes.filter((entry) => typeof entry === "string");
|
|
12
14
|
};
|
|
13
15
|
/**
|
|
14
16
|
* Headless file renderer: a native file input that uploads the chosen file to the upload collection and
|
|
@@ -16,19 +18,26 @@ const acceptOf = (mimeTypes) => {
|
|
|
16
18
|
* stored doc at submit, so the client value is only the id. Shows the uploaded filename with a clear
|
|
17
19
|
* control and surfaces upload errors. Single file (v1); the upload posts to the default `/api` route.
|
|
18
20
|
*/
|
|
19
|
-
const fileRenderer = defineFieldRenderer(({ field, name, value, onChange, onBlur, errors, warnings, required, disabled }) => {
|
|
21
|
+
const fileRenderer = defineFieldRenderer(({ field, name, value, onChange, onBlur, errors, warnings, required, disabled, t }) => {
|
|
20
22
|
const id = useId();
|
|
21
23
|
const describedById = `${id}-desc`;
|
|
22
24
|
const [uploading, setUploading] = useState(false);
|
|
23
25
|
const [filename, setFilename] = useState(void 0);
|
|
24
26
|
const [localError, setLocalError] = useState(void 0);
|
|
25
27
|
const collection = typeof field.relationTo === "string" ? field.relationTo : "form-uploads";
|
|
26
|
-
const
|
|
28
|
+
const types = typesOf(field.mimeTypes);
|
|
29
|
+
const accept = types.length > 0 ? types.join(",") : void 0;
|
|
30
|
+
const maxSize = typeof field.maxSize === "number" && field.maxSize >= 0 ? field.maxSize : void 0;
|
|
31
|
+
const hints = [types.length > 0 ? resolveMessage(t(keys.fileHintAccepted), { types: types.join(", ") }) : void 0, maxSize !== void 0 ? resolveMessage(t(keys.fileHintMaxSize), { max: formatBytes(maxSize) }) : void 0].filter((hint) => typeof hint === "string");
|
|
27
32
|
const allErrors = localError ? [...errors, localError] : errors;
|
|
28
33
|
const displayName = filename ?? (value !== void 0 && value !== null && value !== "" ? "Uploaded file" : void 0);
|
|
29
34
|
const handleChange = async (event) => {
|
|
30
35
|
const selected = event.target.files?.[0];
|
|
31
36
|
if (!selected) return;
|
|
37
|
+
if (maxSize !== void 0 && selected.size > maxSize) {
|
|
38
|
+
setLocalError(resolveMessage(t(keys.fileTooLarge), { max: formatBytes(maxSize) }));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
32
41
|
setUploading(true);
|
|
33
42
|
setLocalError(void 0);
|
|
34
43
|
const result = await uploadFile({
|
|
@@ -68,6 +77,10 @@ const fileRenderer = defineFieldRenderer(({ field, name, value, onChange, onBlur
|
|
|
68
77
|
"aria-invalid": allErrors.length > 0,
|
|
69
78
|
"aria-describedby": describedById
|
|
70
79
|
}),
|
|
80
|
+
hints.length > 0 ? /* @__PURE__ */ jsx("p", {
|
|
81
|
+
className: "fb-field__file-hint",
|
|
82
|
+
children: hints.join(" · ")
|
|
83
|
+
}) : null,
|
|
71
84
|
uploading ? /* @__PURE__ */ jsx("p", {
|
|
72
85
|
className: "fb-field__file-status",
|
|
73
86
|
"aria-live": "polite",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file.js","names":[],"sources":["../../../src/react/renderers/file.tsx"],"sourcesContent":["'use client'\n\nimport { type ChangeEvent, useId, useState } from 'react'\nimport { defineFieldRenderer } from '../contract'\nimport { FieldShell } from '../primitives/FieldShell'\nimport { uploadFile } from '../uploadFile'\n\nconst
|
|
1
|
+
{"version":3,"file":"file.js","names":[],"sources":["../../../src/react/renderers/file.tsx"],"sourcesContent":["'use client'\n\nimport { type ChangeEvent, useId, useState } from 'react'\nimport { keys } from '../../translations/keys'\nimport { formatBytes } from '../../uploads/formatBytes'\nimport { resolveMessage } from '../../validation/message'\nimport { defineFieldRenderer } from '../contract'\nimport { FieldShell } from '../primitives/FieldShell'\nimport { uploadFile } from '../uploadFile'\n\nconst typesOf = (mimeTypes: unknown): string[] => {\n\tif (!Array.isArray(mimeTypes)) {\n\t\treturn []\n\t}\n\treturn mimeTypes.filter((entry): entry is string => typeof entry === 'string')\n}\n\n/**\n * Headless file renderer: a native file input that uploads the chosen file to the upload collection and\n * stores the returned id as the field value. The server re-derives filename/mimeType/filesize from the\n * stored doc at submit, so the client value is only the id. Shows the uploaded filename with a clear\n * control and surfaces upload errors. Single file (v1); the upload posts to the default `/api` route.\n */\nexport const fileRenderer = defineFieldRenderer<string | number>(\n\t({ field, name, value, onChange, onBlur, errors, warnings, required, disabled, t }) => {\n\t\tconst id = useId()\n\t\tconst describedById = `${id}-desc`\n\t\tconst [uploading, setUploading] = useState(false)\n\t\tconst [filename, setFilename] = useState<string | undefined>(undefined)\n\t\tconst [localError, setLocalError] = useState<string | undefined>(undefined)\n\n\t\tconst collection = typeof field.relationTo === 'string' ? field.relationTo : 'form-uploads'\n\t\tconst types = typesOf(field.mimeTypes)\n\t\tconst accept = types.length > 0 ? types.join(',') : undefined\n\t\t// Zero is a real limit, matching resolveFileRef server-side (filesize > maxSize rejects any non-empty file).\n\t\tconst maxSize =\n\t\t\ttypeof field.maxSize === 'number' && field.maxSize >= 0 ? field.maxSize : undefined\n\t\tconst hints = [\n\t\t\ttypes.length > 0\n\t\t\t\t? resolveMessage(t(keys.fileHintAccepted), { types: types.join(', ') })\n\t\t\t\t: undefined,\n\t\t\tmaxSize !== undefined\n\t\t\t\t? resolveMessage(t(keys.fileHintMaxSize), { max: formatBytes(maxSize) })\n\t\t\t\t: undefined,\n\t\t].filter((hint): hint is string => typeof hint === 'string')\n\t\tconst allErrors = localError ? [...errors, localError] : errors\n\t\t// Show the just-uploaded filename, or a generic indicator when the field already holds an id (recall/prefill): the filename is not part of the client value.\n\t\tconst hasValue = value !== undefined && value !== null && value !== ''\n\t\tconst displayName = filename ?? (hasValue ? 'Uploaded file' : undefined)\n\n\t\tconst handleChange = async (event: ChangeEvent<HTMLInputElement>) => {\n\t\t\tconst selected = event.target.files?.[0]\n\t\t\tif (!selected) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (maxSize !== undefined && selected.size > maxSize) {\n\t\t\t\tsetLocalError(resolveMessage(t(keys.fileTooLarge), { max: formatBytes(maxSize) }))\n\t\t\t\treturn\n\t\t\t}\n\t\t\tsetUploading(true)\n\t\t\tsetLocalError(undefined)\n\t\t\tconst result = await uploadFile({ file: selected, collection })\n\t\t\tsetUploading(false)\n\t\t\tif (result.ok) {\n\t\t\t\tsetFilename(selected.name)\n\t\t\t\tonChange(result.id)\n\t\t\t} else {\n\t\t\t\tsetLocalError(result.message ?? 'Upload failed')\n\t\t\t}\n\t\t}\n\n\t\tconst clear = () => {\n\t\t\tsetFilename(undefined)\n\t\t\tsetLocalError(undefined)\n\t\t\tonChange('')\n\t\t}\n\n\t\treturn (\n\t\t\t<FieldShell\n\t\t\t\tid={id}\n\t\t\t\tlabel={field.label}\n\t\t\t\tdescription={typeof field.description === 'string' ? field.description : undefined}\n\t\t\t\trequired={required}\n\t\t\t\terrors={allErrors}\n\t\t\t\twarnings={warnings}\n\t\t\t\tdescribedById={describedById}\n\t\t\t>\n\t\t\t\t<input\n\t\t\t\t\tid={id}\n\t\t\t\t\tname={name}\n\t\t\t\t\ttype=\"file\"\n\t\t\t\t\taccept={accept}\n\t\t\t\t\tdisabled={disabled || uploading}\n\t\t\t\t\tonChange={(event) => {\n\t\t\t\t\t\tvoid handleChange(event)\n\t\t\t\t\t}}\n\t\t\t\t\tonBlur={onBlur}\n\t\t\t\t\taria-invalid={allErrors.length > 0}\n\t\t\t\t\taria-describedby={describedById}\n\t\t\t\t/>\n\t\t\t\t{hints.length > 0 ? <p className=\"fb-field__file-hint\">{hints.join(' · ')}</p> : null}\n\t\t\t\t{uploading ? (\n\t\t\t\t\t<p className=\"fb-field__file-status\" aria-live=\"polite\">\n\t\t\t\t\t\tUploading\n\t\t\t\t\t</p>\n\t\t\t\t) : null}\n\t\t\t\t{displayName ? (\n\t\t\t\t\t<p className=\"fb-field__file-name\">\n\t\t\t\t\t\t<span>{displayName}</span>\n\t\t\t\t\t\t<button\n\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\tclassName=\"fb-field__file-clear\"\n\t\t\t\t\t\t\tonClick={clear}\n\t\t\t\t\t\t\tdisabled={disabled}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\tRemove\n\t\t\t\t\t\t</button>\n\t\t\t\t\t</p>\n\t\t\t\t) : null}\n\t\t\t</FieldShell>\n\t\t)\n\t}\n)\n"],"mappings":";;;;;;;;;;AAUA,MAAM,WAAW,cAAiC;CACjD,IAAI,CAAC,MAAM,QAAQ,SAAS,GAC3B,OAAO,CAAC;CAET,OAAO,UAAU,QAAQ,UAA2B,OAAO,UAAU,QAAQ;AAC9E;;;;;;;AAQA,MAAa,eAAe,qBAC1B,EAAE,OAAO,MAAM,OAAO,UAAU,QAAQ,QAAQ,UAAU,UAAU,UAAU,QAAQ;CACtF,MAAM,KAAK,MAAM;CACjB,MAAM,gBAAgB,GAAG,GAAG;CAC5B,MAAM,CAAC,WAAW,gBAAgB,SAAS,KAAK;CAChD,MAAM,CAAC,UAAU,eAAe,SAA6B,KAAA,CAAS;CACtE,MAAM,CAAC,YAAY,iBAAiB,SAA6B,KAAA,CAAS;CAE1E,MAAM,aAAa,OAAO,MAAM,eAAe,WAAW,MAAM,aAAa;CAC7E,MAAM,QAAQ,QAAQ,MAAM,SAAS;CACrC,MAAM,SAAS,MAAM,SAAS,IAAI,MAAM,KAAK,GAAG,IAAI,KAAA;CAEpD,MAAM,UACL,OAAO,MAAM,YAAY,YAAY,MAAM,WAAW,IAAI,MAAM,UAAU,KAAA;CAC3E,MAAM,QAAQ,CACb,MAAM,SAAS,IACZ,eAAe,EAAE,KAAK,gBAAgB,GAAG,EAAE,OAAO,MAAM,KAAK,IAAI,EAAE,CAAC,IACpE,KAAA,GACH,YAAY,KAAA,IACT,eAAe,EAAE,KAAK,eAAe,GAAG,EAAE,KAAK,YAAY,OAAO,EAAE,CAAC,IACrE,KAAA,CACJ,EAAE,QAAQ,SAAyB,OAAO,SAAS,QAAQ;CAC3D,MAAM,YAAY,aAAa,CAAC,GAAG,QAAQ,UAAU,IAAI;CAGzD,MAAM,cAAc,aADH,UAAU,KAAA,KAAa,UAAU,QAAQ,UAAU,KACxB,kBAAkB,KAAA;CAE9D,MAAM,eAAe,OAAO,UAAyC;EACpE,MAAM,WAAW,MAAM,OAAO,QAAQ;EACtC,IAAI,CAAC,UACJ;EAED,IAAI,YAAY,KAAA,KAAa,SAAS,OAAO,SAAS;GACrD,cAAc,eAAe,EAAE,KAAK,YAAY,GAAG,EAAE,KAAK,YAAY,OAAO,EAAE,CAAC,CAAC;GACjF;EACD;EACA,aAAa,IAAI;EACjB,cAAc,KAAA,CAAS;EACvB,MAAM,SAAS,MAAM,WAAW;GAAE,MAAM;GAAU;EAAW,CAAC;EAC9D,aAAa,KAAK;EAClB,IAAI,OAAO,IAAI;GACd,YAAY,SAAS,IAAI;GACzB,SAAS,OAAO,EAAE;EACnB,OACC,cAAc,OAAO,WAAW,eAAe;CAEjD;CAEA,MAAM,cAAc;EACnB,YAAY,KAAA,CAAS;EACrB,cAAc,KAAA,CAAS;EACvB,SAAS,EAAE;CACZ;CAEA,OACC,qBAAC,YAAD;EACK;EACJ,OAAO,MAAM;EACb,aAAa,OAAO,MAAM,gBAAgB,WAAW,MAAM,cAAc,KAAA;EAC/D;EACV,QAAQ;EACE;EACK;YAPhB;GASC,oBAAC,SAAD;IACK;IACE;IACN,MAAK;IACG;IACR,UAAU,YAAY;IACtB,WAAW,UAAU;KACpB,aAAkB,KAAK;IACxB;IACQ;IACR,gBAAc,UAAU,SAAS;IACjC,oBAAkB;GAClB,CAAA;GACA,MAAM,SAAS,IAAI,oBAAC,KAAD;IAAG,WAAU;cAAuB,MAAM,KAAK,KAAK;GAAK,CAAA,IAAI;GAChF,YACA,oBAAC,KAAD;IAAG,WAAU;IAAwB,aAAU;cAAS;GAErD,CAAA,IACA;GACH,cACA,qBAAC,KAAD;IAAG,WAAU;cAAb,CACC,oBAAC,QAAD,EAAA,UAAO,YAAkB,CAAA,GACzB,oBAAC,UAAD;KACC,MAAK;KACL,WAAU;KACV,SAAS;KACC;eACV;IAEO,CAAA,CACN;QACA;EACO;;AAEd,CACD"}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { calculationRenderer } from "./calculation.js";
|
|
2
2
|
import { checkboxRenderer } from "./checkbox.js";
|
|
3
3
|
import { consentRenderer } from "./consent.js";
|
|
4
|
+
import { dateRenderer } from "./date.js";
|
|
4
5
|
import { emailRenderer } from "./email.js";
|
|
5
6
|
import { fileRenderer } from "./file.js";
|
|
6
7
|
import { numberRenderer } from "./number.js";
|
|
8
|
+
import { repeaterRenderer } from "./repeater.js";
|
|
7
9
|
import { selectRenderer } from "./select.js";
|
|
8
10
|
import { textRenderer } from "./text.js";
|
|
9
11
|
import { textareaRenderer } from "./textarea.js";
|
|
@@ -13,9 +15,11 @@ const defaultRenderers = {
|
|
|
13
15
|
calculation: calculationRenderer,
|
|
14
16
|
checkbox: checkboxRenderer,
|
|
15
17
|
consent: consentRenderer,
|
|
18
|
+
date: dateRenderer,
|
|
16
19
|
email: emailRenderer,
|
|
17
20
|
file: fileRenderer,
|
|
18
21
|
number: numberRenderer,
|
|
22
|
+
repeater: repeaterRenderer,
|
|
19
23
|
select: selectRenderer,
|
|
20
24
|
text: textRenderer,
|
|
21
25
|
textarea: textareaRenderer
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../../src/react/renderers/index.ts"],"sourcesContent":["import type { FieldRenderer } from '../contract'\nimport { calculationRenderer } from './calculation'\nimport { checkboxRenderer } from './checkbox'\nimport { consentRenderer } from './consent'\nimport { emailRenderer } from './email'\nimport { fileRenderer } from './file'\nimport { numberRenderer } from './number'\nimport { selectRenderer } from './select'\nimport { textRenderer } from './text'\nimport { textareaRenderer } from './textarea'\n\n/** The built-in field renderers, keyed by field-type slug. Override via the renderer registry. */\nexport const defaultRenderers: Record<string, FieldRenderer> = {\n\tcalculation: calculationRenderer as FieldRenderer,\n\tcheckbox: checkboxRenderer as FieldRenderer,\n\tconsent: consentRenderer as FieldRenderer,\n\temail: emailRenderer as FieldRenderer,\n\tfile: fileRenderer as FieldRenderer,\n\tnumber: numberRenderer as FieldRenderer,\n\tselect: selectRenderer as FieldRenderer,\n\ttext: textRenderer as FieldRenderer,\n\ttextarea: textareaRenderer as FieldRenderer,\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../src/react/renderers/index.ts"],"sourcesContent":["import type { FieldRenderer } from '../contract'\nimport { calculationRenderer } from './calculation'\nimport { checkboxRenderer } from './checkbox'\nimport { consentRenderer } from './consent'\nimport { dateRenderer } from './date'\nimport { emailRenderer } from './email'\nimport { fileRenderer } from './file'\nimport { numberRenderer } from './number'\nimport { repeaterRenderer } from './repeater'\nimport { selectRenderer } from './select'\nimport { textRenderer } from './text'\nimport { textareaRenderer } from './textarea'\n\n/** The built-in field renderers, keyed by field-type slug. Override via the renderer registry. */\nexport const defaultRenderers: Record<string, FieldRenderer> = {\n\tcalculation: calculationRenderer as FieldRenderer,\n\tcheckbox: checkboxRenderer as FieldRenderer,\n\tconsent: consentRenderer as FieldRenderer,\n\tdate: dateRenderer as FieldRenderer,\n\temail: emailRenderer as FieldRenderer,\n\tfile: fileRenderer as FieldRenderer,\n\tnumber: numberRenderer as FieldRenderer,\n\trepeater: repeaterRenderer as FieldRenderer,\n\tselect: selectRenderer as FieldRenderer,\n\ttext: textRenderer as FieldRenderer,\n\ttextarea: textareaRenderer as FieldRenderer,\n}\n"],"mappings":";;;;;;;;;;;;;AAcA,MAAa,mBAAkD;CAC9D,aAAa;CACb,UAAU;CACV,SAAS;CACT,MAAM;CACN,OAAO;CACP,MAAM;CACN,QAAQ;CACR,UAAU;CACV,QAAQ;CACR,MAAM;CACN,UAAU;AACX"}
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import { defineFieldRenderer } from "../contract.js";
|
|
3
3
|
import { FieldShell } from "../primitives/FieldShell.js";
|
|
4
4
|
import { Input } from "../primitives/Input.js";
|
|
5
|
-
import { useId } from "react";
|
|
6
5
|
import { jsx } from "react/jsx-runtime";
|
|
6
|
+
import { useId } from "react";
|
|
7
7
|
//#region src/react/renderers/number.tsx
|
|
8
8
|
const numberRenderer = defineFieldRenderer(({ field, name, value, onChange, onBlur, errors, warnings, required, disabled }) => {
|
|
9
9
|
const id = useId();
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { keys } from "../../translations/keys.js";
|
|
3
|
+
import { defineFieldRenderer } from "../contract.js";
|
|
4
|
+
import { useFormContext } from "../FormContext.js";
|
|
5
|
+
import { FieldShell } from "../primitives/FieldShell.js";
|
|
6
|
+
import { useField } from "../useField.js";
|
|
7
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
8
|
+
import { createElement, useId } from "react";
|
|
9
|
+
//#region src/react/renderers/repeater.tsx
|
|
10
|
+
/**
|
|
11
|
+
* Thin wrapper so each sub-field renderer is a proper React component (has stable hook identity
|
|
12
|
+
* per row/field). Reads errors and warnings for the composite path `field[row].subField` from
|
|
13
|
+
* form state so server-side validation errors are surfaced inline next to the sub-field input,
|
|
14
|
+
* overriding any static values passed by the parent.
|
|
15
|
+
*/
|
|
16
|
+
const SubFieldWrapper = ({ renderer, name, ...props }) => {
|
|
17
|
+
const { state } = useFormContext();
|
|
18
|
+
const errors = state.errors?.[name] ?? [];
|
|
19
|
+
const warnings = state.warnings?.[name] ?? [];
|
|
20
|
+
return renderer({
|
|
21
|
+
...props,
|
|
22
|
+
name,
|
|
23
|
+
errors,
|
|
24
|
+
warnings
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
const repeaterRenderer = defineFieldRenderer(({ field, id: rootId, errors, warnings, required, t, locale }) => {
|
|
28
|
+
const { rendererRegistry } = useFormContext();
|
|
29
|
+
const { value, setValue, onBlur } = useField(field.name);
|
|
30
|
+
const addId = useId();
|
|
31
|
+
const rows = Array.isArray(value) ? value : [];
|
|
32
|
+
const subFields = Array.isArray(field.subFields) ? field.subFields : [];
|
|
33
|
+
const maxRows = typeof field.maxRows === "number" ? field.maxRows : void 0;
|
|
34
|
+
const minRows = typeof field.minRows === "number" ? field.minRows : 0;
|
|
35
|
+
const addLabel = typeof field.addLabel === "string" && field.addLabel.length > 0 ? field.addLabel : t(keys.repeaterAddRow);
|
|
36
|
+
const addRow = () => {
|
|
37
|
+
if (maxRows != null && rows.length >= maxRows) return;
|
|
38
|
+
setValue([...rows, {}]);
|
|
39
|
+
onBlur();
|
|
40
|
+
};
|
|
41
|
+
const removeRow = (index) => {
|
|
42
|
+
setValue(rows.filter((_, i) => i !== index));
|
|
43
|
+
onBlur();
|
|
44
|
+
};
|
|
45
|
+
const updateRow = (index, fieldName, fieldValue) => {
|
|
46
|
+
setValue(rows.map((row, i) => i === index ? {
|
|
47
|
+
...row,
|
|
48
|
+
[fieldName]: fieldValue
|
|
49
|
+
} : row));
|
|
50
|
+
};
|
|
51
|
+
return /* @__PURE__ */ jsx(FieldShell, {
|
|
52
|
+
id: rootId,
|
|
53
|
+
label: typeof field.label === "string" ? field.label : void 0,
|
|
54
|
+
description: typeof field.description === "string" ? field.description : void 0,
|
|
55
|
+
required,
|
|
56
|
+
errors,
|
|
57
|
+
warnings,
|
|
58
|
+
describedById: `${rootId}-desc`,
|
|
59
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
60
|
+
className: "fb-repeater",
|
|
61
|
+
children: [rows.map((row, rowIndex) => {
|
|
62
|
+
const rowLabel = t(keys.repeaterRow).replace("{n}", String(rowIndex + 1));
|
|
63
|
+
return /* @__PURE__ */ jsxs("fieldset", {
|
|
64
|
+
className: "fb-repeater__row",
|
|
65
|
+
children: [/* @__PURE__ */ jsxs("legend", {
|
|
66
|
+
className: "fb-repeater__row-legend",
|
|
67
|
+
children: [rowLabel, rows.length > minRows && /* @__PURE__ */ jsx("button", {
|
|
68
|
+
type: "button",
|
|
69
|
+
className: "fb-repeater__remove",
|
|
70
|
+
onClick: () => removeRow(rowIndex),
|
|
71
|
+
"aria-label": `${t(keys.repeaterRemoveRow)} ${rowLabel}`,
|
|
72
|
+
children: t(keys.repeaterRemoveRow)
|
|
73
|
+
})]
|
|
74
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
75
|
+
className: "fb-repeater__row-fields",
|
|
76
|
+
children: subFields.map((subField) => {
|
|
77
|
+
const renderer = rendererRegistry.get(subField.blockType);
|
|
78
|
+
if (!renderer) return null;
|
|
79
|
+
return /* @__PURE__ */ jsx("div", {
|
|
80
|
+
className: "fb-repeater__sub-field",
|
|
81
|
+
children: createElement(SubFieldWrapper, {
|
|
82
|
+
renderer,
|
|
83
|
+
field: subField,
|
|
84
|
+
id: `${rootId}-${rowIndex}-${subField.name}`,
|
|
85
|
+
name: `${field.name}[${rowIndex}].${subField.name}`,
|
|
86
|
+
value: row[subField.name],
|
|
87
|
+
onChange: (v) => updateRow(rowIndex, subField.name, v),
|
|
88
|
+
onBlur,
|
|
89
|
+
errors: [],
|
|
90
|
+
warnings: [],
|
|
91
|
+
required: subField.required ?? false,
|
|
92
|
+
disabled: false,
|
|
93
|
+
locale,
|
|
94
|
+
t
|
|
95
|
+
})
|
|
96
|
+
}, subField.name);
|
|
97
|
+
})
|
|
98
|
+
})]
|
|
99
|
+
}, rowIndex);
|
|
100
|
+
}), (maxRows == null || rows.length < maxRows) && /* @__PURE__ */ jsx("button", {
|
|
101
|
+
id: addId,
|
|
102
|
+
type: "button",
|
|
103
|
+
className: "fb-repeater__add",
|
|
104
|
+
onClick: addRow,
|
|
105
|
+
children: addLabel
|
|
106
|
+
})]
|
|
107
|
+
})
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
//#endregion
|
|
111
|
+
export { repeaterRenderer };
|
|
112
|
+
|
|
113
|
+
//# sourceMappingURL=repeater.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"repeater.js","names":[],"sources":["../../../src/react/renderers/repeater.tsx"],"sourcesContent":["'use client'\n\nimport { createElement, useId } from 'react'\nimport type { FormFieldInstance } from '../../submissions/types'\nimport { keys } from '../../translations/keys'\nimport type { FieldRenderer } from '../contract'\nimport { defineFieldRenderer } from '../contract'\nimport { useFormContext } from '../FormContext'\nimport { FieldShell } from '../primitives/FieldShell'\nimport { useField } from '../useField'\n\ntype RepeaterRow = Record<string, unknown>\n\n/**\n * Thin wrapper so each sub-field renderer is a proper React component (has stable hook identity\n * per row/field). Reads errors and warnings for the composite path `field[row].subField` from\n * form state so server-side validation errors are surfaced inline next to the sub-field input,\n * overriding any static values passed by the parent.\n */\nconst SubFieldWrapper = ({\n\trenderer,\n\tname,\n\t...props\n}: { renderer: FieldRenderer } & Parameters<FieldRenderer>[0]) => {\n\tconst { state } = useFormContext()\n\tconst errors = (state.errors as Record<string, string[]>)?.[name] ?? []\n\tconst warnings = (state.warnings as Record<string, string[]>)?.[name] ?? []\n\treturn renderer({ ...props, name, errors, warnings })\n}\n\nexport const repeaterRenderer = defineFieldRenderer<RepeaterRow[]>(\n\t({ field, id: rootId, errors, warnings, required, t, locale }) => {\n\t\tconst { rendererRegistry } = useFormContext()\n\t\tconst { value, setValue, onBlur } = useField<RepeaterRow[]>(field.name)\n\t\tconst addId = useId()\n\n\t\tconst rows = Array.isArray(value) ? value : []\n\t\tconst subFields = Array.isArray(field.subFields) ? (field.subFields as FormFieldInstance[]) : []\n\t\tconst maxRows = typeof field.maxRows === 'number' ? field.maxRows : undefined\n\t\tconst minRows = typeof field.minRows === 'number' ? field.minRows : 0\n\t\tconst addLabel =\n\t\t\ttypeof field.addLabel === 'string' && field.addLabel.length > 0\n\t\t\t\t? field.addLabel\n\t\t\t\t: t(keys.repeaterAddRow)\n\n\t\tconst addRow = () => {\n\t\t\tif (maxRows != null && rows.length >= maxRows) return\n\t\t\tsetValue([...rows, {}])\n\t\t\tonBlur()\n\t\t}\n\n\t\tconst removeRow = (index: number) => {\n\t\t\tsetValue(rows.filter((_, i) => i !== index))\n\t\t\tonBlur()\n\t\t}\n\n\t\tconst updateRow = (index: number, fieldName: string, fieldValue: unknown) => {\n\t\t\tsetValue(rows.map((row, i) => (i === index ? { ...row, [fieldName]: fieldValue } : row)))\n\t\t}\n\n\t\treturn (\n\t\t\t<FieldShell\n\t\t\t\tid={rootId}\n\t\t\t\tlabel={typeof field.label === 'string' ? field.label : undefined}\n\t\t\t\tdescription={typeof field.description === 'string' ? field.description : undefined}\n\t\t\t\trequired={required}\n\t\t\t\terrors={errors}\n\t\t\t\twarnings={warnings}\n\t\t\t\tdescribedById={`${rootId}-desc`}\n\t\t\t>\n\t\t\t\t<div className=\"fb-repeater\">\n\t\t\t\t\t{rows.map((row, rowIndex) => {\n\t\t\t\t\t\tconst rowLabel = t(keys.repeaterRow).replace('{n}', String(rowIndex + 1))\n\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t// biome-ignore lint/suspicious/noArrayIndexKey: repeater rows have no stable ID; index key is intentional\n\t\t\t\t\t\t\t<fieldset key={rowIndex} className=\"fb-repeater__row\">\n\t\t\t\t\t\t\t\t<legend className=\"fb-repeater__row-legend\">\n\t\t\t\t\t\t\t\t\t{rowLabel}\n\t\t\t\t\t\t\t\t\t{rows.length > minRows && (\n\t\t\t\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\t\t\t\tclassName=\"fb-repeater__remove\"\n\t\t\t\t\t\t\t\t\t\t\tonClick={() => removeRow(rowIndex)}\n\t\t\t\t\t\t\t\t\t\t\taria-label={`${t(keys.repeaterRemoveRow)} ${rowLabel}`}\n\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t{t(keys.repeaterRemoveRow)}\n\t\t\t\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t</legend>\n\t\t\t\t\t\t\t\t<div className=\"fb-repeater__row-fields\">\n\t\t\t\t\t\t\t\t\t{subFields.map((subField) => {\n\t\t\t\t\t\t\t\t\t\tconst renderer = rendererRegistry.get(subField.blockType)\n\t\t\t\t\t\t\t\t\t\tif (!renderer) return null\n\t\t\t\t\t\t\t\t\t\tconst subId = `${rootId}-${rowIndex}-${subField.name}`\n\t\t\t\t\t\t\t\t\t\tconst compositeName = `${field.name}[${rowIndex}].${subField.name}`\n\t\t\t\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t\t\t\t<div key={subField.name} className=\"fb-repeater__sub-field\">\n\t\t\t\t\t\t\t\t\t\t\t\t{createElement(SubFieldWrapper, {\n\t\t\t\t\t\t\t\t\t\t\t\t\trenderer,\n\t\t\t\t\t\t\t\t\t\t\t\t\tfield: subField,\n\t\t\t\t\t\t\t\t\t\t\t\t\tid: subId,\n\t\t\t\t\t\t\t\t\t\t\t\t\tname: compositeName,\n\t\t\t\t\t\t\t\t\t\t\t\t\tvalue: row[subField.name],\n\t\t\t\t\t\t\t\t\t\t\t\t\tonChange: (v) => updateRow(rowIndex, subField.name, v),\n\t\t\t\t\t\t\t\t\t\t\t\t\tonBlur,\n\t\t\t\t\t\t\t\t\t\t\t\t\t// SubFieldWrapper overrides these from form state; passed here to satisfy the type\n\t\t\t\t\t\t\t\t\t\t\t\t\terrors: [],\n\t\t\t\t\t\t\t\t\t\t\t\t\twarnings: [],\n\t\t\t\t\t\t\t\t\t\t\t\t\trequired: subField.required ?? false,\n\t\t\t\t\t\t\t\t\t\t\t\t\tdisabled: false,\n\t\t\t\t\t\t\t\t\t\t\t\t\tlocale,\n\t\t\t\t\t\t\t\t\t\t\t\t\tt,\n\t\t\t\t\t\t\t\t\t\t\t\t})}\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t\t})}\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</fieldset>\n\t\t\t\t\t\t)\n\t\t\t\t\t})}\n\t\t\t\t\t{(maxRows == null || rows.length < maxRows) && (\n\t\t\t\t\t\t<button id={addId} type=\"button\" className=\"fb-repeater__add\" onClick={addRow}>\n\t\t\t\t\t\t\t{addLabel}\n\t\t\t\t\t\t</button>\n\t\t\t\t\t)}\n\t\t\t\t</div>\n\t\t\t</FieldShell>\n\t\t)\n\t}\n)\n"],"mappings":";;;;;;;;;;;;;;;AAmBA,MAAM,mBAAmB,EACxB,UACA,MACA,GAAG,YAC8D;CACjE,MAAM,EAAE,UAAU,eAAe;CACjC,MAAM,SAAU,MAAM,SAAsC,SAAS,CAAC;CACtE,MAAM,WAAY,MAAM,WAAwC,SAAS,CAAC;CAC1E,OAAO,SAAS;EAAE,GAAG;EAAO;EAAM;EAAQ;CAAS,CAAC;AACrD;AAEA,MAAa,mBAAmB,qBAC9B,EAAE,OAAO,IAAI,QAAQ,QAAQ,UAAU,UAAU,GAAG,aAAa;CACjE,MAAM,EAAE,qBAAqB,eAAe;CAC5C,MAAM,EAAE,OAAO,UAAU,WAAW,SAAwB,MAAM,IAAI;CACtE,MAAM,QAAQ,MAAM;CAEpB,MAAM,OAAO,MAAM,QAAQ,KAAK,IAAI,QAAQ,CAAC;CAC7C,MAAM,YAAY,MAAM,QAAQ,MAAM,SAAS,IAAK,MAAM,YAAoC,CAAC;CAC/F,MAAM,UAAU,OAAO,MAAM,YAAY,WAAW,MAAM,UAAU,KAAA;CACpE,MAAM,UAAU,OAAO,MAAM,YAAY,WAAW,MAAM,UAAU;CACpE,MAAM,WACL,OAAO,MAAM,aAAa,YAAY,MAAM,SAAS,SAAS,IAC3D,MAAM,WACN,EAAE,KAAK,cAAc;CAEzB,MAAM,eAAe;EACpB,IAAI,WAAW,QAAQ,KAAK,UAAU,SAAS;EAC/C,SAAS,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC;EACtB,OAAO;CACR;CAEA,MAAM,aAAa,UAAkB;EACpC,SAAS,KAAK,QAAQ,GAAG,MAAM,MAAM,KAAK,CAAC;EAC3C,OAAO;CACR;CAEA,MAAM,aAAa,OAAe,WAAmB,eAAwB;EAC5E,SAAS,KAAK,KAAK,KAAK,MAAO,MAAM,QAAQ;GAAE,GAAG;IAAM,YAAY;EAAW,IAAI,GAAI,CAAC;CACzF;CAEA,OACC,oBAAC,YAAD;EACC,IAAI;EACJ,OAAO,OAAO,MAAM,UAAU,WAAW,MAAM,QAAQ,KAAA;EACvD,aAAa,OAAO,MAAM,gBAAgB,WAAW,MAAM,cAAc,KAAA;EAC/D;EACF;EACE;EACV,eAAe,GAAG,OAAO;YAEzB,qBAAC,OAAD;GAAK,WAAU;aAAf,CACE,KAAK,KAAK,KAAK,aAAa;IAC5B,MAAM,WAAW,EAAE,KAAK,WAAW,EAAE,QAAQ,OAAO,OAAO,WAAW,CAAC,CAAC;IACxE,OAEC,qBAAC,YAAD;KAAyB,WAAU;eAAnC,CACC,qBAAC,UAAD;MAAQ,WAAU;gBAAlB,CACE,UACA,KAAK,SAAS,WACd,oBAAC,UAAD;OACC,MAAK;OACL,WAAU;OACV,eAAe,UAAU,QAAQ;OACjC,cAAY,GAAG,EAAE,KAAK,iBAAiB,EAAE,GAAG;iBAE3C,EAAE,KAAK,iBAAiB;MAClB,CAAA,CAEF;SACR,oBAAC,OAAD;MAAK,WAAU;gBACb,UAAU,KAAK,aAAa;OAC5B,MAAM,WAAW,iBAAiB,IAAI,SAAS,SAAS;OACxD,IAAI,CAAC,UAAU,OAAO;OAGtB,OACC,oBAAC,OAAD;QAAyB,WAAU;kBACjC,cAAc,iBAAiB;SAC/B;SACA,OAAO;SACP,IAAI,GAPU,OAAO,GAAG,SAAS,GAAG,SAAS;SAQ7C,MAAM,GAPgB,MAAM,KAAK,GAAG,SAAS,IAAI,SAAS;SAQ1D,OAAO,IAAI,SAAS;SACpB,WAAW,MAAM,UAAU,UAAU,SAAS,MAAM,CAAC;SACrD;SAEA,QAAQ,CAAC;SACT,UAAU,CAAC;SACX,UAAU,SAAS,YAAY;SAC/B,UAAU;SACV;SACA;QACD,CAAC;OACG,GAjBK,SAAS,IAiBd;MAEP,CAAC;KACG,CAAA,CACI;OA1CK,QA0CL;GAEZ,CAAC,IACC,WAAW,QAAQ,KAAK,SAAS,YAClC,oBAAC,UAAD;IAAQ,IAAI;IAAO,MAAK;IAAS,WAAU;IAAmB,SAAS;cACrE;GACM,CAAA,CAEL;;CACM,CAAA;AAEd,CACD"}
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import { defineFieldRenderer } from "../contract.js";
|
|
3
3
|
import { FieldShell } from "../primitives/FieldShell.js";
|
|
4
4
|
import { Select } from "../primitives/Select.js";
|
|
5
|
-
import { useId } from "react";
|
|
6
5
|
import { jsx } from "react/jsx-runtime";
|
|
6
|
+
import { useId } from "react";
|
|
7
7
|
//#region src/react/renderers/select.tsx
|
|
8
8
|
const selectRenderer = defineFieldRenderer(({ field, name, value, onChange, onBlur, errors, warnings, required, disabled }) => {
|
|
9
9
|
const id = useId();
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import { defineFieldRenderer } from "../contract.js";
|
|
3
3
|
import { FieldShell } from "../primitives/FieldShell.js";
|
|
4
4
|
import { Input } from "../primitives/Input.js";
|
|
5
|
-
import { useId } from "react";
|
|
6
5
|
import { jsx } from "react/jsx-runtime";
|
|
6
|
+
import { useId } from "react";
|
|
7
7
|
//#region src/react/renderers/text.tsx
|
|
8
8
|
const textRenderer = defineFieldRenderer(({ field, name, value, onChange, onBlur, errors, warnings, required, disabled }) => {
|
|
9
9
|
const id = useId();
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import { defineFieldRenderer } from "../contract.js";
|
|
3
3
|
import { FieldShell } from "../primitives/FieldShell.js";
|
|
4
4
|
import { Textarea } from "../primitives/Textarea.js";
|
|
5
|
-
import { useId } from "react";
|
|
6
5
|
import { jsx } from "react/jsx-runtime";
|
|
6
|
+
import { useId } from "react";
|
|
7
7
|
//#region src/react/renderers/textarea.tsx
|
|
8
8
|
const textareaRenderer = defineFieldRenderer(({ field, name, value, onChange, onBlur, errors, warnings, required, disabled }) => {
|
|
9
9
|
const id = useId();
|
package/dist/spam/captcha.d.ts
CHANGED
|
@@ -3,8 +3,9 @@ import { CaptchaProvider } from "./types.js";
|
|
|
3
3
|
//#region src/spam/captcha.d.ts
|
|
4
4
|
/**
|
|
5
5
|
* Identity helper for authoring a captcha provider (mirrors `defineAction`/`defineConsentSource`).
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* Bundled adapters (`turnstileProvider`, `recaptchaProvider`, `hcaptchaProvider`) are built with it;
|
|
7
|
+
* use it directly for any other verification service. The submit path verifies a token carried on
|
|
8
|
+
* the submission.
|
|
8
9
|
*/
|
|
9
10
|
declare const defineCaptchaProvider: (provider: CaptchaProvider) => CaptchaProvider;
|
|
10
11
|
//#endregion
|
package/dist/spam/captcha.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
//#region src/spam/captcha.ts
|
|
2
2
|
/**
|
|
3
3
|
* Identity helper for authoring a captcha provider (mirrors `defineAction`/`defineConsentSource`).
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Bundled adapters (`turnstileProvider`, `recaptchaProvider`, `hcaptchaProvider`) are built with it;
|
|
5
|
+
* use it directly for any other verification service. The submit path verifies a token carried on
|
|
6
|
+
* the submission.
|
|
6
7
|
*/
|
|
7
8
|
const defineCaptchaProvider = (provider) => provider;
|
|
8
9
|
//#endregion
|
package/dist/spam/captcha.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"captcha.js","names":[],"sources":["../../src/spam/captcha.ts"],"sourcesContent":["import type { CaptchaProvider } from './types'\n\n/**\n * Identity helper for authoring a captcha provider (mirrors `defineAction`/`defineConsentSource`).\n *
|
|
1
|
+
{"version":3,"file":"captcha.js","names":[],"sources":["../../src/spam/captcha.ts"],"sourcesContent":["import type { CaptchaProvider } from './types'\n\n/**\n * Identity helper for authoring a captcha provider (mirrors `defineAction`/`defineConsentSource`).\n * Bundled adapters (`turnstileProvider`, `recaptchaProvider`, `hcaptchaProvider`) are built with it;\n * use it directly for any other verification service. The submit path verifies a token carried on\n * the submission.\n */\nexport const defineCaptchaProvider = (provider: CaptchaProvider): CaptchaProvider => provider\n"],"mappings":";;;;;;;AAQA,MAAa,yBAAyB,aAA+C"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
//#region src/spam/clientIp.ts
|
|
2
|
+
/** First hop of a comma-separated proxy header (the client, per X-Forwarded-For convention), or undefined. */
|
|
3
|
+
const firstHop = (headers, header) => {
|
|
4
|
+
return headers?.get(header)?.split(",")[0]?.trim() || void 0;
|
|
5
|
+
};
|
|
6
|
+
//#endregion
|
|
7
|
+
export { firstHop };
|
|
8
|
+
|
|
9
|
+
//# sourceMappingURL=clientIp.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clientIp.js","names":[],"sources":["../../src/spam/clientIp.ts"],"sourcesContent":["/** First hop of a comma-separated proxy header (the client, per X-Forwarded-For convention), or undefined. */\nexport const firstHop = (headers: Headers | undefined, header: string): string | undefined => {\n\tconst first = headers?.get(header)?.split(',')[0]?.trim()\n\treturn first || undefined\n}\n"],"mappings":";;AACA,MAAa,YAAY,SAA8B,WAAuC;CAE7F,OADc,SAAS,IAAI,MAAM,GAAG,MAAM,GAAG,EAAE,IAAI,KAAK,KACxC,KAAA;AACjB"}
|
package/dist/spam/identify.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { firstHop } from "./clientIp.js";
|
|
1
2
|
//#region src/spam/identify.ts
|
|
2
3
|
/**
|
|
3
4
|
* Default identity resolution for rate-limiting + upload ownership. Prefers an authenticated user id
|
|
@@ -7,12 +8,8 @@
|
|
|
7
8
|
const defaultIdentify = (ipHeader) => (req) => {
|
|
8
9
|
const userId = req.user?.id;
|
|
9
10
|
if (userId != null) return `user:${String(userId)}`;
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
const first = header.split(",")[0]?.trim();
|
|
13
|
-
if (first) return `ip:${first}`;
|
|
14
|
-
}
|
|
15
|
-
return null;
|
|
11
|
+
const ip = firstHop(req.headers, ipHeader);
|
|
12
|
+
return ip ? `ip:${ip}` : null;
|
|
16
13
|
};
|
|
17
14
|
//#endregion
|
|
18
15
|
export { defaultIdentify };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"identify.js","names":[],"sources":["../../src/spam/identify.ts"],"sourcesContent":["import type { IdentifyFn } from './types'\n\n/**\n * Default identity resolution for rate-limiting + upload ownership. Prefers an authenticated user id\n * (trustworthy); else the first hop of the configured trusted IP header (best-effort, proxy-dependent);\n * else null, in which case the caller fails open (cannot fairly rate-limit an unidentifiable client).\n */\nexport const defaultIdentify =\n\t(ipHeader: string): IdentifyFn =>\n\t(req) => {\n\t\tconst userId = req.user?.id\n\t\tif (userId != null) {\n\t\t\treturn `user:${String(userId)}`\n\t\t}\n\t\tconst
|
|
1
|
+
{"version":3,"file":"identify.js","names":[],"sources":["../../src/spam/identify.ts"],"sourcesContent":["import { firstHop } from './clientIp'\nimport type { IdentifyFn } from './types'\n\n/**\n * Default identity resolution for rate-limiting + upload ownership. Prefers an authenticated user id\n * (trustworthy); else the first hop of the configured trusted IP header (best-effort, proxy-dependent);\n * else null, in which case the caller fails open (cannot fairly rate-limit an unidentifiable client).\n */\nexport const defaultIdentify =\n\t(ipHeader: string): IdentifyFn =>\n\t(req) => {\n\t\tconst userId = req.user?.id\n\t\tif (userId != null) {\n\t\t\treturn `user:${String(userId)}`\n\t\t}\n\t\tconst ip = firstHop(req.headers, ipHeader)\n\t\treturn ip ? `ip:${ip}` : null\n\t}\n"],"mappings":";;;;;;;AAQA,MAAa,mBACX,cACA,QAAQ;CACR,MAAM,SAAS,IAAI,MAAM;CACzB,IAAI,UAAU,MACb,OAAO,QAAQ,OAAO,MAAM;CAE7B,MAAM,KAAK,SAAS,IAAI,SAAS,QAAQ;CACzC,OAAO,KAAK,MAAM,OAAO;AAC1B"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { CaptchaProvider } from "../types.js";
|
|
2
|
+
|
|
3
|
+
//#region src/spam/providers/hcaptcha.d.ts
|
|
4
|
+
type HcaptchaProviderOptions = {
|
|
5
|
+
/** hCaptcha secret key (server-side, never the site key). */secretKey: string; /** Override the siteverify endpoint. */
|
|
6
|
+
verifyUrl?: string; /** Header read for the client IP forwarded as `remoteip`. Default 'x-forwarded-for'. */
|
|
7
|
+
ipHeader?: string; /** Verification request timeout in ms. Default 5000. */
|
|
8
|
+
timeoutMs?: number;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* hCaptcha verification adapter. Network errors, timeouts, and non-2xx responses fail closed
|
|
12
|
+
* (the submission is rejected).
|
|
13
|
+
*/
|
|
14
|
+
declare const hcaptchaProvider: (options: HcaptchaProviderOptions) => CaptchaProvider;
|
|
15
|
+
//#endregion
|
|
16
|
+
export { HcaptchaProviderOptions, hcaptchaProvider };
|
|
17
|
+
//# sourceMappingURL=hcaptcha.d.ts.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { defineCaptchaProvider } from "../captcha.js";
|
|
2
|
+
import { postSiteverify, siteverifyParams } from "./siteverify.js";
|
|
3
|
+
//#region src/spam/providers/hcaptcha.ts
|
|
4
|
+
const HCAPTCHA_VERIFY_URL = "https://api.hcaptcha.com/siteverify";
|
|
5
|
+
/**
|
|
6
|
+
* hCaptcha verification adapter. Network errors, timeouts, and non-2xx responses fail closed
|
|
7
|
+
* (the submission is rejected).
|
|
8
|
+
*/
|
|
9
|
+
const hcaptchaProvider = (options) => defineCaptchaProvider({
|
|
10
|
+
type: "hcaptcha",
|
|
11
|
+
verify: async ({ token, req }) => {
|
|
12
|
+
return (await postSiteverify(options.verifyUrl ?? HCAPTCHA_VERIFY_URL, siteverifyParams(options, token, req), options.timeoutMs ?? 5e3))?.success === true;
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
//#endregion
|
|
16
|
+
export { hcaptchaProvider };
|
|
17
|
+
|
|
18
|
+
//# sourceMappingURL=hcaptcha.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hcaptcha.js","names":[],"sources":["../../../src/spam/providers/hcaptcha.ts"],"sourcesContent":["import { defineCaptchaProvider } from '../captcha'\nimport type { CaptchaProvider } from '../types'\nimport { DEFAULT_SITEVERIFY_TIMEOUT, postSiteverify, siteverifyParams } from './siteverify'\n\nconst HCAPTCHA_VERIFY_URL = 'https://api.hcaptcha.com/siteverify'\n\nexport type HcaptchaProviderOptions = {\n\t/** hCaptcha secret key (server-side, never the site key). */\n\tsecretKey: string\n\t/** Override the siteverify endpoint. */\n\tverifyUrl?: string\n\t/** Header read for the client IP forwarded as `remoteip`. Default 'x-forwarded-for'. */\n\tipHeader?: string\n\t/** Verification request timeout in ms. Default 5000. */\n\ttimeoutMs?: number\n}\n\n/**\n * hCaptcha verification adapter. Network errors, timeouts, and non-2xx responses fail closed\n * (the submission is rejected).\n */\nexport const hcaptchaProvider = (options: HcaptchaProviderOptions): CaptchaProvider =>\n\tdefineCaptchaProvider({\n\t\ttype: 'hcaptcha',\n\t\tverify: async ({ token, req }) => {\n\t\t\tconst result = await postSiteverify(\n\t\t\t\toptions.verifyUrl ?? HCAPTCHA_VERIFY_URL,\n\t\t\t\tsiteverifyParams(options, token, req),\n\t\t\t\toptions.timeoutMs ?? DEFAULT_SITEVERIFY_TIMEOUT\n\t\t\t)\n\t\t\treturn result?.success === true\n\t\t},\n\t})\n"],"mappings":";;;AAIA,MAAM,sBAAsB;;;;;AAiB5B,MAAa,oBAAoB,YAChC,sBAAsB;CACrB,MAAM;CACN,QAAQ,OAAO,EAAE,OAAO,UAAU;EAMjC,QAAO,MALc,eACpB,QAAQ,aAAa,qBACrB,iBAAiB,SAAS,OAAO,GAAG,GACpC,QAAQ,aAAA,GACT,IACe,YAAY;CAC5B;AACD,CAAC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { CaptchaProvider } from "../types.js";
|
|
2
|
+
|
|
3
|
+
//#region src/spam/providers/recaptcha.d.ts
|
|
4
|
+
type RecaptchaProviderOptions = {
|
|
5
|
+
/** reCAPTCHA secret key (server-side, never the site key). */secretKey: string;
|
|
6
|
+
/**
|
|
7
|
+
* Minimum acceptable v3 score (0 to 1). Applied only when the response carries a score,
|
|
8
|
+
* defaulting to 0.5; v2 responses have no score and pass on `success` alone.
|
|
9
|
+
*/
|
|
10
|
+
minScore?: number; /** Override the siteverify endpoint. */
|
|
11
|
+
verifyUrl?: string; /** Header read for the client IP forwarded as `remoteip`. Default 'x-forwarded-for'. */
|
|
12
|
+
ipHeader?: string; /** Verification request timeout in ms. Default 5000. */
|
|
13
|
+
timeoutMs?: number;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Google reCAPTCHA verification adapter for both v2 (checkbox) and v3 (score) tokens. A v3
|
|
17
|
+
* response must also meet `minScore`. Network errors, timeouts, and non-2xx responses fail
|
|
18
|
+
* closed (the submission is rejected).
|
|
19
|
+
*/
|
|
20
|
+
declare const recaptchaProvider: (options: RecaptchaProviderOptions) => CaptchaProvider;
|
|
21
|
+
//#endregion
|
|
22
|
+
export { RecaptchaProviderOptions, recaptchaProvider };
|
|
23
|
+
//# sourceMappingURL=recaptcha.d.ts.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { defineCaptchaProvider } from "../captcha.js";
|
|
2
|
+
import { postSiteverify, siteverifyParams } from "./siteverify.js";
|
|
3
|
+
//#region src/spam/providers/recaptcha.ts
|
|
4
|
+
const RECAPTCHA_VERIFY_URL = "https://www.google.com/recaptcha/api/siteverify";
|
|
5
|
+
const DEFAULT_MIN_SCORE = .5;
|
|
6
|
+
/**
|
|
7
|
+
* Google reCAPTCHA verification adapter for both v2 (checkbox) and v3 (score) tokens. A v3
|
|
8
|
+
* response must also meet `minScore`. Network errors, timeouts, and non-2xx responses fail
|
|
9
|
+
* closed (the submission is rejected).
|
|
10
|
+
*/
|
|
11
|
+
const recaptchaProvider = (options) => defineCaptchaProvider({
|
|
12
|
+
type: "recaptcha",
|
|
13
|
+
verify: async ({ token, req }) => {
|
|
14
|
+
const result = await postSiteverify(options.verifyUrl ?? RECAPTCHA_VERIFY_URL, siteverifyParams(options, token, req), options.timeoutMs ?? 5e3);
|
|
15
|
+
if (result?.success !== true) return false;
|
|
16
|
+
if (typeof result.score === "number") return result.score >= (options.minScore ?? DEFAULT_MIN_SCORE);
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
//#endregion
|
|
21
|
+
export { recaptchaProvider };
|
|
22
|
+
|
|
23
|
+
//# sourceMappingURL=recaptcha.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recaptcha.js","names":[],"sources":["../../../src/spam/providers/recaptcha.ts"],"sourcesContent":["import { defineCaptchaProvider } from '../captcha'\nimport type { CaptchaProvider } from '../types'\nimport { DEFAULT_SITEVERIFY_TIMEOUT, postSiteverify, siteverifyParams } from './siteverify'\n\nconst RECAPTCHA_VERIFY_URL = 'https://www.google.com/recaptcha/api/siteverify'\nconst DEFAULT_MIN_SCORE = 0.5\n\nexport type RecaptchaProviderOptions = {\n\t/** reCAPTCHA secret key (server-side, never the site key). */\n\tsecretKey: string\n\t/**\n\t * Minimum acceptable v3 score (0 to 1). Applied only when the response carries a score,\n\t * defaulting to 0.5; v2 responses have no score and pass on `success` alone.\n\t */\n\tminScore?: number\n\t/** Override the siteverify endpoint. */\n\tverifyUrl?: string\n\t/** Header read for the client IP forwarded as `remoteip`. Default 'x-forwarded-for'. */\n\tipHeader?: string\n\t/** Verification request timeout in ms. Default 5000. */\n\ttimeoutMs?: number\n}\n\n/**\n * Google reCAPTCHA verification adapter for both v2 (checkbox) and v3 (score) tokens. A v3\n * response must also meet `minScore`. Network errors, timeouts, and non-2xx responses fail\n * closed (the submission is rejected).\n */\nexport const recaptchaProvider = (options: RecaptchaProviderOptions): CaptchaProvider =>\n\tdefineCaptchaProvider({\n\t\ttype: 'recaptcha',\n\t\tverify: async ({ token, req }) => {\n\t\t\tconst result = await postSiteverify(\n\t\t\t\toptions.verifyUrl ?? RECAPTCHA_VERIFY_URL,\n\t\t\t\tsiteverifyParams(options, token, req),\n\t\t\t\toptions.timeoutMs ?? DEFAULT_SITEVERIFY_TIMEOUT\n\t\t\t)\n\t\t\tif (result?.success !== true) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tif (typeof result.score === 'number') {\n\t\t\t\treturn result.score >= (options.minScore ?? DEFAULT_MIN_SCORE)\n\t\t\t}\n\t\t\treturn true\n\t\t},\n\t})\n"],"mappings":";;;AAIA,MAAM,uBAAuB;AAC7B,MAAM,oBAAoB;;;;;;AAuB1B,MAAa,qBAAqB,YACjC,sBAAsB;CACrB,MAAM;CACN,QAAQ,OAAO,EAAE,OAAO,UAAU;EACjC,MAAM,SAAS,MAAM,eACpB,QAAQ,aAAa,sBACrB,iBAAiB,SAAS,OAAO,GAAG,GACpC,QAAQ,aAAA,GACT;EACA,IAAI,QAAQ,YAAY,MACvB,OAAO;EAER,IAAI,OAAO,OAAO,UAAU,UAC3B,OAAO,OAAO,UAAU,QAAQ,YAAY;EAE7C,OAAO;CACR;AACD,CAAC"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { firstHop } from "../clientIp.js";
|
|
2
|
+
//#region src/spam/providers/siteverify.ts
|
|
3
|
+
const DEFAULT_SITEVERIFY_TIMEOUT = 5e3;
|
|
4
|
+
/** Form params for a siteverify POST: secret + response, plus `remoteip` when the IP header resolves. */
|
|
5
|
+
const siteverifyParams = (args, token, req) => {
|
|
6
|
+
const params = new URLSearchParams({
|
|
7
|
+
secret: args.secretKey,
|
|
8
|
+
response: token
|
|
9
|
+
});
|
|
10
|
+
const remoteip = firstHop(req.headers, args.ipHeader ?? "x-forwarded-for");
|
|
11
|
+
if (remoteip) params.set("remoteip", remoteip);
|
|
12
|
+
return params;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* POST a form-encoded siteverify request. Network errors, timeouts, and non-2xx responses all
|
|
16
|
+
* resolve to null so adapters fail closed instead of throwing into the submission path.
|
|
17
|
+
*/
|
|
18
|
+
const postSiteverify = async (url, params, timeoutMs) => {
|
|
19
|
+
try {
|
|
20
|
+
const res = await fetch(url, {
|
|
21
|
+
method: "POST",
|
|
22
|
+
body: params,
|
|
23
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
24
|
+
});
|
|
25
|
+
if (!res.ok) return null;
|
|
26
|
+
return await res.json();
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
//#endregion
|
|
32
|
+
export { DEFAULT_SITEVERIFY_TIMEOUT, postSiteverify, siteverifyParams };
|
|
33
|
+
|
|
34
|
+
//# sourceMappingURL=siteverify.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"siteverify.js","names":[],"sources":["../../../src/spam/providers/siteverify.ts"],"sourcesContent":["import type { PayloadRequest } from 'payload'\nimport { firstHop } from '../clientIp'\n\n/** The response shape shared by the Turnstile, reCAPTCHA, and hCaptcha siteverify endpoints. */\nexport type SiteverifyResult = {\n\tsuccess?: boolean\n\tscore?: number\n\t'error-codes'?: string[]\n}\n\nexport const DEFAULT_SITEVERIFY_TIMEOUT = 5_000\n\nexport type SiteverifyParamsArgs = {\n\tsecretKey: string\n\t/** Header read for the client IP forwarded as `remoteip`. Default 'x-forwarded-for'. */\n\tipHeader?: string\n}\n\n/** Form params for a siteverify POST: secret + response, plus `remoteip` when the IP header resolves. */\nexport const siteverifyParams = (\n\targs: SiteverifyParamsArgs,\n\ttoken: string,\n\treq: PayloadRequest\n): URLSearchParams => {\n\tconst params = new URLSearchParams({ secret: args.secretKey, response: token })\n\tconst remoteip = firstHop(req.headers, args.ipHeader ?? 'x-forwarded-for')\n\tif (remoteip) {\n\t\tparams.set('remoteip', remoteip)\n\t}\n\treturn params\n}\n\n/**\n * POST a form-encoded siteverify request. Network errors, timeouts, and non-2xx responses all\n * resolve to null so adapters fail closed instead of throwing into the submission path.\n */\nexport const postSiteverify = async (\n\turl: string,\n\tparams: URLSearchParams,\n\ttimeoutMs: number\n): Promise<SiteverifyResult | null> => {\n\ttry {\n\t\tconst res = await fetch(url, {\n\t\t\tmethod: 'POST',\n\t\t\tbody: params,\n\t\t\tsignal: AbortSignal.timeout(timeoutMs),\n\t\t})\n\t\tif (!res.ok) {\n\t\t\treturn null\n\t\t}\n\t\treturn (await res.json()) as SiteverifyResult\n\t} catch {\n\t\treturn null\n\t}\n}\n"],"mappings":";;AAUA,MAAa,6BAA6B;;AAS1C,MAAa,oBACZ,MACA,OACA,QACqB;CACrB,MAAM,SAAS,IAAI,gBAAgB;EAAE,QAAQ,KAAK;EAAW,UAAU;CAAM,CAAC;CAC9E,MAAM,WAAW,SAAS,IAAI,SAAS,KAAK,YAAY,iBAAiB;CACzE,IAAI,UACH,OAAO,IAAI,YAAY,QAAQ;CAEhC,OAAO;AACR;;;;;AAMA,MAAa,iBAAiB,OAC7B,KACA,QACA,cACsC;CACtC,IAAI;EACH,MAAM,MAAM,MAAM,MAAM,KAAK;GAC5B,QAAQ;GACR,MAAM;GACN,QAAQ,YAAY,QAAQ,SAAS;EACtC,CAAC;EACD,IAAI,CAAC,IAAI,IACR,OAAO;EAER,OAAQ,MAAM,IAAI,KAAK;CACxB,QAAQ;EACP,OAAO;CACR;AACD"}
|