@aglyn/plugins-forms 1.0.0-beta.228 → 1.0.0-beta.230
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aglyn/plugins-forms",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.230",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"homepage": "https://aglyn.com",
|
|
6
6
|
"repository": {
|
|
@@ -25,19 +25,19 @@
|
|
|
25
25
|
"./package.json": "./package.json"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@aglyn/aglyn": "1.0.0-beta.
|
|
29
|
-
"@aglyn/shared-data-enums": "1.0.0-beta.
|
|
30
|
-
"@aglyn/shared-data-mdi": "1.0.0-beta.
|
|
31
|
-
"@aglyn/shared-ui-jsx": "1.0.0-beta.
|
|
32
|
-
"@aglyn/shared-ui-jsx-forms": "1.0.0-beta.
|
|
33
|
-
"@aglyn/shared-ui-snackstack": "1.0.0-beta.
|
|
34
|
-
"@aglyn/shared-util-email": "1.0.0-beta.
|
|
35
|
-
"@aglyn/shared-util-http": "1.0.0-beta.
|
|
36
|
-
"@aglyn/shared-util-timestamp": "1.0.0-beta.
|
|
37
|
-
"@aglyn/shared-util-tools": "1.0.0-beta.
|
|
38
|
-
"@aglyn/tenant-data-admin": "1.0.0-beta.
|
|
39
|
-
"@aglyn/tenant-feature-instance": "1.0.0-beta.
|
|
40
|
-
"@aglyn/tenant-runtime": "1.0.0-beta.
|
|
28
|
+
"@aglyn/aglyn": "1.0.0-beta.230",
|
|
29
|
+
"@aglyn/shared-data-enums": "1.0.0-beta.230",
|
|
30
|
+
"@aglyn/shared-data-mdi": "1.0.0-beta.230",
|
|
31
|
+
"@aglyn/shared-ui-jsx": "1.0.0-beta.230",
|
|
32
|
+
"@aglyn/shared-ui-jsx-forms": "1.0.0-beta.230",
|
|
33
|
+
"@aglyn/shared-ui-snackstack": "1.0.0-beta.230",
|
|
34
|
+
"@aglyn/shared-util-email": "1.0.0-beta.230",
|
|
35
|
+
"@aglyn/shared-util-http": "1.0.0-beta.230",
|
|
36
|
+
"@aglyn/shared-util-timestamp": "1.0.0-beta.230",
|
|
37
|
+
"@aglyn/shared-util-tools": "1.0.0-beta.230",
|
|
38
|
+
"@aglyn/tenant-data-admin": "1.0.0-beta.230",
|
|
39
|
+
"@aglyn/tenant-feature-instance": "1.0.0-beta.230",
|
|
40
|
+
"@aglyn/tenant-runtime": "1.0.0-beta.230",
|
|
41
41
|
"@swc/helpers": "0.5.23"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
@@ -18,6 +18,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
18
18
|
* limitations under the License.
|
|
19
19
|
*/ import { sendAnalyticsBeacon } from "@aglyn/aglyn/app-utils/analytics-beacon";
|
|
20
20
|
import { trackEventBeforeNavigation } from "@aglyn/aglyn/app-utils/analytics-events";
|
|
21
|
+
import { recordSiteJourneyStep, siteJourneyField } from "@aglyn/aglyn/app-utils/site-journey";
|
|
21
22
|
import { utmTouchField } from "@aglyn/aglyn/app-utils/utm-touch";
|
|
22
23
|
import * as Aglyn from "@aglyn/aglyn";
|
|
23
24
|
import { parseFormUnavailableRefusal } from "../model/form-unavailable.js";
|
|
@@ -309,7 +310,7 @@ const REVEAL_NODE_ID_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
|
309
310
|
website
|
|
310
311
|
}, consentGroup ? {
|
|
311
312
|
[CONSENT_GROUP_INPUT]: consentGroup
|
|
312
|
-
} : {}, utmTouchField()))
|
|
313
|
+
} : {}, utmTouchField(), siteJourneyField()))
|
|
313
314
|
});
|
|
314
315
|
if (response.ok) {
|
|
315
316
|
// Site alerts from the actions builder (AGL-148).
|
|
@@ -346,6 +347,9 @@ const REVEAL_NODE_ID_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
|
346
347
|
// property and aglyn.com's reports into ours. It is consent-gated
|
|
347
348
|
// either way: without a grant the script never loaded (AGL-1498)
|
|
348
349
|
// and the delivery path finds no `window.gtag` to call.
|
|
350
|
+
// A funnel step (AGL-3605): the form, by its id. Dropped unless the
|
|
351
|
+
// site records journeys and the visitor's consent grants.
|
|
352
|
+
recordSiteJourneyStep('form', formId);
|
|
349
353
|
await trackEventBeforeNavigation('generate_lead', {
|
|
350
354
|
form_name: formName || 'Form',
|
|
351
355
|
form_location: window.location.pathname
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/forms/src/lib/components/form.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { sendAnalyticsBeacon } from '@aglyn/aglyn/app-utils/analytics-beacon'\nimport { trackEventBeforeNavigation } from '@aglyn/aglyn/app-utils/analytics-events'\nimport { utmTouchField } from '@aglyn/aglyn/app-utils/utm-touch'\nimport * as Aglyn from '@aglyn/aglyn'\nimport {\n type FormUnavailableNotice,\n parseFormUnavailableRefusal,\n} from '../model/form-unavailable'\nimport {\n mdiEmailFastOutline,\n mdiEmailOutline,\n mdiFormTextbox,\n} from '@aglyn/shared-data-mdi'\nimport { isSameOriginPath } from '@aglyn/shared-util-http/safe-redirect'\nimport Alert from '@mui/material/Alert'\nimport Button from '@mui/material/Button'\nimport Checkbox from '@mui/material/Checkbox'\nimport FormControl from '@mui/material/FormControl'\nimport FormControlLabel from '@mui/material/FormControlLabel'\nimport FormGroup from '@mui/material/FormGroup'\nimport FormHelperText from '@mui/material/FormHelperText'\nimport FormLabel from '@mui/material/FormLabel'\nimport Link from '@mui/material/Link'\nimport MenuItem from '@mui/material/MenuItem'\nimport Radio from '@mui/material/Radio'\nimport RadioGroup from '@mui/material/RadioGroup'\nimport Rating from '@mui/material/Rating'\nimport Stack from '@mui/material/Stack'\nimport TextField from '@mui/material/TextField'\nimport {\n createContext,\n type FormEvent,\n forwardRef,\n useCallback,\n useContext,\n useEffect,\n useRef,\n useState,\n} from 'react'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport { generatePresetId } from '../utils/generate-preset-id'\n\n/*\n * The heading and the stack this block's form sits inside are mui elements,\n * so the nodes that carry them say so. A node's `pluginId` names the bundle\n * that REGISTERS its component, not the bundle whose preset placed it.\n *\n * The id comes from the core's constant, as it does for every plugin that\n * composes mui elements: importing the mui plugin for one string would make\n * this plugin unusable without it on the import graph.\n */\nconst { MUI_BUNDLE_ID } = Aglyn\n\n// Component ids are persisted in screen documents; never rename.\nexport const FORM_ID: Aglyn.ComponentId = 'form'\nexport const FORM_FIELD_ID: Aglyn.ComponentId = 'formField'\n\n/** After-submit outcomes (AGL-557); message is the historical default. */\nexport type FormAfterSubmit = 'message' | 'redirect' | 'reveal'\n\nexport interface FormProps {\n /**\n * The form entity this node submits as (`docs/specs/reusable-forms.md` §2c).\n *\n * THE identity. `formName` below is a caption and always was: it is copied\n * onto each submission at write time and reconciled with nothing, so\n * renaming a form split its history in two and two pages sharing a label\n * were one list. An id survives both.\n *\n * This is the whole binding. Reuse across pages needs nothing further —\n * promote the bound subtree once and the id travels inside the definition,\n * so every instance writes the same form's submissions without the author\n * retyping a label. Two instances of one definition are ONE form, which is\n * the point; an author who wants two forms makes two forms.\n */\n formId?: string\n /** Identifies the form in the submissions inbox. */\n formName?: string\n /**\n * Dataset (by id) submissions also append into (AGL-556); the inbox\n * copy is always written. Renaming the dataset never breaks the binding.\n */\n datasetId?: string\n /**\n * Legacy dataset-by-name binding (AGL-141), honored when no `datasetId`\n * is set; kept so persisted nodes keep working.\n */\n datasetName?: string\n /**\n * The dataset binding above, signed by the compose that rendered this form\n * (AGL-2773). The submit route writes a record only where this says, so the\n * page, not the request, decides which dataset a submission reaches. Present\n * on the published site only; the canvas and Preview never submit.\n */\n datasetBindingToken?: string\n submitLabel?: string\n successMessage?: string\n /**\n * What a successful submit does (AGL-557): show the success message\n * (default), redirect the visitor, or reveal a hidden element.\n */\n afterSubmit?: FormAfterSubmit\n /** Redirect target screen — rename-safe id, resolved like ScreenLink. */\n redirectScreenId?: string\n /**\n * Manual redirect target used when no screen is picked: a same-site\n * `/path` or an absolute https URL (everything else is dropped).\n */\n redirectUrl?: string\n /** Node revealed on submit; hidden until then (afterSubmit=reveal). */\n revealNodeId?: string\n children?: React.ReactNode\n}\n\n/**\n * Hidden-input prefix a FormField uses to publish its field → dataset\n * schema-field mapping to the enclosing Form (AGL-556). Rides the DOM so\n * arbitrary nesting between Form and field needs no React context; the\n * `__` prefix keeps it out of the submitted fields.\n */\nexport const FIELD_MAP_INPUT_PREFIX = '__map__'\n\n/**\n * The hidden input the field carrying the consent-group sentence posts its\n * key under (AGL-3320). `__`-prefixed like the map inputs, so it never\n * reaches the submitted fields; the Form lifts it into the request body as\n * `__consentGroup`, where the submit route reads it.\n *\n * Posted by the FIELD, not the Form: the key is proof the sentence was on\n * screen, and the field that rendered it is the only thing that knows it was.\n * A form whose consent field never mounted posts no key, and its opt-in is\n * recorded for the one site the visitor is on.\n */\nexport const CONSENT_GROUP_INPUT = '__consentGroup'\n\n/** The site's consent-group sentence, as `/api/consent/disclosure` answers it. */\nexport interface FormConsentDisclosure {\n /**\n * The field the form declares as its opt-in, or `null` for a form that\n * declares none — the sentence then sits under whichever field the submit\n * route would read an opt-in from by its name.\n */\n fieldName: string | null\n /** What the person is told they are agreeing to hear from. */\n text: string\n /** The key of exactly this sentence, posted back with the submission. */\n key: string\n}\n\n/**\n * The disclosure a Form fetched, handed to the fields inside it. A context\n * because the answer flows DOWN, from the one request the Form makes to the\n * one field it describes; the key flows back up through the DOM, like the\n * dataset mapping does.\n */\nconst FormConsentDisclosureContext =\n createContext<FormConsentDisclosure | null>(null)\n\n/**\n * The route's answer, or `null` when it has no sentence for this site — a\n * site that sends on its own, or an answer that is not the shape it should\n * be. `null` renders nothing and posts no key.\n */\nexport function readFormConsentDisclosure(\n body: unknown,\n): FormConsentDisclosure | null {\n const value = (body ?? {}) as Record<string, unknown>\n const text = typeof value['text'] === 'string' ? value['text'].trim() : ''\n const key = typeof value['key'] === 'string' ? value['key'].trim() : ''\n if (!text || !key) return null\n const fieldName =\n typeof value['fieldName'] === 'string' && value['fieldName'].trim()\n ? value['fieldName'].trim()\n : null\n return { fieldName, text, key }\n}\n\n/**\n * Redirect targets are restricted to same-origin paths (`/thanks`) and\n * absolute https URLs (AGL-557): stored props reach every visitor's\n * browser, so `javascript:`/`data:` URLs — and protocol-relative\n * `//host` forms, which keep the scheme but swap the host — never pass.\n *\n * The relative branch asks {@link isSameOriginPath} rather than reading the\n * string's first two characters (AGL-1881). This site never got the AGL-2486\n * backslash fix, and the character list would not have been enough anyway:\n * the URL parser deletes tab/LF/CR before parsing, so `/<TAB>/evil.com`\n * carries neither `//` nor `\\` and `window.location.assign` — which\n * `formNavigation` below calls — still lands the visitor on `evil.com`. The\n * absolute branch is unchanged: an explicit `https://` target is a documented\n * feature, and for absolute input the parser's verdict was never in doubt.\n */\nexport const sanitizeRedirectUrl = (url?: string): string | undefined => {\n const trimmed = (url ?? '').trim()\n if (!trimmed) return undefined\n if (/^https:\\/\\//i.test(trimmed)) return trimmed\n if (isSameOriginPath(trimmed)) return trimmed\n return undefined\n}\n\n/**\n * Where a redirect outcome sends the visitor: the picked screen's\n * resolved href wins; the manual URL is the fallback, sanitized above.\n * Undefined (deleted screen, bad URL) degrades to the message outcome.\n */\nexport const resolveRedirectTarget = (\n screenHref: string | undefined,\n redirectUrl: string | undefined,\n): string | undefined => screenHref ?? sanitizeRedirectUrl(redirectUrl)\n\n// Reveal ids are interpolated into a CSS selector — restrict to the id\n// alphabet so a stored prop can't smuggle selector syntax (AGL-557).\nconst REVEAL_NODE_ID_PATTERN = /^[A-Za-z0-9_-]+$/\n\n/**\n * Navigation seam for the redirect outcome: jsdom's `window.location`\n * is not patchable, so tests stub this indirection instead.\n */\nexport const formNavigation = {\n assign: (url: string) => window.location.assign(url),\n}\n\n/** DOM event dispatched on every successful submit (AGL-557). */\nexport const FORM_SUBMITTED_EVENT = 'aglyn:form-submitted'\n\n/**\n * Report that a bound form was seen, or typed into.\n *\n * Fire-and-forget, and deliberately the same instrument the overlay counters\n * use: `sendBeacon` hands the request to the browser, which delivers it\n * outside the page's lifetime, so nothing here can delay a render or survive\n * long enough to fail visibly.\n *\n * Requires a bound `formId`. An unbound form has no document to count on, and\n * an id is never invented: the collector's `update` would find nothing, which\n * is the right outcome but a pointless request.\n */\nexport function sendFormBeacon(\n hostId: string | undefined,\n formId: string | undefined,\n event: 'view' | 'start',\n): void {\n if (!hostId || !formId) return\n // Best-effort by construction: `sendAnalyticsBeacon` swallows everything and\n // refuses outright from a non-production build or a browser marked ours. A\n // view that does not report is a view this form's completion rate is\n // measured without, which the console says.\n sendAnalyticsBeacon({ hostId, formId, form: event })\n}\n\n/**\n * Lead-capture form (AGL-76): collects its field children's values and\n * posts them to the tenant's `/api/forms/submit` with the site's host id\n * from SiteContext. Without a site context (besigner canvas, preview) the\n * submit is inert, so editing never creates submissions. Includes a\n * honeypot input for naive bots.\n */\nconst Form = forwardRef<HTMLFormElement, FormProps>((props, ref) => {\n const {\n formId,\n formName,\n // Read by compose, which signs them into `datasetBindingToken`; taken out\n // here only so they never reach the DOM through `rest`.\n datasetId: _datasetId,\n datasetName: _datasetName,\n datasetBindingToken,\n submitLabel,\n successMessage,\n afterSubmit,\n redirectScreenId,\n redirectUrl,\n revealNodeId,\n children,\n ...rest\n } = props\n const { hostId } = Aglyn.useSite()\n const siteFetch = Aglyn.useSiteFetch()\n // Resolves the redirect screen like ScreenLink does (rename-safe) and\n // flags editing surfaces, where outcomes must never fire (AGL-557).\n const { href: redirectScreenHref, suppressNavigation } = Aglyn.useScreenLink(\n afterSubmit === 'redirect' ? redirectScreenId : undefined,\n )\n // Reveal target (AGL-557): hidden through a form-owned <style> tag so\n // SSR paints it hidden from the first frame; the style unmounts with\n // the form after a successful submit — which IS the reveal. Editing\n // surfaces keep the target visible so it stays editable.\n const revealSelector =\n afterSubmit === 'reveal' &&\n revealNodeId &&\n REVEAL_NODE_ID_PATTERN.test(revealNodeId) &&\n !suppressNavigation\n ? `[data-aglyn=\"leaf:${revealNodeId}\"]`\n : undefined\n const [status, setStatus] = useState<\n 'idle' | 'sending' | 'sent' | 'error' | 'preview' | 'paused' | 'unavailable'\n >(\n 'idle',\n )\n /*\n * THE VIEW AND THE START — the two facts a completion rate is taken over.\n *\n * Both are reported ONCE per mounted form, from a ref rather than from\n * state: a counter driven by a re-render would be a Firestore write per\n * keystroke, which is the per-render write this must not become. The ref\n * also survives the re-renders `status` causes, so a visitor who edits a\n * field, submits, and is shown an error is one view and one start.\n *\n * Gated on `!suppressNavigation`, the editing-surface flag: the besigner\n * canvas and the console's preview render this component for its author,\n * and counting an author looking at their own draft as a visitor view would\n * put the merchant into their own denominator.\n */\n const reported = useRef({ view: false, start: false })\n useEffect(() => {\n if (suppressNavigation || reported.current.view) return\n reported.current.view = true\n sendFormBeacon(hostId, formId, 'view')\n }, [hostId, formId, suppressNavigation])\n\n /**\n * A START is the first edit anyone makes to this form.\n *\n * On the form's own `input`, which bubbles from every field — no listener\n * registration, no per-field wiring, and nothing to keep in sync when a\n * field type is added. Focus would be the looser reading and the wrong one:\n * a form that is tabbed through and abandoned was never started, and\n * counting it would inflate the denominator abandonment is measured over.\n */\n const handleFirstInput = useCallback(() => {\n if (suppressNavigation || reported.current.start) return\n reported.current.start = true\n sendFormBeacon(hostId, formId, 'start')\n }, [hostId, formId, suppressNavigation])\n /** The read-only lockdown's own words (AGL-1511); never a hardcoded line. */\n const [pausedMessage, setPausedMessage] = useState('')\n /** The abuse ceiling's visitor notice (AGL-1666); see `unavailable` below. */\n const [unavailable, setUnavailable] =\n useState<FormUnavailableNotice | null>(null)\n const [alerts, setAlerts] = useState<\n Array<{ message: string; severity?: string }>\n >([])\n /*\n * THE CONSENT-GROUP SENTENCE, when this site sends as part of a group\n * (AGL-3320).\n *\n * Asked for on the rendered site only — the canvas has no site to ask\n * about — and handed to the fields, one of which renders it under the\n * opt-in and posts its key. Until the answer arrives, and whenever it has\n * none, nothing renders and nothing is posted: the opt-in is then recorded\n * for this site alone, which is the safe way to be early or wrong.\n */\n const [disclosure, setDisclosure] = useState<FormConsentDisclosure | null>(\n null,\n )\n useEffect(() => {\n if (!hostId) return\n let current = true\n const query = new URLSearchParams({ hostId, ...(formId ? { formId } : {}) })\n siteFetch(`/api/consent/disclosure?${query.toString()}`)\n .then((response) => (response.ok ? response.json() : null))\n .then((body) => {\n if (current) setDisclosure(readFormConsentDisclosure(body))\n })\n .catch(() => {\n if (current) setDisclosure(null)\n })\n return () => {\n current = false\n }\n }, [hostId, formId, siteFetch])\n\n const handleSubmit = useCallback(\n async (event: FormEvent<HTMLFormElement>) => {\n event.preventDefault()\n if (!hostId || status === 'sending') return\n const data = new FormData(event.currentTarget)\n const fields: Record<string, string> = {}\n let website = ''\n let consentGroup = ''\n for (const [key, value] of data.entries()) {\n if (typeof value !== 'string') continue\n if (key === 'website') {\n website = value\n continue\n }\n // The key the consent field posted beside the sentence it rendered.\n if (key === CONSENT_GROUP_INPUT) {\n consentGroup = value\n continue\n }\n // `__`-prefixed inputs are internal controls — the rating field's\n // star radios (AGL-544) and each field's dataset-mapping input — and\n // never submit.\n if (key.startsWith('__')) continue\n // Checkbox groups emit one entry per ticked box; join them under\n // the field name instead of letting the last one win (AGL-544).\n fields[key] = (\n key in fields ? `${fields[key]}, ${value}` : value\n ).slice(0, 2000)\n }\n setStatus('sending')\n try {\n const response = await siteFetch('/api/forms/submit', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n hostId,\n // The bound form entity, when there is one. Sent ALONGSIDE the\n // caption rather than instead of it: the caption keeps the\n // pre-entity `?form=` filter working for every form that has not\n // been adopted yet, and no phase of the migration removes it.\n ...(formId ? { formId } : {}),\n formName: formName || 'Form',\n // The page's signed dataset binding (AGL-2773). The server writes\n // a record only where it says, so nothing this request names\n // chooses a dataset or a field.\n ...(datasetBindingToken\n ? { datasetBinding: datasetBindingToken }\n : {}),\n path: window.location.pathname,\n fields,\n website,\n // Only when the consent field rendered the site's consent-group\n // sentence; the route pools the opt-in on nothing else.\n ...(consentGroup ? { [CONSENT_GROUP_INPUT]: consentGroup } : {}),\n // The campaign this visitor came from, when they came from one.\n // Absent entirely for direct traffic — never a placeholder, so\n // the server can tell \"arrived from nowhere\" from \"this door does\n // not report\".\n ...utmTouchField(),\n }),\n })\n if (response.ok) {\n // Site alerts from the actions builder (AGL-148).\n const payload = await response.json().catch(() => ({}))\n if (Array.isArray(payload?.alerts)) setAlerts(payload.alerts)\n // Announced for reveal-style listeners/analytics (AGL-557).\n window.dispatchEvent(\n new CustomEvent(FORM_SUBMITTED_EVENT, {\n detail: { formName: formName || 'Form' },\n }),\n )\n // GA4 lead conversion (AGL-1561). Inside `response.ok`, so it\n // measures a SUBMISSION, not a click and not a validation failure —\n // the distinction that makes a conversion rate mean anything.\n //\n // AWAITED, because the branch below navigates. Ordering the call\n // ahead of the `assign` is not on its own enough, and the obvious\n // reasoning about beacons is the wrong one: once a hit REACHES gtag\n // a navigation cannot destroy it, so `transport_type: 'beacon'`\n // fixes nothing. What is lost is a hit that never reaches gtag,\n // which is what happens when the surface's transport is async and\n // its initialization promise is still pending — the continuation is\n // scheduled behind the navigation task and never runs. The tenant\n // runtime registers no transport, so this resolves immediately and\n // costs the redirect nothing; it is written this way because the\n // property that makes a bare call safe is invisible from here.\n //\n // `formName` is author-written site content, never a field VALUE:\n // no submitted field ever reaches GA from here.\n //\n // This runs on every tenant site, not only aglyn.com, and that is\n // deliberate — gtag is loaded with whatever measurement id the HOST\n // configured, so a customer's form reports into the customer's own\n // property and aglyn.com's reports into ours. It is consent-gated\n // either way: without a grant the script never loaded (AGL-1498)\n // and the delivery path finds no `window.gtag` to call.\n await trackEventBeforeNavigation('generate_lead', {\n form_name: formName || 'Form',\n form_location: window.location.pathname,\n })\n if (afterSubmit === 'redirect' && !suppressNavigation) {\n const target = resolveRedirectTarget(\n redirectScreenHref,\n redirectUrl,\n )\n // No valid target (deleted screen, rejected URL) degrades to\n // the success message below.\n if (target) formNavigation.assign(target)\n }\n }\n // A refusal is not a failure (AGL-1249). Preview declines writes on\n // purpose, and \"something went wrong — please try again\" invites the\n // author to retry something that is working exactly as intended.\n //\n // Since AGL-1511 there are TWO deliberate 423s a submit can meet, so\n // the status alone no longer identifies one: Preview's client-side\n // refusal, and a read-only lockdown refusing a real visitor's write\n // on a live site. Preview's carries `preview: true` and is settled\n // first; anything else 423 is the lockdown pause, whose own copy is\n // rendered rather than the Preview sentence — telling a customer's\n // visitor \"this works on your published site\" would be nonsense on\n // the published site they are standing on.\n if (!response.ok) {\n const body = await response.json().catch(() => ({}))\n if (Aglyn.isPreviewRefusal(response, body)) {\n setStatus('preview')\n return\n }\n const paused = Aglyn.parseLockdownRefusal(response.status, body)\n if (paused) {\n setPausedMessage(Aglyn.lockdownRefusalText(paused))\n setStatus('paused')\n return\n }\n // The site's form-submission abuse ceiling (AGL-1655/1666). This\n // is a THIRD deliberate refusal, and the only one whose status is\n // already spoken for: the Free plan's monthly wall answers 429\n // too. So it is matched on the body's `code` and never on the\n // status — `parseFormUnavailableRefusal` takes no status for\n // exactly that reason. A plan-wall 429 keeps falling through to\n // the generic branch below, which is right: it is the owner's\n // billing problem, and nothing about waiting fixes it.\n const notice = parseFormUnavailableRefusal(body)\n if (notice) {\n setUnavailable(notice)\n setStatus('unavailable')\n return\n }\n }\n setStatus(response.ok ? 'sent' : 'error')\n } catch {\n setStatus('error')\n }\n },\n [\n hostId,\n status,\n formId,\n formName,\n datasetBindingToken,\n afterSubmit,\n redirectScreenHref,\n redirectUrl,\n suppressNavigation,\n siteFetch,\n ],\n )\n\n if (status === 'sent') {\n // Reveal outcome: the form (with its hide-style below) unmounts and\n // the revealed element is the confirmation. Redirect keeps the\n // message visible as a fallback while the browser navigates.\n if (revealSelector) return null\n return (\n <Alert severity=\"success\">\n {successMessage || 'Thanks — your message has been sent.'}\n </Alert>\n )\n }\n\n return (\n <Stack\n ref={ref}\n component=\"form\"\n spacing={2}\n onSubmit={handleSubmit}\n onInput={handleFirstInput}\n {...rest}\n >\n {/* Hides the reveal target until submit (AGL-557); rendered by the\n form so SSR ships it hidden — no flash, no target-side props.\n MUI Stack spacing already skips <style> children. */}\n {revealSelector ? (\n <style>{`${revealSelector}{display:none !important}`}</style>\n ) : null}\n <FormConsentDisclosureContext.Provider value={disclosure}>\n {children}\n </FormConsentDisclosureContext.Provider>\n {/* Honeypot: humans never see or fill this. */}\n <input\n type=\"text\"\n name=\"website\"\n tabIndex={-1}\n autoComplete=\"off\"\n aria-hidden=\"true\"\n style={{ position: 'absolute', left: '-5000px', height: 0, width: 0 }}\n />\n {status === 'error' ? (\n <Alert severity=\"error\">\n {'Something went wrong — please try again.'}\n </Alert>\n ) : null}\n {status === 'paused' ? (\n // `info`, like the Preview notice and for the same reason: this is a\n // deliberate, temporary pause on a site that is otherwise working,\n // and nothing the visitor typed was lost.\n <Alert severity=\"info\">{pausedMessage}</Alert>\n ) : null}\n {status === 'unavailable' && unavailable ? (\n // `warning`, not `info` and not `error` (AGL-1666). The two `info`\n // notices above are both cases where nothing was lost — Preview\n // never intended to send, and a lockdown pause is minutes long with\n // the visitor's text still in the fields. This one lost a real\n // message on a live site, so it must not read as reassurance. It is\n // not `error` either: nothing is broken and \"please try again\" would\n // walk the visitor into the same refusal.\n <Alert severity=\"warning\">\n {unavailable.message}\n {unavailable.contact ? (\n <>\n {' In the meantime you can reach us at '}\n <Link href={`mailto:${unavailable.contact}`}>\n {unavailable.contact}\n </Link>\n {'.'}\n </>\n ) : null}\n </Alert>\n ) : null}\n {status === 'preview' ? (\n // Deliberately `info`, not `error`, and it does not say \"try again\":\n // nothing failed, and the same submit works on the published site.\n <Alert severity=\"info\">\n {'Preview does not send form submissions. This form works on your ' +\n 'published site.'}\n </Alert>\n ) : null}\n {alerts.map((alert, index) => (\n <Alert key={index} severity={(alert.severity as any) || 'info'}>\n {alert.message}\n </Alert>\n ))}\n <Button\n type=\"submit\"\n variant=\"contained\"\n disabled={status === 'sending'}\n sx={{ alignSelf: 'flex-start' }}\n >\n {status === 'sending' ? 'Sending…' : submitLabel || 'Send'}\n </Button>\n </Stack>\n )\n})\nForm.displayName = 'AglynForm'\n\nexport interface FormFieldProps {\n /** Submission key; also the input's name attribute. */\n fieldName?: string\n /**\n * Stable model fieldId of the parent form's dataset this value is\n * stored under (AGL-556). When unset, values match dataset fields by\n * name — the legacy behavior.\n */\n datasetFieldId?: string\n label?: string\n /**\n * Grey hint shown inside the empty input (AGL-1330) — the frames pair a\n * label above the control with a placeholder inside it. A placeholder is\n * NOT a label: it disappears the moment the visitor types, so the label\n * stays the field's accessible name and is never replaced by this.\n * Ignored by radio, checkbox, and rating fields, which have no text input\n * to hint inside of.\n */\n placeholder?: string\n fieldType?:\n | 'text'\n | 'email'\n | 'textarea'\n | 'select'\n | 'radio'\n | 'checkbox'\n | 'rating'\n /**\n * Choice list for select/radio/checkbox fields (AGL-544): newline- or\n * comma-separated. Ignored by the other types.\n */\n options?: string\n required?: boolean\n}\n\n/**\n * Splits a newline- or comma-separated choice list into trimmed,\n * non-empty entries (AGL-544).\n */\nexport const parseFieldOptions = (options?: string): string[] =>\n (options || '')\n .split(/[\\n,]/)\n .map((entry) => entry.trim())\n .filter(Boolean)\n\n/**\n * Single input inside a Form (AGL-76). Survey field types — select,\n * radio, checkbox group, and star rating — are AGL-544.\n */\nconst FormField = forwardRef<HTMLDivElement, FormFieldProps>((props, ref) => {\n const {\n fieldName,\n datasetFieldId,\n label,\n placeholder,\n fieldType,\n options,\n required,\n ...rest\n } = props\n const name = fieldName || 'field'\n const fieldLabel = label || fieldName || 'Field'\n const choices = parseFieldOptions(options)\n // A CLEARED placeholder must render as no placeholder (AGL-1330). The\n // attributes form persists a cleared text field by dropping the key\n // (ddf maps an emptied field to its clearedValue and final-form's parse\n // turns `''` into `undefined`), and `updateNodeProps` REPLACES the props\n // object — so cleared reaches us as absent. Older documents can still\n // carry `''`/`null`, and a blank string would otherwise reach the DOM as\n // an empty `placeholder` attribute, so blank is normalized to absent too.\n const hint = typeof placeholder === 'string' && placeholder.trim()\n ? placeholder\n : undefined\n // MUI hides a placeholder for as long as the label still sits inside the\n // box, so the hint is only visible once the label is shrunk — and an\n // outlined input with a shrunk label must notch its outline or the label\n // crosses the border. Both are applied ONLY when there is a placeholder,\n // so fields without one keep the historical floating-label behavior.\n const hintSlotProps = hint\n ? { inputLabel: { shrink: true }, input: { notched: true } }\n : undefined\n // Checkbox groups count ticked boxes so \"required\" can mean \"at least\n // one\": native `required` is only asserted while none are ticked.\n const [checkedCount, setCheckedCount] = useState(0)\n // Rating is controlled so a hidden input can serialize the number.\n const [rating, setRating] = useState<number | null>(null)\n // Publishes this field's dataset schema-field mapping to the enclosing\n // Form via the DOM (AGL-556); `__`-prefixed, so never a submitted field.\n const mapInput = datasetFieldId ? (\n <input\n type=\"hidden\"\n name={`${FIELD_MAP_INPUT_PREFIX}${name}`}\n value={datasetFieldId}\n readOnly\n />\n ) : null\n /*\n * THE CONSENT-GROUP SENTENCE, under the field that records the opt-in\n * (AGL-3320): the field the form declares, or — where it declares none —\n * one the submit route reads an opt-in from by its name. The key rides\n * beside it, so a submission carries the key only while the sentence is\n * on screen.\n */\n const disclosure = useContext(FormConsentDisclosureContext)\n const describesOptIn = disclosure\n ? disclosure.fieldName\n ? disclosure.fieldName === name\n : Aglyn.isMarketingConsentFieldName(name)\n : false\n const consentNote =\n disclosure && describesOptIn ? (\n <>\n <FormHelperText>{disclosure.text}</FormHelperText>\n <input\n type=\"hidden\"\n name={CONSENT_GROUP_INPUT}\n value={disclosure.key}\n readOnly\n />\n </>\n ) : null\n\n if (fieldType === 'select') {\n return (\n <>\n <TextField\n ref={ref}\n select\n name={name}\n label={fieldLabel}\n required={Boolean(required)}\n defaultValue=\"\"\n fullWidth\n size=\"small\"\n // A Select has no native placeholder: the hint is what the closed\n // control displays while nothing is chosen, which needs\n // `displayEmpty` (MUI renders nothing for `''` without it).\n slotProps={\n hint\n ? {\n ...hintSlotProps,\n select: {\n displayEmpty: true,\n renderValue: (value: unknown) =>\n value ? (\n String(value)\n ) : (\n <span style={{ opacity: 0.6 }}>{hint}</span>\n ),\n },\n }\n : undefined\n }\n {...rest}\n >\n {choices.map((choice, index) => (\n <MenuItem key={index} value={choice}>\n {choice}\n </MenuItem>\n ))}\n </TextField>\n {mapInput}\n {consentNote}\n </>\n )\n }\n\n if (fieldType === 'radio') {\n return (\n <FormControl ref={ref} required={Boolean(required)} {...rest}>\n <FormLabel>{fieldLabel}</FormLabel>\n <RadioGroup name={name}>\n {choices.map((choice, index) => (\n <FormControlLabel\n key={index}\n value={choice}\n label={choice}\n control={<Radio size=\"small\" required={Boolean(required)} />}\n />\n ))}\n </RadioGroup>\n {mapInput}\n {consentNote}\n </FormControl>\n )\n }\n\n if (fieldType === 'checkbox') {\n return (\n <FormControl ref={ref} required={Boolean(required)} {...rest}>\n <FormLabel>{fieldLabel}</FormLabel>\n <FormGroup>\n {choices.map((choice, index) => (\n <FormControlLabel\n key={index}\n label={choice}\n control={\n <Checkbox\n name={name}\n value={choice}\n size=\"small\"\n required={Boolean(required) && checkedCount === 0}\n onChange={(event) =>\n setCheckedCount(\n (count) => count + (event.target.checked ? 1 : -1),\n )\n }\n />\n }\n />\n ))}\n </FormGroup>\n {mapInput}\n {consentNote}\n </FormControl>\n )\n }\n\n if (fieldType === 'rating') {\n return (\n <FormControl ref={ref} required={Boolean(required)} {...rest}>\n <FormLabel>{fieldLabel}</FormLabel>\n <Rating\n // `__`-prefixed so the star radios themselves are skipped by\n // Form.handleSubmit; the hidden input carries the value.\n name={`__${name}`}\n value={rating}\n onChange={(_event, value) => setRating(value)}\n />\n <input type=\"hidden\" name={name} value={rating ?? ''} />\n {mapInput}\n {consentNote}\n </FormControl>\n )\n }\n\n return (\n <>\n <TextField\n ref={ref}\n name={name}\n label={fieldLabel}\n placeholder={hint}\n type={fieldType === 'email' ? 'email' : 'text'}\n required={Boolean(required)}\n multiline={fieldType === 'textarea'}\n minRows={fieldType === 'textarea' ? 3 : undefined}\n fullWidth\n size=\"small\"\n slotProps={hintSlotProps}\n {...rest}\n />\n {mapInput}\n {consentNote}\n </>\n )\n})\nFormField.displayName = 'AglynFormField'\n\nexport const formSchema: Aglyn.ComponentSchema<FormProps> = {\n $id: FORM_ID,\n pluginId: BUNDLE_ID,\n displayName: 'Form',\n description:\n 'Collects its Form Fields and sends the answers to you, or into a dataset.',\n category: Aglyn.ComponentCategory.FORMS,\n icon: { path: mdiEmailFastOutline.path, sx: { color: 'info.main' } },\n attributes: [\n {\n name: 'formId',\n description:\n 'The form this collects for, from the Forms page. Stored by id, so ' +\n 'renaming the form never splits its submission history and two ' +\n 'pages placing the same form share one list. Leave unset to keep ' +\n 'filing submissions under the name below.',\n component: Aglyn.FieldComponentType.FORM_SELECT,\n label: 'Form',\n },\n {\n name: 'formName',\n description:\n 'Caption shown in the submissions inbox, used when no form above ' +\n 'is picked. A name is not an identity: renaming it splits the ' +\n 'history of everything already submitted under the old one.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n label: 'Form name',\n },\n {\n name: 'datasetId',\n description:\n 'Optional dataset (from the Data page) submissions are appended ' +\n 'to. Stored by id, so renaming the dataset never breaks the ' +\n 'binding (AGL-556). The inbox always gets a copy.',\n component: Aglyn.FieldComponentType.DATASET_SELECT,\n label: 'Write to dataset',\n },\n {\n name: 'datasetName',\n description:\n 'Legacy name-based dataset binding — matched against the ' +\n \"dataset's display name at submit time. Pick the dataset above \" +\n 'and clear this; it only shows while a name is still set.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n label: 'Write to dataset (legacy name)',\n condition: { when: 'datasetName', isNotEmpty: true },\n },\n {\n name: 'submitLabel',\n description: 'Label of the submit button.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n label: 'Submit label',\n },\n {\n name: 'successMessage',\n description: 'Shown after a successful submission.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n label: 'Success message',\n },\n // After-submit outcomes (AGL-557).\n {\n name: 'afterSubmit',\n description:\n 'What a successful submit does: show the success message, send ' +\n 'the visitor to another page, or reveal a hidden element.',\n component: Aglyn.FieldComponentType.SELECT,\n label: 'After submit',\n // `message` is the first member of the declared `FormAfterSubmit`\n // union, so the sentinel already existed in the type and only the\n // option list was spelling it `''` (AGL-1451) — a value the\n // attributes form strips on change (AGL-1191), which meant a form\n // switched to Redirect could not be switched back to its message.\n options: [\n { value: 'message', label: 'Show the success message' },\n { value: 'redirect', label: 'Redirect the visitor' },\n { value: 'reveal', label: 'Reveal a hidden element' },\n ],\n },\n {\n name: 'redirectScreenId',\n description:\n 'Page the visitor lands on after submitting. The address is ' +\n \"resolved at render time, so renaming the page's slug never \" +\n 'breaks the redirect.',\n component: Aglyn.FieldComponentType.SCREEN_SELECT,\n label: 'Redirect to page',\n condition: { when: 'afterSubmit', is: 'redirect' },\n },\n {\n name: 'redirectUrl',\n description:\n 'Used only when no page is selected above: a same-site path ' +\n '(/thanks) or an https URL. Anything else is ignored.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n label: 'Redirect URL',\n condition: { when: 'afterSubmit', is: 'redirect' },\n },\n {\n name: 'revealNodeId',\n description:\n 'Element shown after a successful submit — it stays hidden on ' +\n 'the published page until then.',\n component: Aglyn.FieldComponentType.NODE_SELECT,\n label: 'Element to reveal',\n condition: { when: 'afterSubmit', is: 'reveal' },\n },\n ],\n}\n\nexport const formFieldSchema: Aglyn.ComponentSchema<FormFieldProps> = {\n $id: FORM_FIELD_ID,\n pluginId: BUNDLE_ID,\n displayName: 'Form Field',\n description:\n 'One input inside a form — text, select, radio, checkbox or rating.',\n category: Aglyn.ComponentCategory.FORMS,\n icon: { path: mdiFormTextbox.path, sx: { color: 'info.main' } },\n flags: {\n selfClosing: Aglyn.FEATURE_FLAG.ENABLED,\n },\n attributes: [\n {\n name: 'fieldName',\n description: 'Key this value is stored under in submissions.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n label: 'Field name',\n },\n {\n name: 'datasetFieldId',\n description:\n \"Schema field of the form's dataset this value is stored under. \" +\n 'Stored by id, so renaming the field never breaks the mapping ' +\n '(AGL-556). When unset, values match dataset fields by name.',\n component: Aglyn.FieldComponentType.DATASET_FIELD_SELECT,\n label: 'Maps to schema field',\n },\n {\n name: 'label',\n description: 'Visible input label.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n label: 'Label',\n },\n {\n name: 'placeholder',\n description:\n 'Grey hint inside the empty input — \"you@company.com\" (AGL-1330). ' +\n 'It is not a label: it disappears as soon as the visitor types, so ' +\n 'always keep Label set. Ignored by radio, checkbox, and rating ' +\n 'fields. Clearing it removes the hint.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n label: 'Placeholder',\n },\n {\n name: 'fieldType',\n description: 'Input type.',\n component: Aglyn.FieldComponentType.SELECT,\n label: 'Type',\n // `text` is the first member of this prop's own union, and the value\n // the render falls back to. As `''` it was the one field type an\n // author could not choose and keep (AGL-1191/AGL-1451): a field\n // switched to Email or Multiline could never be made plain text\n // again without deleting it.\n options: [\n { value: 'text', label: 'Text' },\n { value: 'email', label: 'Email' },\n { value: 'textarea', label: 'Multiline' },\n { value: 'select', label: 'Dropdown' },\n { value: 'radio', label: 'Radio choice' },\n { value: 'checkbox', label: 'Checkboxes' },\n { value: 'rating', label: 'Star rating' },\n ],\n },\n {\n name: 'options',\n description:\n 'Choices for dropdown, radio, and checkbox fields — one per line ' +\n '(or comma-separated). Ignored by the other types.',\n component: Aglyn.FieldComponentType.TEXTAREA,\n label: 'Options',\n },\n {\n name: 'required',\n description: 'Whether the field must be filled.',\n component: Aglyn.FieldComponentType.SWITCH,\n label: 'Required?',\n },\n ],\n}\n\nexport const formPresets: Aglyn.PresetSchema[] = [\n {\n $id: generatePresetId(FORM_ID, 'contact'),\n type: Aglyn.NodeType.PRESET,\n displayName: 'Contact Form',\n pluginId: BUNDLE_ID,\n description: 'Name, email, and message with a send button',\n category: Aglyn.ComponentCategory.FORMS,\n icon: { path: mdiEmailFastOutline.path, sx: { color: 'info.main' } },\n data: {\n $id: null,\n componentId: FORM_ID,\n pluginId: BUNDLE_ID,\n props: { formName: 'Contact' },\n nodes: [\n {\n $id: null,\n componentId: FORM_FIELD_ID,\n pluginId: BUNDLE_ID,\n props: { fieldName: 'name', label: 'Name', required: true },\n },\n {\n $id: null,\n componentId: FORM_FIELD_ID,\n pluginId: BUNDLE_ID,\n props: {\n fieldName: 'email',\n label: 'Email',\n fieldType: 'email',\n required: true,\n },\n },\n {\n $id: null,\n componentId: FORM_FIELD_ID,\n pluginId: BUNDLE_ID,\n props: {\n fieldName: 'message',\n label: 'Message',\n fieldType: 'textarea',\n required: true,\n },\n },\n ],\n },\n },\n {\n $id: generatePresetId(FORM_FIELD_ID),\n type: Aglyn.NodeType.PRESET,\n displayName: 'Form Field',\n pluginId: BUNDLE_ID,\n description: 'Single input inside a form',\n category: Aglyn.ComponentCategory.FORMS,\n icon: { path: mdiFormTextbox.path, sx: { color: 'info.main' } },\n data: {\n $id: null,\n componentId: FORM_FIELD_ID,\n pluginId: BUNDLE_ID,\n props: { fieldName: 'field', label: 'Field' },\n },\n },\n /*\n * The marketing consent field as one preset rather than a recipe. Its props\n * are the platform's `MARKETING_CONSENT_FORM_FIELD`, which every place that adds\n * this field reads, so each adds the same unticked one-option checkbox. It\n * records consent once the form names it as its Marketing consent field.\n */\n {\n $id: generatePresetId(FORM_FIELD_ID, 'marketing-consent'),\n type: Aglyn.NodeType.PRESET,\n displayName: 'Marketing consent',\n pluginId: BUNDLE_ID,\n description: 'An unticked checkbox asking permission to send marketing emails',\n category: Aglyn.ComponentCategory.FORMS,\n icon: { path: mdiFormTextbox.path, sx: { color: 'info.main' } },\n data: {\n $id: null,\n componentId: FORM_FIELD_ID,\n pluginId: BUNDLE_ID,\n props: { ...Aglyn.MARKETING_CONSENT_FORM_FIELD },\n },\n },\n]\n\n/**\n * The composed section a form usually arrives inside.\n *\n * It lives here rather than with the generic block library because it PLACES\n * form nodes, and a preset's `data` carries the `pluginId` those nodes are\n * saved with — so authoring it anywhere else stamps every contact form on\n * every new site with the wrong bundle. The heading and the stack around it\n * name `mui`, which is correct: they ARE mui elements, and a page holding this\n * section needs both bundles registered before it paints.\n */\nexport const formBlockPresets: Aglyn.PresetSchema[] = [\n {\n $id: generatePresetId(FORM_ID, 'contact-section'),\n type: Aglyn.NodeType.PRESET,\n displayName: 'Contact Section',\n pluginId: BUNDLE_ID,\n description: 'Heading with a name/email/message form',\n category: Aglyn.ComponentCategory.BLOCKS,\n icon: { path: mdiEmailOutline.path, sx: { color: 'info.main' } },\n data: {\n $id: null,\n componentId: 'muiStack',\n pluginId: MUI_BUNDLE_ID,\n props: { spacing: 2 },\n sx: {\n paddingLeft: 4,\n paddingRight: 4,\n paddingTop: 6,\n paddingBottom: 6,\n maxWidth: 560,\n },\n nodes: [\n {\n $id: null,\n componentId: 'muiTypography',\n pluginId: MUI_BUNDLE_ID,\n props: { variant: 'h4', children: 'Get in touch' },\n },\n {\n $id: null,\n componentId: FORM_ID,\n pluginId: BUNDLE_ID,\n props: {\n formName: 'Contact',\n submitLabel: 'Send message',\n successMessage: 'Thanks — we will get back to you soon.',\n },\n nodes: [\n contactField('name', 'Name'),\n contactField('email', 'Email', { fieldType: 'email' }),\n contactField('message', 'Message', { fieldType: 'textarea' }),\n ],\n },\n ],\n },\n },\n]\n\nfunction contactField(fieldName: string, label: string, extra?: object) {\n return {\n $id: null,\n componentId: FORM_FIELD_ID,\n pluginId: BUNDLE_ID,\n props: { fieldName, label, required: true, ...extra },\n }\n}\n\nexport { Form, FormField }\nexport default Form\n"],"names":["sendAnalyticsBeacon","trackEventBeforeNavigation","utmTouchField","Aglyn","parseFormUnavailableRefusal","mdiEmailFastOutline","mdiEmailOutline","mdiFormTextbox","isSameOriginPath","Alert","Button","Checkbox","FormControl","FormControlLabel","FormGroup","FormHelperText","FormLabel","Link","MenuItem","Radio","RadioGroup","Rating","Stack","TextField","createContext","forwardRef","useCallback","useContext","useEffect","useRef","useState","BUNDLE_ID","generatePresetId","MUI_BUNDLE_ID","FORM_ID","FORM_FIELD_ID","FIELD_MAP_INPUT_PREFIX","CONSENT_GROUP_INPUT","FormConsentDisclosureContext","readFormConsentDisclosure","body","value","text","trim","key","fieldName","sanitizeRedirectUrl","url","trimmed","undefined","test","resolveRedirectTarget","screenHref","redirectUrl","REVEAL_NODE_ID_PATTERN","formNavigation","assign","window","location","FORM_SUBMITTED_EVENT","sendFormBeacon","hostId","formId","event","form","Form","props","ref","formName","datasetId","_datasetId","datasetName","_datasetName","datasetBindingToken","submitLabel","successMessage","afterSubmit","redirectScreenId","revealNodeId","children","rest","useSite","siteFetch","useSiteFetch","href","redirectScreenHref","suppressNavigation","useScreenLink","revealSelector","status","setStatus","reported","view","start","current","handleFirstInput","pausedMessage","setPausedMessage","unavailable","setUnavailable","alerts","setAlerts","disclosure","setDisclosure","query","URLSearchParams","toString","then","response","ok","json","catch","handleSubmit","preventDefault","data","FormData","currentTarget","fields","website","consentGroup","entries","startsWith","slice","method","headers","JSON","stringify","datasetBinding","path","pathname","payload","Array","isArray","dispatchEvent","CustomEvent","detail","form_name","form_location","target","isPreviewRefusal","paused","parseLockdownRefusal","lockdownRefusalText","notice","severity","component","spacing","onSubmit","onInput","style","Provider","input","type","name","tabIndex","autoComplete","aria-hidden","position","left","height","width","message","contact","map","alert","index","variant","disabled","sx","alignSelf","displayName","parseFieldOptions","options","split","entry","filter","Boolean","FormField","datasetFieldId","label","placeholder","fieldType","required","fieldLabel","choices","hint","hintSlotProps","inputLabel","shrink","notched","checkedCount","setCheckedCount","rating","setRating","mapInput","readOnly","describesOptIn","isMarketingConsentFieldName","consentNote","select","defaultValue","fullWidth","size","slotProps","displayEmpty","renderValue","String","span","opacity","choice","control","onChange","count","checked","_event","multiline","minRows","formSchema","$id","pluginId","description","category","ComponentCategory","FORMS","icon","color","attributes","FieldComponentType","FORM_SELECT","TEXT_FIELD","DATASET_SELECT","condition","when","isNotEmpty","SELECT","SCREEN_SELECT","is","NODE_SELECT","formFieldSchema","flags","selfClosing","FEATURE_FLAG","ENABLED","DATASET_FIELD_SELECT","TEXTAREA","SWITCH","formPresets","NodeType","PRESET","componentId","nodes","MARKETING_CONSENT_FORM_FIELD","formBlockPresets","BLOCKS","paddingLeft","paddingRight","paddingTop","paddingBottom","maxWidth","contactField","extra"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/forms/src/lib/components/form.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { sendAnalyticsBeacon } from '@aglyn/aglyn/app-utils/analytics-beacon'\nimport { trackEventBeforeNavigation } from '@aglyn/aglyn/app-utils/analytics-events'\nimport { recordSiteJourneyStep, siteJourneyField } from '@aglyn/aglyn/app-utils/site-journey'\nimport { utmTouchField } from '@aglyn/aglyn/app-utils/utm-touch'\nimport * as Aglyn from '@aglyn/aglyn'\nimport {\n type FormUnavailableNotice,\n parseFormUnavailableRefusal,\n} from '../model/form-unavailable'\nimport {\n mdiEmailFastOutline,\n mdiEmailOutline,\n mdiFormTextbox,\n} from '@aglyn/shared-data-mdi'\nimport { isSameOriginPath } from '@aglyn/shared-util-http/safe-redirect'\nimport Alert from '@mui/material/Alert'\nimport Button from '@mui/material/Button'\nimport Checkbox from '@mui/material/Checkbox'\nimport FormControl from '@mui/material/FormControl'\nimport FormControlLabel from '@mui/material/FormControlLabel'\nimport FormGroup from '@mui/material/FormGroup'\nimport FormHelperText from '@mui/material/FormHelperText'\nimport FormLabel from '@mui/material/FormLabel'\nimport Link from '@mui/material/Link'\nimport MenuItem from '@mui/material/MenuItem'\nimport Radio from '@mui/material/Radio'\nimport RadioGroup from '@mui/material/RadioGroup'\nimport Rating from '@mui/material/Rating'\nimport Stack from '@mui/material/Stack'\nimport TextField from '@mui/material/TextField'\nimport {\n createContext,\n type FormEvent,\n forwardRef,\n useCallback,\n useContext,\n useEffect,\n useRef,\n useState,\n} from 'react'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport { generatePresetId } from '../utils/generate-preset-id'\n\n/*\n * The heading and the stack this block's form sits inside are mui elements,\n * so the nodes that carry them say so. A node's `pluginId` names the bundle\n * that REGISTERS its component, not the bundle whose preset placed it.\n *\n * The id comes from the core's constant, as it does for every plugin that\n * composes mui elements: importing the mui plugin for one string would make\n * this plugin unusable without it on the import graph.\n */\nconst { MUI_BUNDLE_ID } = Aglyn\n\n// Component ids are persisted in screen documents; never rename.\nexport const FORM_ID: Aglyn.ComponentId = 'form'\nexport const FORM_FIELD_ID: Aglyn.ComponentId = 'formField'\n\n/** After-submit outcomes (AGL-557); message is the historical default. */\nexport type FormAfterSubmit = 'message' | 'redirect' | 'reveal'\n\nexport interface FormProps {\n /**\n * The form entity this node submits as (`docs/specs/reusable-forms.md` §2c).\n *\n * THE identity. `formName` below is a caption and always was: it is copied\n * onto each submission at write time and reconciled with nothing, so\n * renaming a form split its history in two and two pages sharing a label\n * were one list. An id survives both.\n *\n * This is the whole binding. Reuse across pages needs nothing further —\n * promote the bound subtree once and the id travels inside the definition,\n * so every instance writes the same form's submissions without the author\n * retyping a label. Two instances of one definition are ONE form, which is\n * the point; an author who wants two forms makes two forms.\n */\n formId?: string\n /** Identifies the form in the submissions inbox. */\n formName?: string\n /**\n * Dataset (by id) submissions also append into (AGL-556); the inbox\n * copy is always written. Renaming the dataset never breaks the binding.\n */\n datasetId?: string\n /**\n * Legacy dataset-by-name binding (AGL-141), honored when no `datasetId`\n * is set; kept so persisted nodes keep working.\n */\n datasetName?: string\n /**\n * The dataset binding above, signed by the compose that rendered this form\n * (AGL-2773). The submit route writes a record only where this says, so the\n * page, not the request, decides which dataset a submission reaches. Present\n * on the published site only; the canvas and Preview never submit.\n */\n datasetBindingToken?: string\n submitLabel?: string\n successMessage?: string\n /**\n * What a successful submit does (AGL-557): show the success message\n * (default), redirect the visitor, or reveal a hidden element.\n */\n afterSubmit?: FormAfterSubmit\n /** Redirect target screen — rename-safe id, resolved like ScreenLink. */\n redirectScreenId?: string\n /**\n * Manual redirect target used when no screen is picked: a same-site\n * `/path` or an absolute https URL (everything else is dropped).\n */\n redirectUrl?: string\n /** Node revealed on submit; hidden until then (afterSubmit=reveal). */\n revealNodeId?: string\n children?: React.ReactNode\n}\n\n/**\n * Hidden-input prefix a FormField uses to publish its field → dataset\n * schema-field mapping to the enclosing Form (AGL-556). Rides the DOM so\n * arbitrary nesting between Form and field needs no React context; the\n * `__` prefix keeps it out of the submitted fields.\n */\nexport const FIELD_MAP_INPUT_PREFIX = '__map__'\n\n/**\n * The hidden input the field carrying the consent-group sentence posts its\n * key under (AGL-3320). `__`-prefixed like the map inputs, so it never\n * reaches the submitted fields; the Form lifts it into the request body as\n * `__consentGroup`, where the submit route reads it.\n *\n * Posted by the FIELD, not the Form: the key is proof the sentence was on\n * screen, and the field that rendered it is the only thing that knows it was.\n * A form whose consent field never mounted posts no key, and its opt-in is\n * recorded for the one site the visitor is on.\n */\nexport const CONSENT_GROUP_INPUT = '__consentGroup'\n\n/** The site's consent-group sentence, as `/api/consent/disclosure` answers it. */\nexport interface FormConsentDisclosure {\n /**\n * The field the form declares as its opt-in, or `null` for a form that\n * declares none — the sentence then sits under whichever field the submit\n * route would read an opt-in from by its name.\n */\n fieldName: string | null\n /** What the person is told they are agreeing to hear from. */\n text: string\n /** The key of exactly this sentence, posted back with the submission. */\n key: string\n}\n\n/**\n * The disclosure a Form fetched, handed to the fields inside it. A context\n * because the answer flows DOWN, from the one request the Form makes to the\n * one field it describes; the key flows back up through the DOM, like the\n * dataset mapping does.\n */\nconst FormConsentDisclosureContext =\n createContext<FormConsentDisclosure | null>(null)\n\n/**\n * The route's answer, or `null` when it has no sentence for this site — a\n * site that sends on its own, or an answer that is not the shape it should\n * be. `null` renders nothing and posts no key.\n */\nexport function readFormConsentDisclosure(\n body: unknown,\n): FormConsentDisclosure | null {\n const value = (body ?? {}) as Record<string, unknown>\n const text = typeof value['text'] === 'string' ? value['text'].trim() : ''\n const key = typeof value['key'] === 'string' ? value['key'].trim() : ''\n if (!text || !key) return null\n const fieldName =\n typeof value['fieldName'] === 'string' && value['fieldName'].trim()\n ? value['fieldName'].trim()\n : null\n return { fieldName, text, key }\n}\n\n/**\n * Redirect targets are restricted to same-origin paths (`/thanks`) and\n * absolute https URLs (AGL-557): stored props reach every visitor's\n * browser, so `javascript:`/`data:` URLs — and protocol-relative\n * `//host` forms, which keep the scheme but swap the host — never pass.\n *\n * The relative branch asks {@link isSameOriginPath} rather than reading the\n * string's first two characters (AGL-1881). This site never got the AGL-2486\n * backslash fix, and the character list would not have been enough anyway:\n * the URL parser deletes tab/LF/CR before parsing, so `/<TAB>/evil.com`\n * carries neither `//` nor `\\` and `window.location.assign` — which\n * `formNavigation` below calls — still lands the visitor on `evil.com`. The\n * absolute branch is unchanged: an explicit `https://` target is a documented\n * feature, and for absolute input the parser's verdict was never in doubt.\n */\nexport const sanitizeRedirectUrl = (url?: string): string | undefined => {\n const trimmed = (url ?? '').trim()\n if (!trimmed) return undefined\n if (/^https:\\/\\//i.test(trimmed)) return trimmed\n if (isSameOriginPath(trimmed)) return trimmed\n return undefined\n}\n\n/**\n * Where a redirect outcome sends the visitor: the picked screen's\n * resolved href wins; the manual URL is the fallback, sanitized above.\n * Undefined (deleted screen, bad URL) degrades to the message outcome.\n */\nexport const resolveRedirectTarget = (\n screenHref: string | undefined,\n redirectUrl: string | undefined,\n): string | undefined => screenHref ?? sanitizeRedirectUrl(redirectUrl)\n\n// Reveal ids are interpolated into a CSS selector — restrict to the id\n// alphabet so a stored prop can't smuggle selector syntax (AGL-557).\nconst REVEAL_NODE_ID_PATTERN = /^[A-Za-z0-9_-]+$/\n\n/**\n * Navigation seam for the redirect outcome: jsdom's `window.location`\n * is not patchable, so tests stub this indirection instead.\n */\nexport const formNavigation = {\n assign: (url: string) => window.location.assign(url),\n}\n\n/** DOM event dispatched on every successful submit (AGL-557). */\nexport const FORM_SUBMITTED_EVENT = 'aglyn:form-submitted'\n\n/**\n * Report that a bound form was seen, or typed into.\n *\n * Fire-and-forget, and deliberately the same instrument the overlay counters\n * use: `sendBeacon` hands the request to the browser, which delivers it\n * outside the page's lifetime, so nothing here can delay a render or survive\n * long enough to fail visibly.\n *\n * Requires a bound `formId`. An unbound form has no document to count on, and\n * an id is never invented: the collector's `update` would find nothing, which\n * is the right outcome but a pointless request.\n */\nexport function sendFormBeacon(\n hostId: string | undefined,\n formId: string | undefined,\n event: 'view' | 'start',\n): void {\n if (!hostId || !formId) return\n // Best-effort by construction: `sendAnalyticsBeacon` swallows everything and\n // refuses outright from a non-production build or a browser marked ours. A\n // view that does not report is a view this form's completion rate is\n // measured without, which the console says.\n sendAnalyticsBeacon({ hostId, formId, form: event })\n}\n\n/**\n * Lead-capture form (AGL-76): collects its field children's values and\n * posts them to the tenant's `/api/forms/submit` with the site's host id\n * from SiteContext. Without a site context (besigner canvas, preview) the\n * submit is inert, so editing never creates submissions. Includes a\n * honeypot input for naive bots.\n */\nconst Form = forwardRef<HTMLFormElement, FormProps>((props, ref) => {\n const {\n formId,\n formName,\n // Read by compose, which signs them into `datasetBindingToken`; taken out\n // here only so they never reach the DOM through `rest`.\n datasetId: _datasetId,\n datasetName: _datasetName,\n datasetBindingToken,\n submitLabel,\n successMessage,\n afterSubmit,\n redirectScreenId,\n redirectUrl,\n revealNodeId,\n children,\n ...rest\n } = props\n const { hostId } = Aglyn.useSite()\n const siteFetch = Aglyn.useSiteFetch()\n // Resolves the redirect screen like ScreenLink does (rename-safe) and\n // flags editing surfaces, where outcomes must never fire (AGL-557).\n const { href: redirectScreenHref, suppressNavigation } = Aglyn.useScreenLink(\n afterSubmit === 'redirect' ? redirectScreenId : undefined,\n )\n // Reveal target (AGL-557): hidden through a form-owned <style> tag so\n // SSR paints it hidden from the first frame; the style unmounts with\n // the form after a successful submit — which IS the reveal. Editing\n // surfaces keep the target visible so it stays editable.\n const revealSelector =\n afterSubmit === 'reveal' &&\n revealNodeId &&\n REVEAL_NODE_ID_PATTERN.test(revealNodeId) &&\n !suppressNavigation\n ? `[data-aglyn=\"leaf:${revealNodeId}\"]`\n : undefined\n const [status, setStatus] = useState<\n 'idle' | 'sending' | 'sent' | 'error' | 'preview' | 'paused' | 'unavailable'\n >(\n 'idle',\n )\n /*\n * THE VIEW AND THE START — the two facts a completion rate is taken over.\n *\n * Both are reported ONCE per mounted form, from a ref rather than from\n * state: a counter driven by a re-render would be a Firestore write per\n * keystroke, which is the per-render write this must not become. The ref\n * also survives the re-renders `status` causes, so a visitor who edits a\n * field, submits, and is shown an error is one view and one start.\n *\n * Gated on `!suppressNavigation`, the editing-surface flag: the besigner\n * canvas and the console's preview render this component for its author,\n * and counting an author looking at their own draft as a visitor view would\n * put the merchant into their own denominator.\n */\n const reported = useRef({ view: false, start: false })\n useEffect(() => {\n if (suppressNavigation || reported.current.view) return\n reported.current.view = true\n sendFormBeacon(hostId, formId, 'view')\n }, [hostId, formId, suppressNavigation])\n\n /**\n * A START is the first edit anyone makes to this form.\n *\n * On the form's own `input`, which bubbles from every field — no listener\n * registration, no per-field wiring, and nothing to keep in sync when a\n * field type is added. Focus would be the looser reading and the wrong one:\n * a form that is tabbed through and abandoned was never started, and\n * counting it would inflate the denominator abandonment is measured over.\n */\n const handleFirstInput = useCallback(() => {\n if (suppressNavigation || reported.current.start) return\n reported.current.start = true\n sendFormBeacon(hostId, formId, 'start')\n }, [hostId, formId, suppressNavigation])\n /** The read-only lockdown's own words (AGL-1511); never a hardcoded line. */\n const [pausedMessage, setPausedMessage] = useState('')\n /** The abuse ceiling's visitor notice (AGL-1666); see `unavailable` below. */\n const [unavailable, setUnavailable] =\n useState<FormUnavailableNotice | null>(null)\n const [alerts, setAlerts] = useState<\n Array<{ message: string; severity?: string }>\n >([])\n /*\n * THE CONSENT-GROUP SENTENCE, when this site sends as part of a group\n * (AGL-3320).\n *\n * Asked for on the rendered site only — the canvas has no site to ask\n * about — and handed to the fields, one of which renders it under the\n * opt-in and posts its key. Until the answer arrives, and whenever it has\n * none, nothing renders and nothing is posted: the opt-in is then recorded\n * for this site alone, which is the safe way to be early or wrong.\n */\n const [disclosure, setDisclosure] = useState<FormConsentDisclosure | null>(\n null,\n )\n useEffect(() => {\n if (!hostId) return\n let current = true\n const query = new URLSearchParams({ hostId, ...(formId ? { formId } : {}) })\n siteFetch(`/api/consent/disclosure?${query.toString()}`)\n .then((response) => (response.ok ? response.json() : null))\n .then((body) => {\n if (current) setDisclosure(readFormConsentDisclosure(body))\n })\n .catch(() => {\n if (current) setDisclosure(null)\n })\n return () => {\n current = false\n }\n }, [hostId, formId, siteFetch])\n\n const handleSubmit = useCallback(\n async (event: FormEvent<HTMLFormElement>) => {\n event.preventDefault()\n if (!hostId || status === 'sending') return\n const data = new FormData(event.currentTarget)\n const fields: Record<string, string> = {}\n let website = ''\n let consentGroup = ''\n for (const [key, value] of data.entries()) {\n if (typeof value !== 'string') continue\n if (key === 'website') {\n website = value\n continue\n }\n // The key the consent field posted beside the sentence it rendered.\n if (key === CONSENT_GROUP_INPUT) {\n consentGroup = value\n continue\n }\n // `__`-prefixed inputs are internal controls — the rating field's\n // star radios (AGL-544) and each field's dataset-mapping input — and\n // never submit.\n if (key.startsWith('__')) continue\n // Checkbox groups emit one entry per ticked box; join them under\n // the field name instead of letting the last one win (AGL-544).\n fields[key] = (\n key in fields ? `${fields[key]}, ${value}` : value\n ).slice(0, 2000)\n }\n setStatus('sending')\n try {\n const response = await siteFetch('/api/forms/submit', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n hostId,\n // The bound form entity, when there is one. Sent ALONGSIDE the\n // caption rather than instead of it: the caption keeps the\n // pre-entity `?form=` filter working for every form that has not\n // been adopted yet, and no phase of the migration removes it.\n ...(formId ? { formId } : {}),\n formName: formName || 'Form',\n // The page's signed dataset binding (AGL-2773). The server writes\n // a record only where it says, so nothing this request names\n // chooses a dataset or a field.\n ...(datasetBindingToken\n ? { datasetBinding: datasetBindingToken }\n : {}),\n path: window.location.pathname,\n fields,\n website,\n // Only when the consent field rendered the site's consent-group\n // sentence; the route pools the opt-in on nothing else.\n ...(consentGroup ? { [CONSENT_GROUP_INPUT]: consentGroup } : {}),\n // The campaign this visitor came from, when they came from one.\n // Absent entirely for direct traffic — never a placeholder, so\n // the server can tell \"arrived from nowhere\" from \"this door does\n // not report\".\n ...utmTouchField(),\n // The visit this submission ends, when the site records visits\n // for its funnels and the visitor's consent allows it (AGL-3605):\n // the server ties it to the address submitted, so a funnel's\n // drop-off follow-up can reach this person.\n ...siteJourneyField(),\n }),\n })\n if (response.ok) {\n // Site alerts from the actions builder (AGL-148).\n const payload = await response.json().catch(() => ({}))\n if (Array.isArray(payload?.alerts)) setAlerts(payload.alerts)\n // Announced for reveal-style listeners/analytics (AGL-557).\n window.dispatchEvent(\n new CustomEvent(FORM_SUBMITTED_EVENT, {\n detail: { formName: formName || 'Form' },\n }),\n )\n // GA4 lead conversion (AGL-1561). Inside `response.ok`, so it\n // measures a SUBMISSION, not a click and not a validation failure —\n // the distinction that makes a conversion rate mean anything.\n //\n // AWAITED, because the branch below navigates. Ordering the call\n // ahead of the `assign` is not on its own enough, and the obvious\n // reasoning about beacons is the wrong one: once a hit REACHES gtag\n // a navigation cannot destroy it, so `transport_type: 'beacon'`\n // fixes nothing. What is lost is a hit that never reaches gtag,\n // which is what happens when the surface's transport is async and\n // its initialization promise is still pending — the continuation is\n // scheduled behind the navigation task and never runs. The tenant\n // runtime registers no transport, so this resolves immediately and\n // costs the redirect nothing; it is written this way because the\n // property that makes a bare call safe is invisible from here.\n //\n // `formName` is author-written site content, never a field VALUE:\n // no submitted field ever reaches GA from here.\n //\n // This runs on every tenant site, not only aglyn.com, and that is\n // deliberate — gtag is loaded with whatever measurement id the HOST\n // configured, so a customer's form reports into the customer's own\n // property and aglyn.com's reports into ours. It is consent-gated\n // either way: without a grant the script never loaded (AGL-1498)\n // and the delivery path finds no `window.gtag` to call.\n // A funnel step (AGL-3605): the form, by its id. Dropped unless the\n // site records journeys and the visitor's consent grants.\n recordSiteJourneyStep('form', formId)\n await trackEventBeforeNavigation('generate_lead', {\n form_name: formName || 'Form',\n form_location: window.location.pathname,\n })\n if (afterSubmit === 'redirect' && !suppressNavigation) {\n const target = resolveRedirectTarget(\n redirectScreenHref,\n redirectUrl,\n )\n // No valid target (deleted screen, rejected URL) degrades to\n // the success message below.\n if (target) formNavigation.assign(target)\n }\n }\n // A refusal is not a failure (AGL-1249). Preview declines writes on\n // purpose, and \"something went wrong — please try again\" invites the\n // author to retry something that is working exactly as intended.\n //\n // Since AGL-1511 there are TWO deliberate 423s a submit can meet, so\n // the status alone no longer identifies one: Preview's client-side\n // refusal, and a read-only lockdown refusing a real visitor's write\n // on a live site. Preview's carries `preview: true` and is settled\n // first; anything else 423 is the lockdown pause, whose own copy is\n // rendered rather than the Preview sentence — telling a customer's\n // visitor \"this works on your published site\" would be nonsense on\n // the published site they are standing on.\n if (!response.ok) {\n const body = await response.json().catch(() => ({}))\n if (Aglyn.isPreviewRefusal(response, body)) {\n setStatus('preview')\n return\n }\n const paused = Aglyn.parseLockdownRefusal(response.status, body)\n if (paused) {\n setPausedMessage(Aglyn.lockdownRefusalText(paused))\n setStatus('paused')\n return\n }\n // The site's form-submission abuse ceiling (AGL-1655/1666). This\n // is a THIRD deliberate refusal, and the only one whose status is\n // already spoken for: the Free plan's monthly wall answers 429\n // too. So it is matched on the body's `code` and never on the\n // status — `parseFormUnavailableRefusal` takes no status for\n // exactly that reason. A plan-wall 429 keeps falling through to\n // the generic branch below, which is right: it is the owner's\n // billing problem, and nothing about waiting fixes it.\n const notice = parseFormUnavailableRefusal(body)\n if (notice) {\n setUnavailable(notice)\n setStatus('unavailable')\n return\n }\n }\n setStatus(response.ok ? 'sent' : 'error')\n } catch {\n setStatus('error')\n }\n },\n [\n hostId,\n status,\n formId,\n formName,\n datasetBindingToken,\n afterSubmit,\n redirectScreenHref,\n redirectUrl,\n suppressNavigation,\n siteFetch,\n ],\n )\n\n if (status === 'sent') {\n // Reveal outcome: the form (with its hide-style below) unmounts and\n // the revealed element is the confirmation. Redirect keeps the\n // message visible as a fallback while the browser navigates.\n if (revealSelector) return null\n return (\n <Alert severity=\"success\">\n {successMessage || 'Thanks — your message has been sent.'}\n </Alert>\n )\n }\n\n return (\n <Stack\n ref={ref}\n component=\"form\"\n spacing={2}\n onSubmit={handleSubmit}\n onInput={handleFirstInput}\n {...rest}\n >\n {/* Hides the reveal target until submit (AGL-557); rendered by the\n form so SSR ships it hidden — no flash, no target-side props.\n MUI Stack spacing already skips <style> children. */}\n {revealSelector ? (\n <style>{`${revealSelector}{display:none !important}`}</style>\n ) : null}\n <FormConsentDisclosureContext.Provider value={disclosure}>\n {children}\n </FormConsentDisclosureContext.Provider>\n {/* Honeypot: humans never see or fill this. */}\n <input\n type=\"text\"\n name=\"website\"\n tabIndex={-1}\n autoComplete=\"off\"\n aria-hidden=\"true\"\n style={{ position: 'absolute', left: '-5000px', height: 0, width: 0 }}\n />\n {status === 'error' ? (\n <Alert severity=\"error\">\n {'Something went wrong — please try again.'}\n </Alert>\n ) : null}\n {status === 'paused' ? (\n // `info`, like the Preview notice and for the same reason: this is a\n // deliberate, temporary pause on a site that is otherwise working,\n // and nothing the visitor typed was lost.\n <Alert severity=\"info\">{pausedMessage}</Alert>\n ) : null}\n {status === 'unavailable' && unavailable ? (\n // `warning`, not `info` and not `error` (AGL-1666). The two `info`\n // notices above are both cases where nothing was lost — Preview\n // never intended to send, and a lockdown pause is minutes long with\n // the visitor's text still in the fields. This one lost a real\n // message on a live site, so it must not read as reassurance. It is\n // not `error` either: nothing is broken and \"please try again\" would\n // walk the visitor into the same refusal.\n <Alert severity=\"warning\">\n {unavailable.message}\n {unavailable.contact ? (\n <>\n {' In the meantime you can reach us at '}\n <Link href={`mailto:${unavailable.contact}`}>\n {unavailable.contact}\n </Link>\n {'.'}\n </>\n ) : null}\n </Alert>\n ) : null}\n {status === 'preview' ? (\n // Deliberately `info`, not `error`, and it does not say \"try again\":\n // nothing failed, and the same submit works on the published site.\n <Alert severity=\"info\">\n {'Preview does not send form submissions. This form works on your ' +\n 'published site.'}\n </Alert>\n ) : null}\n {alerts.map((alert, index) => (\n <Alert key={index} severity={(alert.severity as any) || 'info'}>\n {alert.message}\n </Alert>\n ))}\n <Button\n type=\"submit\"\n variant=\"contained\"\n disabled={status === 'sending'}\n sx={{ alignSelf: 'flex-start' }}\n >\n {status === 'sending' ? 'Sending…' : submitLabel || 'Send'}\n </Button>\n </Stack>\n )\n})\nForm.displayName = 'AglynForm'\n\nexport interface FormFieldProps {\n /** Submission key; also the input's name attribute. */\n fieldName?: string\n /**\n * Stable model fieldId of the parent form's dataset this value is\n * stored under (AGL-556). When unset, values match dataset fields by\n * name — the legacy behavior.\n */\n datasetFieldId?: string\n label?: string\n /**\n * Grey hint shown inside the empty input (AGL-1330) — the frames pair a\n * label above the control with a placeholder inside it. A placeholder is\n * NOT a label: it disappears the moment the visitor types, so the label\n * stays the field's accessible name and is never replaced by this.\n * Ignored by radio, checkbox, and rating fields, which have no text input\n * to hint inside of.\n */\n placeholder?: string\n fieldType?:\n | 'text'\n | 'email'\n | 'textarea'\n | 'select'\n | 'radio'\n | 'checkbox'\n | 'rating'\n /**\n * Choice list for select/radio/checkbox fields (AGL-544): newline- or\n * comma-separated. Ignored by the other types.\n */\n options?: string\n required?: boolean\n}\n\n/**\n * Splits a newline- or comma-separated choice list into trimmed,\n * non-empty entries (AGL-544).\n */\nexport const parseFieldOptions = (options?: string): string[] =>\n (options || '')\n .split(/[\\n,]/)\n .map((entry) => entry.trim())\n .filter(Boolean)\n\n/**\n * Single input inside a Form (AGL-76). Survey field types — select,\n * radio, checkbox group, and star rating — are AGL-544.\n */\nconst FormField = forwardRef<HTMLDivElement, FormFieldProps>((props, ref) => {\n const {\n fieldName,\n datasetFieldId,\n label,\n placeholder,\n fieldType,\n options,\n required,\n ...rest\n } = props\n const name = fieldName || 'field'\n const fieldLabel = label || fieldName || 'Field'\n const choices = parseFieldOptions(options)\n // A CLEARED placeholder must render as no placeholder (AGL-1330). The\n // attributes form persists a cleared text field by dropping the key\n // (ddf maps an emptied field to its clearedValue and final-form's parse\n // turns `''` into `undefined`), and `updateNodeProps` REPLACES the props\n // object — so cleared reaches us as absent. Older documents can still\n // carry `''`/`null`, and a blank string would otherwise reach the DOM as\n // an empty `placeholder` attribute, so blank is normalized to absent too.\n const hint = typeof placeholder === 'string' && placeholder.trim()\n ? placeholder\n : undefined\n // MUI hides a placeholder for as long as the label still sits inside the\n // box, so the hint is only visible once the label is shrunk — and an\n // outlined input with a shrunk label must notch its outline or the label\n // crosses the border. Both are applied ONLY when there is a placeholder,\n // so fields without one keep the historical floating-label behavior.\n const hintSlotProps = hint\n ? { inputLabel: { shrink: true }, input: { notched: true } }\n : undefined\n // Checkbox groups count ticked boxes so \"required\" can mean \"at least\n // one\": native `required` is only asserted while none are ticked.\n const [checkedCount, setCheckedCount] = useState(0)\n // Rating is controlled so a hidden input can serialize the number.\n const [rating, setRating] = useState<number | null>(null)\n // Publishes this field's dataset schema-field mapping to the enclosing\n // Form via the DOM (AGL-556); `__`-prefixed, so never a submitted field.\n const mapInput = datasetFieldId ? (\n <input\n type=\"hidden\"\n name={`${FIELD_MAP_INPUT_PREFIX}${name}`}\n value={datasetFieldId}\n readOnly\n />\n ) : null\n /*\n * THE CONSENT-GROUP SENTENCE, under the field that records the opt-in\n * (AGL-3320): the field the form declares, or — where it declares none —\n * one the submit route reads an opt-in from by its name. The key rides\n * beside it, so a submission carries the key only while the sentence is\n * on screen.\n */\n const disclosure = useContext(FormConsentDisclosureContext)\n const describesOptIn = disclosure\n ? disclosure.fieldName\n ? disclosure.fieldName === name\n : Aglyn.isMarketingConsentFieldName(name)\n : false\n const consentNote =\n disclosure && describesOptIn ? (\n <>\n <FormHelperText>{disclosure.text}</FormHelperText>\n <input\n type=\"hidden\"\n name={CONSENT_GROUP_INPUT}\n value={disclosure.key}\n readOnly\n />\n </>\n ) : null\n\n if (fieldType === 'select') {\n return (\n <>\n <TextField\n ref={ref}\n select\n name={name}\n label={fieldLabel}\n required={Boolean(required)}\n defaultValue=\"\"\n fullWidth\n size=\"small\"\n // A Select has no native placeholder: the hint is what the closed\n // control displays while nothing is chosen, which needs\n // `displayEmpty` (MUI renders nothing for `''` without it).\n slotProps={\n hint\n ? {\n ...hintSlotProps,\n select: {\n displayEmpty: true,\n renderValue: (value: unknown) =>\n value ? (\n String(value)\n ) : (\n <span style={{ opacity: 0.6 }}>{hint}</span>\n ),\n },\n }\n : undefined\n }\n {...rest}\n >\n {choices.map((choice, index) => (\n <MenuItem key={index} value={choice}>\n {choice}\n </MenuItem>\n ))}\n </TextField>\n {mapInput}\n {consentNote}\n </>\n )\n }\n\n if (fieldType === 'radio') {\n return (\n <FormControl ref={ref} required={Boolean(required)} {...rest}>\n <FormLabel>{fieldLabel}</FormLabel>\n <RadioGroup name={name}>\n {choices.map((choice, index) => (\n <FormControlLabel\n key={index}\n value={choice}\n label={choice}\n control={<Radio size=\"small\" required={Boolean(required)} />}\n />\n ))}\n </RadioGroup>\n {mapInput}\n {consentNote}\n </FormControl>\n )\n }\n\n if (fieldType === 'checkbox') {\n return (\n <FormControl ref={ref} required={Boolean(required)} {...rest}>\n <FormLabel>{fieldLabel}</FormLabel>\n <FormGroup>\n {choices.map((choice, index) => (\n <FormControlLabel\n key={index}\n label={choice}\n control={\n <Checkbox\n name={name}\n value={choice}\n size=\"small\"\n required={Boolean(required) && checkedCount === 0}\n onChange={(event) =>\n setCheckedCount(\n (count) => count + (event.target.checked ? 1 : -1),\n )\n }\n />\n }\n />\n ))}\n </FormGroup>\n {mapInput}\n {consentNote}\n </FormControl>\n )\n }\n\n if (fieldType === 'rating') {\n return (\n <FormControl ref={ref} required={Boolean(required)} {...rest}>\n <FormLabel>{fieldLabel}</FormLabel>\n <Rating\n // `__`-prefixed so the star radios themselves are skipped by\n // Form.handleSubmit; the hidden input carries the value.\n name={`__${name}`}\n value={rating}\n onChange={(_event, value) => setRating(value)}\n />\n <input type=\"hidden\" name={name} value={rating ?? ''} />\n {mapInput}\n {consentNote}\n </FormControl>\n )\n }\n\n return (\n <>\n <TextField\n ref={ref}\n name={name}\n label={fieldLabel}\n placeholder={hint}\n type={fieldType === 'email' ? 'email' : 'text'}\n required={Boolean(required)}\n multiline={fieldType === 'textarea'}\n minRows={fieldType === 'textarea' ? 3 : undefined}\n fullWidth\n size=\"small\"\n slotProps={hintSlotProps}\n {...rest}\n />\n {mapInput}\n {consentNote}\n </>\n )\n})\nFormField.displayName = 'AglynFormField'\n\nexport const formSchema: Aglyn.ComponentSchema<FormProps> = {\n $id: FORM_ID,\n pluginId: BUNDLE_ID,\n displayName: 'Form',\n description:\n 'Collects its Form Fields and sends the answers to you, or into a dataset.',\n category: Aglyn.ComponentCategory.FORMS,\n icon: { path: mdiEmailFastOutline.path, sx: { color: 'info.main' } },\n attributes: [\n {\n name: 'formId',\n description:\n 'The form this collects for, from the Forms page. Stored by id, so ' +\n 'renaming the form never splits its submission history and two ' +\n 'pages placing the same form share one list. Leave unset to keep ' +\n 'filing submissions under the name below.',\n component: Aglyn.FieldComponentType.FORM_SELECT,\n label: 'Form',\n },\n {\n name: 'formName',\n description:\n 'Caption shown in the submissions inbox, used when no form above ' +\n 'is picked. A name is not an identity: renaming it splits the ' +\n 'history of everything already submitted under the old one.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n label: 'Form name',\n },\n {\n name: 'datasetId',\n description:\n 'Optional dataset (from the Data page) submissions are appended ' +\n 'to. Stored by id, so renaming the dataset never breaks the ' +\n 'binding (AGL-556). The inbox always gets a copy.',\n component: Aglyn.FieldComponentType.DATASET_SELECT,\n label: 'Write to dataset',\n },\n {\n name: 'datasetName',\n description:\n 'Legacy name-based dataset binding — matched against the ' +\n \"dataset's display name at submit time. Pick the dataset above \" +\n 'and clear this; it only shows while a name is still set.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n label: 'Write to dataset (legacy name)',\n condition: { when: 'datasetName', isNotEmpty: true },\n },\n {\n name: 'submitLabel',\n description: 'Label of the submit button.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n label: 'Submit label',\n },\n {\n name: 'successMessage',\n description: 'Shown after a successful submission.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n label: 'Success message',\n },\n // After-submit outcomes (AGL-557).\n {\n name: 'afterSubmit',\n description:\n 'What a successful submit does: show the success message, send ' +\n 'the visitor to another page, or reveal a hidden element.',\n component: Aglyn.FieldComponentType.SELECT,\n label: 'After submit',\n // `message` is the first member of the declared `FormAfterSubmit`\n // union, so the sentinel already existed in the type and only the\n // option list was spelling it `''` (AGL-1451) — a value the\n // attributes form strips on change (AGL-1191), which meant a form\n // switched to Redirect could not be switched back to its message.\n options: [\n { value: 'message', label: 'Show the success message' },\n { value: 'redirect', label: 'Redirect the visitor' },\n { value: 'reveal', label: 'Reveal a hidden element' },\n ],\n },\n {\n name: 'redirectScreenId',\n description:\n 'Page the visitor lands on after submitting. The address is ' +\n \"resolved at render time, so renaming the page's slug never \" +\n 'breaks the redirect.',\n component: Aglyn.FieldComponentType.SCREEN_SELECT,\n label: 'Redirect to page',\n condition: { when: 'afterSubmit', is: 'redirect' },\n },\n {\n name: 'redirectUrl',\n description:\n 'Used only when no page is selected above: a same-site path ' +\n '(/thanks) or an https URL. Anything else is ignored.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n label: 'Redirect URL',\n condition: { when: 'afterSubmit', is: 'redirect' },\n },\n {\n name: 'revealNodeId',\n description:\n 'Element shown after a successful submit — it stays hidden on ' +\n 'the published page until then.',\n component: Aglyn.FieldComponentType.NODE_SELECT,\n label: 'Element to reveal',\n condition: { when: 'afterSubmit', is: 'reveal' },\n },\n ],\n}\n\nexport const formFieldSchema: Aglyn.ComponentSchema<FormFieldProps> = {\n $id: FORM_FIELD_ID,\n pluginId: BUNDLE_ID,\n displayName: 'Form Field',\n description:\n 'One input inside a form — text, select, radio, checkbox or rating.',\n category: Aglyn.ComponentCategory.FORMS,\n icon: { path: mdiFormTextbox.path, sx: { color: 'info.main' } },\n flags: {\n selfClosing: Aglyn.FEATURE_FLAG.ENABLED,\n },\n attributes: [\n {\n name: 'fieldName',\n description: 'Key this value is stored under in submissions.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n label: 'Field name',\n },\n {\n name: 'datasetFieldId',\n description:\n \"Schema field of the form's dataset this value is stored under. \" +\n 'Stored by id, so renaming the field never breaks the mapping ' +\n '(AGL-556). When unset, values match dataset fields by name.',\n component: Aglyn.FieldComponentType.DATASET_FIELD_SELECT,\n label: 'Maps to schema field',\n },\n {\n name: 'label',\n description: 'Visible input label.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n label: 'Label',\n },\n {\n name: 'placeholder',\n description:\n 'Grey hint inside the empty input — \"you@company.com\" (AGL-1330). ' +\n 'It is not a label: it disappears as soon as the visitor types, so ' +\n 'always keep Label set. Ignored by radio, checkbox, and rating ' +\n 'fields. Clearing it removes the hint.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n label: 'Placeholder',\n },\n {\n name: 'fieldType',\n description: 'Input type.',\n component: Aglyn.FieldComponentType.SELECT,\n label: 'Type',\n // `text` is the first member of this prop's own union, and the value\n // the render falls back to. As `''` it was the one field type an\n // author could not choose and keep (AGL-1191/AGL-1451): a field\n // switched to Email or Multiline could never be made plain text\n // again without deleting it.\n options: [\n { value: 'text', label: 'Text' },\n { value: 'email', label: 'Email' },\n { value: 'textarea', label: 'Multiline' },\n { value: 'select', label: 'Dropdown' },\n { value: 'radio', label: 'Radio choice' },\n { value: 'checkbox', label: 'Checkboxes' },\n { value: 'rating', label: 'Star rating' },\n ],\n },\n {\n name: 'options',\n description:\n 'Choices for dropdown, radio, and checkbox fields — one per line ' +\n '(or comma-separated). Ignored by the other types.',\n component: Aglyn.FieldComponentType.TEXTAREA,\n label: 'Options',\n },\n {\n name: 'required',\n description: 'Whether the field must be filled.',\n component: Aglyn.FieldComponentType.SWITCH,\n label: 'Required?',\n },\n ],\n}\n\nexport const formPresets: Aglyn.PresetSchema[] = [\n {\n $id: generatePresetId(FORM_ID, 'contact'),\n type: Aglyn.NodeType.PRESET,\n displayName: 'Contact Form',\n pluginId: BUNDLE_ID,\n description: 'Name, email, and message with a send button',\n category: Aglyn.ComponentCategory.FORMS,\n icon: { path: mdiEmailFastOutline.path, sx: { color: 'info.main' } },\n data: {\n $id: null,\n componentId: FORM_ID,\n pluginId: BUNDLE_ID,\n props: { formName: 'Contact' },\n nodes: [\n {\n $id: null,\n componentId: FORM_FIELD_ID,\n pluginId: BUNDLE_ID,\n props: { fieldName: 'name', label: 'Name', required: true },\n },\n {\n $id: null,\n componentId: FORM_FIELD_ID,\n pluginId: BUNDLE_ID,\n props: {\n fieldName: 'email',\n label: 'Email',\n fieldType: 'email',\n required: true,\n },\n },\n {\n $id: null,\n componentId: FORM_FIELD_ID,\n pluginId: BUNDLE_ID,\n props: {\n fieldName: 'message',\n label: 'Message',\n fieldType: 'textarea',\n required: true,\n },\n },\n ],\n },\n },\n {\n $id: generatePresetId(FORM_FIELD_ID),\n type: Aglyn.NodeType.PRESET,\n displayName: 'Form Field',\n pluginId: BUNDLE_ID,\n description: 'Single input inside a form',\n category: Aglyn.ComponentCategory.FORMS,\n icon: { path: mdiFormTextbox.path, sx: { color: 'info.main' } },\n data: {\n $id: null,\n componentId: FORM_FIELD_ID,\n pluginId: BUNDLE_ID,\n props: { fieldName: 'field', label: 'Field' },\n },\n },\n /*\n * The marketing consent field as one preset rather than a recipe. Its props\n * are the platform's `MARKETING_CONSENT_FORM_FIELD`, which every place that adds\n * this field reads, so each adds the same unticked one-option checkbox. It\n * records consent once the form names it as its Marketing consent field.\n */\n {\n $id: generatePresetId(FORM_FIELD_ID, 'marketing-consent'),\n type: Aglyn.NodeType.PRESET,\n displayName: 'Marketing consent',\n pluginId: BUNDLE_ID,\n description: 'An unticked checkbox asking permission to send marketing emails',\n category: Aglyn.ComponentCategory.FORMS,\n icon: { path: mdiFormTextbox.path, sx: { color: 'info.main' } },\n data: {\n $id: null,\n componentId: FORM_FIELD_ID,\n pluginId: BUNDLE_ID,\n props: { ...Aglyn.MARKETING_CONSENT_FORM_FIELD },\n },\n },\n]\n\n/**\n * The composed section a form usually arrives inside.\n *\n * It lives here rather than with the generic block library because it PLACES\n * form nodes, and a preset's `data` carries the `pluginId` those nodes are\n * saved with — so authoring it anywhere else stamps every contact form on\n * every new site with the wrong bundle. The heading and the stack around it\n * name `mui`, which is correct: they ARE mui elements, and a page holding this\n * section needs both bundles registered before it paints.\n */\nexport const formBlockPresets: Aglyn.PresetSchema[] = [\n {\n $id: generatePresetId(FORM_ID, 'contact-section'),\n type: Aglyn.NodeType.PRESET,\n displayName: 'Contact Section',\n pluginId: BUNDLE_ID,\n description: 'Heading with a name/email/message form',\n category: Aglyn.ComponentCategory.BLOCKS,\n icon: { path: mdiEmailOutline.path, sx: { color: 'info.main' } },\n data: {\n $id: null,\n componentId: 'muiStack',\n pluginId: MUI_BUNDLE_ID,\n props: { spacing: 2 },\n sx: {\n paddingLeft: 4,\n paddingRight: 4,\n paddingTop: 6,\n paddingBottom: 6,\n maxWidth: 560,\n },\n nodes: [\n {\n $id: null,\n componentId: 'muiTypography',\n pluginId: MUI_BUNDLE_ID,\n props: { variant: 'h4', children: 'Get in touch' },\n },\n {\n $id: null,\n componentId: FORM_ID,\n pluginId: BUNDLE_ID,\n props: {\n formName: 'Contact',\n submitLabel: 'Send message',\n successMessage: 'Thanks — we will get back to you soon.',\n },\n nodes: [\n contactField('name', 'Name'),\n contactField('email', 'Email', { fieldType: 'email' }),\n contactField('message', 'Message', { fieldType: 'textarea' }),\n ],\n },\n ],\n },\n },\n]\n\nfunction contactField(fieldName: string, label: string, extra?: object) {\n return {\n $id: null,\n componentId: FORM_FIELD_ID,\n pluginId: BUNDLE_ID,\n props: { fieldName, label, required: true, ...extra },\n }\n}\n\nexport { Form, FormField }\nexport default Form\n"],"names":["sendAnalyticsBeacon","trackEventBeforeNavigation","recordSiteJourneyStep","siteJourneyField","utmTouchField","Aglyn","parseFormUnavailableRefusal","mdiEmailFastOutline","mdiEmailOutline","mdiFormTextbox","isSameOriginPath","Alert","Button","Checkbox","FormControl","FormControlLabel","FormGroup","FormHelperText","FormLabel","Link","MenuItem","Radio","RadioGroup","Rating","Stack","TextField","createContext","forwardRef","useCallback","useContext","useEffect","useRef","useState","BUNDLE_ID","generatePresetId","MUI_BUNDLE_ID","FORM_ID","FORM_FIELD_ID","FIELD_MAP_INPUT_PREFIX","CONSENT_GROUP_INPUT","FormConsentDisclosureContext","readFormConsentDisclosure","body","value","text","trim","key","fieldName","sanitizeRedirectUrl","url","trimmed","undefined","test","resolveRedirectTarget","screenHref","redirectUrl","REVEAL_NODE_ID_PATTERN","formNavigation","assign","window","location","FORM_SUBMITTED_EVENT","sendFormBeacon","hostId","formId","event","form","Form","props","ref","formName","datasetId","_datasetId","datasetName","_datasetName","datasetBindingToken","submitLabel","successMessage","afterSubmit","redirectScreenId","revealNodeId","children","rest","useSite","siteFetch","useSiteFetch","href","redirectScreenHref","suppressNavigation","useScreenLink","revealSelector","status","setStatus","reported","view","start","current","handleFirstInput","pausedMessage","setPausedMessage","unavailable","setUnavailable","alerts","setAlerts","disclosure","setDisclosure","query","URLSearchParams","toString","then","response","ok","json","catch","handleSubmit","preventDefault","data","FormData","currentTarget","fields","website","consentGroup","entries","startsWith","slice","method","headers","JSON","stringify","datasetBinding","path","pathname","payload","Array","isArray","dispatchEvent","CustomEvent","detail","form_name","form_location","target","isPreviewRefusal","paused","parseLockdownRefusal","lockdownRefusalText","notice","severity","component","spacing","onSubmit","onInput","style","Provider","input","type","name","tabIndex","autoComplete","aria-hidden","position","left","height","width","message","contact","map","alert","index","variant","disabled","sx","alignSelf","displayName","parseFieldOptions","options","split","entry","filter","Boolean","FormField","datasetFieldId","label","placeholder","fieldType","required","fieldLabel","choices","hint","hintSlotProps","inputLabel","shrink","notched","checkedCount","setCheckedCount","rating","setRating","mapInput","readOnly","describesOptIn","isMarketingConsentFieldName","consentNote","select","defaultValue","fullWidth","size","slotProps","displayEmpty","renderValue","String","span","opacity","choice","control","onChange","count","checked","_event","multiline","minRows","formSchema","$id","pluginId","description","category","ComponentCategory","FORMS","icon","color","attributes","FieldComponentType","FORM_SELECT","TEXT_FIELD","DATASET_SELECT","condition","when","isNotEmpty","SELECT","SCREEN_SELECT","is","NODE_SELECT","formFieldSchema","flags","selfClosing","FEATURE_FLAG","ENABLED","DATASET_FIELD_SELECT","TEXTAREA","SWITCH","formPresets","NodeType","PRESET","componentId","nodes","MARKETING_CONSENT_FORM_FIELD","formBlockPresets","BLOCKS","paddingLeft","paddingRight","paddingTop","paddingBottom","maxWidth","contactField","extra"],"mappings":"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"}
|
|
@@ -14,7 +14,8 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
14
14
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
15
|
* See the License for the specific language governing permissions and
|
|
16
16
|
* limitations under the License.
|
|
17
|
-
*/ import
|
|
17
|
+
*/ import { SITE_JOURNEY_ID_PATTERN, SITE_JOURNEY_SUBMISSION_FIELD } from "@aglyn/aglyn/app-utils/site-journey";
|
|
18
|
+
import * as Aglyn from "@aglyn/aglyn/server";
|
|
18
19
|
import { extractEmailFromFields } from "@aglyn/aglyn/server";
|
|
19
20
|
import { consumeRateLimit, firebaseAdmin, getOrgForHost, notifyHostManagers, visitorWriteRefusal } from "@aglyn/tenant-data-admin";
|
|
20
21
|
import { emitHostEvent } from "@aglyn/tenant-runtime";
|
|
@@ -292,7 +293,7 @@ const json = (body, status = 200)=>Response.json(body, {
|
|
|
292
293
|
});
|
|
293
294
|
}
|
|
294
295
|
try {
|
|
295
|
-
var _ref, _form_get, _ref1;
|
|
296
|
+
var _ref, _form_get, _ref1, _payload_SITE_JOURNEY_SUBMISSION_FIELD;
|
|
296
297
|
const firestore = firebaseAdmin.app().firestore();
|
|
297
298
|
const hostRef = firestore.collection('hosts').doc(hostId);
|
|
298
299
|
const hostSnapshot = await hostRef.get();
|
|
@@ -827,14 +828,18 @@ const json = (body, status = 200)=>Response.json(body, {
|
|
|
827
828
|
path: String(path != null ? path : '').slice(0, 500)
|
|
828
829
|
}, submittedFields, form ? {
|
|
829
830
|
formId: form.id
|
|
830
|
-
} : {}), // The visitor who submitted, by the address they typed if any (AGL-3376)
|
|
831
|
-
|
|
831
|
+
} : {}), // The visitor who submitted, by the address they typed if any (AGL-3376),
|
|
832
|
+
// and the recorded visit this submission ends (AGL-3605), when the page
|
|
833
|
+
// sent one in the shape the recorder mints.
|
|
834
|
+
_extends({
|
|
832
835
|
actor: _extends({
|
|
833
836
|
kind: 'visitor'
|
|
834
837
|
}, submittedEmail ? {
|
|
835
838
|
email: submittedEmail
|
|
836
839
|
} : {})
|
|
837
|
-
})
|
|
840
|
+
}, SITE_JOURNEY_ID_PATTERN.test(String((_payload_SITE_JOURNEY_SUBMISSION_FIELD = payload[SITE_JOURNEY_SUBMISSION_FIELD]) != null ? _payload_SITE_JOURNEY_SUBMISSION_FIELD : '')) ? {
|
|
841
|
+
journeyId: String(payload[SITE_JOURNEY_SUBMISSION_FIELD])
|
|
842
|
+
} : {}));
|
|
838
843
|
return json({
|
|
839
844
|
received: true,
|
|
840
845
|
alerts
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/forms/src/lib/server/form-submit.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport * as Aglyn from '@aglyn/aglyn/server'\nimport { extractEmailFromFields } from '@aglyn/aglyn/server'\nimport {\n consumeRateLimit,\n firebaseAdmin,\n getOrgForHost,\n notifyHostManagers,\n visitorWriteRefusal,\n} from '@aglyn/tenant-data-admin'\nimport { emitHostEvent } from '@aglyn/tenant-runtime'\nimport recordCapturedContact from '@aglyn/aglyn/plugin-manager/record-captured-contact'\n// By path: the server-only contract a door credits its outcome through.\nimport {\n CONVERSION_TOUCH_DETAIL,\n conversionDescriptionSentences,\n creditConversion,\n describeConversion,\n resolveConversionTouch,\n} from '@aglyn/aglyn/plugin-manager/plugin-conversion-credit'\nimport { normalizeContainerIds } from '@aglyn/aglyn/app-utils/container-membership'\nimport {\n findScreenIdByRoutePath,\n screenRoutePathFromUrl,\n} from '@aglyn/aglyn/app-utils/screen-route'\n// The leaf: the Inbox list's search keys, the same function its backfill\n// restates and its query's normalizers read (AGL-3321).\nimport { messageSearchFields } from '@aglyn/aglyn/app-utils/message-search'\n// By path: the server-only contract a submission is filed as a record through.\nimport { writeFormRecordTarget } from '@aglyn/aglyn/plugin-manager/submission-record-target'\n// By path: where a manager reads submissions, declared by the plugin whose\n// page shows them, for the notifications below (AGL-3080).\nimport { pluginRecordPageLink } from '@aglyn/aglyn/plugin-manager/plugin-record-pages'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { isCredentialFieldName } from '@aglyn/shared-util-email/hosted-page-screen'\nimport { FORMS_OFF_FOR_SITE_REFUSAL, formSubmissionDoorPlugin } from '../model/form-door'\nimport { FORM_ABUSE_CEILING_CODE } from '../model/form-unavailable'\nimport { incrementFormStats } from './increment-form-stats'\nimport {\n NO_CLIENT_ADDRESS_BUCKET,\n readClientIp,\n} from '@aglyn/aglyn/app-utils/request-ip'\n\nconst MAX_FIELDS = 20\nconst MAX_PAYLOAD_CHARS = 10000\n\n// Per-IP submission limit (AGL-794). Previously a per-instance Map, which on\n// serverless resets each cold start and is held per concurrent instance — so\n// a spammer got roughly RATE_MAX × instances and could widen it by going\n// wider. The monthly plan quota was never the right thing to be defended BY:\n// burning a site's whole allowance is the damage, not the protection — and\n// since AGL-1280 it is not even a wall on a paid plan, it meters. This\n// counter is global, keyed per (site, IP).\n//\n// It is the SHAPE of abuse control, not the whole of it (AGL-1655): 10/60s\n// per address is 14,400/day from one IP and multiplies by the number of\n// addresses, so per-IP alone leaves the site's total unbounded. The per-site\n// monthly ceiling below is what bounds the total.\nconst RATE_WINDOW_MS = 60_000\nconst RATE_MAX = 10\n\nconst json = (body: unknown, status = 200) => Response.json(body, { status })\n\n/**\n * The ceiling is a per-MONTH count, so the honest retry hint is the month\n * boundary rather than a made-up interval. UTC, matching `monthKey`.\n */\nconst secondsUntilNextMonth = (now = new Date()): number => {\n const next = Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 1)\n return Math.max(1, Math.ceil((next - now.getTime()) / 1000))\n}\n\n/**\n * The one useful thing a refused visitor can be given (AGL-1666): the site's\n * OWN published support address, so a lost lead has somewhere else to go.\n *\n * Read through the host-token registry rather than reaching into the\n * document, because that registry is what defines this field as the address\n * a site publishes to visitors — its description is literally \"where\n * visitors should write for help\". Anything else on the host document is\n * account data and must not leave the console.\n *\n * Free: the host snapshot is already loaded for the existence check.\n */\nconst publishedContactEmail = (\n host: Record<string, any> | undefined,\n): string | undefined =>\n host ? Aglyn.HOST_TOKENS['supportEmail']?.resolve(host) : undefined\n\n/**\n * Make a tripped ceiling OBSERVABLE (AGL-1655) rather than a silent drop.\n *\n * Two audiences, one write. Staff get a durable per-month refusal count at\n * `hosts/{id}/counters/formSubmissionsRefused` — the same counters-document\n * shape `contactsDropped` already uses for free-tier contact drops, and\n * client-unwritable for the same reason (AGL-1367). The site's managers get\n * one in-app notification, on the FIRST refusal of the month only: a\n * notification per refused bot request would be the flood again, delivered.\n *\n * Best-effort throughout. A bookkeeping failure must not turn a contained\n * refusal into a 500, because a 500 is a retry invitation.\n */\nasync function recordAbuseCeilingTrip(\n hostRef: FirebaseFirestore.DocumentReference,\n hostId: string,\n monthKey: string,\n ceiling: number,\n): Promise<void> {\n try {\n const refusedRef = hostRef\n .collection('counters')\n .doc('formSubmissionsRefused')\n const refusedSnapshot = await refusedRef.get()\n const alreadyRefused = Number(refusedSnapshot.get(monthKey) ?? 0)\n await refusedRef.set(\n {\n [monthKey]: FieldValue.increment(1),\n // Explicit values only — Firestore rejects `undefined`.\n ceiling,\n lastRefusedAtMs: Date.now(),\n },\n { merge: true },\n )\n if (alreadyRefused === 0) {\n // Every fact here is the route's own (AGL-3432): the refusal sits before\n // every write, so a refused submission is neither stored nor counted\n // toward the bill, and the counter is month-keyed, so forms accept\n // again at the UTC month boundary — the same date the inbox notice\n // prints. `{site}` is the site's name, filled by `notifyHostManagers`.\n const reopens = Aglyn.nextUtcMonthStart().toLocaleDateString('en-US', {\n month: 'long',\n day: 'numeric',\n timeZone: 'UTC',\n })\n await notifyHostManagers(hostId, {\n type: 'system.formSubmissionsPaused',\n title: 'Form submissions paused on {site} — unusual volume',\n body:\n `Forms on {site} stopped accepting submissions: the site reached ` +\n `${ceiling.toLocaleString()} this month, the safety limit for ` +\n 'unusual volume. Submissions sent now are refused and not saved, ' +\n `and none of them are billed. Forms accept submissions again on ` +\n `${reopens}. Contact support if this is real traffic.`,\n ...submissionsLink(hostId),\n })\n }\n } catch (error) {\n console.error('form abuse ceiling bookkeeping failed', error)\n }\n}\n\n/**\n * A notification's link to where the site's managers read submissions — the\n * page the plugin that shows `formSubmission` records declares, opened on one\n * submission when `submissionId` is given and that page can open one — and no\n * link where no plugin in this build shows them. A key left out rather than\n * set to `undefined`, which Firestore rejects.\n */\nfunction submissionsLink(hostId: string, submissionId?: string): { link?: string } {\n const link = pluginRecordPageLink('formSubmission', hostId, submissionId)\n return link ? { link } : {}\n}\n\n/**\n * Count a honeypot hit so the bot rate is a NUMBER, not a guess (AGL-1664).\n *\n * The attestation decision (App Check / CAPTCHA / neither) asked to be made\n * on a measured per-host bot rate, and the honeypot was the instrument named\n * for it — but a hit used to return fake success and record nothing, so the\n * number it was supposed to produce did not exist. This writes the same\n * counters-document shape `formSubmissionsRefused` uses (per-month key,\n * client-unwritable, AGL-1367), keyed per host because \"which site is the\n * bot filling in\" is the decision input.\n *\n * The host-existence read is the orphan guard: this path runs BEFORE the\n * route's shape validation, and an attacker spraying invented host ids must\n * not mint counter documents under hosts that do not exist. Cost per hit is\n * one read + one write — the same order the legitimate path already pays the\n * rate limiter — where the old path cost nothing; that is the price of the\n * measurement, paid only on requests that self-identified as bots.\n *\n * No notification, ever: honeypot hits are routine background noise, and a\n * flood of them delivered as alerts would be the flood again (the\n * `formSubmissionsPaused` lesson). Best-effort for the same reason the\n * refusal bookkeeping is — a bookkeeping failure must not turn a dropped bot\n * request into a 500 retry invitation.\n */\nasync function recordHoneypotHit(hostId: unknown): Promise<void> {\n try {\n if (typeof hostId !== 'string' || !hostId || hostId.length > 128) return\n const hostRef = firebaseAdmin\n .app()\n .firestore()\n .collection('hosts')\n .doc(hostId)\n if (!(await hostRef.get()).exists) return\n await hostRef\n .collection('counters')\n .doc('formSubmissionsSpam')\n .set(\n {\n [Aglyn.utcMonthKey()]: FieldValue.increment(1),\n lastSpamAtMs: Date.now(),\n },\n { merge: true },\n )\n } catch (error) {\n console.error('honeypot bookkeeping failed', error)\n }\n}\n\n/**\n * The opt-in a form CARRIES, or `false`.\n *\n * `upsertHostContact` has accepted `marketingConsent` since AGL-301 and would\n * stamp `marketingConsentAtMs` from it, and this route passed it zero times —\n * so a subscribe checkbox a merchant put on their form was collected, stored\n * as a submission field, and then dropped on the way to the contact. That is\n * the missing INPUT for the send-time consent join\n * (`docs/specs/email-overhaul.md` §1d): the join can only ever be as good as\n * what the capture surfaces record.\n *\n * ⛔ THE FACT OF SUBMISSION IS NOT AN OPT-IN. Only a field whose VALUE is a\n * tick counts, and only under a name that means marketing consent. A form is\n * submitted to ask a question, book a table or claim a refund, and treating\n * any of those as a subscription is exactly the inference the consent arc\n * refused to make.\n *\n * What a tick is comes from `isConsentCheckboxTicked`. A box with no text of\n * its own posts an affirmative value on any form. A Checkboxes field posts\n * the text of its option, which is a tick only where a BOUND form's stored\n * `declarations` show that field is a checkbox with exactly one option: an\n * unbound form has no declaration, and the request never supplies one.\n *\n * The name list is `MARKETING_CONSENT_FIELD_NAMES`, closed rather than a\n * substring match on \"consent\" (a clinic intake form's `consentToTreatment`\n * is a different instrument), and shared with the publish check: a design\n * `checkFormContract` accepts as recording consent is one this route reads\n * an opt-in from.\n */\nfunction readDeclaredMarketingConsent(\n payload: Record<string, any>,\n fields: Record<string, unknown>,\n declarations: readonly Aglyn.FormFieldDecl[],\n): boolean {\n // A first-class body field, for a caller that models the checkbox\n // explicitly rather than as one more form field.\n if (payload['marketingConsent'] === true) return true\n for (const [key, value] of Object.entries(fields)) {\n if (!Aglyn.isMarketingConsentFieldName(key)) continue\n const declaration = declarations.find((entry) => entry?.fieldName === key)\n if (Aglyn.isConsentCheckboxTicked(value, declaration)) return true\n }\n return false\n}\n\n/**\n * `POST /api/forms/submit` — every form's door, and a Marketing popup's\n * (AGL-76, AGL-3080).\n *\n * This plugin's route, served by the tenant's plugin API dispatcher at the\n * address it has always had, and registered as a route that KEEPS ITS OWN\n * VISITOR GATES (`ownVisitorGates`): the dispatcher hands it the request\n * untouched, because every gate below is a published answer — which plugin's\n * switch decides a submission (the door it came through, not this route's\n * owner), the \"not accepting\" notice a site with Forms off gives, the\n * per-(site, address) limit and the monthly ceiling, and where lockdown sits\n * among them. The dispatcher's own versions of those would answer first and\n * differently.\n *\n * Lead-capture submissions endpoint (AGL-76): validates the target host,\n * drops honeypot hits silently, applies the plan's monthly submission quota\n * via a per-month counter — a hard wall on free, a meter on plans that carry\n * the infra pass-through (AGL-1280) — refuses above the per-site abuse\n * ceiling (AGL-1655), and stores the submission for the console inbox. Writes\n * go through the admin SDK, so client rules never allow arbitrary submission\n * writes.\n *\n * The two 429s are different answers and must stay distinguishable: the plan\n * wall means the customer's allowance is spent, the ceiling means the site is\n * being flooded. Only the ceiling refusal carries `code`.\n */\nexport async function POST(request: Request): Promise<Response> {\n // The dispatcher serves every method at its catch-all; this door takes a\n // POST alone, as the route it replaced did.\n if (request.method !== 'POST') {\n return new Response(null, { status: 405, headers: { Allow: 'POST' } })\n }\n const payload = (await request.json().catch(() => ({}))) as Record<string, any>\n // `datasetId`, `dataset` and `fieldMap` are deliberately not read: the\n // dataset a record lands in comes from the page's signed binding (below).\n const { hostId, formId, formName, path, fields, website } = payload\n\n // Honeypot filled → pretend success so bots learn nothing, counted so the\n // owner of the attestation decision learns something (AGL-1664).\n if (typeof website === 'string' && website.trim()) {\n await recordHoneypotHit(hostId)\n return json({ received: true })\n }\n if (\n typeof hostId !== 'string' ||\n !hostId ||\n typeof fields !== 'object' ||\n fields === null ||\n Array.isArray(fields) ||\n Object.keys(fields).length === 0 ||\n Object.keys(fields).length > MAX_FIELDS ||\n JSON.stringify(fields).length > MAX_PAYLOAD_CHARS\n ) {\n return json({ error: 'Invalid submission' }, 400)\n }\n // Keeps counting under the no-address bucket rather than being skipped: a\n // public form endpoint that stops counting is an open write, and the key\n // already carries the site, so an unreadable address shares one budget per\n // SITE rather than one across the deployment.\n const ip = readClientIp(request.headers) ?? NO_CLIENT_ADDRESS_BUCKET\n const rate = await consumeRateLimit(`form:${hostId}:${ip}`, {\n limit: RATE_MAX,\n windowMs: RATE_WINDOW_MS,\n })\n if (!rate.allowed) {\n // Note this is NOT told to the caller, degraded or not. The body and the\n // status are identical either way: \"the global limiter is currently a\n // per-instance one\" is exactly the sentence an abuser would spend the\n // window on, and this endpoint is public and unauthenticated.\n return Response.json(\n { error: 'Too many submissions' },\n {\n status: 429,\n headers: {\n 'Retry-After': String(\n Math.max(1, Math.ceil((rate.resetMs - Date.now()) / 1000)),\n ),\n },\n },\n )\n }\n\n try {\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const hostSnapshot = await hostRef.get()\n if (!hostSnapshot.exists) {\n return json({ error: 'Unknown site' }, 404)\n }\n\n // Lockdown (AGL-1511). Placed after the host exists and BEFORE the quota\n // read and every write: a paused submission must not consume the\n // customer's monthly allowance, and it must not be half-recorded.\n // Refused with the visitor pause copy — the site around this form is\n // still serving, so an outage page here would be both a lie and a lost\n // lead. The `/api` surface is outside the middleware matcher, so this is\n // the only lockdown enforcement a form POST ever meets, full or\n // read-only.\n const paused = await visitorWriteRefusal({\n hostId,\n request,\n surface: 'form',\n })\n if (paused) return paused\n\n // Monthly quota by the owning org's plan (dark-launch: orgs\n // without a plan are uncapped, matching every other gate).\n // Plan/quota gates ride the owning org's doc (AGL-238).\n const owningOrg = await getOrgForHost(hostId)\n const orgBilling = owningOrg?.org\n\n /*\n * A site that switched Forms off accepts no form submission (AGL-3029).\n *\n * Asked of the site's own plugin set — the org's resolved set minus this\n * host's deny-list, the same answer the tenant draws the page from — and\n * about the plugin whose door the submission came through: a Marketing\n * popup's capture belongs to Marketing, and keeps working on a site that\n * still shows the popup. Core imports no plugin, so both are ids.\n *\n * Placed after the site is known to exist and before the quota read and\n * every write: a refused submission is not counted, not billed and not\n * stored. 404, the answer the route already gives a retired form, because\n * the honest reply to the visitor is the same — this site takes no form\n * here — and the sentence names no setting a stranger has no use for.\n */\n if (\n !Aglyn.isHostPluginEnabled(\n orgBilling as never,\n hostSnapshot.data() as never,\n formSubmissionDoorPlugin(payload),\n )\n ) {\n return json({ error: FORMS_OFF_FOR_SITE_REFUSAL }, 404)\n }\n // Shared with the console surface that reads these counters back\n // (AGL-1666) — a differently-derived key there would read 0 refusals on\n // exactly the sites being refused.\n const monthKey = Aglyn.utcMonthKey()\n const counterRef = hostRef.collection('counters').doc('formSubmissions')\n {\n // Plan-less orgs resolve as free (AGL-247) — the cap always runs.\n const counterSnapshot = await counterRef.get()\n const used = Number(counterSnapshot.get(monthKey) ?? 0)\n // AGL-1280: a plan with the metered infra pass-through is never cut\n // off — submissions past the included count bill at cost × 1.3 like\n // storage, API requests and contacts already do. Free keeps the hard\n // wall, because there is no subscription to meter onto. A dropped\n // submission is a lost lead, so this is the meter where being wrong\n // in the \"cut them off\" direction costs the customer the most.\n if (!Aglyn.checkFormSubmissionQuota(orgBilling as any, used).allowed) {\n return json({ error: 'Submission limit reached' }, 429)\n }\n // Abuse ceiling (AGL-1655). The plan gate above cannot refuse a\n // metered plan by design, so on every paying customer it was the only\n // thing between a bot flood and an unbounded invoice — and it always\n // said yes. The per-IP limiter is 10/60s, which one address turns into\n // 14,400/day and any number of addresses multiplies.\n //\n // This is the ceiling that can say no. It reuses the count already\n // read above (no extra Firestore read on the happy path) and sits\n // BEFORE every write, so a refused submission never touches\n // `counters/formSubmissions` — the document /api/billing/report-usage\n // invoices from. Refusing and still billing would have been the same\n // bug with a smaller number.\n const ceiling = Aglyn.checkFormSubmissionAbuseCeiling(\n orgBilling as any,\n used,\n )\n if (ceiling.exceeded) {\n await recordAbuseCeilingTrip(hostRef, hostId, monthKey, ceiling.ceiling)\n const contact = publishedContactEmail(hostSnapshot.data())\n return Response.json(\n {\n error: 'Submissions are paused for this site',\n // Machine-readable so a client can tell containment apart from\n // the plan wall it shares a status with.\n code: FORM_ABUSE_CEILING_CODE,\n // The site's own published support address, when it has one\n // (AGL-1666). NOT the ceiling, the count or the plan: this body\n // is read by a stranger to the site, and the reason a site\n // stopped accepting is the owner's to see, not a caller's.\n ...(contact ? { contact } : {}),\n },\n {\n status: 429,\n headers: { 'Retry-After': String(secondsUntilNextMonth()) },\n },\n )\n }\n }\n\n /*\n * NO CREDENTIALS, whatever the page asked for (AGL-3356).\n *\n * A published page that asks for a password, a card number or a\n * one-time code in a field of its own is held by the page review before\n * a visitor sees it. This endpoint is the one path that does not pass a\n * publish: it takes whatever names a request carries, so a page the\n * review never read — a marketplace plugin's frame, a hand-built request\n * — could still post one here. Such a field is dropped, never stored,\n * never forwarded to a workflow or a contact.\n */\n const sanitizedFields: Record<string, string> = {}\n for (const [key, value] of Object.entries(fields)) {\n if (isCredentialFieldName(String(key))) continue\n sanitizedFields[String(key).slice(0, 64)] = String(value).slice(0, 2000)\n }\n if (!Object.keys(sanitizedFields).length) {\n return json({ error: 'Invalid submission' }, 400)\n }\n\n /*\n * The bound form entity (`docs/specs/reusable-forms.md` §2c).\n *\n * READ AND VERIFIED, never trusted. `formId` arrives in a public,\n * unauthenticated body, so stamping it unchecked would let anyone file\n * rows into any form's list on any site — the per-form list is the\n * surface this id exists to make correct, and an unverified id would make\n * it wrong in a way that looks right.\n *\n * One extra document read, and only when a form is bound. A form with no\n * `formId` pays nothing, which is the same shape the dataset binding and\n * `rateDegraded` already take.\n */\n const boundFormId = String(formId ?? '').trim().slice(0, 128)\n const formSnapshot = boundFormId\n ? await hostRef.collection('forms').doc(boundFormId).get()\n : null\n const form = formSnapshot?.exists ? formSnapshot : null\n /*\n * A RETIRED form collects nothing (AGL-2671).\n *\n * Retirement is what a merchant does with a form they have finished with\n * but must keep — the submissions, the leads and the contact timeline\n * those built are filed under it. Leaving it able to collect would make\n * the retirement a UI illusion: the row would be gone from the list and\n * out of the funnel's grading while a form still placed on a published\n * page kept filing leads nobody was looking at, which is the quiet half\n * of a lost lead rather than a retirement.\n *\n * Refused HERE, on the verified entity, rather than by unpublishing the\n * page: a form is placed by whoever placed it, on any number of pages, and\n * the document is the one place that knows it is retired.\n *\n * 404 rather than a 4xx that explains: the visitor's honest answer is that\n * this form is not here any more, and the shape matches `Unknown site`\n * above. Only ever for a BOUND form — a submission carrying no `formId`\n * has no entity to be retired and is untouched.\n */\n if (Aglyn.isFormArchived(form?.data() as { archivedAt?: number } | undefined)) {\n return json({ error: 'This form is no longer accepting submissions' }, 404)\n }\n /*\n * The caption follows the entity once one is bound.\n *\n * `formName` stays written either way: it is what the pre-entity `?form=`\n * filter reads, and no phase of this work removes it. Resolving it from\n * the form's own `displayName` keeps the Inbox's label from going stale\n * the moment somebody renames a form.\n *\n * ⚠️ This does NOT make the legacy filter rename-safe, and it is not\n * meant to — renaming still splits what `?form=` returns, exactly as it\n * always did. `formId` is the filter that survives a rename; that is the\n * whole reason it exists.\n */\n const resolvedFormName = form\n ? String(form.get('displayName') ?? formName ?? 'Form').slice(0, 100)\n : String(formName ?? 'Form').slice(0, 100)\n /*\n * Consent, from the field the FORM declares.\n *\n * ⛔ Still never inferred from the fact of submission. What the entity\n * adds is a declared place to look: `consentFieldName` names the one\n * field that IS the opt-in, so a merchant's checkbox called anything at\n * all is readable, and a clinic intake form's `consentToTreatment` stays\n * the different instrument it is.\n *\n * A bound form that declares NO consent field falls through to the closed\n * name list below rather than recording nothing. Adoption must not be the\n * moment a form that was capturing opt-ins stops — consent is carried\n * forward, never dropped as a side effect of a migration.\n *\n * Both readers are handed the form's STORED declaration: a Checkboxes\n * field posts the text of the option that was ticked, and only the\n * declaration says a consent field has exactly one option, whose text is\n * then the tick. An unbound form has none, so its boxes count only by an\n * affirmative value.\n */\n const formFieldDecls = Array.isArray(form?.get('fields'))\n ? (form?.get('fields') as Aglyn.FormFieldDecl[])\n : []\n const declaredMarketingConsent = form?.get('consentFieldName')\n ? Aglyn.readFormDeclaredConsent(\n {\n consentFieldName: String(form.get('consentFieldName')),\n fields: formFieldDecls,\n },\n sanitizedFields,\n )\n : readDeclaredMarketingConsent(payload, fields, formFieldDecls)\n /*\n * THE DISCLOSURE THE FORM SHOWED beside that opt-in (AGL-3320).\n *\n * The Form block renders the site's consent-group sentence under its\n * consent field and posts back the key of what it rendered\n * (`/api/consent/disclosure`). Passed to the record system as it arrived:\n * the writer pools the opt-in across the group only when this is the\n * group's CURRENT key, so a page rendered before a rename or before a\n * site joined records the one site the visitor was on. Trusting it costs\n * nothing — it can only ever narrow what the checkbox above already\n * claims, never widen it past the declaration.\n */\n const disclosedConsentGroup =\n typeof payload['__consentGroup'] === 'string'\n ? payload['__consentGroup'].trim().slice(0, 64)\n : ''\n /*\n * THE CAMPAIGNS THE FORM IS FILED UNDER.\n *\n * Off the form document this route has already read, so it costs nothing,\n * and off the VERIFIED form for the same reason the id is: a membership\n * taken from the request body would let anyone file people into any\n * campaign on any site.\n *\n * ⚠️ NOT the campaign touch resolved below, and the two never write each\n * other's field. The touch is where this visitor came FROM and is\n * browser-supplied; this is which campaigns the merchant put the form in,\n * and it is true of every person who fills that form in — including the\n * one who arrived by typing the address. Recording only the touch is what\n * left three forms assigned to a campaign producing contacts that belonged\n * to none.\n */\n const formCampaignIds = form\n ? Aglyn.readContainerIds(form.data() as Record<string, unknown>, 'campaign')\n : []\n /*\n * THE CAMPAIGNS THE PAGE IS FILED UNDER (AGL-3461), stamped beside the\n * form's for the same reason and with the same caveat: where the merchant\n * filed the page, true of everyone who submits on it, and never a credit.\n *\n * The screen is found in the routing map on the host snapshot this route\n * already holds, from the path the browser sent, so a path naming no page\n * this site serves names no screen and costs nothing. Its campaigns are\n * read off the screen document — one read, only when the path resolves.\n * A failed read stamps nothing rather than failing the submission.\n */\n const pageRoutePath = screenRoutePathFromUrl(typeof path === 'string' ? path : '')\n const pageScreenId = pageRoutePath\n ? findScreenIdByRoutePath(\n hostSnapshot.get('screens') as Record<string, string> | undefined,\n pageRoutePath,\n )\n : undefined\n const pageCampaignIds = pageScreenId\n ? await hostRef\n .collection('screens')\n .doc(pageScreenId)\n .get()\n .then((screen) =>\n screen.exists\n ? Aglyn.readContainerIds(screen.data() as Record<string, unknown>, 'campaign')\n : [],\n )\n .catch((error: unknown) => {\n console.error('form submission page campaigns read failed', error)\n return [] as string[]\n })\n : []\n /*\n * THE CUSTOM CONTACT FIELDS THIS FORM SAVES TO (AGL-2601).\n *\n * Off the VERIFIED form's declaration, like the campaigns above and for\n * the same reason: a mapping taken from the request body would let anyone\n * write any value under any field of any contact on any site. The\n * definitions are read from the owning org — one bounded read of a small\n * collection — and only when the declaration maps at least one field, so\n * a form that saves nowhere pays nothing.\n *\n * Coerced by the definition's TYPE before it goes anywhere near the\n * contact: `collectMappedContactCustom` is the same rule the API applies\n * to a `custom` body, so a number typed into a form and a number sent\n * over the API are stored as the same thing. A blank field contributes no\n * key at all, so a submission never clears a value the merchant set.\n */\n const mappedContactCustom =\n form && owningOrg?.orgId && formFieldDecls.some((decl) => decl?.contactFieldKey)\n ? Aglyn.collectMappedContactCustom({\n fields: sanitizedFields,\n decls: formFieldDecls,\n definitions: (\n await firestore\n .collection('orgs')\n .doc(owningOrg.orgId)\n .collection(Aglyn.CRM_COLLECTIONS.contactFields)\n .limit(Aglyn.CONTACT_FIELDS_MAX_PER_ORG)\n .get()\n ).docs.map((snapshot) => snapshot.data() as Aglyn.ContactFieldDefinition),\n })\n : {}\n // One instant for the whole submission. The attribution window is\n // measured against it in three places below, and three calls to\n // `Date.now()` would let a slow write decide whether a touch was inside\n // the window for the contact and outside it for the lead.\n const submittedAtMs = Date.now()\n /*\n * The organization this site belongs to, off the two documents this\n * route has already read — the host, whose `orgId` only the org APIs\n * write, and the `hostIndex` mirror the plan gate resolved through.\n */\n const hostOrgId = hostSnapshot.get('orgId')\n const submissionOrgId =\n typeof hostOrgId === 'string' && hostOrgId ? hostOrgId : owningOrg?.orgId\n const submissionRef = await hostRef.collection('formSubmissions').add({\n /*\n * WHERE THE ROW LIVES, as fields (AGL-3303). The organization's Inbox\n * reads every site's submissions with ONE collection-group query,\n * `where('orgId', '==', …)` newest first, and the rules admit that\n * read on the org named here; `hostId` is what that list addresses a\n * row's site by. Both are frozen against a client update, and a site\n * with no org carries no `orgId`, so it is in no organization's list.\n */\n ...(submissionOrgId ? { orgId: submissionOrgId } : {}),\n hostId,\n // Stamped only for a form that exists on THIS site. An unverified id\n // never reaches the row, so the per-form list cannot be written into\n // from outside.\n ...(form ? { formId: form.id } : {}),\n /*\n * The form's campaign membership, copied onto the row.\n *\n * A STAMP of what the form said at the moment this arrived, not a live\n * edge: nothing edits a submission's campaigns afterwards, and refiling\n * the form later does not rewrite the submissions it already produced.\n * That is why this collection is not in the Marketing plugin's\n * `CAMPAIGN_MEMBER_HOST_COLLECTIONS` — the deletion pass walks the\n * collections a PICKER writes, and a campaign's removal must not rewrite\n * an unbounded, billed history collection to tidy up a field that is\n * only ever read alongside the campaigns a picker still offers.\n *\n * Written only when the form names one, so the field is absent on the\n * rows that would carry an empty array forever.\n */\n ...(formCampaignIds.length ? { campaignIds: formCampaignIds } : {}),\n // And the page's, beside them rather than merged in: `campaignIds` is\n // the form's membership and is read as exactly that (AGL-3461).\n ...(pageCampaignIds.length ? { pageCampaignIds } : {}),\n formName: resolvedFormName,\n path: String(path ?? '').slice(0, 500),\n fields: sanitizedFields,\n /*\n * WHAT THE INBOX'S LIST ASKS (AGL-3321). Its From filter and its\n * search are `array-contains` on these word-prefix arrays, and Read is\n * an equality on `read` — so every row carries all three from the\n * moment it exists. The fields never change after this write, so no\n * other writer restamps them; `backfill-form-submission-filters.mjs`\n * stamps the rows written before.\n */\n ...messageSearchFields(sanitizedFields),\n read: false,\n createdAt: FieldValue.serverTimestamp(),\n // Accepted while the durable limiter was degraded (AGL-1667).\n //\n // The limiter fails soft by design (AGL-794, reaffirmed in AGL-1679) and\n // that is not reversed here: a Firestore blip must not stop a customer's\n // site collecting leads, and the fallback can only ever allow MORE than\n // 10/min, never fewer, so nothing legitimate is refused by it. What was\n // wrong is that the route read `allowed` and `resetMs` and never\n // `degraded`, so the window left no trace on THIS endpoint at all.\n //\n // The AGL-1679 marker records that the store degraded; it cannot say\n // which submissions came in under the wider cap, and that is the\n // correlation the incident actually needs — these rows are billed\n // (`/api/billing/report-usage` prices `counters/formSubmissions`) and a\n // customer disputing a spike is owed a per-row answer, not \"the limiter\n // was unwell around then\".\n //\n // Stamped rather than counted because it rides a write that is already\n // happening: a separate counter would mean an EXTRA Firestore write per\n // submission, issued precisely while Firestore is the thing that is\n // failing. Written only when true — an absent field is the healthy case\n // and costs nothing on the millions of rows that are fine.\n ...(rate.degraded ? { rateDegraded: true } : {}),\n })\n // Contacts ingestion (AGL-197): forms don't guarantee an email field —\n // best-effort extraction; never blocks the submission.\n const contactEmail = extractEmailFromFields(sanitizedFields)\n /*\n * Whether this submission was filed to the site's Leads.\n *\n * The form counts PEOPLE (AGL-3330). The lead silo keys one person to one\n * document, so a returning visitor's second submission updates the lead\n * it created the first time; the form's lead count moves only when the\n * capture puts this form's source on a lead for the first time\n * (`leadCounted`), which is exactly what a recount of the leads'\n * `sources` finds — so the increment and the recount agree, and a lead\n * erased later can be taken back off. A routed submission by somebody\n * the workspace already holds as a contact lands on the contact and files\n * no lead (AGL-3232), and is not counted.\n */\n let leadStored = false\n /** Whether that lead is one more person on this form's lead count. */\n let leadCounted = false\n /**\n * The lead or contact the person was filed as, for the Inbox to link to\n * (AGL-3461) — by the id the record system answered with, which is the\n * id its own record route addresses.\n */\n let capturedRecord: { kind: 'lead' | 'contact'; id: string } | null = null\n /*\n * WHERE THE VISITOR ARRIVED FROM, RESOLVED ONCE FOR THE WHOLE SUBMISSION.\n *\n * One visitor action lands in three collections here — the submission,\n * the contact and (when the form declares itself a lead surface) the\n * lead — and all three are the same person arriving from the same place.\n * Resolving here and handing the result down is what keeps the lookup at\n * one per submission instead of three, and it is also what guarantees the\n * three records cannot disagree about what to credit.\n *\n * The answer is the crediting plugin's (`plugin-conversion-credit.ts`),\n * asked with what the visitor's browser sent under `campaignTouch` —\n * untrusted, so the creditor re-parses and window-checks it before it\n * can name anything. A submission carrying none — direct traffic, or a\n * visitor whose consent posture never let a touch be remembered — and a\n * workspace whose plugins credit nothing both resolve to null and write\n * no credit at all.\n */\n const arrival = await resolveConversionTouch({\n hostId,\n wire: payload['campaignTouch'],\n email: contactEmail,\n atMs: submittedAtMs,\n })\n // The submission itself is an outcome whether or not it named anybody:\n // an anonymous enquiry from a campaign link is a conversion that campaign\n // caused, and refusing to count it because no email field was filled in\n // would under-report exactly the forms that ask for the least.\n void creditConversion({\n hostId,\n kind: 'form',\n refId: submissionRef.id,\n touch: arrival,\n convertedAtMs: submittedAtMs,\n })\n /*\n * THE CAMPAIGNS IN WORDS, for the managers' alert below (AGL-3461): the\n * one the visitor arrived through and the ones the form and page are\n * filed under, named by the plugin that keeps campaigns. Started here so\n * its reads overlap the capture and the counters; asked only when there\n * is something to name, and it never throws.\n */\n const filedUnder = normalizeContainerIds([...formCampaignIds, ...pageCampaignIds])\n const describing =\n arrival || filedUnder.length\n ? describeConversion({ hostId, touch: arrival, containerIds: filedUnder })\n : Promise.resolve(null)\n if (contactEmail) {\n /*\n * WHICH RECORD THE PERSON LANDS ON is the record system's decision\n * (AGL-3232), and the form says only what kind of surface it is: a\n * lead surface when its author switched lead routing on\n * (`docs/specs/reusable-forms.md` §4a), a touch otherwise. The writer\n * files a lead — through `addHostLead`, keyed on the person and\n * counted against the ceiling — unless the workspace already holds the\n * address as a contact, and answers which record it wrote.\n *\n * AWAITED, unlike the capture of every other door, and only because\n * the answer is counted: the form's `stats.leads` rides the same\n * `update` the submission counters ride below, and a `void` call with\n * the counter chained onto its `then` would put the one figure on this\n * page that is about routing on a promise the runtime is free to\n * abandon once the response is sent. The posture is unchanged —\n * `recordCapturedContact` never throws — so awaiting it cannot fail the\n * submission any more than voiding it could.\n */\n const routed = form?.get('routing')?.lead === true\n const captured = await recordCapturedContact({\n /*\n * Not gated on the org resolving. The record system keys a person on\n * the SITE they were met on, so an org lookup that came back empty\n * costs the capture nothing — and refusing to record the person over\n * it would turn a lookup failure into a lost contact, silently, which\n * is the failure this whole seam is arranged to avoid.\n */\n orgId: owningOrg?.orgId ?? '',\n hostId,\n identity: {\n email: contactEmail,\n name: sanitizedFields['name'] ?? sanitizedFields['fullName'],\n },\n interaction: {\n source: 'form',\n refId: submissionRef.id,\n summary: `Submitted \"${resolvedFormName.slice(0, 60)}\"`,\n },\n surface: routed ? 'lead' : 'touch',\n ...(declaredMarketingConsent ? { marketingConsent: true } : {}),\n ...(declaredMarketingConsent && disclosedConsentGroup\n ? { disclosedConsentGroup }\n : {}),\n // Filed under the form's campaigns, inside this site's own facet on a\n // row the whole org shares. Membership is not consent, and this passes\n // none: `marketingConsent` above is the only input that records one.\n ...(formCampaignIds.length\n ? { containers: { campaign: formCampaignIds } }\n : {}),\n // The mapped custom field values. Absent when nothing mapped, so the\n // owner adds no `custom` key for nothing.\n ...(Object.keys(mappedContactCustom).length\n ? { profile: { custom: mappedContactCustom } }\n : {}),\n detail: {\n /*\n * THE ENTRY POINT, for the owner to put on the person's timeline.\n *\n * Which form and which page a person came in through is a fact\n * about this capture rather than about the person, which is why it\n * rides `detail` and not `profile`. `sources` says only that SOME\n * form produced this contact — every form sets the same flag — and\n * answering \"which one\" by reading the submission back would be a\n * document read per row of a timeline the console renders straight\n * out of the contact.\n *\n * The id only for a VERIFIED form, matching the submission: an\n * unverified id never reaches a stored row on this path.\n */\n ...(form ? { formId: form.id } : {}),\n ...(typeof path === 'string' && path ? { path } : {}),\n // Where the visitor ARRIVED from, already resolved above — never\n // the same fact as `campaignIds`, which is where the merchant filed\n // the form.\n ...(arrival ? { [CONVERSION_TOUCH_DETAIL]: arrival } : {}),\n },\n })\n leadStored = captured?.ok === true && captured.record === 'lead'\n /*\n * A form counts the PEOPLE it filed (AGL-3330): the capture says\n * whether this one was new to the form, and a writer that does not\n * say is read as whether the lead itself was new.\n */\n leadCounted =\n leadStored &&\n captured?.ok === true &&\n captured.record === 'lead' &&\n (captured.sourceAdded ?? captured.created) === true\n if (captured?.ok === true) {\n const id = captured.record === 'lead' ? captured.leadId : captured.contactId\n if (id) capturedRecord = { kind: captured.record, id }\n }\n }\n /*\n * WHERE ELSE THE SUBMISSION IS FILED (AGL-3080): a record in whatever\n * plugin keeps the records a form may write into — a dataset row, today.\n *\n * Asked AFTER the submission is stored, through the platform's contract\n * (`plugin-manager/submission-record-target.ts`), and the target decides from\n * what the page's compose stamped onto this form, never from the body\n * alone: the route is public. It answers the submission's `routing`,\n * which the Inbox reads to say where it went or why it did not. The Inbox\n * copy above is canonical either way, and nothing here can fail the\n * submission.\n */\n const filed = await writeFormRecordTarget({\n hostId,\n orgId: owningOrg?.orgId ?? null,\n orgBilling,\n body: payload,\n fields: sanitizedFields,\n })\n /*\n * What the submission led to, on the row in ONE update: where else it\n * was filed, and the lead or contact it filed (AGL-3461). Written only\n * when there is either, so the ordinary anonymous enquiry pays nothing.\n */\n const outcomeNotes = {\n ...(filed.routing ? { routing: filed.routing } : {}),\n ...(capturedRecord ? { capturedRecord } : {}),\n }\n if (Object.keys(outcomeNotes).length) {\n try {\n await submissionRef.update(outcomeNotes)\n } catch (error) {\n console.error('form submission routing note failed', error)\n }\n }\n await counterRef.set({ [monthKey]: FieldValue.increment(1) }, { merge: true })\n /*\n * Per-form counters (`docs/specs/reusable-forms.md` §5a), through the\n * one writer that retries and REPORTS a failure rather than swallowing\n * it (AGL-3330) — see `incrementFormStats`.\n *\n * `stats.leads` is here rather than beside the lead writer for the same\n * reason the submission counters are: a second write to say a lead was\n * filed would be a second write per submission on the collection the\n * customer is billed against.\n */\n if (form) {\n await incrementFormStats({\n formRef: form.ref,\n hostId,\n monthKey,\n submittedAtMs,\n leadCounted,\n })\n }\n // Event trigger (AGL-128/148): field values join the automation\n // scope; action-produced site alerts ride back to the visitor.\n // In-app notification to the site's managers (AGL-259).\n /*\n * The body names the form and the site on its own (AGL-3432): a manager\n * of several sites reads the email body, not always the subject, and\n * \"Page: /contact\" said neither. `{site}` is filled by\n * `notifyHostManagers` from the host doc it already reads.\n *\n * Then the campaigns (AGL-3461): the one the visitor was credited to and\n * how, and what the form and page are filed under, as the crediting\n * plugin names them.\n *\n * It opens THIS submission, not the list: the address the plugin that\n * shows submissions declares for one record (`submissionsLink`), stored\n * in the host-link shape every host notification uses and rewritten onto\n * the site's address when it is followed, in the console and in the email\n * alike. A notification holds one link, so the lead and the campaign are\n * links inside the reader.\n */\n const alertPath = typeof path === 'string' ? path.slice(0, 500) : ''\n void notifyHostManagers(hostId, {\n type: 'content.formSubmission',\n title: `New form submission — ${resolvedFormName}`,\n body: [\n `Someone submitted “${resolvedFormName}” on {site}` +\n (alertPath ? ` (page ${alertPath}).` : '.'),\n ...conversionDescriptionSentences(await describing),\n ].join(' '),\n ...submissionsLink(hostId, submissionRef.id),\n })\n const submittedEmail =\n typeof sanitizedFields['email'] === 'string' ? sanitizedFields['email'] : ''\n /*\n * `formId` is the VERIFIED form's id (AGL-3458), the way `contactCreated`\n * carries it: a condition keyed on it survives the form being renamed,\n * where one on `formName` silently stops matching. It is written last\n * and only from the form this route read, so a submitted field that\n * calls itself `formId` can neither claim another form's automations\n * nor stand in for a form the site does not have.\n */\n const submittedFields = Object.fromEntries(\n Object.entries(sanitizedFields).filter(([key]) => key !== 'formId'),\n )\n const { alerts } = await emitHostEvent(\n hostId,\n 'formSubmission',\n {\n formName: resolvedFormName,\n path: String(path ?? '').slice(0, 500),\n ...submittedFields,\n ...(form ? { formId: form.id } : {}),\n },\n // The visitor who submitted, by the address they typed if any (AGL-3376).\n { actor: { kind: 'visitor', ...(submittedEmail ? { email: submittedEmail } : {}) } },\n )\n return json({ received: true, alerts })\n } catch (error) {\n console.error(error)\n return json({ error: 'Submission failed' }, 500)\n }\n}\n"],"names":["Aglyn","extractEmailFromFields","consumeRateLimit","firebaseAdmin","getOrgForHost","notifyHostManagers","visitorWriteRefusal","emitHostEvent","recordCapturedContact","CONVERSION_TOUCH_DETAIL","conversionDescriptionSentences","creditConversion","describeConversion","resolveConversionTouch","normalizeContainerIds","findScreenIdByRoutePath","screenRoutePathFromUrl","messageSearchFields","writeFormRecordTarget","pluginRecordPageLink","FieldValue","isCredentialFieldName","FORMS_OFF_FOR_SITE_REFUSAL","formSubmissionDoorPlugin","FORM_ABUSE_CEILING_CODE","incrementFormStats","NO_CLIENT_ADDRESS_BUCKET","readClientIp","MAX_FIELDS","MAX_PAYLOAD_CHARS","RATE_WINDOW_MS","RATE_MAX","json","body","status","Response","secondsUntilNextMonth","now","Date","next","UTC","getUTCFullYear","getUTCMonth","Math","max","ceil","getTime","publishedContactEmail","host","HOST_TOKENS","resolve","undefined","recordAbuseCeilingTrip","hostRef","hostId","monthKey","ceiling","refusedSnapshot","refusedRef","collection","doc","get","alreadyRefused","Number","set","increment","lastRefusedAtMs","merge","reopens","nextUtcMonthStart","toLocaleDateString","month","day","timeZone","type","title","toLocaleString","submissionsLink","error","console","submissionId","link","recordHoneypotHit","length","app","firestore","exists","utcMonthKey","lastSpamAtMs","readDeclaredMarketingConsent","payload","fields","declarations","key","value","Object","entries","isMarketingConsentFieldName","declaration","find","entry","fieldName","isConsentCheckboxTicked","POST","request","method","headers","Allow","catch","formId","formName","path","website","trim","received","Array","isArray","keys","JSON","stringify","ip","rate","limit","windowMs","allowed","String","resetMs","form","hostSnapshot","paused","surface","owningOrg","orgBilling","org","isHostPluginEnabled","data","counterRef","counterSnapshot","used","checkFormSubmissionQuota","checkFormSubmissionAbuseCeiling","exceeded","contact","code","sanitizedFields","slice","boundFormId","formSnapshot","isFormArchived","resolvedFormName","formFieldDecls","declaredMarketingConsent","readFormDeclaredConsent","consentFieldName","disclosedConsentGroup","formCampaignIds","readContainerIds","pageRoutePath","pageScreenId","pageCampaignIds","then","screen","mappedContactCustom","orgId","some","decl","contactFieldKey","collectMappedContactCustom","decls","definitions","CRM_COLLECTIONS","contactFields","CONTACT_FIELDS_MAX_PER_ORG","docs","map","snapshot","submittedAtMs","hostOrgId","submissionOrgId","submissionRef","add","id","campaignIds","read","createdAt","serverTimestamp","degraded","rateDegraded","contactEmail","leadStored","leadCounted","capturedRecord","arrival","wire","email","atMs","kind","refId","touch","convertedAtMs","filedUnder","describing","containerIds","Promise","captured","routed","lead","identity","name","interaction","source","summary","marketingConsent","containers","campaign","profile","custom","detail","ok","record","sourceAdded","created","leadId","contactId","filed","outcomeNotes","routing","update","formRef","ref","alertPath","join","submittedEmail","submittedFields","fromEntries","filter","alerts","actor"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/forms/src/lib/server/form-submit.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n SITE_JOURNEY_ID_PATTERN,\n SITE_JOURNEY_SUBMISSION_FIELD,\n} from '@aglyn/aglyn/app-utils/site-journey'\nimport * as Aglyn from '@aglyn/aglyn/server'\nimport { extractEmailFromFields } from '@aglyn/aglyn/server'\nimport {\n consumeRateLimit,\n firebaseAdmin,\n getOrgForHost,\n notifyHostManagers,\n visitorWriteRefusal,\n} from '@aglyn/tenant-data-admin'\nimport { emitHostEvent } from '@aglyn/tenant-runtime'\nimport recordCapturedContact from '@aglyn/aglyn/plugin-manager/record-captured-contact'\n// By path: the server-only contract a door credits its outcome through.\nimport {\n CONVERSION_TOUCH_DETAIL,\n conversionDescriptionSentences,\n creditConversion,\n describeConversion,\n resolveConversionTouch,\n} from '@aglyn/aglyn/plugin-manager/plugin-conversion-credit'\nimport { normalizeContainerIds } from '@aglyn/aglyn/app-utils/container-membership'\nimport {\n findScreenIdByRoutePath,\n screenRoutePathFromUrl,\n} from '@aglyn/aglyn/app-utils/screen-route'\n// The leaf: the Inbox list's search keys, the same function its backfill\n// restates and its query's normalizers read (AGL-3321).\nimport { messageSearchFields } from '@aglyn/aglyn/app-utils/message-search'\n// By path: the server-only contract a submission is filed as a record through.\nimport { writeFormRecordTarget } from '@aglyn/aglyn/plugin-manager/submission-record-target'\n// By path: where a manager reads submissions, declared by the plugin whose\n// page shows them, for the notifications below (AGL-3080).\nimport { pluginRecordPageLink } from '@aglyn/aglyn/plugin-manager/plugin-record-pages'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { isCredentialFieldName } from '@aglyn/shared-util-email/hosted-page-screen'\nimport { FORMS_OFF_FOR_SITE_REFUSAL, formSubmissionDoorPlugin } from '../model/form-door'\nimport { FORM_ABUSE_CEILING_CODE } from '../model/form-unavailable'\nimport { incrementFormStats } from './increment-form-stats'\nimport {\n NO_CLIENT_ADDRESS_BUCKET,\n readClientIp,\n} from '@aglyn/aglyn/app-utils/request-ip'\n\nconst MAX_FIELDS = 20\nconst MAX_PAYLOAD_CHARS = 10000\n\n// Per-IP submission limit (AGL-794). Previously a per-instance Map, which on\n// serverless resets each cold start and is held per concurrent instance — so\n// a spammer got roughly RATE_MAX × instances and could widen it by going\n// wider. The monthly plan quota was never the right thing to be defended BY:\n// burning a site's whole allowance is the damage, not the protection — and\n// since AGL-1280 it is not even a wall on a paid plan, it meters. This\n// counter is global, keyed per (site, IP).\n//\n// It is the SHAPE of abuse control, not the whole of it (AGL-1655): 10/60s\n// per address is 14,400/day from one IP and multiplies by the number of\n// addresses, so per-IP alone leaves the site's total unbounded. The per-site\n// monthly ceiling below is what bounds the total.\nconst RATE_WINDOW_MS = 60_000\nconst RATE_MAX = 10\n\nconst json = (body: unknown, status = 200) => Response.json(body, { status })\n\n/**\n * The ceiling is a per-MONTH count, so the honest retry hint is the month\n * boundary rather than a made-up interval. UTC, matching `monthKey`.\n */\nconst secondsUntilNextMonth = (now = new Date()): number => {\n const next = Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 1)\n return Math.max(1, Math.ceil((next - now.getTime()) / 1000))\n}\n\n/**\n * The one useful thing a refused visitor can be given (AGL-1666): the site's\n * OWN published support address, so a lost lead has somewhere else to go.\n *\n * Read through the host-token registry rather than reaching into the\n * document, because that registry is what defines this field as the address\n * a site publishes to visitors — its description is literally \"where\n * visitors should write for help\". Anything else on the host document is\n * account data and must not leave the console.\n *\n * Free: the host snapshot is already loaded for the existence check.\n */\nconst publishedContactEmail = (\n host: Record<string, any> | undefined,\n): string | undefined =>\n host ? Aglyn.HOST_TOKENS['supportEmail']?.resolve(host) : undefined\n\n/**\n * Make a tripped ceiling OBSERVABLE (AGL-1655) rather than a silent drop.\n *\n * Two audiences, one write. Staff get a durable per-month refusal count at\n * `hosts/{id}/counters/formSubmissionsRefused` — the same counters-document\n * shape `contactsDropped` already uses for free-tier contact drops, and\n * client-unwritable for the same reason (AGL-1367). The site's managers get\n * one in-app notification, on the FIRST refusal of the month only: a\n * notification per refused bot request would be the flood again, delivered.\n *\n * Best-effort throughout. A bookkeeping failure must not turn a contained\n * refusal into a 500, because a 500 is a retry invitation.\n */\nasync function recordAbuseCeilingTrip(\n hostRef: FirebaseFirestore.DocumentReference,\n hostId: string,\n monthKey: string,\n ceiling: number,\n): Promise<void> {\n try {\n const refusedRef = hostRef\n .collection('counters')\n .doc('formSubmissionsRefused')\n const refusedSnapshot = await refusedRef.get()\n const alreadyRefused = Number(refusedSnapshot.get(monthKey) ?? 0)\n await refusedRef.set(\n {\n [monthKey]: FieldValue.increment(1),\n // Explicit values only — Firestore rejects `undefined`.\n ceiling,\n lastRefusedAtMs: Date.now(),\n },\n { merge: true },\n )\n if (alreadyRefused === 0) {\n // Every fact here is the route's own (AGL-3432): the refusal sits before\n // every write, so a refused submission is neither stored nor counted\n // toward the bill, and the counter is month-keyed, so forms accept\n // again at the UTC month boundary — the same date the inbox notice\n // prints. `{site}` is the site's name, filled by `notifyHostManagers`.\n const reopens = Aglyn.nextUtcMonthStart().toLocaleDateString('en-US', {\n month: 'long',\n day: 'numeric',\n timeZone: 'UTC',\n })\n await notifyHostManagers(hostId, {\n type: 'system.formSubmissionsPaused',\n title: 'Form submissions paused on {site} — unusual volume',\n body:\n `Forms on {site} stopped accepting submissions: the site reached ` +\n `${ceiling.toLocaleString()} this month, the safety limit for ` +\n 'unusual volume. Submissions sent now are refused and not saved, ' +\n `and none of them are billed. Forms accept submissions again on ` +\n `${reopens}. Contact support if this is real traffic.`,\n ...submissionsLink(hostId),\n })\n }\n } catch (error) {\n console.error('form abuse ceiling bookkeeping failed', error)\n }\n}\n\n/**\n * A notification's link to where the site's managers read submissions — the\n * page the plugin that shows `formSubmission` records declares, opened on one\n * submission when `submissionId` is given and that page can open one — and no\n * link where no plugin in this build shows them. A key left out rather than\n * set to `undefined`, which Firestore rejects.\n */\nfunction submissionsLink(hostId: string, submissionId?: string): { link?: string } {\n const link = pluginRecordPageLink('formSubmission', hostId, submissionId)\n return link ? { link } : {}\n}\n\n/**\n * Count a honeypot hit so the bot rate is a NUMBER, not a guess (AGL-1664).\n *\n * The attestation decision (App Check / CAPTCHA / neither) asked to be made\n * on a measured per-host bot rate, and the honeypot was the instrument named\n * for it — but a hit used to return fake success and record nothing, so the\n * number it was supposed to produce did not exist. This writes the same\n * counters-document shape `formSubmissionsRefused` uses (per-month key,\n * client-unwritable, AGL-1367), keyed per host because \"which site is the\n * bot filling in\" is the decision input.\n *\n * The host-existence read is the orphan guard: this path runs BEFORE the\n * route's shape validation, and an attacker spraying invented host ids must\n * not mint counter documents under hosts that do not exist. Cost per hit is\n * one read + one write — the same order the legitimate path already pays the\n * rate limiter — where the old path cost nothing; that is the price of the\n * measurement, paid only on requests that self-identified as bots.\n *\n * No notification, ever: honeypot hits are routine background noise, and a\n * flood of them delivered as alerts would be the flood again (the\n * `formSubmissionsPaused` lesson). Best-effort for the same reason the\n * refusal bookkeeping is — a bookkeeping failure must not turn a dropped bot\n * request into a 500 retry invitation.\n */\nasync function recordHoneypotHit(hostId: unknown): Promise<void> {\n try {\n if (typeof hostId !== 'string' || !hostId || hostId.length > 128) return\n const hostRef = firebaseAdmin\n .app()\n .firestore()\n .collection('hosts')\n .doc(hostId)\n if (!(await hostRef.get()).exists) return\n await hostRef\n .collection('counters')\n .doc('formSubmissionsSpam')\n .set(\n {\n [Aglyn.utcMonthKey()]: FieldValue.increment(1),\n lastSpamAtMs: Date.now(),\n },\n { merge: true },\n )\n } catch (error) {\n console.error('honeypot bookkeeping failed', error)\n }\n}\n\n/**\n * The opt-in a form CARRIES, or `false`.\n *\n * `upsertHostContact` has accepted `marketingConsent` since AGL-301 and would\n * stamp `marketingConsentAtMs` from it, and this route passed it zero times —\n * so a subscribe checkbox a merchant put on their form was collected, stored\n * as a submission field, and then dropped on the way to the contact. That is\n * the missing INPUT for the send-time consent join\n * (`docs/specs/email-overhaul.md` §1d): the join can only ever be as good as\n * what the capture surfaces record.\n *\n * ⛔ THE FACT OF SUBMISSION IS NOT AN OPT-IN. Only a field whose VALUE is a\n * tick counts, and only under a name that means marketing consent. A form is\n * submitted to ask a question, book a table or claim a refund, and treating\n * any of those as a subscription is exactly the inference the consent arc\n * refused to make.\n *\n * What a tick is comes from `isConsentCheckboxTicked`. A box with no text of\n * its own posts an affirmative value on any form. A Checkboxes field posts\n * the text of its option, which is a tick only where a BOUND form's stored\n * `declarations` show that field is a checkbox with exactly one option: an\n * unbound form has no declaration, and the request never supplies one.\n *\n * The name list is `MARKETING_CONSENT_FIELD_NAMES`, closed rather than a\n * substring match on \"consent\" (a clinic intake form's `consentToTreatment`\n * is a different instrument), and shared with the publish check: a design\n * `checkFormContract` accepts as recording consent is one this route reads\n * an opt-in from.\n */\nfunction readDeclaredMarketingConsent(\n payload: Record<string, any>,\n fields: Record<string, unknown>,\n declarations: readonly Aglyn.FormFieldDecl[],\n): boolean {\n // A first-class body field, for a caller that models the checkbox\n // explicitly rather than as one more form field.\n if (payload['marketingConsent'] === true) return true\n for (const [key, value] of Object.entries(fields)) {\n if (!Aglyn.isMarketingConsentFieldName(key)) continue\n const declaration = declarations.find((entry) => entry?.fieldName === key)\n if (Aglyn.isConsentCheckboxTicked(value, declaration)) return true\n }\n return false\n}\n\n/**\n * `POST /api/forms/submit` — every form's door, and a Marketing popup's\n * (AGL-76, AGL-3080).\n *\n * This plugin's route, served by the tenant's plugin API dispatcher at the\n * address it has always had, and registered as a route that KEEPS ITS OWN\n * VISITOR GATES (`ownVisitorGates`): the dispatcher hands it the request\n * untouched, because every gate below is a published answer — which plugin's\n * switch decides a submission (the door it came through, not this route's\n * owner), the \"not accepting\" notice a site with Forms off gives, the\n * per-(site, address) limit and the monthly ceiling, and where lockdown sits\n * among them. The dispatcher's own versions of those would answer first and\n * differently.\n *\n * Lead-capture submissions endpoint (AGL-76): validates the target host,\n * drops honeypot hits silently, applies the plan's monthly submission quota\n * via a per-month counter — a hard wall on free, a meter on plans that carry\n * the infra pass-through (AGL-1280) — refuses above the per-site abuse\n * ceiling (AGL-1655), and stores the submission for the console inbox. Writes\n * go through the admin SDK, so client rules never allow arbitrary submission\n * writes.\n *\n * The two 429s are different answers and must stay distinguishable: the plan\n * wall means the customer's allowance is spent, the ceiling means the site is\n * being flooded. Only the ceiling refusal carries `code`.\n */\nexport async function POST(request: Request): Promise<Response> {\n // The dispatcher serves every method at its catch-all; this door takes a\n // POST alone, as the route it replaced did.\n if (request.method !== 'POST') {\n return new Response(null, { status: 405, headers: { Allow: 'POST' } })\n }\n const payload = (await request.json().catch(() => ({}))) as Record<string, any>\n // `datasetId`, `dataset` and `fieldMap` are deliberately not read: the\n // dataset a record lands in comes from the page's signed binding (below).\n const { hostId, formId, formName, path, fields, website } = payload\n\n // Honeypot filled → pretend success so bots learn nothing, counted so the\n // owner of the attestation decision learns something (AGL-1664).\n if (typeof website === 'string' && website.trim()) {\n await recordHoneypotHit(hostId)\n return json({ received: true })\n }\n if (\n typeof hostId !== 'string' ||\n !hostId ||\n typeof fields !== 'object' ||\n fields === null ||\n Array.isArray(fields) ||\n Object.keys(fields).length === 0 ||\n Object.keys(fields).length > MAX_FIELDS ||\n JSON.stringify(fields).length > MAX_PAYLOAD_CHARS\n ) {\n return json({ error: 'Invalid submission' }, 400)\n }\n // Keeps counting under the no-address bucket rather than being skipped: a\n // public form endpoint that stops counting is an open write, and the key\n // already carries the site, so an unreadable address shares one budget per\n // SITE rather than one across the deployment.\n const ip = readClientIp(request.headers) ?? NO_CLIENT_ADDRESS_BUCKET\n const rate = await consumeRateLimit(`form:${hostId}:${ip}`, {\n limit: RATE_MAX,\n windowMs: RATE_WINDOW_MS,\n })\n if (!rate.allowed) {\n // Note this is NOT told to the caller, degraded or not. The body and the\n // status are identical either way: \"the global limiter is currently a\n // per-instance one\" is exactly the sentence an abuser would spend the\n // window on, and this endpoint is public and unauthenticated.\n return Response.json(\n { error: 'Too many submissions' },\n {\n status: 429,\n headers: {\n 'Retry-After': String(\n Math.max(1, Math.ceil((rate.resetMs - Date.now()) / 1000)),\n ),\n },\n },\n )\n }\n\n try {\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const hostSnapshot = await hostRef.get()\n if (!hostSnapshot.exists) {\n return json({ error: 'Unknown site' }, 404)\n }\n\n // Lockdown (AGL-1511). Placed after the host exists and BEFORE the quota\n // read and every write: a paused submission must not consume the\n // customer's monthly allowance, and it must not be half-recorded.\n // Refused with the visitor pause copy — the site around this form is\n // still serving, so an outage page here would be both a lie and a lost\n // lead. The `/api` surface is outside the middleware matcher, so this is\n // the only lockdown enforcement a form POST ever meets, full or\n // read-only.\n const paused = await visitorWriteRefusal({\n hostId,\n request,\n surface: 'form',\n })\n if (paused) return paused\n\n // Monthly quota by the owning org's plan (dark-launch: orgs\n // without a plan are uncapped, matching every other gate).\n // Plan/quota gates ride the owning org's doc (AGL-238).\n const owningOrg = await getOrgForHost(hostId)\n const orgBilling = owningOrg?.org\n\n /*\n * A site that switched Forms off accepts no form submission (AGL-3029).\n *\n * Asked of the site's own plugin set — the org's resolved set minus this\n * host's deny-list, the same answer the tenant draws the page from — and\n * about the plugin whose door the submission came through: a Marketing\n * popup's capture belongs to Marketing, and keeps working on a site that\n * still shows the popup. Core imports no plugin, so both are ids.\n *\n * Placed after the site is known to exist and before the quota read and\n * every write: a refused submission is not counted, not billed and not\n * stored. 404, the answer the route already gives a retired form, because\n * the honest reply to the visitor is the same — this site takes no form\n * here — and the sentence names no setting a stranger has no use for.\n */\n if (\n !Aglyn.isHostPluginEnabled(\n orgBilling as never,\n hostSnapshot.data() as never,\n formSubmissionDoorPlugin(payload),\n )\n ) {\n return json({ error: FORMS_OFF_FOR_SITE_REFUSAL }, 404)\n }\n // Shared with the console surface that reads these counters back\n // (AGL-1666) — a differently-derived key there would read 0 refusals on\n // exactly the sites being refused.\n const monthKey = Aglyn.utcMonthKey()\n const counterRef = hostRef.collection('counters').doc('formSubmissions')\n {\n // Plan-less orgs resolve as free (AGL-247) — the cap always runs.\n const counterSnapshot = await counterRef.get()\n const used = Number(counterSnapshot.get(monthKey) ?? 0)\n // AGL-1280: a plan with the metered infra pass-through is never cut\n // off — submissions past the included count bill at cost × 1.3 like\n // storage, API requests and contacts already do. Free keeps the hard\n // wall, because there is no subscription to meter onto. A dropped\n // submission is a lost lead, so this is the meter where being wrong\n // in the \"cut them off\" direction costs the customer the most.\n if (!Aglyn.checkFormSubmissionQuota(orgBilling as any, used).allowed) {\n return json({ error: 'Submission limit reached' }, 429)\n }\n // Abuse ceiling (AGL-1655). The plan gate above cannot refuse a\n // metered plan by design, so on every paying customer it was the only\n // thing between a bot flood and an unbounded invoice — and it always\n // said yes. The per-IP limiter is 10/60s, which one address turns into\n // 14,400/day and any number of addresses multiplies.\n //\n // This is the ceiling that can say no. It reuses the count already\n // read above (no extra Firestore read on the happy path) and sits\n // BEFORE every write, so a refused submission never touches\n // `counters/formSubmissions` — the document /api/billing/report-usage\n // invoices from. Refusing and still billing would have been the same\n // bug with a smaller number.\n const ceiling = Aglyn.checkFormSubmissionAbuseCeiling(\n orgBilling as any,\n used,\n )\n if (ceiling.exceeded) {\n await recordAbuseCeilingTrip(hostRef, hostId, monthKey, ceiling.ceiling)\n const contact = publishedContactEmail(hostSnapshot.data())\n return Response.json(\n {\n error: 'Submissions are paused for this site',\n // Machine-readable so a client can tell containment apart from\n // the plan wall it shares a status with.\n code: FORM_ABUSE_CEILING_CODE,\n // The site's own published support address, when it has one\n // (AGL-1666). NOT the ceiling, the count or the plan: this body\n // is read by a stranger to the site, and the reason a site\n // stopped accepting is the owner's to see, not a caller's.\n ...(contact ? { contact } : {}),\n },\n {\n status: 429,\n headers: { 'Retry-After': String(secondsUntilNextMonth()) },\n },\n )\n }\n }\n\n /*\n * NO CREDENTIALS, whatever the page asked for (AGL-3356).\n *\n * A published page that asks for a password, a card number or a\n * one-time code in a field of its own is held by the page review before\n * a visitor sees it. This endpoint is the one path that does not pass a\n * publish: it takes whatever names a request carries, so a page the\n * review never read — a marketplace plugin's frame, a hand-built request\n * — could still post one here. Such a field is dropped, never stored,\n * never forwarded to a workflow or a contact.\n */\n const sanitizedFields: Record<string, string> = {}\n for (const [key, value] of Object.entries(fields)) {\n if (isCredentialFieldName(String(key))) continue\n sanitizedFields[String(key).slice(0, 64)] = String(value).slice(0, 2000)\n }\n if (!Object.keys(sanitizedFields).length) {\n return json({ error: 'Invalid submission' }, 400)\n }\n\n /*\n * The bound form entity (`docs/specs/reusable-forms.md` §2c).\n *\n * READ AND VERIFIED, never trusted. `formId` arrives in a public,\n * unauthenticated body, so stamping it unchecked would let anyone file\n * rows into any form's list on any site — the per-form list is the\n * surface this id exists to make correct, and an unverified id would make\n * it wrong in a way that looks right.\n *\n * One extra document read, and only when a form is bound. A form with no\n * `formId` pays nothing, which is the same shape the dataset binding and\n * `rateDegraded` already take.\n */\n const boundFormId = String(formId ?? '').trim().slice(0, 128)\n const formSnapshot = boundFormId\n ? await hostRef.collection('forms').doc(boundFormId).get()\n : null\n const form = formSnapshot?.exists ? formSnapshot : null\n /*\n * A RETIRED form collects nothing (AGL-2671).\n *\n * Retirement is what a merchant does with a form they have finished with\n * but must keep — the submissions, the leads and the contact timeline\n * those built are filed under it. Leaving it able to collect would make\n * the retirement a UI illusion: the row would be gone from the list and\n * out of the funnel's grading while a form still placed on a published\n * page kept filing leads nobody was looking at, which is the quiet half\n * of a lost lead rather than a retirement.\n *\n * Refused HERE, on the verified entity, rather than by unpublishing the\n * page: a form is placed by whoever placed it, on any number of pages, and\n * the document is the one place that knows it is retired.\n *\n * 404 rather than a 4xx that explains: the visitor's honest answer is that\n * this form is not here any more, and the shape matches `Unknown site`\n * above. Only ever for a BOUND form — a submission carrying no `formId`\n * has no entity to be retired and is untouched.\n */\n if (Aglyn.isFormArchived(form?.data() as { archivedAt?: number } | undefined)) {\n return json({ error: 'This form is no longer accepting submissions' }, 404)\n }\n /*\n * The caption follows the entity once one is bound.\n *\n * `formName` stays written either way: it is what the pre-entity `?form=`\n * filter reads, and no phase of this work removes it. Resolving it from\n * the form's own `displayName` keeps the Inbox's label from going stale\n * the moment somebody renames a form.\n *\n * ⚠️ This does NOT make the legacy filter rename-safe, and it is not\n * meant to — renaming still splits what `?form=` returns, exactly as it\n * always did. `formId` is the filter that survives a rename; that is the\n * whole reason it exists.\n */\n const resolvedFormName = form\n ? String(form.get('displayName') ?? formName ?? 'Form').slice(0, 100)\n : String(formName ?? 'Form').slice(0, 100)\n /*\n * Consent, from the field the FORM declares.\n *\n * ⛔ Still never inferred from the fact of submission. What the entity\n * adds is a declared place to look: `consentFieldName` names the one\n * field that IS the opt-in, so a merchant's checkbox called anything at\n * all is readable, and a clinic intake form's `consentToTreatment` stays\n * the different instrument it is.\n *\n * A bound form that declares NO consent field falls through to the closed\n * name list below rather than recording nothing. Adoption must not be the\n * moment a form that was capturing opt-ins stops — consent is carried\n * forward, never dropped as a side effect of a migration.\n *\n * Both readers are handed the form's STORED declaration: a Checkboxes\n * field posts the text of the option that was ticked, and only the\n * declaration says a consent field has exactly one option, whose text is\n * then the tick. An unbound form has none, so its boxes count only by an\n * affirmative value.\n */\n const formFieldDecls = Array.isArray(form?.get('fields'))\n ? (form?.get('fields') as Aglyn.FormFieldDecl[])\n : []\n const declaredMarketingConsent = form?.get('consentFieldName')\n ? Aglyn.readFormDeclaredConsent(\n {\n consentFieldName: String(form.get('consentFieldName')),\n fields: formFieldDecls,\n },\n sanitizedFields,\n )\n : readDeclaredMarketingConsent(payload, fields, formFieldDecls)\n /*\n * THE DISCLOSURE THE FORM SHOWED beside that opt-in (AGL-3320).\n *\n * The Form block renders the site's consent-group sentence under its\n * consent field and posts back the key of what it rendered\n * (`/api/consent/disclosure`). Passed to the record system as it arrived:\n * the writer pools the opt-in across the group only when this is the\n * group's CURRENT key, so a page rendered before a rename or before a\n * site joined records the one site the visitor was on. Trusting it costs\n * nothing — it can only ever narrow what the checkbox above already\n * claims, never widen it past the declaration.\n */\n const disclosedConsentGroup =\n typeof payload['__consentGroup'] === 'string'\n ? payload['__consentGroup'].trim().slice(0, 64)\n : ''\n /*\n * THE CAMPAIGNS THE FORM IS FILED UNDER.\n *\n * Off the form document this route has already read, so it costs nothing,\n * and off the VERIFIED form for the same reason the id is: a membership\n * taken from the request body would let anyone file people into any\n * campaign on any site.\n *\n * ⚠️ NOT the campaign touch resolved below, and the two never write each\n * other's field. The touch is where this visitor came FROM and is\n * browser-supplied; this is which campaigns the merchant put the form in,\n * and it is true of every person who fills that form in — including the\n * one who arrived by typing the address. Recording only the touch is what\n * left three forms assigned to a campaign producing contacts that belonged\n * to none.\n */\n const formCampaignIds = form\n ? Aglyn.readContainerIds(form.data() as Record<string, unknown>, 'campaign')\n : []\n /*\n * THE CAMPAIGNS THE PAGE IS FILED UNDER (AGL-3461), stamped beside the\n * form's for the same reason and with the same caveat: where the merchant\n * filed the page, true of everyone who submits on it, and never a credit.\n *\n * The screen is found in the routing map on the host snapshot this route\n * already holds, from the path the browser sent, so a path naming no page\n * this site serves names no screen and costs nothing. Its campaigns are\n * read off the screen document — one read, only when the path resolves.\n * A failed read stamps nothing rather than failing the submission.\n */\n const pageRoutePath = screenRoutePathFromUrl(typeof path === 'string' ? path : '')\n const pageScreenId = pageRoutePath\n ? findScreenIdByRoutePath(\n hostSnapshot.get('screens') as Record<string, string> | undefined,\n pageRoutePath,\n )\n : undefined\n const pageCampaignIds = pageScreenId\n ? await hostRef\n .collection('screens')\n .doc(pageScreenId)\n .get()\n .then((screen) =>\n screen.exists\n ? Aglyn.readContainerIds(screen.data() as Record<string, unknown>, 'campaign')\n : [],\n )\n .catch((error: unknown) => {\n console.error('form submission page campaigns read failed', error)\n return [] as string[]\n })\n : []\n /*\n * THE CUSTOM CONTACT FIELDS THIS FORM SAVES TO (AGL-2601).\n *\n * Off the VERIFIED form's declaration, like the campaigns above and for\n * the same reason: a mapping taken from the request body would let anyone\n * write any value under any field of any contact on any site. The\n * definitions are read from the owning org — one bounded read of a small\n * collection — and only when the declaration maps at least one field, so\n * a form that saves nowhere pays nothing.\n *\n * Coerced by the definition's TYPE before it goes anywhere near the\n * contact: `collectMappedContactCustom` is the same rule the API applies\n * to a `custom` body, so a number typed into a form and a number sent\n * over the API are stored as the same thing. A blank field contributes no\n * key at all, so a submission never clears a value the merchant set.\n */\n const mappedContactCustom =\n form && owningOrg?.orgId && formFieldDecls.some((decl) => decl?.contactFieldKey)\n ? Aglyn.collectMappedContactCustom({\n fields: sanitizedFields,\n decls: formFieldDecls,\n definitions: (\n await firestore\n .collection('orgs')\n .doc(owningOrg.orgId)\n .collection(Aglyn.CRM_COLLECTIONS.contactFields)\n .limit(Aglyn.CONTACT_FIELDS_MAX_PER_ORG)\n .get()\n ).docs.map((snapshot) => snapshot.data() as Aglyn.ContactFieldDefinition),\n })\n : {}\n // One instant for the whole submission. The attribution window is\n // measured against it in three places below, and three calls to\n // `Date.now()` would let a slow write decide whether a touch was inside\n // the window for the contact and outside it for the lead.\n const submittedAtMs = Date.now()\n /*\n * The organization this site belongs to, off the two documents this\n * route has already read — the host, whose `orgId` only the org APIs\n * write, and the `hostIndex` mirror the plan gate resolved through.\n */\n const hostOrgId = hostSnapshot.get('orgId')\n const submissionOrgId =\n typeof hostOrgId === 'string' && hostOrgId ? hostOrgId : owningOrg?.orgId\n const submissionRef = await hostRef.collection('formSubmissions').add({\n /*\n * WHERE THE ROW LIVES, as fields (AGL-3303). The organization's Inbox\n * reads every site's submissions with ONE collection-group query,\n * `where('orgId', '==', …)` newest first, and the rules admit that\n * read on the org named here; `hostId` is what that list addresses a\n * row's site by. Both are frozen against a client update, and a site\n * with no org carries no `orgId`, so it is in no organization's list.\n */\n ...(submissionOrgId ? { orgId: submissionOrgId } : {}),\n hostId,\n // Stamped only for a form that exists on THIS site. An unverified id\n // never reaches the row, so the per-form list cannot be written into\n // from outside.\n ...(form ? { formId: form.id } : {}),\n /*\n * The form's campaign membership, copied onto the row.\n *\n * A STAMP of what the form said at the moment this arrived, not a live\n * edge: nothing edits a submission's campaigns afterwards, and refiling\n * the form later does not rewrite the submissions it already produced.\n * That is why this collection is not in the Marketing plugin's\n * `CAMPAIGN_MEMBER_HOST_COLLECTIONS` — the deletion pass walks the\n * collections a PICKER writes, and a campaign's removal must not rewrite\n * an unbounded, billed history collection to tidy up a field that is\n * only ever read alongside the campaigns a picker still offers.\n *\n * Written only when the form names one, so the field is absent on the\n * rows that would carry an empty array forever.\n */\n ...(formCampaignIds.length ? { campaignIds: formCampaignIds } : {}),\n // And the page's, beside them rather than merged in: `campaignIds` is\n // the form's membership and is read as exactly that (AGL-3461).\n ...(pageCampaignIds.length ? { pageCampaignIds } : {}),\n formName: resolvedFormName,\n path: String(path ?? '').slice(0, 500),\n fields: sanitizedFields,\n /*\n * WHAT THE INBOX'S LIST ASKS (AGL-3321). Its From filter and its\n * search are `array-contains` on these word-prefix arrays, and Read is\n * an equality on `read` — so every row carries all three from the\n * moment it exists. The fields never change after this write, so no\n * other writer restamps them; `backfill-form-submission-filters.mjs`\n * stamps the rows written before.\n */\n ...messageSearchFields(sanitizedFields),\n read: false,\n createdAt: FieldValue.serverTimestamp(),\n // Accepted while the durable limiter was degraded (AGL-1667).\n //\n // The limiter fails soft by design (AGL-794, reaffirmed in AGL-1679) and\n // that is not reversed here: a Firestore blip must not stop a customer's\n // site collecting leads, and the fallback can only ever allow MORE than\n // 10/min, never fewer, so nothing legitimate is refused by it. What was\n // wrong is that the route read `allowed` and `resetMs` and never\n // `degraded`, so the window left no trace on THIS endpoint at all.\n //\n // The AGL-1679 marker records that the store degraded; it cannot say\n // which submissions came in under the wider cap, and that is the\n // correlation the incident actually needs — these rows are billed\n // (`/api/billing/report-usage` prices `counters/formSubmissions`) and a\n // customer disputing a spike is owed a per-row answer, not \"the limiter\n // was unwell around then\".\n //\n // Stamped rather than counted because it rides a write that is already\n // happening: a separate counter would mean an EXTRA Firestore write per\n // submission, issued precisely while Firestore is the thing that is\n // failing. Written only when true — an absent field is the healthy case\n // and costs nothing on the millions of rows that are fine.\n ...(rate.degraded ? { rateDegraded: true } : {}),\n })\n // Contacts ingestion (AGL-197): forms don't guarantee an email field —\n // best-effort extraction; never blocks the submission.\n const contactEmail = extractEmailFromFields(sanitizedFields)\n /*\n * Whether this submission was filed to the site's Leads.\n *\n * The form counts PEOPLE (AGL-3330). The lead silo keys one person to one\n * document, so a returning visitor's second submission updates the lead\n * it created the first time; the form's lead count moves only when the\n * capture puts this form's source on a lead for the first time\n * (`leadCounted`), which is exactly what a recount of the leads'\n * `sources` finds — so the increment and the recount agree, and a lead\n * erased later can be taken back off. A routed submission by somebody\n * the workspace already holds as a contact lands on the contact and files\n * no lead (AGL-3232), and is not counted.\n */\n let leadStored = false\n /** Whether that lead is one more person on this form's lead count. */\n let leadCounted = false\n /**\n * The lead or contact the person was filed as, for the Inbox to link to\n * (AGL-3461) — by the id the record system answered with, which is the\n * id its own record route addresses.\n */\n let capturedRecord: { kind: 'lead' | 'contact'; id: string } | null = null\n /*\n * WHERE THE VISITOR ARRIVED FROM, RESOLVED ONCE FOR THE WHOLE SUBMISSION.\n *\n * One visitor action lands in three collections here — the submission,\n * the contact and (when the form declares itself a lead surface) the\n * lead — and all three are the same person arriving from the same place.\n * Resolving here and handing the result down is what keeps the lookup at\n * one per submission instead of three, and it is also what guarantees the\n * three records cannot disagree about what to credit.\n *\n * The answer is the crediting plugin's (`plugin-conversion-credit.ts`),\n * asked with what the visitor's browser sent under `campaignTouch` —\n * untrusted, so the creditor re-parses and window-checks it before it\n * can name anything. A submission carrying none — direct traffic, or a\n * visitor whose consent posture never let a touch be remembered — and a\n * workspace whose plugins credit nothing both resolve to null and write\n * no credit at all.\n */\n const arrival = await resolveConversionTouch({\n hostId,\n wire: payload['campaignTouch'],\n email: contactEmail,\n atMs: submittedAtMs,\n })\n // The submission itself is an outcome whether or not it named anybody:\n // an anonymous enquiry from a campaign link is a conversion that campaign\n // caused, and refusing to count it because no email field was filled in\n // would under-report exactly the forms that ask for the least.\n void creditConversion({\n hostId,\n kind: 'form',\n refId: submissionRef.id,\n touch: arrival,\n convertedAtMs: submittedAtMs,\n })\n /*\n * THE CAMPAIGNS IN WORDS, for the managers' alert below (AGL-3461): the\n * one the visitor arrived through and the ones the form and page are\n * filed under, named by the plugin that keeps campaigns. Started here so\n * its reads overlap the capture and the counters; asked only when there\n * is something to name, and it never throws.\n */\n const filedUnder = normalizeContainerIds([...formCampaignIds, ...pageCampaignIds])\n const describing =\n arrival || filedUnder.length\n ? describeConversion({ hostId, touch: arrival, containerIds: filedUnder })\n : Promise.resolve(null)\n if (contactEmail) {\n /*\n * WHICH RECORD THE PERSON LANDS ON is the record system's decision\n * (AGL-3232), and the form says only what kind of surface it is: a\n * lead surface when its author switched lead routing on\n * (`docs/specs/reusable-forms.md` §4a), a touch otherwise. The writer\n * files a lead — through `addHostLead`, keyed on the person and\n * counted against the ceiling — unless the workspace already holds the\n * address as a contact, and answers which record it wrote.\n *\n * AWAITED, unlike the capture of every other door, and only because\n * the answer is counted: the form's `stats.leads` rides the same\n * `update` the submission counters ride below, and a `void` call with\n * the counter chained onto its `then` would put the one figure on this\n * page that is about routing on a promise the runtime is free to\n * abandon once the response is sent. The posture is unchanged —\n * `recordCapturedContact` never throws — so awaiting it cannot fail the\n * submission any more than voiding it could.\n */\n const routed = form?.get('routing')?.lead === true\n const captured = await recordCapturedContact({\n /*\n * Not gated on the org resolving. The record system keys a person on\n * the SITE they were met on, so an org lookup that came back empty\n * costs the capture nothing — and refusing to record the person over\n * it would turn a lookup failure into a lost contact, silently, which\n * is the failure this whole seam is arranged to avoid.\n */\n orgId: owningOrg?.orgId ?? '',\n hostId,\n identity: {\n email: contactEmail,\n name: sanitizedFields['name'] ?? sanitizedFields['fullName'],\n },\n interaction: {\n source: 'form',\n refId: submissionRef.id,\n summary: `Submitted \"${resolvedFormName.slice(0, 60)}\"`,\n },\n surface: routed ? 'lead' : 'touch',\n ...(declaredMarketingConsent ? { marketingConsent: true } : {}),\n ...(declaredMarketingConsent && disclosedConsentGroup\n ? { disclosedConsentGroup }\n : {}),\n // Filed under the form's campaigns, inside this site's own facet on a\n // row the whole org shares. Membership is not consent, and this passes\n // none: `marketingConsent` above is the only input that records one.\n ...(formCampaignIds.length\n ? { containers: { campaign: formCampaignIds } }\n : {}),\n // The mapped custom field values. Absent when nothing mapped, so the\n // owner adds no `custom` key for nothing.\n ...(Object.keys(mappedContactCustom).length\n ? { profile: { custom: mappedContactCustom } }\n : {}),\n detail: {\n /*\n * THE ENTRY POINT, for the owner to put on the person's timeline.\n *\n * Which form and which page a person came in through is a fact\n * about this capture rather than about the person, which is why it\n * rides `detail` and not `profile`. `sources` says only that SOME\n * form produced this contact — every form sets the same flag — and\n * answering \"which one\" by reading the submission back would be a\n * document read per row of a timeline the console renders straight\n * out of the contact.\n *\n * The id only for a VERIFIED form, matching the submission: an\n * unverified id never reaches a stored row on this path.\n */\n ...(form ? { formId: form.id } : {}),\n ...(typeof path === 'string' && path ? { path } : {}),\n // Where the visitor ARRIVED from, already resolved above — never\n // the same fact as `campaignIds`, which is where the merchant filed\n // the form.\n ...(arrival ? { [CONVERSION_TOUCH_DETAIL]: arrival } : {}),\n },\n })\n leadStored = captured?.ok === true && captured.record === 'lead'\n /*\n * A form counts the PEOPLE it filed (AGL-3330): the capture says\n * whether this one was new to the form, and a writer that does not\n * say is read as whether the lead itself was new.\n */\n leadCounted =\n leadStored &&\n captured?.ok === true &&\n captured.record === 'lead' &&\n (captured.sourceAdded ?? captured.created) === true\n if (captured?.ok === true) {\n const id = captured.record === 'lead' ? captured.leadId : captured.contactId\n if (id) capturedRecord = { kind: captured.record, id }\n }\n }\n /*\n * WHERE ELSE THE SUBMISSION IS FILED (AGL-3080): a record in whatever\n * plugin keeps the records a form may write into — a dataset row, today.\n *\n * Asked AFTER the submission is stored, through the platform's contract\n * (`plugin-manager/submission-record-target.ts`), and the target decides from\n * what the page's compose stamped onto this form, never from the body\n * alone: the route is public. It answers the submission's `routing`,\n * which the Inbox reads to say where it went or why it did not. The Inbox\n * copy above is canonical either way, and nothing here can fail the\n * submission.\n */\n const filed = await writeFormRecordTarget({\n hostId,\n orgId: owningOrg?.orgId ?? null,\n orgBilling,\n body: payload,\n fields: sanitizedFields,\n })\n /*\n * What the submission led to, on the row in ONE update: where else it\n * was filed, and the lead or contact it filed (AGL-3461). Written only\n * when there is either, so the ordinary anonymous enquiry pays nothing.\n */\n const outcomeNotes = {\n ...(filed.routing ? { routing: filed.routing } : {}),\n ...(capturedRecord ? { capturedRecord } : {}),\n }\n if (Object.keys(outcomeNotes).length) {\n try {\n await submissionRef.update(outcomeNotes)\n } catch (error) {\n console.error('form submission routing note failed', error)\n }\n }\n await counterRef.set({ [monthKey]: FieldValue.increment(1) }, { merge: true })\n /*\n * Per-form counters (`docs/specs/reusable-forms.md` §5a), through the\n * one writer that retries and REPORTS a failure rather than swallowing\n * it (AGL-3330) — see `incrementFormStats`.\n *\n * `stats.leads` is here rather than beside the lead writer for the same\n * reason the submission counters are: a second write to say a lead was\n * filed would be a second write per submission on the collection the\n * customer is billed against.\n */\n if (form) {\n await incrementFormStats({\n formRef: form.ref,\n hostId,\n monthKey,\n submittedAtMs,\n leadCounted,\n })\n }\n // Event trigger (AGL-128/148): field values join the automation\n // scope; action-produced site alerts ride back to the visitor.\n // In-app notification to the site's managers (AGL-259).\n /*\n * The body names the form and the site on its own (AGL-3432): a manager\n * of several sites reads the email body, not always the subject, and\n * \"Page: /contact\" said neither. `{site}` is filled by\n * `notifyHostManagers` from the host doc it already reads.\n *\n * Then the campaigns (AGL-3461): the one the visitor was credited to and\n * how, and what the form and page are filed under, as the crediting\n * plugin names them.\n *\n * It opens THIS submission, not the list: the address the plugin that\n * shows submissions declares for one record (`submissionsLink`), stored\n * in the host-link shape every host notification uses and rewritten onto\n * the site's address when it is followed, in the console and in the email\n * alike. A notification holds one link, so the lead and the campaign are\n * links inside the reader.\n */\n const alertPath = typeof path === 'string' ? path.slice(0, 500) : ''\n void notifyHostManagers(hostId, {\n type: 'content.formSubmission',\n title: `New form submission — ${resolvedFormName}`,\n body: [\n `Someone submitted “${resolvedFormName}” on {site}` +\n (alertPath ? ` (page ${alertPath}).` : '.'),\n ...conversionDescriptionSentences(await describing),\n ].join(' '),\n ...submissionsLink(hostId, submissionRef.id),\n })\n const submittedEmail =\n typeof sanitizedFields['email'] === 'string' ? sanitizedFields['email'] : ''\n /*\n * `formId` is the VERIFIED form's id (AGL-3458), the way `contactCreated`\n * carries it: a condition keyed on it survives the form being renamed,\n * where one on `formName` silently stops matching. It is written last\n * and only from the form this route read, so a submitted field that\n * calls itself `formId` can neither claim another form's automations\n * nor stand in for a form the site does not have.\n */\n const submittedFields = Object.fromEntries(\n Object.entries(sanitizedFields).filter(([key]) => key !== 'formId'),\n )\n const { alerts } = await emitHostEvent(\n hostId,\n 'formSubmission',\n {\n formName: resolvedFormName,\n path: String(path ?? '').slice(0, 500),\n ...submittedFields,\n ...(form ? { formId: form.id } : {}),\n },\n // The visitor who submitted, by the address they typed if any (AGL-3376),\n // and the recorded visit this submission ends (AGL-3605), when the page\n // sent one in the shape the recorder mints.\n {\n actor: { kind: 'visitor', ...(submittedEmail ? { email: submittedEmail } : {}) },\n ...(SITE_JOURNEY_ID_PATTERN.test(String(payload[SITE_JOURNEY_SUBMISSION_FIELD] ?? ''))\n ? { journeyId: String(payload[SITE_JOURNEY_SUBMISSION_FIELD]) }\n : {}),\n },\n )\n return json({ received: true, alerts })\n } catch (error) {\n console.error(error)\n return json({ error: 'Submission failed' }, 500)\n }\n}\n"],"names":["SITE_JOURNEY_ID_PATTERN","SITE_JOURNEY_SUBMISSION_FIELD","Aglyn","extractEmailFromFields","consumeRateLimit","firebaseAdmin","getOrgForHost","notifyHostManagers","visitorWriteRefusal","emitHostEvent","recordCapturedContact","CONVERSION_TOUCH_DETAIL","conversionDescriptionSentences","creditConversion","describeConversion","resolveConversionTouch","normalizeContainerIds","findScreenIdByRoutePath","screenRoutePathFromUrl","messageSearchFields","writeFormRecordTarget","pluginRecordPageLink","FieldValue","isCredentialFieldName","FORMS_OFF_FOR_SITE_REFUSAL","formSubmissionDoorPlugin","FORM_ABUSE_CEILING_CODE","incrementFormStats","NO_CLIENT_ADDRESS_BUCKET","readClientIp","MAX_FIELDS","MAX_PAYLOAD_CHARS","RATE_WINDOW_MS","RATE_MAX","json","body","status","Response","secondsUntilNextMonth","now","Date","next","UTC","getUTCFullYear","getUTCMonth","Math","max","ceil","getTime","publishedContactEmail","host","HOST_TOKENS","resolve","undefined","recordAbuseCeilingTrip","hostRef","hostId","monthKey","ceiling","refusedSnapshot","refusedRef","collection","doc","get","alreadyRefused","Number","set","increment","lastRefusedAtMs","merge","reopens","nextUtcMonthStart","toLocaleDateString","month","day","timeZone","type","title","toLocaleString","submissionsLink","error","console","submissionId","link","recordHoneypotHit","length","app","firestore","exists","utcMonthKey","lastSpamAtMs","readDeclaredMarketingConsent","payload","fields","declarations","key","value","Object","entries","isMarketingConsentFieldName","declaration","find","entry","fieldName","isConsentCheckboxTicked","POST","request","method","headers","Allow","catch","formId","formName","path","website","trim","received","Array","isArray","keys","JSON","stringify","ip","rate","limit","windowMs","allowed","String","resetMs","form","hostSnapshot","paused","surface","owningOrg","orgBilling","org","isHostPluginEnabled","data","counterRef","counterSnapshot","used","checkFormSubmissionQuota","checkFormSubmissionAbuseCeiling","exceeded","contact","code","sanitizedFields","slice","boundFormId","formSnapshot","isFormArchived","resolvedFormName","formFieldDecls","declaredMarketingConsent","readFormDeclaredConsent","consentFieldName","disclosedConsentGroup","formCampaignIds","readContainerIds","pageRoutePath","pageScreenId","pageCampaignIds","then","screen","mappedContactCustom","orgId","some","decl","contactFieldKey","collectMappedContactCustom","decls","definitions","CRM_COLLECTIONS","contactFields","CONTACT_FIELDS_MAX_PER_ORG","docs","map","snapshot","submittedAtMs","hostOrgId","submissionOrgId","submissionRef","add","id","campaignIds","read","createdAt","serverTimestamp","degraded","rateDegraded","contactEmail","leadStored","leadCounted","capturedRecord","arrival","wire","email","atMs","kind","refId","touch","convertedAtMs","filedUnder","describing","containerIds","Promise","captured","routed","lead","identity","name","interaction","source","summary","marketingConsent","containers","campaign","profile","custom","detail","ok","record","sourceAdded","created","leadId","contactId","filed","outcomeNotes","routing","update","formRef","ref","alertPath","join","submittedEmail","submittedFields","fromEntries","filter","alerts","actor","test","journeyId"],"mappings":"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"}
|