@aglyn/plugins-forms 1.0.0-beta.231 → 1.0.0-beta.232

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.231",
3
+ "version": "1.0.0-beta.232",
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.231",
29
- "@aglyn/shared-data-enums": "1.0.0-beta.231",
30
- "@aglyn/shared-data-mdi": "1.0.0-beta.231",
31
- "@aglyn/shared-ui-jsx": "1.0.0-beta.231",
32
- "@aglyn/shared-ui-jsx-forms": "1.0.0-beta.231",
33
- "@aglyn/shared-ui-snackstack": "1.0.0-beta.231",
34
- "@aglyn/shared-util-email": "1.0.0-beta.231",
35
- "@aglyn/shared-util-http": "1.0.0-beta.231",
36
- "@aglyn/shared-util-timestamp": "1.0.0-beta.231",
37
- "@aglyn/shared-util-tools": "1.0.0-beta.231",
38
- "@aglyn/tenant-data-admin": "1.0.0-beta.231",
39
- "@aglyn/tenant-feature-instance": "1.0.0-beta.231",
40
- "@aglyn/tenant-runtime": "1.0.0-beta.231",
28
+ "@aglyn/aglyn": "1.0.0-beta.232",
29
+ "@aglyn/shared-data-enums": "1.0.0-beta.232",
30
+ "@aglyn/shared-data-mdi": "1.0.0-beta.232",
31
+ "@aglyn/shared-ui-jsx": "1.0.0-beta.232",
32
+ "@aglyn/shared-ui-jsx-forms": "1.0.0-beta.232",
33
+ "@aglyn/shared-ui-snackstack": "1.0.0-beta.232",
34
+ "@aglyn/shared-util-email": "1.0.0-beta.232",
35
+ "@aglyn/shared-util-http": "1.0.0-beta.232",
36
+ "@aglyn/shared-util-timestamp": "1.0.0-beta.232",
37
+ "@aglyn/shared-util-tools": "1.0.0-beta.232",
38
+ "@aglyn/tenant-data-admin": "1.0.0-beta.232",
39
+ "@aglyn/tenant-feature-instance": "1.0.0-beta.232",
40
+ "@aglyn/tenant-runtime": "1.0.0-beta.232",
41
41
  "@swc/helpers": "0.5.23"
42
42
  },
