@frigear-nu/forms 0.0.6 → 0.0.8
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/README.md +16 -1
- package/dist/module.d.mts +148 -8
- package/dist/module.json +1 -1
- package/dist/module.mjs +65 -35
- package/dist/runtime/app/components/FormBuilder.d.vue.ts +26 -0
- package/dist/runtime/app/components/FormBuilder.vue +220 -0
- package/dist/runtime/app/components/FormBuilder.vue.d.ts +26 -0
- package/dist/runtime/app/components/FormBuilderAccess.d.vue.ts +16 -0
- package/dist/runtime/app/components/FormBuilderAccess.vue +404 -0
- package/dist/runtime/app/components/FormBuilderAccess.vue.d.ts +16 -0
- package/dist/runtime/app/components/FormBuilderCaptchaSettings.d.vue.ts +15 -0
- package/dist/runtime/app/components/FormBuilderCaptchaSettings.vue +126 -0
- package/dist/runtime/app/components/FormBuilderCaptchaSettings.vue.d.ts +15 -0
- package/dist/runtime/app/components/FormBuilderCondition.d.vue.ts +16 -0
- package/dist/runtime/app/components/FormBuilderCondition.vue +62 -0
- package/dist/runtime/app/components/FormBuilderCondition.vue.d.ts +16 -0
- package/dist/runtime/app/components/FormBuilderFieldRow.d.vue.ts +27 -0
- package/dist/runtime/app/components/FormBuilderFieldRow.vue +87 -0
- package/dist/runtime/app/components/FormBuilderFieldRow.vue.d.ts +27 -0
- package/dist/runtime/app/components/FormBuilderFieldSettings.d.vue.ts +25 -0
- package/dist/runtime/app/components/FormBuilderFieldSettings.vue +170 -0
- package/dist/runtime/app/components/FormBuilderFieldSettings.vue.d.ts +25 -0
- package/dist/runtime/app/components/FormBuilderInsertRow.d.vue.ts +20 -0
- package/dist/runtime/app/components/FormBuilderInsertRow.vue +51 -0
- package/dist/runtime/app/components/FormBuilderInsertRow.vue.d.ts +20 -0
- package/dist/runtime/app/components/FormBuilderOptions.d.vue.ts +11 -0
- package/dist/runtime/app/components/FormBuilderOptions.vue +81 -0
- package/dist/runtime/app/components/FormBuilderOptions.vue.d.ts +11 -0
- package/dist/runtime/app/components/FormBuilderPhoneSettings.d.vue.ts +11 -0
- package/dist/runtime/app/components/FormBuilderPhoneSettings.vue +63 -0
- package/dist/runtime/app/components/FormBuilderPhoneSettings.vue.d.ts +11 -0
- package/dist/runtime/app/components/FormBuilderSettings.d.vue.ts +11 -0
- package/dist/runtime/app/components/FormBuilderSettings.vue +233 -0
- package/dist/runtime/app/components/FormBuilderSettings.vue.d.ts +11 -0
- package/dist/runtime/app/components/FormBuilderStepHeader.d.vue.ts +27 -0
- package/dist/runtime/app/components/FormBuilderStepHeader.vue +71 -0
- package/dist/runtime/app/components/FormBuilderStepHeader.vue.d.ts +27 -0
- package/dist/runtime/app/components/FormBuilderStepSettings.d.vue.ts +13 -0
- package/dist/runtime/app/components/FormBuilderStepSettings.vue +33 -0
- package/dist/runtime/app/components/FormBuilderStepSettings.vue.d.ts +13 -0
- package/dist/runtime/app/components/FormBuilderText.d.vue.ts +19 -0
- package/dist/runtime/app/components/FormBuilderText.vue +127 -0
- package/dist/runtime/app/components/FormBuilderText.vue.d.ts +19 -0
- package/dist/runtime/app/components/FormCaptcha.d.vue.ts +19 -0
- package/dist/runtime/app/components/FormCaptcha.vue +95 -0
- package/dist/runtime/app/components/FormCaptcha.vue.d.ts +19 -0
- package/dist/runtime/app/components/FormEmbed.d.vue.ts +17 -0
- package/dist/runtime/app/components/FormEmbed.vue +113 -0
- package/dist/runtime/app/components/FormEmbed.vue.d.ts +17 -0
- package/dist/runtime/app/components/FormField.d.vue.ts +4 -0
- package/dist/runtime/app/components/FormField.vue +35 -3
- package/dist/runtime/app/components/FormField.vue.d.ts +4 -0
- package/dist/runtime/app/components/FormPhoneInput.d.vue.ts +20 -0
- package/dist/runtime/app/components/FormPhoneInput.vue +56 -0
- package/dist/runtime/app/components/FormPhoneInput.vue.d.ts +20 -0
- package/dist/runtime/app/components/FormPreview.d.vue.ts +12 -0
- package/dist/runtime/app/components/FormPreview.vue +244 -0
- package/dist/runtime/app/components/FormPreview.vue.d.ts +12 -0
- package/dist/runtime/app/components/FormRenderer.d.vue.ts +6 -0
- package/dist/runtime/app/components/FormRenderer.vue +16 -7
- package/dist/runtime/app/components/FormRenderer.vue.d.ts +6 -0
- package/dist/runtime/app/components/FormSubmissionView.vue +2 -2
- package/dist/runtime/app/composables/index.d.ts +4 -1
- package/dist/runtime/app/composables/index.js +4 -1
- package/dist/runtime/app/composables/useCaptcha.d.ts +22 -0
- package/dist/runtime/app/composables/useCaptcha.js +39 -0
- package/dist/runtime/app/composables/useCaptchaSecrets.d.ts +7 -0
- package/dist/runtime/app/composables/useCaptchaSecrets.js +69 -0
- package/dist/runtime/app/composables/useFormBuilderEditor.d.ts +49 -0
- package/dist/runtime/app/composables/useFormBuilderEditor.js +210 -0
- package/dist/runtime/app/composables/useFormBuilderLabels.d.ts +8 -0
- package/dist/runtime/app/composables/useFormBuilderLabels.js +41 -0
- package/dist/runtime/app/composables/useForms.d.ts +18 -3
- package/dist/runtime/app/composables/useForms.js +38 -5
- package/dist/runtime/app/pages/admin-form.vue +112 -69
- package/dist/runtime/app/pages/admin-submission.vue +7 -16
- package/dist/runtime/app/pages/admin-submissions.vue +41 -46
- package/dist/runtime/app/pages/admin.vue +128 -44
- package/dist/runtime/app/pages/form.vue +71 -5
- package/dist/runtime/i18n/lang/da.json +231 -3
- package/dist/runtime/i18n/lang/en.json +231 -3
- package/dist/runtime/menus.js +10 -0
- package/dist/runtime/server/db/schema.sqlite.d.ts +283 -0
- package/dist/runtime/server/db/schema.sqlite.js +33 -1
- package/dist/runtime/server/mcp/schemas.d.ts +9 -0
- package/dist/runtime/server/mcp/schemas.js +8 -0
- package/dist/runtime/server/mcp/tools/admin/create-form.d.ts +2 -0
- package/dist/runtime/server/mcp/tools/admin/create-form.js +21 -0
- package/dist/runtime/server/mcp/tools/admin/get-form-definition.d.ts +2 -0
- package/dist/runtime/server/mcp/tools/admin/get-form-definition.js +14 -0
- package/dist/runtime/server/mcp/tools/admin/get-submission.d.ts +2 -0
- package/dist/runtime/server/mcp/tools/admin/get-submission.js +26 -0
- package/dist/runtime/server/mcp/tools/admin/list-all-forms.d.ts +2 -0
- package/dist/runtime/server/mcp/tools/admin/list-all-forms.js +21 -0
- package/dist/runtime/server/mcp/tools/admin/list-submissions.d.ts +2 -0
- package/dist/runtime/server/mcp/tools/admin/list-submissions.js +22 -0
- package/dist/runtime/server/mcp/tools/admin/update-form.d.ts +2 -0
- package/dist/runtime/server/mcp/tools/admin/update-form.js +24 -0
- package/dist/runtime/server/mcp/tools/get-form.d.ts +2 -0
- package/dist/runtime/server/mcp/tools/get-form.js +12 -0
- package/dist/runtime/server/mcp/tools/list-forms.d.ts +2 -0
- package/dist/runtime/server/mcp/tools/list-forms.js +11 -0
- package/dist/runtime/server/mcp/tools/submit-form.d.ts +2 -0
- package/dist/runtime/server/mcp/tools/submit-form.js +17 -0
- package/dist/runtime/server/plugins/email-delivery.js +16 -11
- package/dist/runtime/server/plugins/encryption.d.ts +2 -0
- package/dist/runtime/server/plugins/encryption.js +4 -0
- package/dist/runtime/server/plugins/staff-scopes.d.ts +2 -0
- package/dist/runtime/server/plugins/staff-scopes.js +1 -0
- package/dist/runtime/server/routes/admin/form-clone.post.d.ts +2 -0
- package/dist/runtime/server/routes/admin/form-clone.post.js +17 -0
- package/dist/runtime/server/routes/admin/form-password.delete.d.ts +2 -0
- package/dist/runtime/server/routes/admin/form-password.delete.js +8 -0
- package/dist/runtime/server/routes/admin/form-password.get.d.ts +2 -0
- package/dist/runtime/server/routes/admin/form-password.get.js +8 -0
- package/dist/runtime/server/routes/admin/form-password.put.d.ts +2 -0
- package/dist/runtime/server/routes/admin/form-password.put.js +14 -0
- package/dist/runtime/server/routes/admin/form-secret.delete.d.ts +2 -0
- package/dist/runtime/server/routes/admin/form-secret.delete.js +8 -0
- package/dist/runtime/server/routes/admin/form-secrets.get.d.ts +2 -0
- package/dist/runtime/server/routes/admin/form-secrets.get.js +5 -0
- package/dist/runtime/server/routes/admin/form-secrets.put.d.ts +2 -0
- package/dist/runtime/server/routes/admin/form-secrets.put.js +12 -0
- package/dist/runtime/server/routes/admin/form-token.delete.d.ts +2 -0
- package/dist/runtime/server/routes/admin/form-token.delete.js +12 -0
- package/dist/runtime/server/routes/admin/form-tokens.get.d.ts +2 -0
- package/dist/runtime/server/routes/admin/form-tokens.get.js +8 -0
- package/dist/runtime/server/routes/admin/form-tokens.post.d.ts +2 -0
- package/dist/runtime/server/routes/admin/form-tokens.post.js +12 -0
- package/dist/runtime/server/routes/admin/form.get.js +5 -1
- package/dist/runtime/server/routes/admin/form.patch.js +3 -12
- package/dist/runtime/server/routes/admin/forms.post.js +1 -6
- package/dist/runtime/server/routes/public/access.get.d.ts +2 -0
- package/dist/runtime/server/routes/public/access.get.js +3 -0
- package/dist/runtime/server/routes/public/form.get.js +3 -1
- package/dist/runtime/server/routes/public/submit.post.js +2 -2
- package/dist/runtime/server/utils/access.d.ts +27 -0
- package/dist/runtime/server/utils/access.js +63 -0
- package/dist/runtime/server/utils/admin-schemas.d.ts +219 -15
- package/dist/runtime/server/utils/auth.d.ts +7 -7
- package/dist/runtime/server/utils/auth.js +4 -4
- package/dist/runtime/server/utils/captcha.d.ts +10 -0
- package/dist/runtime/server/utils/captcha.js +25 -0
- package/dist/runtime/server/utils/crypto.d.ts +18 -0
- package/dist/runtime/server/utils/crypto.js +48 -0
- package/dist/runtime/server/utils/forms.d.ts +6 -1
- package/dist/runtime/server/utils/forms.js +28 -0
- package/dist/runtime/server/utils/mcp-access.d.ts +9 -0
- package/dist/runtime/server/utils/mcp-access.js +8 -0
- package/dist/runtime/server/utils/rate-limit.d.ts +3 -0
- package/dist/runtime/server/utils/rate-limit.js +19 -0
- package/dist/runtime/server/utils/render.d.ts +2 -11
- package/dist/runtime/server/utils/render.js +1 -26
- package/dist/runtime/server/utils/secrets.d.ts +28 -0
- package/dist/runtime/server/utils/secrets.js +60 -0
- package/dist/runtime/server/utils/submit.d.ts +12 -1
- package/dist/runtime/server/utils/submit.js +18 -4
- package/dist/runtime/utils/builder.d.ts +73 -0
- package/dist/runtime/utils/builder.js +220 -0
- package/dist/runtime/utils/captcha.d.ts +9 -0
- package/dist/runtime/utils/captcha.js +7 -0
- package/dist/runtime/utils/conditions.d.ts +3 -1
- package/dist/runtime/utils/conditions.js +4 -0
- package/dist/runtime/utils/definition.d.ts +116 -6
- package/dist/runtime/utils/definition.js +53 -4
- package/dist/runtime/utils/embed.d.ts +8 -0
- package/dist/runtime/utils/embed.js +7 -0
- package/dist/runtime/utils/index.d.ts +16 -3
- package/dist/runtime/utils/index.js +66 -3
- package/dist/runtime/utils/markdown.d.ts +1 -5
- package/dist/runtime/utils/markdown.js +1 -5
- package/dist/runtime/utils/phone.d.ts +29 -0
- package/dist/runtime/utils/phone.js +294 -0
- package/dist/runtime/utils/preview.d.ts +12 -0
- package/dist/runtime/utils/preview.js +17 -0
- package/dist/runtime/utils/schedule.d.ts +1 -0
- package/dist/runtime/utils/schedule.js +1 -0
- package/dist/runtime/utils/schema.d.ts +2 -0
- package/dist/runtime/utils/schema.js +12 -4
- package/dist/runtime/utils/template.d.ts +57 -0
- package/dist/runtime/utils/template.js +147 -0
- package/dist/runtime/utils/text.d.ts +0 -3
- package/dist/runtime/utils/text.js +0 -5
- package/dist/types.d.mts +5 -1
- package/package.json +13 -5
|
@@ -1,26 +1 @@
|
|
|
1
|
-
|
|
2
|
-
const optionLabel = (field, value, locale) => translate(field.options?.find((option) => option.value === value)?.label, locale) || value;
|
|
3
|
-
function formatValue(field, value, locale) {
|
|
4
|
-
if (value === void 0 || value === null || value === "") return "";
|
|
5
|
-
if (field.type === "checkbox") return value === true ? "Yes" : "No";
|
|
6
|
-
if (field.type === "file") return Array.isArray(value) ? value.map((file) => file.name).join(", ") : "";
|
|
7
|
-
if ((field.type === "select" || field.type === "radio") && typeof value === "string") return optionLabel(field, value, locale);
|
|
8
|
-
return String(value);
|
|
9
|
-
}
|
|
10
|
-
export function renderAnswers(definition, answers, locale) {
|
|
11
|
-
return resolveVisibility(definition, answers).flatMap(({ fields }) => fields).filter(isInputField).map((field) => ({
|
|
12
|
-
key: field.key,
|
|
13
|
-
label: translate(field.label, locale) || field.key,
|
|
14
|
-
value: formatValue(field, answers[field.key], locale)
|
|
15
|
-
}));
|
|
16
|
-
}
|
|
17
|
-
export const escapeHtml = (value) => value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
18
|
-
export function answersToText(rows) {
|
|
19
|
-
return rows.map((row) => `${row.label}: ${row.value || "-"}`).join("\n");
|
|
20
|
-
}
|
|
21
|
-
export function answersToHtml(rows) {
|
|
22
|
-
const body = rows.map(
|
|
23
|
-
(row) => `<tr><td style="padding:4px 12px 4px 0;vertical-align:top;font-weight:600">${escapeHtml(row.label)}</td><td style="padding:4px 0;white-space:pre-wrap">${escapeHtml(row.value) || "-"}</td></tr>`
|
|
24
|
-
).join("");
|
|
25
|
-
return `<table style="border-collapse:collapse;font-family:sans-serif;font-size:14px">${body}</table>`;
|
|
26
|
-
}
|
|
1
|
+
export { renderAnswers, escapeHtml, answersToText, answersToHtml } from "../../utils/template.js";
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { H3Event } from 'h3';
|
|
2
|
+
import type { CaptchaProvider, FormSecretsDto, SecretSource } from '../../../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Per-form secrets live in `form_secrets`, encrypted (AES-256-GCM). Values are write-only: nothing here
|
|
5
|
+
* is returned by an API, logged, or copied into definitions, snapshots, deliveries or exports.
|
|
6
|
+
*/
|
|
7
|
+
export declare const captchaSecretKey: (provider: CaptchaProvider) => string;
|
|
8
|
+
/** Secret names that may be stored. Future integrations (webhook signing secrets, ...) add theirs here. */
|
|
9
|
+
export declare const formSecretKeys: readonly string[];
|
|
10
|
+
export declare const isFormSecretKey: (key: string) => boolean;
|
|
11
|
+
/** The decrypted secrets of a form; `unreadable` when they were encrypted with another key. */
|
|
12
|
+
export declare function readFormSecrets(formId: string): Promise<{
|
|
13
|
+
secrets: Record<string, string>;
|
|
14
|
+
unreadable: boolean;
|
|
15
|
+
}>;
|
|
16
|
+
export declare function getFormSecret(formId: string, key: string): Promise<string | undefined>;
|
|
17
|
+
/** Stores one secret, replacing an existing value; unreadable old secrets are discarded. */
|
|
18
|
+
export declare function setFormSecret(formId: string, key: string, value: string): Promise<void>;
|
|
19
|
+
export declare function deleteFormSecret(formId: string, key: string): Promise<void>;
|
|
20
|
+
/** Secret key for a provider from the environment / module options (`NUXT_FORMS_CAPTCHA_<PROVIDER>_SECRET_KEY`). */
|
|
21
|
+
export declare function envCaptchaSecret(event: H3Event | undefined, provider: CaptchaProvider): string | undefined;
|
|
22
|
+
/** The secret key a captcha check of this form uses: the form's own secret, else the environment one. */
|
|
23
|
+
export declare function resolveCaptchaSecret(event: H3Event | undefined, provider: CaptchaProvider, formId?: string): Promise<{
|
|
24
|
+
secretKey?: string;
|
|
25
|
+
source: SecretSource;
|
|
26
|
+
}>;
|
|
27
|
+
/** What an admin may know: which secrets are set (never their values) and where each captcha key comes from. */
|
|
28
|
+
export declare function describeFormSecrets(event: H3Event, formId: string): Promise<FormSecretsDto>;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { eq } from "drizzle-orm";
|
|
2
|
+
import { db, schema } from "@nuxthub/db";
|
|
3
|
+
import { captchaProviders } from "../../utils/index.js";
|
|
4
|
+
export const captchaSecretKey = (provider) => `captcha.${provider}.secretKey`;
|
|
5
|
+
export const formSecretKeys = captchaProviders.map(captchaSecretKey);
|
|
6
|
+
export const isFormSecretKey = (key) => formSecretKeys.includes(key);
|
|
7
|
+
export async function readFormSecrets(formId) {
|
|
8
|
+
try {
|
|
9
|
+
const [row] = await db.select({ secrets: schema.formSecrets.secrets }).from(schema.formSecrets).where(eq(schema.formSecrets.formId, formId)).limit(1);
|
|
10
|
+
return { secrets: row?.secrets ?? {}, unreadable: false };
|
|
11
|
+
} catch (error) {
|
|
12
|
+
if (!isDecryptionError(error)) throw error;
|
|
13
|
+
console.error(`[forms] the stored secrets of form ${formId} cannot be decrypted (changed encryption key?); they must be entered again.`);
|
|
14
|
+
return { secrets: {}, unreadable: true };
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export async function getFormSecret(formId, key) {
|
|
18
|
+
return (await readFormSecrets(formId)).secrets[key] || void 0;
|
|
19
|
+
}
|
|
20
|
+
async function writeFormSecrets(formId, secrets) {
|
|
21
|
+
if (!Object.keys(secrets).length) {
|
|
22
|
+
await db.delete(schema.formSecrets).where(eq(schema.formSecrets.formId, formId));
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
await db.insert(schema.formSecrets).values({ formId, secrets }).onConflictDoUpdate({
|
|
26
|
+
target: schema.formSecrets.formId,
|
|
27
|
+
set: { secrets, updatedAt: /* @__PURE__ */ new Date() }
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
export async function setFormSecret(formId, key, value) {
|
|
31
|
+
const { secrets } = await readFormSecrets(formId);
|
|
32
|
+
await writeFormSecrets(formId, { ...secrets, [key]: value });
|
|
33
|
+
}
|
|
34
|
+
export async function deleteFormSecret(formId, key) {
|
|
35
|
+
const { secrets, unreadable } = await readFormSecrets(formId);
|
|
36
|
+
if (unreadable) {
|
|
37
|
+
await writeFormSecrets(formId, {});
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (!(key in secrets)) return;
|
|
41
|
+
const { [key]: _removed, ...rest } = secrets;
|
|
42
|
+
await writeFormSecrets(formId, rest);
|
|
43
|
+
}
|
|
44
|
+
export function envCaptchaSecret(event, provider) {
|
|
45
|
+
return useRuntimeConfig(event).forms.captcha?.[provider]?.secretKey || void 0;
|
|
46
|
+
}
|
|
47
|
+
export async function resolveCaptchaSecret(event, provider, formId) {
|
|
48
|
+
const own = formId ? await getFormSecret(formId, captchaSecretKey(provider)) : void 0;
|
|
49
|
+
if (own) return { secretKey: own, source: "form" };
|
|
50
|
+
const env = envCaptchaSecret(event, provider);
|
|
51
|
+
return env ? { secretKey: env, source: "env" } : { source: "unset" };
|
|
52
|
+
}
|
|
53
|
+
export async function describeFormSecrets(event, formId) {
|
|
54
|
+
const { secrets, unreadable } = await readFormSecrets(formId);
|
|
55
|
+
const captcha = Object.fromEntries(captchaProviders.map((provider) => [
|
|
56
|
+
provider,
|
|
57
|
+
{ source: secrets[captchaSecretKey(provider)] ? "form" : envCaptchaSecret(event, provider) ? "env" : "unset" }
|
|
58
|
+
]));
|
|
59
|
+
return { keys: Object.keys(secrets).filter((key) => secrets[key]).sort(), captcha, unreadable };
|
|
60
|
+
}
|
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
import type { H3Event } from 'h3';
|
|
2
2
|
import { schema } from '@nuxthub/db';
|
|
3
|
+
type Upload = {
|
|
4
|
+
filename: string;
|
|
5
|
+
type: string;
|
|
6
|
+
data: Uint8Array;
|
|
7
|
+
};
|
|
3
8
|
type FormRow = typeof schema.forms.$inferSelect;
|
|
9
|
+
/** A submission body that is already parsed (MCP tools pass answers as arguments instead of a request body). */
|
|
10
|
+
export type SubmissionBody = {
|
|
11
|
+
answers: Record<string, unknown>;
|
|
12
|
+
locale: string;
|
|
13
|
+
uploads: Map<string, Upload[]>;
|
|
14
|
+
};
|
|
4
15
|
/** Validates and processes one submission for a published form. */
|
|
5
|
-
export declare function processSubmission(event: H3Event, form: FormRow, userId: number | null): Promise<{
|
|
16
|
+
export declare function processSubmission(event: H3Event, form: FormRow, userId: number | null, body?: SubmissionBody): Promise<{
|
|
6
17
|
ok: true;
|
|
7
18
|
id: string | null;
|
|
8
19
|
}>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { eq } from "drizzle-orm";
|
|
2
2
|
import { db, schema } from "@nuxthub/db";
|
|
3
3
|
import { blob } from "@nuxthub/blob";
|
|
4
|
-
import { buildFormSchema, newId, validatedFields } from "../../utils/index.js";
|
|
4
|
+
import { buildFormSchema, newId, resolveAccess, resolveCaptcha, validatedFields } from "../../utils/index.js";
|
|
5
5
|
const safeFilename = (name) => name.replace(/[^\w.-]+/g, "_").replace(/^\.+/, "").slice(0, 120) || "file";
|
|
6
6
|
async function readSubmission(event) {
|
|
7
7
|
const uploads = /* @__PURE__ */ new Map();
|
|
@@ -29,16 +29,17 @@ async function readSubmission(event) {
|
|
|
29
29
|
const body = await readBody(event);
|
|
30
30
|
return { answers: body?.answers ?? {}, locale: String(body?.locale ?? "en").slice(0, 20), uploads };
|
|
31
31
|
}
|
|
32
|
-
export async function processSubmission(event, form, userId) {
|
|
32
|
+
export async function processSubmission(event, form, userId, body) {
|
|
33
33
|
const definition = form.definition;
|
|
34
|
-
const { answers: raw, locale, uploads } = await readSubmission(event);
|
|
34
|
+
const { answers: raw, locale, uploads } = body ?? await readSubmission(event);
|
|
35
35
|
const answers = { ...raw };
|
|
36
36
|
for (const field of validatedFields(definition, raw)) {
|
|
37
37
|
if (field.type !== "file") continue;
|
|
38
38
|
const files2 = uploads.get(field.key) ?? [];
|
|
39
39
|
answers[field.key] = files2.map((file) => ({ name: file.filename, size: file.data.byteLength, type: file.type }));
|
|
40
40
|
}
|
|
41
|
-
const
|
|
41
|
+
const skipCaptcha = resolveAccess(definition).auth === "token";
|
|
42
|
+
const parsed = buildFormSchema(definition, answers, { locale, skipCaptcha }).safeParse(answers);
|
|
42
43
|
if (!parsed.success) {
|
|
43
44
|
throw createError({
|
|
44
45
|
statusCode: 422,
|
|
@@ -47,6 +48,19 @@ export async function processSubmission(event, form, userId) {
|
|
|
47
48
|
});
|
|
48
49
|
}
|
|
49
50
|
const validated = parsed.data;
|
|
51
|
+
for (const field of validatedFields(definition, raw).filter((f) => f.type === "captcha")) {
|
|
52
|
+
const token = validated[field.key];
|
|
53
|
+
Reflect.deleteProperty(validated, field.key);
|
|
54
|
+
Reflect.deleteProperty(answers, field.key);
|
|
55
|
+
if (skipCaptcha) continue;
|
|
56
|
+
if (!await verifyCaptcha(event, resolveCaptcha(field, useRuntimeConfig(event).public.forms?.captcha?.defaultProvider).provider, String(token), form.id)) {
|
|
57
|
+
throw createError({
|
|
58
|
+
statusCode: 422,
|
|
59
|
+
statusMessage: "Invalid submission.",
|
|
60
|
+
data: { issues: [{ path: [field.key], message: "Captcha verification failed. Please try again." }] }
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
50
64
|
const stored = definition.storage !== "none";
|
|
51
65
|
const submissionId = stored ? newId("fsb") : null;
|
|
52
66
|
const files = [];
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { FieldType, FormAccess, FormDefinition, FormDelivery, FormStorage, FormField, FormStep } from '../../types.js';
|
|
2
|
+
/** Where a field sits in a definition. */
|
|
3
|
+
export type FieldPosition = {
|
|
4
|
+
step: number;
|
|
5
|
+
index: number;
|
|
6
|
+
};
|
|
7
|
+
/** Field types in the order the builder palette lists them. */
|
|
8
|
+
export declare const fieldTypes: FieldType[];
|
|
9
|
+
/** Moves an item inside an array (returns a new array). `to` is the index in the resulting array. */
|
|
10
|
+
export declare function arrayMove<T>(items: readonly T[], from: number, to: number): T[];
|
|
11
|
+
/**
|
|
12
|
+
* Moves a field to another position, possibly into another step. `to.index` is the index in the
|
|
13
|
+
* target step *after* the field was taken out of its old place, which is what a drop indicator
|
|
14
|
+
* "between two rows" naturally yields once adjusted by `dropIndex`.
|
|
15
|
+
*/
|
|
16
|
+
export declare function moveField(definition: FormDefinition, from: FieldPosition, to: FieldPosition): FormDefinition;
|
|
17
|
+
/**
|
|
18
|
+
* The insertion index to hand to `moveField` for a drop "before the row at `slot`" (a gap number
|
|
19
|
+
* counted in the target step *before* removal): dragging downwards inside the same step shifts
|
|
20
|
+
* everything above the slot up by one.
|
|
21
|
+
*/
|
|
22
|
+
export declare function dropIndex(from: FieldPosition | undefined, to: FieldPosition): number;
|
|
23
|
+
export declare function moveStep(definition: FormDefinition, from: number, to: number): FormDefinition;
|
|
24
|
+
export declare function insertField(definition: FormDefinition, at: FieldPosition, field: FormField): FormDefinition;
|
|
25
|
+
export declare function removeField(definition: FormDefinition, at: FieldPosition): FormDefinition;
|
|
26
|
+
export declare function updateField(definition: FormDefinition, at: FieldPosition, patch: Partial<FormField>): FormDefinition;
|
|
27
|
+
export declare function updateStep(definition: FormDefinition, index: number, patch: Partial<FormStep>): FormDefinition;
|
|
28
|
+
/** Every field key of the definition (markdown blocks included). */
|
|
29
|
+
export declare function allFieldKeys(definition: FormDefinition): Set<string>;
|
|
30
|
+
/** A field key that is valid and not used yet, e.g. `text`, `text_2`, `text_3`. */
|
|
31
|
+
export declare function uniqueFieldKey(definition: FormDefinition, base: string): string;
|
|
32
|
+
export declare function uniqueStepId(definition: FormDefinition, base?: string): string;
|
|
33
|
+
/** A new field of the given type with sensible defaults. */
|
|
34
|
+
export declare function createField(definition: FormDefinition, type: FieldType): FormField;
|
|
35
|
+
export declare function createStep(definition: FormDefinition): FormStep;
|
|
36
|
+
/** Inserts a step before `index` (the end when out of range). */
|
|
37
|
+
export declare function insertStep(definition: FormDefinition, index: number, step: FormStep): FormDefinition;
|
|
38
|
+
export declare function duplicateField(definition: FormDefinition, at: FieldPosition): FormDefinition;
|
|
39
|
+
/**
|
|
40
|
+
* Changes the key of a field and rewrites everything that points at the old key (`showIf` of fields and steps,
|
|
41
|
+
* the email route field, the reply-to field and `allowResubmit.keep`), so renaming never leaves a dangling reference.
|
|
42
|
+
*/
|
|
43
|
+
export declare function renameFieldKey(definition: FormDefinition, at: FieldPosition, key: string): FormDefinition;
|
|
44
|
+
/** Removes `showIf`s, routes, reply-to and `keep` entries that point at a field that no longer exists. */
|
|
45
|
+
export declare function pruneReferences(definition: FormDefinition): FormDefinition;
|
|
46
|
+
/** Switching to `single` merges every step into one; switching to `stepped` keeps the steps as they are. */
|
|
47
|
+
export declare function changeMode(definition: FormDefinition, mode: FormDefinition['mode']): FormDefinition;
|
|
48
|
+
/** Deletes a step; its fields move to the previous step (or the next one) so no answer field is lost silently. */
|
|
49
|
+
export declare function removeStep(definition: FormDefinition, index: number, { keepFields }?: {
|
|
50
|
+
keepFields?: boolean | undefined;
|
|
51
|
+
}): FormDefinition;
|
|
52
|
+
type EmailDelivery = NonNullable<FormDelivery['email']>;
|
|
53
|
+
/** Sets the default recipients. An empty list switches email delivery off and drops its subject, body, reply-to and routes. */
|
|
54
|
+
export declare function setEmailRecipients(definition: FormDefinition, to: readonly string[]): FormDefinition;
|
|
55
|
+
/** Patches subject / reply-to of an existing email delivery (no-op without recipients); `undefined` values are removed. */
|
|
56
|
+
export declare function patchEmailDelivery(definition: FormDefinition, patch: Partial<Pick<EmailDelivery, 'subject' | 'body' | 'replyToField'>>): FormDefinition;
|
|
57
|
+
/** Chooses the field whose answer routes the email; `undefined` removes all routes. Switching the field starts with no routes. */
|
|
58
|
+
export declare function setRouteField(definition: FormDefinition, field: string | undefined): FormDefinition;
|
|
59
|
+
/** Sets the recipients for one answer value of the route field; an empty list removes that route. */
|
|
60
|
+
export declare function setRoute(definition: FormDefinition, value: string, to: readonly string[]): FormDefinition;
|
|
61
|
+
export declare function setStorage(definition: FormDefinition, storage: FormStorage): FormDefinition;
|
|
62
|
+
/** Sets who may use the form. Token access forces API-only (a token cannot be typed into a page). */
|
|
63
|
+
export declare function setAccess(definition: FormDefinition, patch: Partial<FormAccess>): FormDefinition;
|
|
64
|
+
/** Problems of a storage choice: file fields cannot work without storage, and a form that neither stores nor emails loses its answers. */
|
|
65
|
+
export declare function storageWarnings(definition: FormDefinition): {
|
|
66
|
+
files: boolean;
|
|
67
|
+
lost: boolean;
|
|
68
|
+
};
|
|
69
|
+
/** Email-type fields that can serve as the reply-to address. */
|
|
70
|
+
export declare function emailFieldKeys(definition: FormDefinition): string[];
|
|
71
|
+
/** Select / radio fields whose answer can route the email. */
|
|
72
|
+
export declare function routableFields(definition: FormDefinition): FormField[];
|
|
73
|
+
export {};
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
export const fieldTypes = ["text", "textarea", "email", "tel", "number", "date", "select", "radio", "checkbox", "file", "markdown", "captcha"];
|
|
2
|
+
const clamp = (value, max) => Math.max(0, Math.min(value, max));
|
|
3
|
+
export function arrayMove(items, from, to) {
|
|
4
|
+
const next = [...items];
|
|
5
|
+
if (from < 0 || from >= next.length) return next;
|
|
6
|
+
const [item] = next.splice(from, 1);
|
|
7
|
+
next.splice(clamp(to, next.length), 0, item);
|
|
8
|
+
return next;
|
|
9
|
+
}
|
|
10
|
+
export function moveField(definition, from, to) {
|
|
11
|
+
const source = definition.steps[from.step]?.fields[from.index];
|
|
12
|
+
if (!source || !definition.steps[to.step]) return definition;
|
|
13
|
+
const steps = definition.steps.map((step) => ({ ...step, fields: [...step.fields] }));
|
|
14
|
+
steps[from.step].fields.splice(from.index, 1);
|
|
15
|
+
steps[to.step].fields.splice(clamp(to.index, steps[to.step].fields.length), 0, source);
|
|
16
|
+
return { ...definition, steps };
|
|
17
|
+
}
|
|
18
|
+
export function dropIndex(from, to) {
|
|
19
|
+
return from && from.step === to.step && from.index < to.index ? to.index - 1 : to.index;
|
|
20
|
+
}
|
|
21
|
+
export function moveStep(definition, from, to) {
|
|
22
|
+
return { ...definition, steps: arrayMove(definition.steps, from, to) };
|
|
23
|
+
}
|
|
24
|
+
export function insertField(definition, at, field) {
|
|
25
|
+
if (!definition.steps[at.step]) return definition;
|
|
26
|
+
const steps = definition.steps.map((step) => ({ ...step, fields: [...step.fields] }));
|
|
27
|
+
steps[at.step].fields.splice(clamp(at.index, steps[at.step].fields.length), 0, field);
|
|
28
|
+
return { ...definition, steps };
|
|
29
|
+
}
|
|
30
|
+
export function removeField(definition, at) {
|
|
31
|
+
if (!definition.steps[at.step]?.fields[at.index]) return definition;
|
|
32
|
+
const steps = definition.steps.map((step) => ({ ...step, fields: [...step.fields] }));
|
|
33
|
+
steps[at.step].fields.splice(at.index, 1);
|
|
34
|
+
return pruneReferences({ ...definition, steps });
|
|
35
|
+
}
|
|
36
|
+
export function updateField(definition, at, patch) {
|
|
37
|
+
if (!definition.steps[at.step]?.fields[at.index]) return definition;
|
|
38
|
+
const steps = definition.steps.map((step, stepIndex) => stepIndex === at.step ? { ...step, fields: step.fields.map((field, index) => index === at.index ? { ...field, ...patch } : field) } : step);
|
|
39
|
+
return { ...definition, steps };
|
|
40
|
+
}
|
|
41
|
+
export function updateStep(definition, index, patch) {
|
|
42
|
+
if (!definition.steps[index]) return definition;
|
|
43
|
+
return { ...definition, steps: definition.steps.map((step, i) => i === index ? { ...step, ...patch } : step) };
|
|
44
|
+
}
|
|
45
|
+
export function allFieldKeys(definition) {
|
|
46
|
+
return new Set(definition.steps.flatMap((step) => step.fields.map((field) => field.key)));
|
|
47
|
+
}
|
|
48
|
+
function uniqueName(taken, base, separator) {
|
|
49
|
+
if (!taken.has(base)) return base;
|
|
50
|
+
let n = 2;
|
|
51
|
+
while (taken.has(`${base}${separator}${n}`)) n++;
|
|
52
|
+
return `${base}${separator}${n}`;
|
|
53
|
+
}
|
|
54
|
+
export function uniqueFieldKey(definition, base) {
|
|
55
|
+
const clean = base.replace(/\W+/g, "_").replace(/^[^a-z]+/i, "").replace(/_+$/, "") || "field";
|
|
56
|
+
return uniqueName(allFieldKeys(definition), clean, "_");
|
|
57
|
+
}
|
|
58
|
+
export function uniqueStepId(definition, base = "step") {
|
|
59
|
+
return uniqueName(new Set(definition.steps.map((step) => step.id)), base, "-");
|
|
60
|
+
}
|
|
61
|
+
export function createField(definition, type) {
|
|
62
|
+
const field = { key: uniqueFieldKey(definition, type), type };
|
|
63
|
+
if (type === "markdown") field.content = "Text";
|
|
64
|
+
else if (type !== "captcha") field.label = "Untitled";
|
|
65
|
+
if (type === "select" || type === "radio") {
|
|
66
|
+
field.options = [{ value: "option_1", label: "Option 1" }, { value: "option_2", label: "Option 2" }];
|
|
67
|
+
}
|
|
68
|
+
if (type === "email") field.autocomplete = "email";
|
|
69
|
+
if (type === "tel") field.autocomplete = "tel";
|
|
70
|
+
if (type === "captcha") field.captcha = { provider: "turnstile" };
|
|
71
|
+
return field;
|
|
72
|
+
}
|
|
73
|
+
export function createStep(definition) {
|
|
74
|
+
const id = uniqueStepId(definition);
|
|
75
|
+
return { id, title: `Step ${definition.steps.length + 1}`, fields: [] };
|
|
76
|
+
}
|
|
77
|
+
export function insertStep(definition, index, step) {
|
|
78
|
+
const steps = [...definition.steps];
|
|
79
|
+
steps.splice(clamp(index, steps.length), 0, step);
|
|
80
|
+
return { ...definition, steps };
|
|
81
|
+
}
|
|
82
|
+
export function duplicateField(definition, at) {
|
|
83
|
+
const field = definition.steps[at.step]?.fields[at.index];
|
|
84
|
+
if (!field) return definition;
|
|
85
|
+
const copy = structuredClone(field);
|
|
86
|
+
copy.key = uniqueFieldKey(definition, field.key);
|
|
87
|
+
delete copy.showIf;
|
|
88
|
+
return insertField(definition, { step: at.step, index: at.index + 1 }, copy);
|
|
89
|
+
}
|
|
90
|
+
export function renameFieldKey(definition, at, key) {
|
|
91
|
+
const current = definition.steps[at.step]?.fields[at.index];
|
|
92
|
+
if (!current || current.key === key) return definition;
|
|
93
|
+
const from = current.key;
|
|
94
|
+
const retarget = (item) => item.showIf?.field === from ? { ...item, showIf: { ...item.showIf, field: key } } : item;
|
|
95
|
+
const steps = definition.steps.map((step, stepIndex) => ({
|
|
96
|
+
...retarget(step),
|
|
97
|
+
fields: step.fields.map((field, index) => stepIndex === at.step && index === at.index ? { ...retarget(field), key } : retarget(field))
|
|
98
|
+
}));
|
|
99
|
+
const next = { ...definition, steps };
|
|
100
|
+
const email = definition.delivery?.email;
|
|
101
|
+
if (email) {
|
|
102
|
+
next.delivery = {
|
|
103
|
+
...definition.delivery,
|
|
104
|
+
email: {
|
|
105
|
+
...email,
|
|
106
|
+
...email.routes?.field === from ? { routes: { ...email.routes, field: key } } : {},
|
|
107
|
+
...email.replyToField === from ? { replyToField: key } : {}
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
const resubmit = definition.submit?.allowResubmit;
|
|
112
|
+
if (resubmit?.keep.includes(from)) {
|
|
113
|
+
next.submit = { ...definition.submit, allowResubmit: { keep: resubmit.keep.map((entry) => entry === from ? key : entry) } };
|
|
114
|
+
}
|
|
115
|
+
return next;
|
|
116
|
+
}
|
|
117
|
+
export function pruneReferences(definition) {
|
|
118
|
+
const keys = allFieldKeys(definition);
|
|
119
|
+
const seen = /* @__PURE__ */ new Set();
|
|
120
|
+
const ok = (condition) => !condition || seen.has(condition.field);
|
|
121
|
+
const steps = definition.steps.map((step) => {
|
|
122
|
+
const stepOk = ok(step.showIf);
|
|
123
|
+
const fields = step.fields.map((field) => {
|
|
124
|
+
const keep = ok(field.showIf);
|
|
125
|
+
if (field.type !== "markdown" && field.type !== "captcha") seen.add(field.key);
|
|
126
|
+
if (keep) return field;
|
|
127
|
+
const { showIf: _dropped2, ...rest2 } = field;
|
|
128
|
+
return rest2;
|
|
129
|
+
});
|
|
130
|
+
if (stepOk) return { ...step, fields };
|
|
131
|
+
const { showIf: _dropped, ...rest } = step;
|
|
132
|
+
return { ...rest, fields };
|
|
133
|
+
});
|
|
134
|
+
const next = { ...definition, steps };
|
|
135
|
+
const email = definition.delivery?.email;
|
|
136
|
+
if (email) {
|
|
137
|
+
const { routes, replyToField, ...rest } = email;
|
|
138
|
+
next.delivery = {
|
|
139
|
+
...definition.delivery,
|
|
140
|
+
email: {
|
|
141
|
+
...rest,
|
|
142
|
+
...routes && seen.has(routes.field) ? { routes } : {},
|
|
143
|
+
...replyToField && keys.has(replyToField) ? { replyToField } : {}
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
const resubmit = definition.submit?.allowResubmit;
|
|
148
|
+
if (resubmit) next.submit = { ...definition.submit, allowResubmit: { keep: resubmit.keep.filter((key) => keys.has(key)) } };
|
|
149
|
+
return next;
|
|
150
|
+
}
|
|
151
|
+
export function changeMode(definition, mode) {
|
|
152
|
+
if (mode === definition.mode) return definition;
|
|
153
|
+
if (mode === "stepped") return { ...definition, mode };
|
|
154
|
+
const first = definition.steps[0] ?? { id: "main", fields: [] };
|
|
155
|
+
const { showIf: _dropped, ...rest } = first;
|
|
156
|
+
return pruneReferences({ ...definition, mode, steps: [{ ...rest, fields: definition.steps.flatMap((step) => step.fields) }] });
|
|
157
|
+
}
|
|
158
|
+
export function removeStep(definition, index, { keepFields = false } = {}) {
|
|
159
|
+
if (definition.steps.length <= 1 || !definition.steps[index]) return definition;
|
|
160
|
+
const removed = definition.steps[index];
|
|
161
|
+
const steps = definition.steps.filter((_, i) => i !== index).map((step) => ({ ...step, fields: [...step.fields] }));
|
|
162
|
+
if (keepFields) steps[Math.max(0, index - 1)].fields.push(...removed.fields);
|
|
163
|
+
return pruneReferences({ ...definition, steps });
|
|
164
|
+
}
|
|
165
|
+
const cleanAddresses = (to) => [...new Set(to.map((address) => address.trim()).filter(Boolean))];
|
|
166
|
+
function withEmail(definition, email) {
|
|
167
|
+
const { delivery: _dropped, ...rest } = definition;
|
|
168
|
+
const delivery = { ...definition.delivery };
|
|
169
|
+
if (email) delivery.email = email;
|
|
170
|
+
else delete delivery.email;
|
|
171
|
+
return Object.keys(delivery).length ? { ...rest, delivery } : rest;
|
|
172
|
+
}
|
|
173
|
+
export function setEmailRecipients(definition, to) {
|
|
174
|
+
const addresses = cleanAddresses(to);
|
|
175
|
+
if (!addresses.length) return withEmail(definition, void 0);
|
|
176
|
+
return withEmail(definition, { ...definition.delivery?.email, to: addresses });
|
|
177
|
+
}
|
|
178
|
+
export function patchEmailDelivery(definition, patch) {
|
|
179
|
+
const email = definition.delivery?.email;
|
|
180
|
+
if (!email) return definition;
|
|
181
|
+
const merged = { ...email, ...patch };
|
|
182
|
+
const { subject, body, replyToField, ...rest } = merged;
|
|
183
|
+
return withEmail(definition, { ...rest, ...subject ? { subject } : {}, ...body ? { body } : {}, ...replyToField ? { replyToField } : {} });
|
|
184
|
+
}
|
|
185
|
+
export function setRouteField(definition, field) {
|
|
186
|
+
const email = definition.delivery?.email;
|
|
187
|
+
if (!email) return definition;
|
|
188
|
+
const { routes, ...rest } = email;
|
|
189
|
+
if (!field) return withEmail(definition, rest);
|
|
190
|
+
return withEmail(definition, { ...rest, routes: routes?.field === field ? routes : { field, to: {} } });
|
|
191
|
+
}
|
|
192
|
+
export function setRoute(definition, value, to) {
|
|
193
|
+
const email = definition.delivery?.email;
|
|
194
|
+
if (!email?.routes) return definition;
|
|
195
|
+
const addresses = cleanAddresses(to);
|
|
196
|
+
const routed = Object.fromEntries(Object.entries(email.routes.to).filter(([key]) => key !== value));
|
|
197
|
+
if (addresses.length) routed[value] = addresses;
|
|
198
|
+
return withEmail(definition, { ...email, routes: { field: email.routes.field, to: routed } });
|
|
199
|
+
}
|
|
200
|
+
export function setStorage(definition, storage) {
|
|
201
|
+
return definition.storage === storage ? definition : { ...definition, storage };
|
|
202
|
+
}
|
|
203
|
+
export function setAccess(definition, patch) {
|
|
204
|
+
const next = { availability: "web", auth: "none", ...definition.access, ...patch };
|
|
205
|
+
if (next.auth === "token") next.availability = "api";
|
|
206
|
+
return { ...definition, access: next };
|
|
207
|
+
}
|
|
208
|
+
export function storageWarnings(definition) {
|
|
209
|
+
if (definition.storage !== "none") return { files: false, lost: false };
|
|
210
|
+
return {
|
|
211
|
+
files: definition.steps.some((step) => step.fields.some((field) => field.type === "file")),
|
|
212
|
+
lost: !definition.delivery?.email
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
export function emailFieldKeys(definition) {
|
|
216
|
+
return definition.steps.flatMap((step) => step.fields.filter((field) => field.type === "email").map((field) => field.key));
|
|
217
|
+
}
|
|
218
|
+
export function routableFields(definition) {
|
|
219
|
+
return definition.steps.flatMap((step) => step.fields.filter((field) => (field.type === "select" || field.type === "radio") && field.options?.length));
|
|
220
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { CaptchaConfig, CaptchaProvider, FormField } from '../../types.js';
|
|
2
|
+
export declare const captchaProviders: CaptchaProvider[];
|
|
3
|
+
/** A configured provider name, or `turnstile` when it is missing or not a known provider. */
|
|
4
|
+
export declare function normalizeCaptchaProvider(value: unknown): CaptchaProvider;
|
|
5
|
+
/**
|
|
6
|
+
* Provider and (optional) site key override of a captcha field. A field without a provider uses
|
|
7
|
+
* `defaultProvider` (`runtimeConfig.public.forms.captcha.defaultProvider`), `turnstile` when omitted.
|
|
8
|
+
*/
|
|
9
|
+
export declare function resolveCaptcha(field: Pick<FormField, 'captcha'>, defaultProvider?: unknown): Required<Pick<CaptchaConfig, 'provider'>> & CaptchaConfig;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export const captchaProviders = ["turnstile", "recaptcha", "hcaptcha"];
|
|
2
|
+
export function normalizeCaptchaProvider(value) {
|
|
3
|
+
return captchaProviders.includes(value) ? value : "turnstile";
|
|
4
|
+
}
|
|
5
|
+
export function resolveCaptcha(field, defaultProvider) {
|
|
6
|
+
return { ...field.captcha, provider: field.captcha?.provider ?? normalizeCaptchaProvider(defaultProvider) };
|
|
7
|
+
}
|
|
@@ -15,5 +15,7 @@ export type VisibleStep = {
|
|
|
15
15
|
*/
|
|
16
16
|
export declare function resolveVisibility(definition: FormDefinition, answers: Answers): VisibleStep[];
|
|
17
17
|
export declare function isFieldVisible(definition: FormDefinition, key: string, answers: Answers): boolean;
|
|
18
|
-
/** Fields that
|
|
18
|
+
/** Fields that take part in validation (everything except static `markdown`; includes `captcha`). */
|
|
19
19
|
export declare const isInputField: (field: FormField) => boolean;
|
|
20
|
+
/** Fields whose answer is stored, shown and delivered: input fields except `captcha` (its token is never kept). */
|
|
21
|
+
export declare const isAnswerField: (field: FormField) => boolean;
|
|
@@ -3,6 +3,9 @@ export function evaluateCondition(condition, answers) {
|
|
|
3
3
|
if (!condition) return true;
|
|
4
4
|
const answer = answers[condition.field];
|
|
5
5
|
switch (condition.op) {
|
|
6
|
+
case "empty":
|
|
7
|
+
return isEmpty(answer);
|
|
8
|
+
case "notEmpty":
|
|
6
9
|
case "truthy":
|
|
7
10
|
return !isEmpty(answer);
|
|
8
11
|
case "eq":
|
|
@@ -34,3 +37,4 @@ export function isFieldVisible(definition, key, answers) {
|
|
|
34
37
|
return resolveVisibility(definition, answers).some(({ fields }) => fields.some((field) => field.key === key));
|
|
35
38
|
}
|
|
36
39
|
export const isInputField = (field) => field.type !== "markdown";
|
|
40
|
+
export const isAnswerField = (field) => field.type !== "markdown" && field.type !== "captcha";
|