import 'server-only'; import type { BasePayload } from 'payload'; export type SubmitFormArgs = { /** Submitted field data — shape matches the form's fields. */ data: Record; /** Form-builder form ID this submission belongs to. */ formId: string; /** Client IP — used for Turnstile and rate limiting. */ ip?: string; /** * Rate limit: max submissions per IP per minute. Defaults to 5. * Set to 0 to disable (e.g. when a real limiter sits in front). */ maxPerMinute?: number; payload: BasePayload; /** Turnstile token; when present it is verified, when absent it is skipped. */ turnstileToken?: string; }; /** * Why a submission failed — a code, never a user-facing string. * * The plugin knows what went wrong; it deliberately doesn't decide how to say * it. The frontend maps these to its own copy, in its own language, and renders * whatever component fits — a field error, a toast, a full message. The plugin * has no business choosing the wording or the locale. * * - `rate_limited` — too many submissions from this IP * - `turnstile` — bot check failed * - `validation` — a field is missing/too long, or unknown keys were sent; * `field` and `kind` narrow it down when a specific field is * at fault (absent for whole-payload problems like unknown keys) * - `not_found` — no form with this id * - `error` — persistence failed unexpectedly */ export type SubmitFailure = { /** The offending field's name, when one field is at fault. */ field?: string; /** What was wrong with it. */ kind?: 'required' | 'too_long' | 'unknown_fields'; reason: 'validation'; success: false; } | { reason: 'error'; success: false; } | { reason: 'not_found'; success: false; } | { reason: 'rate_limited'; success: false; } | { reason: 'turnstile'; success: false; }; export type SubmitFormResult = { submissionId: number | string; success: true; } | SubmitFailure; /** * Handles a form submission end to end: rate limit, verify Turnstile, validate * against the form's own schema, then store. * * The order is cost-ascending on purpose — the cheapest checks reject first, so * a flood never reaches Turnstile's network call or the database. * * Emails aren't sent here. The form-builder sends whatever an editor configured * under the form's "Emails" tab (form-submissions hook → payload.sendEmail), * which goes out over panelSmtpAdapter. Storing the submission is enough. * * server-only: touches the Turnstile secret. */ export declare function submitForm({ data, formId, ip, maxPerMinute, payload, turnstileToken, }: SubmitFormArgs): Promise;