43
43
  "peerDependencies": {
@@ -671,7 +671,10 @@ const FORM_HIDDEN_COLUMNS = hiddenFilterVisibility(FORM_LIST_FILTER_FIELDS, FORM
671
671
  noRowsLabel: 'No forms yet',
672
672
  noRowsDescription: 'A form collects submissions, dedupes the people who send them, and can route them to a lead. Its design is drawn in the besigner and published like any other artifact.',
673
673
  noRowsAction: /*#__PURE__*/ _jsxs(Stack, {
674
- direction: "row",
674
+ direction: {
675
+ xs: 'column',
676
+ sm: 'row'
677
+ },
675
678
  spacing: 1,
676
679
  children: [
677
680
  CreateZone ? /*#__PURE__*/ _jsx(CreateZone, {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/forms/src/lib/components/host-forms-card.component.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'use client'\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport type { AglynOrgBilling } from '@aglyn/aglyn'\nimport {\n buildRoute,\n isFormArchived,\n PageHeaderActions,\n pluginDocsHelp,\n Route,\n useTransferLauncher,\n} from '@aglyn/aglyn'\nimport { useConsoleWidgetSlot } from '@aglyn/aglyn/app-utils/console-widget-slot-context'\nimport { ICON_VARIANT_SHOW_DETAIL } from '@aglyn/shared-data-enums'\nimport {\n mdiArchiveArrowDownOutline,\n mdiArchiveArrowUpOutline,\n mdiContentCopy,\n mdiEyeOutline,\n mdiVectorSquare,\n} from '@aglyn/shared-data-mdi'\nimport { AppLink, CardDisplay, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport ListFilterChips from '@aglyn/shared-ui-jsx/components/list-filter-chips.component'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport ListQueryNotices, {\n listQueryRefusals,\n} from '@aglyn/shared-ui-jsx/components/list-query-notices.component'\nimport ListTable, {\n ListRowActions,\n listActionsColumn,\n} from '@aglyn/shared-ui-jsx/components/list-table.component'\nimport { hiddenFilterVisibility } from '@aglyn/shared-ui-jsx/const/list-filter'\nimport {\n type ListFilterClause,\n type ListFilterOption,\n listFilterGridColumns,\n} from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport { TABLE_ROW_HEIGHT } from '@aglyn/shared-ui-jsx/const/table-pagination'\nimport { useListGridFilter } from '@aglyn/shared-ui-jsx/hooks/use-list-grid-filter'\nimport QuotaReadoutComponent from '@aglyn/shared-ui-jsx/components/quota-readout.component'\nimport { CreateArtifactDrawer } from '@aglyn/shared-ui-jsx-forms'\nimport { Timestamp } from '@aglyn/shared-util-timestamp'\nimport { Alert, Button, Stack, Typography } from '@mui/material'\nimport type { GridColDef } from '@mui/x-data-grid'\nimport {\n collection,\n doc,\n getCountFromServer,\n query,\n updateDoc,\n where,\n} from 'firebase/firestore'\nimport {\n DUPLICATE_MENU_LABEL,\n useConsoleHostRoute,\n useDuplicateResource,\n useFirestore,\n useSiteContainerOptions,\n useHostResourceApi,\n useLiveArtifactCount,\n} from '@aglyn/tenant-feature-instance'\nimport { useListQuery } from '@aglyn/tenant-feature-instance/hooks/use-list-query'\nimport { useRouter } from 'next/navigation'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport { BUNDLE_ID, FORMS_DOCUMENT_SEGMENT } from '../constants/bundle-common'\nimport {\n FORM_IN_CAMPAIGN_OPTIONS,\n FORM_LEAD_ROUTING_OPTIONS,\n FORM_LIST_FILTER_FIELDS,\n FORM_LIST_FILTER_HEADERS,\n FORM_LIST_QUERY,\n FORM_LIST_SELECT_FIELDS,\n FORM_STATUS_OPTIONS,\n formListRequest,\n} from './form-list-query'\nimport { FORM_SUBMISSIONS_TRANSFER_KEY } from '../transfer/form-submissions-transfer-key'\nimport { HOST_FORMS_ZONE } from './form-zones'\n\n/**\n * The filterable columns the table shows. The rest of the declaration —\n * Status, Lead routing, Campaign, In a campaign — reaches the Filters panel as hidden\n * columns (`hiddenFilterColumns`).\n */\nconst FORM_VISIBLE_FILTER_COLUMNS = [\n 'displayName',\n 'slug',\n 'submissions',\n 'leads',\n 'lastSubmission',\n 'updatedAt',\n]\nconst FORM_HIDDEN_COLUMNS = hiddenFilterVisibility(\n FORM_LIST_FILTER_FIELDS,\n FORM_VISIBLE_FILTER_COLUMNS,\n)\n\nexport interface HostFormsCardProps {\n hostId: string\n /**\n * The Forms surface's own absolute console path, from the shell.\n *\n * A row's link is this plus the form's id, resolved synchronously. The\n * alternative is `useConsoleHostRoute`, which answers `null` for a paint\n * while it reads two documents — and a table whose every row links to\n * `/null/...` on first render is worse than one that pays nothing.\n */\n basePath?: string\n /**\n * The org billing document, from the shell.\n *\n * The quota readout's denominator resolves from it. `undefined` means the\n * plan is not known rather than that it is Free — the shell holds this\n * surface behind a spinner until the org read settles, so it reads that way\n * only when the read failed — and the readout says so instead of naming a\n * cap it cannot support.\n */\n org?: Partial<AglynOrgBilling>\n}\n\n/**\n * THE FORM CATALOG, WITH ITS SHAPE VISIBLE BEFORE THERE IS ANYTHING IN IT.\n *\n * The list is the components list's table, deliberately and not\n * approximately: `ListTable` gives it the row grammar every artifact list has\n * — the row opens the detail page, rows are not selectable, one quick action\n * then the overflow — and `ListPagination` gives it the console's one footer.\n * A form is an artifact like a component, so a reader who has used one list\n * has used this one.\n *\n * ## Why the empty state is a table and not a sentence\n *\n * A paragraph and a button reads as a smaller feature than this is: a form is\n * a thing with a slug, a submission count and a version history, and none of\n * that would be visible until the reader had already committed to making one.\n * Rendering the columns with the empty overlay inside them teaches the shape\n * of the artifact BEFORE the first one exists, which is what the components\n * list has always done.\n *\n * ## The two numeric columns, and when one is honestly blank\n *\n * `stats.submissions` and `stats.leads` are both incremented by\n * `/api/forms/submit` on a write that was happening anyway, and recounted\n * from their rows after a removal (AGL-3330), so both are real numbers.\n * `leads` is counted only for a form whose `routing.lead` is set — it holds\n * `0` there until the first lead — so a form that has never routed to leads\n * carries no figure at all and renders as a dash rather than as `0`. A zero\n * would say this form has produced no leads, which for an unrouted form is a\n * measurement nobody took.\n *\n * ## Export submissions, in the card header\n *\n * Every form's submissions on this site, through the console's export dialog\n * (`forms.submissions`, unfiltered): each form's questions are a group of\n * columns, so one file holds the whole site. One form's alone is exported\n * from that form's page. There is no Import: a submission is what a visitor\n * sent, and a file cannot have sent one.\n */\nexport function HostFormsCard(props: HostFormsCardProps) {\n const { hostId, basePath, org } = props\n const router = useRouter()\n const transfer = useTransferLauncher()\n // Duplicate (AGL-2936): the copy is a form of its own — its submissions\n // start empty — and opens on its detail page once made.\n const duplicate = useDuplicateResource({\n hostId,\n onDuplicated: (_kind, copy) => router.push(formHref(copy.id)),\n })\n const { orgSlug, subdomain: host } = useConsoleHostRoute(hostId)\n const firestore = useFirestore()\n const createHostResource = useHostResourceApi()\n /**\n * The shell's zone renderer (AGL-3043), for other ways to start a form; `null`\n * outside the console shell, where there is no workspace to gate on.\n */\n const CreateZone = useConsoleWidgetSlot()\n\n /**\n * One form's page, beneath this surface's own path.\n *\n * `basePath` is the shell's answer and needs no read; the route table is the\n * fallback for a caller that has none, and it is the one that can be `null`\n * for a paint.\n */\n const formHref = useCallback(\n (formId: string) =>\n basePath\n ? `${basePath}/${formId}`\n : buildRoute(Route.FORM_DETAILS, {\n orgSlug: orgSlug ?? '',\n host: host ?? '',\n formId,\n }),\n [basePath, orgSlug, host],\n )\n\n const [createOpen, setCreateOpen] = useState(false)\n const [createError, setCreateError] = useState<string | null>(null)\n const [creating, setCreating] = useState(false)\n const [retireError, setRetireError] = useState<string | null>(null)\n /** Bumped by each retire or restore, so the retired count is asked again. */\n const [retiredEpoch, setRetiredEpoch] = useState(0)\n\n /**\n * Retire a form, or put it back (AGL-2671).\n *\n * A write of one field, client-side like every other edit on this document:\n * the rules already admit it for a host writer, fencing off only `stats`.\n *\n * ⛔ NOT a delete, and the difference is the whole feature. The submissions,\n * the leads and the contact timeline they built are filed under this form,\n * so the document is what makes that history readable; deleting it to stop\n * collection would throw away the reason the merchant kept the form.\n *\n * `Date.now()` is the browser's clock rather than `serverTimestamp()`\n * because {@link isFormArchived} asks for a number greater than zero, and a\n * server sentinel reads as `null` on the local echo — the row would stay\n * looking live until the write round-tripped. The value is a marker, never\n * an audit fact: nothing computes a duration from it.\n */\n const setRetired = useCallback(\n async (formId: string, retired: boolean) => {\n setRetireError(null)\n try {\n await updateDoc(doc(firestore, 'hosts', hostId, 'forms', formId), {\n archivedAt: retired ? Date.now() : null,\n // The mirror the list's query asks (AGL-3330).\n retired,\n // Retiring and restoring are edits, and Updated says when the form\n // was last edited (AGL-3330): a write that skipped it left a form\n // retired today reading as untouched for weeks.\n updatedAt: Timestamp.now(),\n })\n setRetiredEpoch((epoch) => epoch + 1)\n } catch {\n setRetireError(\n retired\n ? 'Could not retire that form. Try again shortly.'\n : 'Could not restore that form. Try again shortly.',\n )\n }\n },\n [firestore, hostId],\n )\n\n /*\n * THE LIST IS ITS QUERY (AGL-3330, on the AGL-3321 list query plan).\n *\n * The grid's Filters panel and search box edit the clauses and the words;\n * `useListQuery` puts every one of them on ONE Firestore query and pages\n * that query, so a page is a page of matches and a form whose only match\n * is past the first page is still found. What one query cannot hold — a\n * second range, a second array clause — is refused by name above the list\n * (`ListQueryNotices`) rather than matched over the rows read. See\n * `form-list-query.ts` for every field and the indexes they cost.\n *\n * Unfiltered it reads in document order, as it always has: `orderBy` on a\n * name would drop every form stored without one rather than mis-sort it.\n *\n * A retired form is a TOMBSTONE, not a row, unless the reader asks for\n * Status (AGL-2671). The query leaves them out (`retired == false`), so a\n * page is a page of forms in use; a Status clause replaces that with the\n * reader's own choice. Retiring a form removes it from the list, and\n * without the Status filter that would be a one-way door: the row is the\n * only route back to Restore.\n */\n const [formClauses, setFormClauses] = useState<ListFilterClause[]>([])\n const gridFilter = useListGridFilter({\n clauses: formClauses,\n onChange: setFormClauses,\n selectFields: FORM_LIST_SELECT_FIELDS,\n })\n const filtering = gridFilter.clauses.length > 0 || gridFilter.searchWords.length > 0\n const formsCollection = useMemo(\n () => collection(firestore, 'hosts', hostId, 'forms'),\n [firestore, hostId],\n )\n const {\n status,\n rows: forms,\n hasMore,\n page,\n setPage,\n pageSize,\n setPageSize,\n plan,\n } = useListQuery<any>({\n collection: formsCollection,\n declaration: FORM_LIST_QUERY,\n request: formListRequest(gridFilter.clauses, gridFilter.searchWords),\n deps: [firestore, hostId],\n idField: '$id',\n })\n\n /*\n * The Campaign filter's choices: the campaigns placed on this site, by\n * name, and any campaign a clause names that the list does not, by its id,\n * so the chip can still be read and cleared.\n *\n * Read once the reader opens the Filters panel, not on every visit: the\n * names cost the org lookup and up to fifty campaign documents, which a\n * reader who came to open one form would pay for a filter they never\n * touched. Until they land the column has no choices, and the panel lists\n * it a moment later.\n */\n const [wantsCampaigns, setWantsCampaigns] = useState(false)\n const siteCampaigns = useSiteContainerOptions('campaign', hostId, {\n enabled: wantsCampaigns || formClauses.some((clause) => clause.field === 'campaignIds'),\n })\n const formFilterOptions = useMemo<Record<string, ListFilterOption[]>>(() => {\n const named = siteCampaigns.options.map((option) => ({\n value: option.value,\n label: option.label,\n }))\n const unnamed = formClauses\n .filter((clause) => clause.field === 'campaignIds')\n .flatMap((clause) => clause.value.split(','))\n .map((value) => value.trim())\n .filter((value) => value && !named.some((option) => option.value === value))\n .map((value) => ({ value, label: value }))\n return {\n status: FORM_STATUS_OPTIONS,\n leadRouting: FORM_LEAD_ROUTING_OPTIONS,\n campaignIds: [...named, ...unnamed],\n inCampaign: FORM_IN_CAMPAIGN_OPTIONS,\n }\n }, [siteCampaigns.options, formClauses])\n\n /*\n * The COUNT is a server aggregate, not the length of a page. `forms` is one\n * page, and publishing that as the site's form count would read as room to\n * spare on a site that is already at the ceiling.\n */\n const liveFormCount = useLiveArtifactCount(hostId, 'forms')\n // Pending or refused, the page stands in: a LOWER bound, never a confident\n // zero.\n const formsUsed = liveFormCount ?? forms.length\n /*\n * How many forms are retired, for the \"keep their slot\" line (AGL-2674).\n * An aggregate, because the list no longer reads a retired row unless it is\n * asked for one; asked again after this card retires or restores a form.\n */\n const [retiredCount, setRetiredCount] = useState<number | null>(null)\n useEffect(() => {\n let active = true\n getCountFromServer(query(formsCollection, where('retired', '==', true)))\n .then((snapshot) => {\n if (active) setRetiredCount(snapshot.data().count)\n })\n .catch(() => {\n // The line is a courtesy; without the count it is simply not shown.\n })\n return () => {\n active = false\n }\n }, [formsCollection, retiredEpoch])\n\n /**\n * Name first, then create (AGL-700).\n *\n * A form is created with BOTH halves seeded. `fields` is the declaration the\n * submission path reads and starts empty; the canvas is the design, seeded\n * with a root and a form node already bound to this id — so the besigner\n * opens on something that satisfies `checkFormContract` rather than on a\n * blank page whose first publish is a list of violations.\n */\n const handleCreate = useCallback(\n async (values: Record<string, any>) => {\n if (creating) return\n setCreating(true)\n setCreateError(null)\n try {\n const formId = Aglyn.createResourceUid()\n await createHostResource({\n hostId,\n resource: 'form',\n id: formId,\n data: {\n displayName: values['displayName'],\n slug: Aglyn.normalizeFormSlug(values['displayName']) || formId,\n fields: [],\n rootId: Aglyn.CANVAS_ROOT_ELEMENT_ID,\n nodes: {\n [Aglyn.CANVAS_ROOT_ELEMENT_ID]: {\n $id: Aglyn.CANVAS_ROOT_ELEMENT_ID,\n componentId: 'div',\n nodes: ['formRoot'],\n },\n formRoot: {\n $id: 'formRoot',\n componentId: 'form',\n pluginId: BUNDLE_ID,\n parentId: Aglyn.CANVAS_ROOT_ELEMENT_ID,\n props: { formId, formName: values['displayName'] },\n nodes: [],\n },\n },\n },\n })\n setCreateOpen(false)\n router.push(formHref(formId))\n } catch (error) {\n console.error(error)\n // The route's own words when it refused: a spent `formsPerHost`\n // allowance answers \"Your plan includes N forms — upgrade in Billing\n // for more\", which is the upgrade path a generic failure would hide.\n setCreateError(\n error instanceof Error && error.message && error.message !== 'Create failed'\n ? error.message\n : 'Could not create that form',\n )\n } finally {\n setCreating(false)\n }\n },\n [creating, createHostResource, hostId, router, formHref],\n )\n\n const columns: GridColDef[] = [\n {\n field: 'displayName',\n headerName: 'Display name',\n minWidth: 220,\n type: 'string',\n renderCell: ({ id, value }: any) => (\n <AppLink href={formHref(id as string)}>\n {value || (id as string)}\n </AppLink>\n ),\n },\n {\n field: 'slug',\n headerName: 'Slug',\n minWidth: 160,\n flex: 1,\n type: 'string',\n // Blank reads as a rendering gap; '--' reads as \"nothing here\".\n valueFormatter: (value: any) => value || '--',\n },\n {\n field: 'submissions',\n headerName: 'Submissions',\n minWidth: 130,\n type: 'number',\n // Head AND body. `type: 'number'` right-aligns both, and the explicit\n // pair says so at the call site rather than relying on a grid default\n // that a later `renderCell` would silently override.\n align: 'right',\n headerAlign: 'right',\n valueGetter: (_value: any, row: any) => row?.stats?.submissions ?? null,\n valueFormatter: (value: any) =>\n typeof value === 'number' ? value.toLocaleString() : '--',\n },\n {\n field: 'leads',\n headerName: 'Leads',\n minWidth: 100,\n type: 'number',\n align: 'right',\n headerAlign: 'right',\n /*\n * A dash rather than a `0` whenever the field holds no number.\n *\n * `stats.leads` is counted only for a form whose `routing.lead` is set\n * (`0` there until its first lead), so a form that has never routed to\n * leads has no recorded figure. Rendering `0` there would read as \"this\n * form has produced no leads\", which is a claim about a measurement\n * nobody took.\n */\n valueGetter: (_value: any, row: any) =>\n typeof row?.stats?.leads === 'number' ? row.stats.leads : null,\n valueFormatter: (value: any) =>\n typeof value === 'number' ? value.toLocaleString() : '--',\n },\n {\n field: 'lastSubmission',\n headerName: 'Last submission',\n minWidth: 170,\n flex: 1,\n type: 'date',\n valueGetter: (_value: any, row: any) =>\n typeof row?.stats?.lastSubmissionAtMs === 'number'\n ? new Date(row.stats.lastSubmissionAtMs)\n : null,\n valueFormatter: (value: any) => value?.toLocaleString?.() || '--',\n },\n {\n field: 'updatedAt',\n headerName: 'Updated',\n minWidth: 170,\n flex: 1,\n type: 'date',\n // MUI X v9 passes the value positionally. The v6 object form silently\n // destructures undefined off a Date and every row renders '--'.\n valueGetter: (value: any) => value?.toDate?.() ?? null,\n valueFormatter: (value: any) => value?.toLocaleString?.() || '--',\n },\n /*\n * Status and Lead routing, hidden until Columns shows them (AGL-3330).\n * Each is a select the Filters panel offers either way, and draws the\n * choice its value names. Status reads the FACT, `archivedAt`, rather\n * than the `retired` mirror the query asks, so a form the backfill has\n * not reached yet still reads true.\n */\n {\n field: 'status',\n headerName: 'Status',\n minWidth: 110,\n valueGetter: (_value: any, row: any) => String(isFormArchived(row)),\n },\n {\n field: 'leadRouting',\n headerName: 'Lead routing',\n minWidth: 120,\n valueGetter: (_value: any, row: any) => String(row?.routing?.lead === true),\n },\n /*\n * In a campaign, hidden like the two above. Reads the ids the form is\n * filed under — the fact `inCampaign` mirrors for the query — so a form\n * the backfill has not reached still draws the truth.\n */\n {\n field: 'inCampaign',\n headerName: 'In a campaign',\n minWidth: 130,\n valueGetter: (_value: any, row: any) =>\n String(Aglyn.readContainerIds(row, 'campaign').length > 0),\n },\n listActionsColumn((row: any) => {\n const form = { ...row, $id: row.$id as string }\n const versionId = form.versionId as string | undefined\n return (\n <ListRowActions\n label={form.displayName ?? form.$id}\n quick={{\n icon: mdiEyeOutline.path,\n label: 'Preview',\n // A form with no version has never been opened in the besigner, so\n // there is no snapshot to render. Disabled and saying so, rather\n // than a link to an empty preview.\n ...(versionId && orgSlug && host\n ? {\n to: buildRoute(Route.PLUGIN_DOCUMENT_PREVIEW, {\n orgSlug,\n host,\n documentSegment: FORMS_DOCUMENT_SEGMENT,\n docId: form.$id,\n versionId,\n }),\n }\n : {\n unavailableReason: versionId\n ? 'Resolving this site’s address…'\n : 'Nothing to preview yet — open it in the besigner once.',\n }),\n }}\n items={[\n {\n key: 'details',\n label: 'View details',\n icon: <MdiIcon path={ICON_VARIANT_SHOW_DETAIL.path} size={0.8} />,\n href: formHref(form.$id),\n },\n {\n key: 'besigner',\n label: 'Edit in besigner',\n icon: <MdiIcon path={mdiVectorSquare.path} size={0.8} />,\n /*\n * A LINK only once the form has a version. A form that has never\n * been opened has none, and opening it MINTS the first one —\n * that is a write, and the detail page is the one place that\n * decides what an initial version looks like. Sending the reader\n * there rather than minting a second way is what keeps the two\n * from drifting.\n */\n href:\n versionId && orgSlug && host\n ? buildRoute(Route.PLUGIN_DOCUMENT_BESIGNER, {\n orgSlug,\n host,\n documentSegment: FORMS_DOCUMENT_SEGMENT,\n docId: form.$id,\n versionId,\n })\n : formHref(form.$id),\n },\n /*\n * Retire / Restore (AGL-2671). Last in the menu, and the only item\n * here that writes.\n *\n * The label says what it DOES to the form rather than what it does\n * to this list — \"Archive\" reads like a filing action, and the\n * consequence a merchant needs to weigh is that the form stops\n * collecting. `/api/forms/submit` refuses a retired form, so one\n * still placed on a published page will turn visitors away.\n */\n {\n key: 'duplicate',\n label: DUPLICATE_MENU_LABEL,\n icon: <MdiIcon path={mdiContentCopy.path} size={0.8} />,\n onClick: () =>\n duplicate.request('form', {\n id: form.$id,\n name: form.displayName ?? '',\n }),\n },\n isFormArchived(form)\n ? {\n key: 'restore',\n label: 'Restore — start collecting again',\n icon: <MdiIcon path={mdiArchiveArrowUpOutline.path} size={0.8} />,\n onClick: () => void setRetired(form.$id, false),\n }\n : {\n key: 'retire',\n label: 'Retire — stop collecting, keep the history',\n icon: <MdiIcon path={mdiArchiveArrowDownOutline.path} size={0.8} />,\n onClick: () => void setRetired(form.$id, true),\n },\n ]}\n />\n )\n }),\n ]\n\n return (\n <>\n {duplicate.dialog}\n {/*\n The readout leads the create button, in the PAGE header — where Sites,\n screens, layouts, components and templates put theirs. Forms declares\n no sections, so it has no vertical rail for a card-header cluster to\n belong beside, and the controls are about the whole surface rather\n than about anything the card is showing.\n Published from the card because the card owns what they say: the count\n comes from the listener already open here, and a page that counted for\n itself would be a second source for one fact and a second read for one\n collection. It publishes from the LIST, so a form's own detail route —\n a different component — leaves the header with nothing to create into.\n */}\n <PageHeaderActions>\n <Stack direction=\"row\" spacing={2} sx={{ alignItems: 'center' }}>\n {/*\n The denominator is `formsPerHost` — the allowance\n `/api/hosts/resources` refuses the create at, resolved through the\n same `checkQuota` the route calls, so the number offered and the\n number enforced cannot drift apart. `FORMS_MAX_PER_HOST` bounds a\n READ of the catalog and sits above every plan's allowance, so\n quoting it advertises room the server will not honor.\n */}\n <QuotaReadoutComponent\n ready={org != null}\n used={formsUsed}\n limit={Aglyn.checkQuota(org, 'formsPerHost', formsUsed).limit}\n noun=\"form\"\n />\n {/*\n A retired form keeps its slot, and the readout says so rather than\n leaving a merchant to work out why a catalog of three is using five\n (AGL-2674). The count comes from the same aggregate\n `/api/hosts/resources` enforces on, which subtracts deleted forms\n and nothing else — so retirement is deliberately not a way to buy\n room back, and the number here is the number the server will\n refuse a create at.\n\n Shown only when the site has retired a form (an aggregate — the\n list reads no retired row unless asked): the line answers a\n question nobody is asking on a site that has never retired\n anything.\n */}\n {retiredCount ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Retired forms keep their slot'}\n </Typography>\n ) : null}\n <Stack direction=\"row\" spacing={1}>\n {/*\n Other ways to start a form, from plugins (AGL-3043): the\n `hostForms` zone this plugin declares, drawn through the shell's\n own gated slot. A plugin page cannot mount that slot itself, so\n the shell hands it down, and a widget here passes the gates a\n console page's would.\n */}\n {CreateZone ? (\n <CreateZone slot={HOST_FORMS_ZONE.id} hostId={hostId} orgId={org?.$id} />\n ) : null}\n <Button\n size=\"small\"\n variant=\"contained\"\n disabled={creating}\n onClick={() => {\n setCreateError(null)\n setCreateOpen(true)\n }}\n >\n {creating ? 'Creating…' : 'Create Form'}\n </Button>\n </Stack>\n </Stack>\n </PageHeaderActions>\n <CardDisplay\n header=\"Forms\"\n help={pluginDocsHelp('forms', {\n anchor: '#find-a-form',\n excerpt:\n 'A form collects submissions into the Inbox, and its design is ' +\n 'drawn in the besigner like any other artifact.',\n })}\n HeaderProps={\n transfer?.can('export', { resource: FORM_SUBMISSIONS_TRANSFER_KEY, scope: 'host', hostId })\n ? {\n action: (\n <Button\n size=\"small\"\n onClick={() =>\n transfer?.openExport({\n resource: FORM_SUBMISSIONS_TRANSFER_KEY,\n scope: 'host',\n hostId,\n })\n }\n >\n {'Export submissions'}\n </Button>\n ),\n }\n : undefined\n }\n >\n {retireError ? (\n <Alert severity=\"error\" sx={{ mb: 2 }}>\n {retireError}\n </Alert>\n ) : null}\n <ListFilterChips\n fields={FORM_LIST_FILTER_FIELDS}\n headers={FORM_LIST_FILTER_HEADERS}\n clauses={gridFilter.clauses}\n onChange={gridFilter.setClauses}\n options={formFilterOptions}\n />\n <ListQueryNotices\n refused={listQueryRefusals(plan.refused, {\n fields: FORM_LIST_FILTER_FIELDS,\n headers: FORM_LIST_FILTER_HEADERS,\n options: formFilterOptions,\n })}\n notices={plan.notices}\n />\n <ListTable\n rowHeight={TABLE_ROW_HEIGHT}\n columns={listFilterGridColumns(\n columns,\n FORM_LIST_FILTER_FIELDS,\n formFilterOptions,\n FORM_LIST_FILTER_HEADERS,\n )}\n initialState={{ columns: { columnVisibilityModel: FORM_HIDDEN_COLUMNS } }}\n /*\n * The grid must NOT also filter. The query answers the panel and\n * the search, so a second pass in the browser could only drop rows\n * that already matched — it compares what a column DRAWS, and the\n * query compares what the document stores.\n */\n filterMode=\"server\"\n filterModel={gridFilter.filterModel}\n onFilterModelChange={gridFilter.onFilterModelChange}\n quickFilter\n // The Campaign filter's names are read once the panel is opened.\n onPreferencePanelOpen={(params) => {\n if (params.openedPanelValue === 'filters') setWantsCampaigns(true)\n }}\n {...(filtering\n ? { noRowsLabel: 'No forms match these filters' }\n : {\n noRowsLabel: 'No forms yet',\n noRowsDescription:\n 'A form collects submissions, dedupes the people who send them, and can route them to a lead. Its design is drawn in the besigner and published like any other artifact.',\n noRowsAction: (\n <Stack direction=\"row\" spacing={1}>\n {/* Other ways to start a form, the header's zone again. */}\n {CreateZone ? (\n <CreateZone slot={HOST_FORMS_ZONE.id} hostId={hostId} orgId={org?.$id} />\n ) : null}\n <Button variant=\"contained\" onClick={() => setCreateOpen(true)}>\n {'Create your first form'}\n </Button>\n </Stack>\n ),\n })}\n rows={forms}\n // The whole row opens the detail page; the action cluster stops\n // propagation so a menu click never navigates underneath it.\n onOpen={(id) => router.push(formHref(String(id)))}\n // An empty table while the read is in flight reads as \"you have none\"\n // rather than \"these are on their way\".\n loading={status === 'loading'}\n // Paged by the footer below, so the grid must not also slice.\n hideFooter\n // The rows keep the query's order; a header sort would order only\n // the page on screen and read as the whole list's.\n disableColumnSorting\n />\n <ListPagination\n page={page}\n pageSize={pageSize}\n rowCount={forms.length}\n hasMore={hasMore}\n onPageChange={setPage}\n onPageSizeChange={setPageSize}\n />\n {/*\n The console's own create drawer, from the shared library rather than a\n second one that looks like it. The empty state and the header open the\n SAME one, so the state that says \"creating…\" is one state.\n */}\n <CreateArtifactDrawer\n open={createOpen}\n onClose={() => setCreateOpen(false)}\n title=\"Create new form\"\n // A form document stores no description: `/api/hosts/resources` filters\n // `data` through a per-kind allow-list, so one typed here is dropped\n // without a word.\n includeDescription={false}\n onSubmit={handleCreate}\n errorSlot={\n createError ? (\n <Alert severity=\"error\" sx={{ mt: 2, mb: 1 }}>\n {createError}\n </Alert>\n ) : null\n }\n />\n </CardDisplay>\n </>\n )\n}\nHostFormsCard.displayName = 'HostFormsCard'\n\nexport default HostFormsCard\n"],"names":["Aglyn","buildRoute","isFormArchived","PageHeaderActions","pluginDocsHelp","Route","useTransferLauncher","useConsoleWidgetSlot","ICON_VARIANT_SHOW_DETAIL","mdiArchiveArrowDownOutline","mdiArchiveArrowUpOutline","mdiContentCopy","mdiEyeOutline","mdiVectorSquare","AppLink","CardDisplay","MdiIcon","ListFilterChips","ListPagination","ListQueryNotices","listQueryRefusals","ListTable","ListRowActions","listActionsColumn","hiddenFilterVisibility","listFilterGridColumns","TABLE_ROW_HEIGHT","useListGridFilter","QuotaReadoutComponent","CreateArtifactDrawer","Timestamp","Alert","Button","Stack","Typography","collection","doc","getCountFromServer","query","updateDoc","where","DUPLICATE_MENU_LABEL","useConsoleHostRoute","useDuplicateResource","useFirestore","useSiteContainerOptions","useHostResourceApi","useLiveArtifactCount","useListQuery","useRouter","useCallback","useEffect","useMemo","useState","BUNDLE_ID","FORMS_DOCUMENT_SEGMENT","FORM_IN_CAMPAIGN_OPTIONS","FORM_LEAD_ROUTING_OPTIONS","FORM_LIST_FILTER_FIELDS","FORM_LIST_FILTER_HEADERS","FORM_LIST_QUERY","FORM_LIST_SELECT_FIELDS","FORM_STATUS_OPTIONS","formListRequest","FORM_SUBMISSIONS_TRANSFER_KEY","HOST_FORMS_ZONE","FORM_VISIBLE_FILTER_COLUMNS","FORM_HIDDEN_COLUMNS","HostFormsCard","props","hostId","basePath","org","router","transfer","duplicate","onDuplicated","_kind","copy","push","formHref","id","orgSlug","subdomain","host","firestore","createHostResource","CreateZone","formId","FORM_DETAILS","createOpen","setCreateOpen","createError","setCreateError","creating","setCreating","retireError","setRetireError","retiredEpoch","setRetiredEpoch","setRetired","retired","archivedAt","Date","now","updatedAt","epoch","formClauses","setFormClauses","gridFilter","clauses","onChange","selectFields","filtering","length","searchWords","formsCollection","status","rows","forms","hasMore","page","setPage","pageSize","setPageSize","plan","declaration","request","deps","idField","wantsCampaigns","setWantsCampaigns","siteCampaigns","enabled","some","clause","field","formFilterOptions","named","options","map","option","value","label","unnamed","filter","flatMap","split","trim","leadRouting","campaignIds","inCampaign","liveFormCount","formsUsed","retiredCount","setRetiredCount","active","then","snapshot","data","count","catch","handleCreate","values","createResourceUid","resource","displayName","slug","normalizeFormSlug","fields","rootId","CANVAS_ROOT_ELEMENT_ID","nodes","$id","componentId","formRoot","pluginId","parentId","formName","error","console","Error","message","columns","headerName","minWidth","type","renderCell","href","flex","valueFormatter","align","headerAlign","valueGetter","_value","row","stats","submissions","toLocaleString","leads","lastSubmissionAtMs","toDate","String","routing","lead","readContainerIds","form","versionId","quick","icon","path","to","PLUGIN_DOCUMENT_PREVIEW","documentSegment","docId","unavailableReason","items","key","size","PLUGIN_DOCUMENT_BESIGNER","onClick","name","dialog","direction","spacing","sx","alignItems","ready","used","limit","checkQuota","noun","variant","color","slot","orgId","disabled","header","help","anchor","excerpt","HeaderProps","can","scope","action","openExport","undefined","severity","mb","headers","setClauses","refused","notices","rowHeight","initialState","columnVisibilityModel","filterMode","filterModel","onFilterModelChange","quickFilter","onPreferencePanelOpen","params","openedPanelValue","noRowsLabel","noRowsDescription","noRowsAction","onOpen","loading","hideFooter","disableColumnSorting","rowCount","onPageChange","onPageSizeChange","open","onClose","title","includeDescription","onSubmit","errorSlot","mt"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/forms/src/lib/components/host-forms-card.component.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'use client'\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport type { AglynOrgBilling } from '@aglyn/aglyn'\nimport {\n buildRoute,\n isFormArchived,\n PageHeaderActions,\n pluginDocsHelp,\n Route,\n useTransferLauncher,\n} from '@aglyn/aglyn'\nimport { useConsoleWidgetSlot } from '@aglyn/aglyn/app-utils/console-widget-slot-context'\nimport { ICON_VARIANT_SHOW_DETAIL } from '@aglyn/shared-data-enums'\nimport {\n mdiArchiveArrowDownOutline,\n mdiArchiveArrowUpOutline,\n mdiContentCopy,\n mdiEyeOutline,\n mdiVectorSquare,\n} from '@aglyn/shared-data-mdi'\nimport { AppLink, CardDisplay, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport ListFilterChips from '@aglyn/shared-ui-jsx/components/list-filter-chips.component'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport ListQueryNotices, {\n listQueryRefusals,\n} from '@aglyn/shared-ui-jsx/components/list-query-notices.component'\nimport ListTable, {\n ListRowActions,\n listActionsColumn,\n} from '@aglyn/shared-ui-jsx/components/list-table.component'\nimport { hiddenFilterVisibility } from '@aglyn/shared-ui-jsx/const/list-filter'\nimport {\n type ListFilterClause,\n type ListFilterOption,\n listFilterGridColumns,\n} from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport { TABLE_ROW_HEIGHT } from '@aglyn/shared-ui-jsx/const/table-pagination'\nimport { useListGridFilter } from '@aglyn/shared-ui-jsx/hooks/use-list-grid-filter'\nimport QuotaReadoutComponent from '@aglyn/shared-ui-jsx/components/quota-readout.component'\nimport { CreateArtifactDrawer } from '@aglyn/shared-ui-jsx-forms'\nimport { Timestamp } from '@aglyn/shared-util-timestamp'\nimport { Alert, Button, Stack, Typography } from '@mui/material'\nimport type { GridColDef } from '@mui/x-data-grid'\nimport {\n collection,\n doc,\n getCountFromServer,\n query,\n updateDoc,\n where,\n} from 'firebase/firestore'\nimport {\n DUPLICATE_MENU_LABEL,\n useConsoleHostRoute,\n useDuplicateResource,\n useFirestore,\n useSiteContainerOptions,\n useHostResourceApi,\n useLiveArtifactCount,\n} from '@aglyn/tenant-feature-instance'\nimport { useListQuery } from '@aglyn/tenant-feature-instance/hooks/use-list-query'\nimport { useRouter } from 'next/navigation'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport { BUNDLE_ID, FORMS_DOCUMENT_SEGMENT } from '../constants/bundle-common'\nimport {\n FORM_IN_CAMPAIGN_OPTIONS,\n FORM_LEAD_ROUTING_OPTIONS,\n FORM_LIST_FILTER_FIELDS,\n FORM_LIST_FILTER_HEADERS,\n FORM_LIST_QUERY,\n FORM_LIST_SELECT_FIELDS,\n FORM_STATUS_OPTIONS,\n formListRequest,\n} from './form-list-query'\nimport { FORM_SUBMISSIONS_TRANSFER_KEY } from '../transfer/form-submissions-transfer-key'\nimport { HOST_FORMS_ZONE } from './form-zones'\n\n/**\n * The filterable columns the table shows. The rest of the declaration —\n * Status, Lead routing, Campaign, In a campaign — reaches the Filters panel as hidden\n * columns (`hiddenFilterColumns`).\n */\nconst FORM_VISIBLE_FILTER_COLUMNS = [\n 'displayName',\n 'slug',\n 'submissions',\n 'leads',\n 'lastSubmission',\n 'updatedAt',\n]\nconst FORM_HIDDEN_COLUMNS = hiddenFilterVisibility(\n FORM_LIST_FILTER_FIELDS,\n FORM_VISIBLE_FILTER_COLUMNS,\n)\n\nexport interface HostFormsCardProps {\n hostId: string\n /**\n * The Forms surface's own absolute console path, from the shell.\n *\n * A row's link is this plus the form's id, resolved synchronously. The\n * alternative is `useConsoleHostRoute`, which answers `null` for a paint\n * while it reads two documents — and a table whose every row links to\n * `/null/...` on first render is worse than one that pays nothing.\n */\n basePath?: string\n /**\n * The org billing document, from the shell.\n *\n * The quota readout's denominator resolves from it. `undefined` means the\n * plan is not known rather than that it is Free — the shell holds this\n * surface behind a spinner until the org read settles, so it reads that way\n * only when the read failed — and the readout says so instead of naming a\n * cap it cannot support.\n */\n org?: Partial<AglynOrgBilling>\n}\n\n/**\n * THE FORM CATALOG, WITH ITS SHAPE VISIBLE BEFORE THERE IS ANYTHING IN IT.\n *\n * The list is the components list's table, deliberately and not\n * approximately: `ListTable` gives it the row grammar every artifact list has\n * — the row opens the detail page, rows are not selectable, one quick action\n * then the overflow — and `ListPagination` gives it the console's one footer.\n * A form is an artifact like a component, so a reader who has used one list\n * has used this one.\n *\n * ## Why the empty state is a table and not a sentence\n *\n * A paragraph and a button reads as a smaller feature than this is: a form is\n * a thing with a slug, a submission count and a version history, and none of\n * that would be visible until the reader had already committed to making one.\n * Rendering the columns with the empty overlay inside them teaches the shape\n * of the artifact BEFORE the first one exists, which is what the components\n * list has always done.\n *\n * ## The two numeric columns, and when one is honestly blank\n *\n * `stats.submissions` and `stats.leads` are both incremented by\n * `/api/forms/submit` on a write that was happening anyway, and recounted\n * from their rows after a removal (AGL-3330), so both are real numbers.\n * `leads` is counted only for a form whose `routing.lead` is set — it holds\n * `0` there until the first lead — so a form that has never routed to leads\n * carries no figure at all and renders as a dash rather than as `0`. A zero\n * would say this form has produced no leads, which for an unrouted form is a\n * measurement nobody took.\n *\n * ## Export submissions, in the card header\n *\n * Every form's submissions on this site, through the console's export dialog\n * (`forms.submissions`, unfiltered): each form's questions are a group of\n * columns, so one file holds the whole site. One form's alone is exported\n * from that form's page. There is no Import: a submission is what a visitor\n * sent, and a file cannot have sent one.\n */\nexport function HostFormsCard(props: HostFormsCardProps) {\n const { hostId, basePath, org } = props\n const router = useRouter()\n const transfer = useTransferLauncher()\n // Duplicate (AGL-2936): the copy is a form of its own — its submissions\n // start empty — and opens on its detail page once made.\n const duplicate = useDuplicateResource({\n hostId,\n onDuplicated: (_kind, copy) => router.push(formHref(copy.id)),\n })\n const { orgSlug, subdomain: host } = useConsoleHostRoute(hostId)\n const firestore = useFirestore()\n const createHostResource = useHostResourceApi()\n /**\n * The shell's zone renderer (AGL-3043), for other ways to start a form; `null`\n * outside the console shell, where there is no workspace to gate on.\n */\n const CreateZone = useConsoleWidgetSlot()\n\n /**\n * One form's page, beneath this surface's own path.\n *\n * `basePath` is the shell's answer and needs no read; the route table is the\n * fallback for a caller that has none, and it is the one that can be `null`\n * for a paint.\n */\n const formHref = useCallback(\n (formId: string) =>\n basePath\n ? `${basePath}/${formId}`\n : buildRoute(Route.FORM_DETAILS, {\n orgSlug: orgSlug ?? '',\n host: host ?? '',\n formId,\n }),\n [basePath, orgSlug, host],\n )\n\n const [createOpen, setCreateOpen] = useState(false)\n const [createError, setCreateError] = useState<string | null>(null)\n const [creating, setCreating] = useState(false)\n const [retireError, setRetireError] = useState<string | null>(null)\n /** Bumped by each retire or restore, so the retired count is asked again. */\n const [retiredEpoch, setRetiredEpoch] = useState(0)\n\n /**\n * Retire a form, or put it back (AGL-2671).\n *\n * A write of one field, client-side like every other edit on this document:\n * the rules already admit it for a host writer, fencing off only `stats`.\n *\n * ⛔ NOT a delete, and the difference is the whole feature. The submissions,\n * the leads and the contact timeline they built are filed under this form,\n * so the document is what makes that history readable; deleting it to stop\n * collection would throw away the reason the merchant kept the form.\n *\n * `Date.now()` is the browser's clock rather than `serverTimestamp()`\n * because {@link isFormArchived} asks for a number greater than zero, and a\n * server sentinel reads as `null` on the local echo — the row would stay\n * looking live until the write round-tripped. The value is a marker, never\n * an audit fact: nothing computes a duration from it.\n */\n const setRetired = useCallback(\n async (formId: string, retired: boolean) => {\n setRetireError(null)\n try {\n await updateDoc(doc(firestore, 'hosts', hostId, 'forms', formId), {\n archivedAt: retired ? Date.now() : null,\n // The mirror the list's query asks (AGL-3330).\n retired,\n // Retiring and restoring are edits, and Updated says when the form\n // was last edited (AGL-3330): a write that skipped it left a form\n // retired today reading as untouched for weeks.\n updatedAt: Timestamp.now(),\n })\n setRetiredEpoch((epoch) => epoch + 1)\n } catch {\n setRetireError(\n retired\n ? 'Could not retire that form. Try again shortly.'\n : 'Could not restore that form. Try again shortly.',\n )\n }\n },\n [firestore, hostId],\n )\n\n /*\n * THE LIST IS ITS QUERY (AGL-3330, on the AGL-3321 list query plan).\n *\n * The grid's Filters panel and search box edit the clauses and the words;\n * `useListQuery` puts every one of them on ONE Firestore query and pages\n * that query, so a page is a page of matches and a form whose only match\n * is past the first page is still found. What one query cannot hold — a\n * second range, a second array clause — is refused by name above the list\n * (`ListQueryNotices`) rather than matched over the rows read. See\n * `form-list-query.ts` for every field and the indexes they cost.\n *\n * Unfiltered it reads in document order, as it always has: `orderBy` on a\n * name would drop every form stored without one rather than mis-sort it.\n *\n * A retired form is a TOMBSTONE, not a row, unless the reader asks for\n * Status (AGL-2671). The query leaves them out (`retired == false`), so a\n * page is a page of forms in use; a Status clause replaces that with the\n * reader's own choice. Retiring a form removes it from the list, and\n * without the Status filter that would be a one-way door: the row is the\n * only route back to Restore.\n */\n const [formClauses, setFormClauses] = useState<ListFilterClause[]>([])\n const gridFilter = useListGridFilter({\n clauses: formClauses,\n onChange: setFormClauses,\n selectFields: FORM_LIST_SELECT_FIELDS,\n })\n const filtering = gridFilter.clauses.length > 0 || gridFilter.searchWords.length > 0\n const formsCollection = useMemo(\n () => collection(firestore, 'hosts', hostId, 'forms'),\n [firestore, hostId],\n )\n const {\n status,\n rows: forms,\n hasMore,\n page,\n setPage,\n pageSize,\n setPageSize,\n plan,\n } = useListQuery<any>({\n collection: formsCollection,\n declaration: FORM_LIST_QUERY,\n request: formListRequest(gridFilter.clauses, gridFilter.searchWords),\n deps: [firestore, hostId],\n idField: '$id',\n })\n\n /*\n * The Campaign filter's choices: the campaigns placed on this site, by\n * name, and any campaign a clause names that the list does not, by its id,\n * so the chip can still be read and cleared.\n *\n * Read once the reader opens the Filters panel, not on every visit: the\n * names cost the org lookup and up to fifty campaign documents, which a\n * reader who came to open one form would pay for a filter they never\n * touched. Until they land the column has no choices, and the panel lists\n * it a moment later.\n */\n const [wantsCampaigns, setWantsCampaigns] = useState(false)\n const siteCampaigns = useSiteContainerOptions('campaign', hostId, {\n enabled: wantsCampaigns || formClauses.some((clause) => clause.field === 'campaignIds'),\n })\n const formFilterOptions = useMemo<Record<string, ListFilterOption[]>>(() => {\n const named = siteCampaigns.options.map((option) => ({\n value: option.value,\n label: option.label,\n }))\n const unnamed = formClauses\n .filter((clause) => clause.field === 'campaignIds')\n .flatMap((clause) => clause.value.split(','))\n .map((value) => value.trim())\n .filter((value) => value && !named.some((option) => option.value === value))\n .map((value) => ({ value, label: value }))\n return {\n status: FORM_STATUS_OPTIONS,\n leadRouting: FORM_LEAD_ROUTING_OPTIONS,\n campaignIds: [...named, ...unnamed],\n inCampaign: FORM_IN_CAMPAIGN_OPTIONS,\n }\n }, [siteCampaigns.options, formClauses])\n\n /*\n * The COUNT is a server aggregate, not the length of a page. `forms` is one\n * page, and publishing that as the site's form count would read as room to\n * spare on a site that is already at the ceiling.\n */\n const liveFormCount = useLiveArtifactCount(hostId, 'forms')\n // Pending or refused, the page stands in: a LOWER bound, never a confident\n // zero.\n const formsUsed = liveFormCount ?? forms.length\n /*\n * How many forms are retired, for the \"keep their slot\" line (AGL-2674).\n * An aggregate, because the list no longer reads a retired row unless it is\n * asked for one; asked again after this card retires or restores a form.\n */\n const [retiredCount, setRetiredCount] = useState<number | null>(null)\n useEffect(() => {\n let active = true\n getCountFromServer(query(formsCollection, where('retired', '==', true)))\n .then((snapshot) => {\n if (active) setRetiredCount(snapshot.data().count)\n })\n .catch(() => {\n // The line is a courtesy; without the count it is simply not shown.\n })\n return () => {\n active = false\n }\n }, [formsCollection, retiredEpoch])\n\n /**\n * Name first, then create (AGL-700).\n *\n * A form is created with BOTH halves seeded. `fields` is the declaration the\n * submission path reads and starts empty; the canvas is the design, seeded\n * with a root and a form node already bound to this id — so the besigner\n * opens on something that satisfies `checkFormContract` rather than on a\n * blank page whose first publish is a list of violations.\n */\n const handleCreate = useCallback(\n async (values: Record<string, any>) => {\n if (creating) return\n setCreating(true)\n setCreateError(null)\n try {\n const formId = Aglyn.createResourceUid()\n await createHostResource({\n hostId,\n resource: 'form',\n id: formId,\n data: {\n displayName: values['displayName'],\n slug: Aglyn.normalizeFormSlug(values['displayName']) || formId,\n fields: [],\n rootId: Aglyn.CANVAS_ROOT_ELEMENT_ID,\n nodes: {\n [Aglyn.CANVAS_ROOT_ELEMENT_ID]: {\n $id: Aglyn.CANVAS_ROOT_ELEMENT_ID,\n componentId: 'div',\n nodes: ['formRoot'],\n },\n formRoot: {\n $id: 'formRoot',\n componentId: 'form',\n pluginId: BUNDLE_ID,\n parentId: Aglyn.CANVAS_ROOT_ELEMENT_ID,\n props: { formId, formName: values['displayName'] },\n nodes: [],\n },\n },\n },\n })\n setCreateOpen(false)\n router.push(formHref(formId))\n } catch (error) {\n console.error(error)\n // The route's own words when it refused: a spent `formsPerHost`\n // allowance answers \"Your plan includes N forms — upgrade in Billing\n // for more\", which is the upgrade path a generic failure would hide.\n setCreateError(\n error instanceof Error && error.message && error.message !== 'Create failed'\n ? error.message\n : 'Could not create that form',\n )\n } finally {\n setCreating(false)\n }\n },\n [creating, createHostResource, hostId, router, formHref],\n )\n\n const columns: GridColDef[] = [\n {\n field: 'displayName',\n headerName: 'Display name',\n minWidth: 220,\n type: 'string',\n renderCell: ({ id, value }: any) => (\n <AppLink href={formHref(id as string)}>\n {value || (id as string)}\n </AppLink>\n ),\n },\n {\n field: 'slug',\n headerName: 'Slug',\n minWidth: 160,\n flex: 1,\n type: 'string',\n // Blank reads as a rendering gap; '--' reads as \"nothing here\".\n valueFormatter: (value: any) => value || '--',\n },\n {\n field: 'submissions',\n headerName: 'Submissions',\n minWidth: 130,\n type: 'number',\n // Head AND body. `type: 'number'` right-aligns both, and the explicit\n // pair says so at the call site rather than relying on a grid default\n // that a later `renderCell` would silently override.\n align: 'right',\n headerAlign: 'right',\n valueGetter: (_value: any, row: any) => row?.stats?.submissions ?? null,\n valueFormatter: (value: any) =>\n typeof value === 'number' ? value.toLocaleString() : '--',\n },\n {\n field: 'leads',\n headerName: 'Leads',\n minWidth: 100,\n type: 'number',\n align: 'right',\n headerAlign: 'right',\n /*\n * A dash rather than a `0` whenever the field holds no number.\n *\n * `stats.leads` is counted only for a form whose `routing.lead` is set\n * (`0` there until its first lead), so a form that has never routed to\n * leads has no recorded figure. Rendering `0` there would read as \"this\n * form has produced no leads\", which is a claim about a measurement\n * nobody took.\n */\n valueGetter: (_value: any, row: any) =>\n typeof row?.stats?.leads === 'number' ? row.stats.leads : null,\n valueFormatter: (value: any) =>\n typeof value === 'number' ? value.toLocaleString() : '--',\n },\n {\n field: 'lastSubmission',\n headerName: 'Last submission',\n minWidth: 170,\n flex: 1,\n type: 'date',\n valueGetter: (_value: any, row: any) =>\n typeof row?.stats?.lastSubmissionAtMs === 'number'\n ? new Date(row.stats.lastSubmissionAtMs)\n : null,\n valueFormatter: (value: any) => value?.toLocaleString?.() || '--',\n },\n {\n field: 'updatedAt',\n headerName: 'Updated',\n minWidth: 170,\n flex: 1,\n type: 'date',\n // MUI X v9 passes the value positionally. The v6 object form silently\n // destructures undefined off a Date and every row renders '--'.\n valueGetter: (value: any) => value?.toDate?.() ?? null,\n valueFormatter: (value: any) => value?.toLocaleString?.() || '--',\n },\n /*\n * Status and Lead routing, hidden until Columns shows them (AGL-3330).\n * Each is a select the Filters panel offers either way, and draws the\n * choice its value names. Status reads the FACT, `archivedAt`, rather\n * than the `retired` mirror the query asks, so a form the backfill has\n * not reached yet still reads true.\n */\n {\n field: 'status',\n headerName: 'Status',\n minWidth: 110,\n valueGetter: (_value: any, row: any) => String(isFormArchived(row)),\n },\n {\n field: 'leadRouting',\n headerName: 'Lead routing',\n minWidth: 120,\n valueGetter: (_value: any, row: any) => String(row?.routing?.lead === true),\n },\n /*\n * In a campaign, hidden like the two above. Reads the ids the form is\n * filed under — the fact `inCampaign` mirrors for the query — so a form\n * the backfill has not reached still draws the truth.\n */\n {\n field: 'inCampaign',\n headerName: 'In a campaign',\n minWidth: 130,\n valueGetter: (_value: any, row: any) =>\n String(Aglyn.readContainerIds(row, 'campaign').length > 0),\n },\n listActionsColumn((row: any) => {\n const form = { ...row, $id: row.$id as string }\n const versionId = form.versionId as string | undefined\n return (\n <ListRowActions\n label={form.displayName ?? form.$id}\n quick={{\n icon: mdiEyeOutline.path,\n label: 'Preview',\n // A form with no version has never been opened in the besigner, so\n // there is no snapshot to render. Disabled and saying so, rather\n // than a link to an empty preview.\n ...(versionId && orgSlug && host\n ? {\n to: buildRoute(Route.PLUGIN_DOCUMENT_PREVIEW, {\n orgSlug,\n host,\n documentSegment: FORMS_DOCUMENT_SEGMENT,\n docId: form.$id,\n versionId,\n }),\n }\n : {\n unavailableReason: versionId\n ? 'Resolving this site’s address…'\n : 'Nothing to preview yet — open it in the besigner once.',\n }),\n }}\n items={[\n {\n key: 'details',\n label: 'View details',\n icon: <MdiIcon path={ICON_VARIANT_SHOW_DETAIL.path} size={0.8} />,\n href: formHref(form.$id),\n },\n {\n key: 'besigner',\n label: 'Edit in besigner',\n icon: <MdiIcon path={mdiVectorSquare.path} size={0.8} />,\n /*\n * A LINK only once the form has a version. A form that has never\n * been opened has none, and opening it MINTS the first one —\n * that is a write, and the detail page is the one place that\n * decides what an initial version looks like. Sending the reader\n * there rather than minting a second way is what keeps the two\n * from drifting.\n */\n href:\n versionId && orgSlug && host\n ? buildRoute(Route.PLUGIN_DOCUMENT_BESIGNER, {\n orgSlug,\n host,\n documentSegment: FORMS_DOCUMENT_SEGMENT,\n docId: form.$id,\n versionId,\n })\n : formHref(form.$id),\n },\n /*\n * Retire / Restore (AGL-2671). Last in the menu, and the only item\n * here that writes.\n *\n * The label says what it DOES to the form rather than what it does\n * to this list — \"Archive\" reads like a filing action, and the\n * consequence a merchant needs to weigh is that the form stops\n * collecting. `/api/forms/submit` refuses a retired form, so one\n * still placed on a published page will turn visitors away.\n */\n {\n key: 'duplicate',\n label: DUPLICATE_MENU_LABEL,\n icon: <MdiIcon path={mdiContentCopy.path} size={0.8} />,\n onClick: () =>\n duplicate.request('form', {\n id: form.$id,\n name: form.displayName ?? '',\n }),\n },\n isFormArchived(form)\n ? {\n key: 'restore',\n label: 'Restore — start collecting again',\n icon: <MdiIcon path={mdiArchiveArrowUpOutline.path} size={0.8} />,\n onClick: () => void setRetired(form.$id, false),\n }\n : {\n key: 'retire',\n label: 'Retire — stop collecting, keep the history',\n icon: <MdiIcon path={mdiArchiveArrowDownOutline.path} size={0.8} />,\n onClick: () => void setRetired(form.$id, true),\n },\n ]}\n />\n )\n }),\n ]\n\n return (\n <>\n {duplicate.dialog}\n {/*\n The readout leads the create button, in the PAGE header — where Sites,\n screens, layouts, components and templates put theirs. Forms declares\n no sections, so it has no vertical rail for a card-header cluster to\n belong beside, and the controls are about the whole surface rather\n than about anything the card is showing.\n Published from the card because the card owns what they say: the count\n comes from the listener already open here, and a page that counted for\n itself would be a second source for one fact and a second read for one\n collection. It publishes from the LIST, so a form's own detail route —\n a different component — leaves the header with nothing to create into.\n */}\n <PageHeaderActions>\n <Stack direction=\"row\" spacing={2} sx={{ alignItems: 'center' }}>\n {/*\n The denominator is `formsPerHost` — the allowance\n `/api/hosts/resources` refuses the create at, resolved through the\n same `checkQuota` the route calls, so the number offered and the\n number enforced cannot drift apart. `FORMS_MAX_PER_HOST` bounds a\n READ of the catalog and sits above every plan's allowance, so\n quoting it advertises room the server will not honor.\n */}\n <QuotaReadoutComponent\n ready={org != null}\n used={formsUsed}\n limit={Aglyn.checkQuota(org, 'formsPerHost', formsUsed).limit}\n noun=\"form\"\n />\n {/*\n A retired form keeps its slot, and the readout says so rather than\n leaving a merchant to work out why a catalog of three is using five\n (AGL-2674). The count comes from the same aggregate\n `/api/hosts/resources` enforces on, which subtracts deleted forms\n and nothing else — so retirement is deliberately not a way to buy\n room back, and the number here is the number the server will\n refuse a create at.\n\n Shown only when the site has retired a form (an aggregate — the\n list reads no retired row unless asked): the line answers a\n question nobody is asking on a site that has never retired\n anything.\n */}\n {retiredCount ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Retired forms keep their slot'}\n </Typography>\n ) : null}\n <Stack direction=\"row\" spacing={1}>\n {/*\n Other ways to start a form, from plugins (AGL-3043): the\n `hostForms` zone this plugin declares, drawn through the shell's\n own gated slot. A plugin page cannot mount that slot itself, so\n the shell hands it down, and a widget here passes the gates a\n console page's would.\n */}\n {CreateZone ? (\n <CreateZone slot={HOST_FORMS_ZONE.id} hostId={hostId} orgId={org?.$id} />\n ) : null}\n <Button\n size=\"small\"\n variant=\"contained\"\n disabled={creating}\n onClick={() => {\n setCreateError(null)\n setCreateOpen(true)\n }}\n >\n {creating ? 'Creating…' : 'Create Form'}\n </Button>\n </Stack>\n </Stack>\n </PageHeaderActions>\n <CardDisplay\n header=\"Forms\"\n help={pluginDocsHelp('forms', {\n anchor: '#find-a-form',\n excerpt:\n 'A form collects submissions into the Inbox, and its design is ' +\n 'drawn in the besigner like any other artifact.',\n })}\n HeaderProps={\n transfer?.can('export', { resource: FORM_SUBMISSIONS_TRANSFER_KEY, scope: 'host', hostId })\n ? {\n action: (\n <Button\n size=\"small\"\n onClick={() =>\n transfer?.openExport({\n resource: FORM_SUBMISSIONS_TRANSFER_KEY,\n scope: 'host',\n hostId,\n })\n }\n >\n {'Export submissions'}\n </Button>\n ),\n }\n : undefined\n }\n >\n {retireError ? (\n <Alert severity=\"error\" sx={{ mb: 2 }}>\n {retireError}\n </Alert>\n ) : null}\n <ListFilterChips\n fields={FORM_LIST_FILTER_FIELDS}\n headers={FORM_LIST_FILTER_HEADERS}\n clauses={gridFilter.clauses}\n onChange={gridFilter.setClauses}\n options={formFilterOptions}\n />\n <ListQueryNotices\n refused={listQueryRefusals(plan.refused, {\n fields: FORM_LIST_FILTER_FIELDS,\n headers: FORM_LIST_FILTER_HEADERS,\n options: formFilterOptions,\n })}\n notices={plan.notices}\n />\n <ListTable\n rowHeight={TABLE_ROW_HEIGHT}\n columns={listFilterGridColumns(\n columns,\n FORM_LIST_FILTER_FIELDS,\n formFilterOptions,\n FORM_LIST_FILTER_HEADERS,\n )}\n initialState={{ columns: { columnVisibilityModel: FORM_HIDDEN_COLUMNS } }}\n /*\n * The grid must NOT also filter. The query answers the panel and\n * the search, so a second pass in the browser could only drop rows\n * that already matched — it compares what a column DRAWS, and the\n * query compares what the document stores.\n */\n filterMode=\"server\"\n filterModel={gridFilter.filterModel}\n onFilterModelChange={gridFilter.onFilterModelChange}\n quickFilter\n // The Campaign filter's names are read once the panel is opened.\n onPreferencePanelOpen={(params) => {\n if (params.openedPanelValue === 'filters') setWantsCampaigns(true)\n }}\n {...(filtering\n ? { noRowsLabel: 'No forms match these filters' }\n : {\n noRowsLabel: 'No forms yet',\n noRowsDescription:\n 'A form collects submissions, dedupes the people who send them, and can route them to a lead. Its design is drawn in the besigner and published like any other artifact.',\n noRowsAction: (\n <Stack direction={{ xs: 'column', sm: 'row' }} spacing={1}>\n {/* Other ways to start a form, the header's zone again. */}\n {CreateZone ? (\n <CreateZone slot={HOST_FORMS_ZONE.id} hostId={hostId} orgId={org?.$id} />\n ) : null}\n <Button variant=\"contained\" onClick={() => setCreateOpen(true)}>\n {'Create your first form'}\n </Button>\n </Stack>\n ),\n })}\n rows={forms}\n // The whole row opens the detail page; the action cluster stops\n // propagation so a menu click never navigates underneath it.\n onOpen={(id) => router.push(formHref(String(id)))}\n // An empty table while the read is in flight reads as \"you have none\"\n // rather than \"these are on their way\".\n loading={status === 'loading'}\n // Paged by the footer below, so the grid must not also slice.\n hideFooter\n // The rows keep the query's order; a header sort would order only\n // the page on screen and read as the whole list's.\n disableColumnSorting\n />\n <ListPagination\n page={page}\n pageSize={pageSize}\n rowCount={forms.length}\n hasMore={hasMore}\n onPageChange={setPage}\n onPageSizeChange={setPageSize}\n />\n {/*\n The console's own create drawer, from the shared library rather than a\n second one that looks like it. The empty state and the header open the\n SAME one, so the state that says \"creating…\" is one state.\n */}\n <CreateArtifactDrawer\n open={createOpen}\n onClose={() => setCreateOpen(false)}\n title=\"Create new form\"\n // A form document stores no description: `/api/hosts/resources` filters\n // `data` through a per-kind allow-list, so one typed here is dropped\n // without a word.\n includeDescription={false}\n onSubmit={handleCreate}\n errorSlot={\n createError ? (\n <Alert severity=\"error\" sx={{ mt: 2, mb: 1 }}>\n {createError}\n </Alert>\n ) : null\n }\n />\n </CardDisplay>\n </>\n )\n}\nHostFormsCard.displayName = 'HostFormsCard'\n\nexport default HostFormsCard\n"],"names":["Aglyn","buildRoute","isFormArchived","PageHeaderActions","pluginDocsHelp","Route","useTransferLauncher","useConsoleWidgetSlot","ICON_VARIANT_SHOW_DETAIL","mdiArchiveArrowDownOutline","mdiArchiveArrowUpOutline","mdiContentCopy","mdiEyeOutline","mdiVectorSquare","AppLink","CardDisplay","MdiIcon","ListFilterChips","ListPagination","ListQueryNotices","listQueryRefusals","ListTable","ListRowActions","listActionsColumn","hiddenFilterVisibility","listFilterGridColumns","TABLE_ROW_HEIGHT","useListGridFilter","QuotaReadoutComponent","CreateArtifactDrawer","Timestamp","Alert","Button","Stack","Typography","collection","doc","getCountFromServer","query","updateDoc","where","DUPLICATE_MENU_LABEL","useConsoleHostRoute","useDuplicateResource","useFirestore","useSiteContainerOptions","useHostResourceApi","useLiveArtifactCount","useListQuery","useRouter","useCallback","useEffect","useMemo","useState","BUNDLE_ID","FORMS_DOCUMENT_SEGMENT","FORM_IN_CAMPAIGN_OPTIONS","FORM_LEAD_ROUTING_OPTIONS","FORM_LIST_FILTER_FIELDS","FORM_LIST_FILTER_HEADERS","FORM_LIST_QUERY","FORM_LIST_SELECT_FIELDS","FORM_STATUS_OPTIONS","formListRequest","FORM_SUBMISSIONS_TRANSFER_KEY","HOST_FORMS_ZONE","FORM_VISIBLE_FILTER_COLUMNS","FORM_HIDDEN_COLUMNS","HostFormsCard","props","hostId","basePath","org","router","transfer","duplicate","onDuplicated","_kind","copy","push","formHref","id","orgSlug","subdomain","host","firestore","createHostResource","CreateZone","formId","FORM_DETAILS","createOpen","setCreateOpen","createError","setCreateError","creating","setCreating","retireError","setRetireError","retiredEpoch","setRetiredEpoch","setRetired","retired","archivedAt","Date","now","updatedAt","epoch","formClauses","setFormClauses","gridFilter","clauses","onChange","selectFields","filtering","length","searchWords","formsCollection","status","rows","forms","hasMore","page","setPage","pageSize","setPageSize","plan","declaration","request","deps","idField","wantsCampaigns","setWantsCampaigns","siteCampaigns","enabled","some","clause","field","formFilterOptions","named","options","map","option","value","label","unnamed","filter","flatMap","split","trim","leadRouting","campaignIds","inCampaign","liveFormCount","formsUsed","retiredCount","setRetiredCount","active","then","snapshot","data","count","catch","handleCreate","values","createResourceUid","resource","displayName","slug","normalizeFormSlug","fields","rootId","CANVAS_ROOT_ELEMENT_ID","nodes","$id","componentId","formRoot","pluginId","parentId","formName","error","console","Error","message","columns","headerName","minWidth","type","renderCell","href","flex","valueFormatter","align","headerAlign","valueGetter","_value","row","stats","submissions","toLocaleString","leads","lastSubmissionAtMs","toDate","String","routing","lead","readContainerIds","form","versionId","quick","icon","path","to","PLUGIN_DOCUMENT_PREVIEW","documentSegment","docId","unavailableReason","items","key","size","PLUGIN_DOCUMENT_BESIGNER","onClick","name","dialog","direction","spacing","sx","alignItems","ready","used","limit","checkQuota","noun","variant","color","slot","orgId","disabled","header","help","anchor","excerpt","HeaderProps","can","scope","action","openExport","undefined","severity","mb","headers","setClauses","refused","notices","rowHeight","initialState","columnVisibilityModel","filterMode","filterModel","onFilterModelChange","quickFilter","onPreferencePanelOpen","params","openedPanelValue","noRowsLabel","noRowsDescription","noRowsAction","xs","sm","onOpen","loading","hideFooter","disableColumnSorting","rowCount","onPageChange","onPageSizeChange","open","onClose","title","includeDescription","onSubmit","errorSlot","mt"],"mappings":"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"}
@@ -836,7 +836,10 @@ const json = (body, status = 200)=>Response.json(body, {
836
836
  kind: 'visitor'
837
837
  }, submittedEmail ? {
838
838
  email: submittedEmail
839
- } : {})
839
+ } : {}),
840
+ // The stored submission (AGL-3643), for a listener that hands it to
841
+ // an outside system by the id `/v1/…/form-submissions` reads.
842
+ recordId: submissionRef.id
840
843
  }, SITE_JOURNEY_ID_PATTERN.test(String((_payload_SITE_JOURNEY_SUBMISSION_FIELD = payload[SITE_JOURNEY_SUBMISSION_FIELD]) != null ? _payload_SITE_JOURNEY_SUBMISSION_FIELD : '')) ? {
841
844
  journeyId: String(payload[SITE_JOURNEY_SUBMISSION_FIELD])
842
845
  } : {}));
@@ -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 {\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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"}
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 // The stored submission (AGL-3643), for a listener that hands it to\n // an outside system by the id `/v1/…/form-submissions` reads.\n recordId: submissionRef.id,\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","recordId","test","journeyId"],"mappings":"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"}