@aglyn/aglyn 1.0.0-beta.219 → 1.0.0-beta.220
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 +12 -12
- package/src/lib/app-utils/actions.d.ts +1 -59
- package/src/lib/app-utils/actions.js +7 -72
- package/src/lib/app-utils/actions.js.map +1 -1
- package/src/lib/app-utils/consent-groups.d.ts +1 -1
- package/src/lib/app-utils/consent-groups.js.map +1 -1
- package/src/lib/app-utils/console-routes.d.ts +0 -36
- package/src/lib/app-utils/console-routes.js +2 -37
- package/src/lib/app-utils/console-routes.js.map +1 -1
- package/src/lib/app-utils/contacts.d.ts +1 -1
- package/src/lib/app-utils/contacts.js.map +1 -1
- package/src/lib/app-utils/container-membership.d.ts +93 -0
- package/src/lib/app-utils/container-membership.js +165 -0
- package/src/lib/app-utils/container-membership.js.map +1 -0
- package/src/lib/app-utils/crm-task-reminders.d.ts +23 -6
- package/src/lib/app-utils/crm-task-reminders.js +32 -11
- package/src/lib/app-utils/crm-task-reminders.js.map +1 -1
- package/src/lib/app-utils/crm.d.ts +24 -4
- package/src/lib/app-utils/crm.js +23 -0
- package/src/lib/app-utils/crm.js.map +1 -1
- package/src/lib/app-utils/csv-upload.d.ts +1 -1
- package/src/lib/app-utils/csv-upload.js +1 -1
- package/src/lib/app-utils/csv-upload.js.map +1 -1
- package/src/lib/app-utils/{dataset-csv.d.ts → csv.d.ts} +10 -43
- package/src/lib/app-utils/{dataset-csv.js → csv.js} +10 -81
- package/src/lib/app-utils/csv.js.map +1 -0
- package/src/lib/app-utils/datasets.d.ts +0 -19
- package/src/lib/app-utils/datasets.js +0 -13
- package/src/lib/app-utils/datasets.js.map +1 -1
- package/src/lib/app-utils/docs-help.generated.d.ts +2 -2
- package/src/lib/app-utils/docs-help.generated.js +2 -0
- package/src/lib/app-utils/docs-help.generated.js.map +1 -1
- package/src/lib/app-utils/docs-index.generated.js +63 -35
- package/src/lib/app-utils/docs-index.generated.js.map +1 -1
- package/src/lib/app-utils/dynamic-list-rule.js +3 -3
- package/src/lib/app-utils/dynamic-list-rule.js.map +1 -1
- package/src/lib/app-utils/email-text.d.ts +80 -0
- package/src/lib/app-utils/email-text.js +189 -0
- package/src/lib/app-utils/email-text.js.map +1 -0
- package/src/lib/app-utils/{list-assignment-policy.d.ts → enrollment-basis.d.ts} +4 -0
- package/src/lib/app-utils/{list-assignment-policy.js → enrollment-basis.js} +5 -1
- package/src/lib/app-utils/enrollment-basis.js.map +1 -0
- package/src/lib/app-utils/expand-repeatables.js +4 -4
- package/src/lib/app-utils/expand-repeatables.js.map +1 -1
- package/src/lib/app-utils/forms.js +2 -2
- package/src/lib/app-utils/forms.js.map +1 -1
- package/src/lib/app-utils/health-report.d.ts +2 -3
- package/src/lib/app-utils/health-report.js +8 -6
- package/src/lib/app-utils/health-report.js.map +1 -1
- package/src/lib/app-utils/inbound-mail-domain.d.ts +27 -0
- package/src/lib/app-utils/inbound-mail-domain.js +31 -0
- package/src/lib/app-utils/inbound-mail-domain.js.map +1 -0
- package/src/lib/app-utils/lockdown.d.ts +11 -0
- package/src/lib/app-utils/lockdown.js +33 -3
- package/src/lib/app-utils/lockdown.js.map +1 -1
- package/src/lib/app-utils/member-email-aliases.d.ts +25 -0
- package/src/lib/app-utils/member-email-aliases.js +29 -0
- package/src/lib/app-utils/member-email-aliases.js.map +1 -1
- package/src/lib/app-utils/notifications.d.ts +69 -0
- package/src/lib/app-utils/notifications.js +119 -0
- package/src/lib/app-utils/notifications.js.map +1 -1
- package/src/lib/app-utils/operator-alerts.d.ts +15 -1
- package/src/lib/app-utils/operator-alerts.js +47 -10
- package/src/lib/app-utils/operator-alerts.js.map +1 -1
- package/src/lib/app-utils/plan-entitlements.d.ts +164 -178
- package/src/lib/app-utils/plan-entitlements.js +200 -224
- package/src/lib/app-utils/plan-entitlements.js.map +1 -1
- package/src/lib/app-utils/platform-marketing-consent.d.ts +56 -2
- package/src/lib/app-utils/platform-marketing-consent.js +6 -5
- package/src/lib/app-utils/platform-marketing-consent.js.map +1 -1
- package/src/lib/app-utils/plugin-starter-templates.generated.d.ts +12 -0
- package/src/lib/app-utils/plugin-starter-templates.generated.js +711 -0
- package/src/lib/app-utils/plugin-starter-templates.generated.js.map +1 -0
- package/src/lib/app-utils/repeat-query.d.ts +56 -0
- package/src/lib/app-utils/{dataset-query.js → repeat-query.js} +16 -16
- package/src/lib/app-utils/repeat-query.js.map +1 -0
- package/src/lib/app-utils/server.d.ts +7 -9
- package/src/lib/app-utils/server.js +18 -26
- package/src/lib/app-utils/server.js.map +1 -1
- package/src/lib/app-utils/starter-template-nodes.d.ts +137 -0
- package/src/lib/app-utils/starter-template-nodes.js +129 -0
- package/src/lib/app-utils/starter-template-nodes.js.map +1 -0
- package/src/lib/app-utils/starter-templates.d.ts +11 -34
- package/src/lib/app-utils/starter-templates.js +19 -230
- package/src/lib/app-utils/starter-templates.js.map +1 -1
- package/src/lib/app-utils/{email-topics.d.ts → subscription-topics.d.ts} +64 -48
- package/src/lib/app-utils/{email-topics.js → subscription-topics.js} +39 -109
- package/src/lib/app-utils/subscription-topics.js.map +1 -0
- package/src/lib/app-utils/usage-budget.d.ts +73 -188
- package/src/lib/app-utils/usage-budget.js +58 -194
- package/src/lib/app-utils/usage-budget.js.map +1 -1
- package/src/lib/app-utils/utm-attribution.d.ts +1 -1
- package/src/lib/app-utils/utm-attribution.js +1 -1
- package/src/lib/app-utils/utm-attribution.js.map +1 -1
- package/src/lib/app-utils/{campaign-touch.d.ts → utm-touch.d.ts} +21 -13
- package/src/lib/app-utils/{campaign-touch.js → utm-touch.js} +30 -22
- package/src/lib/app-utils/utm-touch.js.map +1 -0
- package/src/lib/app-utils/variables.d.ts +9 -9
- package/src/lib/app-utils/variables.js.map +1 -1
- package/src/lib/app-utils/visitor-record-ceiling.d.ts +32 -0
- package/src/lib/app-utils/visitor-record-ceiling.js +39 -0
- package/src/lib/app-utils/visitor-record-ceiling.js.map +1 -1
- package/src/lib/foundation/definitions/org-billing.types.d.ts +2 -2
- package/src/lib/foundation/definitions/org-billing.types.js.map +1 -1
- package/src/lib/plugin-manager/feature-plugins.d.ts +80 -79
- package/src/lib/plugin-manager/feature-plugins.js +22 -35
- package/src/lib/plugin-manager/feature-plugins.js.map +1 -1
- package/src/lib/plugin-manager/first-party-plugins.generated.d.ts +12 -1
- package/src/lib/plugin-manager/first-party-plugins.generated.js +58 -2
- package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
- package/src/lib/plugin-manager/index.d.ts +0 -1
- package/src/lib/plugin-manager/index.js +0 -1
- package/src/lib/plugin-manager/index.js.map +1 -1
- package/src/lib/plugin-manager/plugin-contact-capture.d.ts +9 -4
- package/src/lib/plugin-manager/plugin-contact-capture.js.map +1 -1
- package/src/lib/plugin-manager/plugin-containers.d.ts +68 -0
- package/src/lib/plugin-manager/plugin-containers.generated.d.ts +10 -0
- package/src/lib/plugin-manager/plugin-containers.generated.js +20 -0
- package/src/lib/plugin-manager/plugin-containers.generated.js.map +1 -0
- package/src/lib/plugin-manager/plugin-containers.js +27 -0
- package/src/lib/plugin-manager/plugin-containers.js.map +1 -0
- package/src/lib/plugin-manager/plugin-entitlements.d.ts +8 -0
- package/src/lib/plugin-manager/plugin-entitlements.js.map +1 -1
- package/src/lib/plugin-manager/plugin-membership-detach.js +1 -1
- package/src/lib/plugin-manager/plugin-membership-detach.js.map +1 -1
- package/src/lib/plugin-manager/plugin-record-email-state.d.ts +69 -0
- package/src/lib/plugin-manager/plugin-record-email-state.js +32 -0
- package/src/lib/plugin-manager/plugin-record-email-state.js.map +1 -1
- package/src/lib/plugin-manager/plugin-record-lists.d.ts +115 -0
- package/src/lib/plugin-manager/plugin-record-lists.js +75 -0
- package/src/lib/plugin-manager/plugin-record-lists.js.map +1 -0
- package/src/lib/plugin-manager/plugin-record-timeline.d.ts +42 -8
- package/src/lib/plugin-manager/plugin-record-timeline.js.map +1 -1
- package/src/lib/plugin-manager/plugin-site-beacons.d.ts +71 -0
- package/src/lib/plugin-manager/plugin-site-beacons.js +54 -0
- package/src/lib/plugin-manager/plugin-site-beacons.js.map +1 -0
- package/src/lib/plugin-manager/plugin-subscription-topics.generated.d.ts +11 -0
- package/src/lib/plugin-manager/plugin-subscription-topics.generated.js +41 -0
- package/src/lib/plugin-manager/plugin-subscription-topics.generated.js.map +1 -0
- package/src/lib/plugin-manager/plugin-usage-axes.d.ts +212 -0
- package/src/lib/plugin-manager/plugin-usage-axes.js +82 -3
- package/src/lib/plugin-manager/plugin-usage-axes.js.map +1 -1
- package/src/lib/plugin-manager/plugin-usage-meters.d.ts +108 -0
- package/src/lib/plugin-manager/plugin-usage-meters.js +105 -0
- package/src/lib/plugin-manager/plugin-usage-meters.js.map +1 -0
- package/src/lib/plugin-manager/usage-alert-contributors.d.ts +76 -6
- package/src/lib/plugin-manager/usage-alert-contributors.js +23 -10
- package/src/lib/plugin-manager/usage-alert-contributors.js.map +1 -1
- package/src/lib/app-utils/assist-credits.d.ts +0 -482
- package/src/lib/app-utils/assist-credits.js +0 -510
- package/src/lib/app-utils/assist-credits.js.map +0 -1
- package/src/lib/app-utils/campaign-membership.d.ts +0 -121
- package/src/lib/app-utils/campaign-membership.js +0 -189
- package/src/lib/app-utils/campaign-membership.js.map +0 -1
- package/src/lib/app-utils/campaign-touch.js.map +0 -1
- package/src/lib/app-utils/console-record-links.d.ts +0 -84
- package/src/lib/app-utils/console-record-links.js +0 -76
- package/src/lib/app-utils/console-record-links.js.map +0 -1
- package/src/lib/app-utils/crm-booking.d.ts +0 -75
- package/src/lib/app-utils/crm-booking.js +0 -133
- package/src/lib/app-utils/crm-booking.js.map +0 -1
- package/src/lib/app-utils/crm-inbound.d.ts +0 -245
- package/src/lib/app-utils/crm-inbound.js +0 -407
- package/src/lib/app-utils/crm-inbound.js.map +0 -1
- package/src/lib/app-utils/crm-reports.d.ts +0 -479
- package/src/lib/app-utils/crm-reports.js +0 -680
- package/src/lib/app-utils/crm-reports.js.map +0 -1
- package/src/lib/app-utils/dataset-csv.js.map +0 -1
- package/src/lib/app-utils/dataset-query.d.ts +0 -57
- package/src/lib/app-utils/dataset-query.js.map +0 -1
- package/src/lib/app-utils/email-topics.js.map +0 -1
- package/src/lib/app-utils/list-assignment-policy.js.map +0 -1
- package/src/lib/plugin-manager/plugin-metered-lines.d.ts +0 -97
- package/src/lib/plugin-manager/plugin-metered-lines.js +0 -123
- package/src/lib/plugin-manager/plugin-metered-lines.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/consent-groups.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\n/**\n * CONSENT GROUPS — the declared unit a marketing basis is given to.\n *\n * ## The two organizations this exists to serve at once\n *\n * An AGENCY runs twelve unrelated client brands out of one account. A person\n * who ticked a box on one client's form has agreed to hear from that client\n * and from nobody else, and mailing them on behalf of the other eleven is a\n * consent violation whatever the account structure says.\n *\n * A SINGLE BUSINESS runs three sites — a shop, a booking page, a blog — under\n * one name. It collects addresses on all three and mails them from any of\n * them, because there is one controller and one brand and the visitor knows\n * it. Refusing that is not caution, it is a product that cannot serve a\n * perfectly ordinary customer.\n *\n * The org boundary cannot tell them apart, and neither can anything else the\n * database already holds. So the difference is DECLARED: a group names the\n * sites that are one sender. Undeclared, every site is its own group of one,\n * which is the agency's answer and costs the agency nothing to get right.\n *\n * ## ⛔ NOTHING HERE IS INFERRED\n *\n * Not from shared ownership, not from a shared org, not from a shared sending\n * domain, not from a shared theme. Sharing an account is a billing fact.\n * There is exactly one input — {@link CONSENT_GROUPS_FIELD} on the org — and\n * a site that is not named in it is alone.\n *\n * ## A group must be DISCLOSABLE, so it must have a NAME\n *\n * Pooling is only legitimate if the person was told about it: the checkbox\n * has to say who they will hear from. A group with no display name cannot be\n * rendered on a capture surface, so it cannot be disclosed, so it does not\n * pool — {@link consentGroupForHost} returns the group of one for every site\n * in a nameless group rather than pooling silently. That refusal is the whole\n * mechanism by which \"declared\" is stronger than \"configured\".\n *\n * ## …and a grant pools only where the name was actually SHOWN\n *\n * Disclosable is not disclosed. A form rendered before the group existed, a\n * checkout that has never displayed the sentence, and a page cached across a\n * rename all capture under a declaration their visitor never read. So a\n * capture surface that renders {@link consentGroupDisclosure} sends back the\n * {@link consentGroupDisclosureKey} of what it rendered, and\n * {@link consentGroupForGrant} pools the grant only when that key matches the\n * group as it stands. Anything else — no key, a stale key, a surface that\n * never renders the sentence — records the capturing site alone. Visibility\n * is not narrowed with it: that axis follows the group, see below.\n *\n * ## Pooling applies FORWARD only\n *\n * A grant records the group AS DISCLOSED at the moment it was given, in the\n * entry itself, and every covered site gets its own entry. Adding a site to a\n * group therefore reaches captures made after the change and none made\n * before. An org-level switch that widened existing grants would be the leak\n * wearing a different hat: the people already on the list were told a\n * different thing. The same holds for a grant carried from one record to\n * another — a list enrollment, a lead becoming a contact: it is copied as it\n * was recorded, never re-derived from the group as it stands now.\n *\n * OPT-OUT runs the other way — read against the CURRENT group, so a site\n * joining a group inherits every refusal already standing against it. The\n * asymmetry is the same one the rest of this area keeps: a permissive fact is\n * written narrowly and read exactly, a restrictive fact is read as broadly as\n * it could possibly apply. Every opt-out the send paths consult is read this\n * way — the site suppression lists, the topic opt-outs and the recipient's\n * cadence (`email-suppression.ts`), and the consent refusal\n * (`marketing-consent.ts`) — and the preference pages lift one across the\n * group, since a person rejoining the sender has rejoined all of it.\n *\n * ## A pending CONFIRMATION is the asking site's, unless the org says the group waits\n *\n * A site that asks for a confirmation click holds its own mail on that stream\n * until the click comes. A pending question is not a refusal, so by default\n * the rule above does not carry it: the sibling sites go on mailing. An org\n * may turn {@link CONSENT_GROUPS_AWAIT_CONFIRMATION_FIELD} on, and then every\n * site of a declared group waits for the click the way the asking site does —\n * a pending entry on any of them holds the stream from all of them, read\n * across the CURRENT group exactly as a refusal is.\n *\n * The switch is resolved INTO the group ({@link ConsentGroup.awaitsConfirmation}),\n * so a send path already holding the group holds the answer and reads nothing\n * more for it. A group of one never waits on anybody: there is nobody else.\n * Off is the default and the absence, and an org that never set it reads\n * exactly the documents it read before the field existed.\n *\n * ⚠️ The one change that runs AGAINST reading is sites SEPARATING — a site\n * leaving a group, moving to another, or a group dissolving. A refusal is\n * stored on the site the person acted on, so once two sites stop being one\n * sender, each stops seeing the refusals filed against the other, and every\n * one of them was a refusal of the sender both sites were at the time. So a\n * change to this declaration carries them BOTH WAYS before it takes effect:\n * the sites that stay keep every opt-out filed on the site that leaves, and\n * the site that leaves keeps every opt-out filed on the sites it leaves —\n * the preference page promised the person that an opt-out from the group\n * covers all of it, and separating the sites must not quietly break that.\n * The consent group change (the pure plan in `consent-group-change.ts`, run\n * by the executor in `@aglyn/tenant-data-admin`, started from the Emails\n * hub's Consent groups section through `POST /api/orgs/consent-groups`) is\n * the one writer of this field, and it carries the site suppression rows,\n * the topic opt-outs, the pace and a contact's per-site refusal before it\n * flips the declaration.\n * A site that is in a group, or in a change still running, cannot be\n * deleted until it is out: deletion would destroy refusals a sibling still\n * reads.\n *\n * ## VISIBILITY IS A SEPARATE AXIS\n *\n * Seeing a contact is not permission to mail them. A group decides consent\n * and opt-out; `visibleTo` decides who can read the row, and an org may\n * widen that for reporting while consent stays where it was given. The two\n * are resolved by different functions on purpose, and neither reads the\n * other.\n */\n\nimport type { TopicSubscriptionState } from './email-topics'\nimport { hostScopeToken, MAX_SCOPE_HOSTS, type ScopeToken } from './scope-tokens'\n\n/**\n * The org field holding the declarations:\n * `{ [groupId]: { name, hostIds } }`.\n *\n * On the org rather than on each host, because a group is a statement ABOUT\n * a set and a set stored as N per-host pointers can disagree with itself —\n * site A naming a group that site B has left is a half-declared controller,\n * and there is no reading of that which is safe to pool on.\n */\nexport const CONSENT_GROUPS_FIELD = 'consentGroups'\n\n/**\n * The org field that makes a declared group's sites wait for each other's\n * confirmation click (AGL-3316): `true` is on; absent is off.\n *\n * One switch for the org beside {@link CONSENT_GROUPS_FIELD} rather than a\n * flag on each declaration, because a declaration decides WHICH sites are one\n * sender and this decides how the org treats a question one of them has put.\n * Written only by `/api/orgs/settings`; the rules deny both fields to every\n * client.\n */\nexport const CONSENT_GROUPS_AWAIT_CONFIRMATION_FIELD =\n 'consentGroupsAwaitConfirmation'\n\n/**\n * The most sites one group may name.\n *\n * The same ceiling `visibleTo` carries, and for a related reason: the send\n * path reads a group's opt-out lists in one `getAll`, so the group multiplies\n * that round trip's size. It is also a sanity bound on a disclosure — a\n * checkbox that has to name forty brands has not disclosed anything.\n */\nexport const MAX_CONSENT_GROUP_HOSTS = MAX_SCOPE_HOSTS\n\n/** One declared group, as stored. */\nexport interface StoredConsentGroup {\n /** Shown on the capture surface. Without it the group cannot pool. */\n name: string\n /** The sites that are one sender. */\n hostIds: string[]\n}\n\n/**\n * The group a host belongs to, resolved FOR that host.\n *\n * It carries the asking site as well as the set, which is what lets one value\n * be passed where both are needed. Every consent read needs both — the grant\n * is looked up under the asking site, while a refusal is honored across the\n * whole group — and two arguments that must agree is two arguments that can\n * disagree.\n */\nexport interface ConsentGroup {\n /** The site this was resolved for. Always a member of {@link hostIds}. */\n hostId: string\n /**\n * The group's id, or the host's own id for an undeclared site.\n *\n * Recorded on every grant so a stored basis says which controller it was\n * given to even after the declaration changes.\n */\n groupId: string\n /**\n * What the capture surface must display. `null` for a group of one, where\n * the site's own name is what a form already shows and inventing a second\n * one would put a name in front of a person that nothing else uses.\n */\n name: string | null\n /** Every site covered, sorted, always including the host asked about. */\n hostIds: string[]\n /** False for the implicit group of one. */\n declared: boolean\n /**\n * Whether a confirmation one site of the group is waiting on holds the\n * other sites' mail too — the org's\n * {@link CONSENT_GROUPS_AWAIT_CONFIRMATION_FIELD}, for a declared group.\n * Always `false` for a group of one, which has no other site to hold.\n */\n awaitsConfirmation: boolean\n}\n\n/**\n * The group of one — a site that has declared no pooling.\n *\n * Exported because it is a value callers state deliberately: a writer that\n * means \"this site only\" says so, rather than passing a host id and letting\n * a helper decide what it covers.\n */\nexport function soloConsentGroup(hostId: string): ConsentGroup {\n if (!hostId) {\n throw new Error('[consent-groups] a consent group must name a site')\n }\n return {\n hostId,\n groupId: hostId,\n name: null,\n hostIds: [hostId],\n declared: false,\n awaitsConfirmation: false,\n }\n}\n\n/**\n * Whether the org turned {@link CONSENT_GROUPS_AWAIT_CONFIRMATION_FIELD} on.\n *\n * Only a stored `true` is on. Absent, `false`, or anything a stray write left\n * is off — the state every org was in before the field existed.\n */\nexport function consentGroupsAwaitConfirmation(\n org: Record<string, unknown> | null | undefined,\n): boolean {\n return (org ?? {})[CONSENT_GROUPS_AWAIT_CONFIRMATION_FIELD] === true\n}\n\n/**\n * Reads the declared groups off an org document, dropping every entry that\n * cannot be honored.\n *\n * A malformed declaration reads as ABSENT, never as a wider group: the whole\n * value of this field is that pooling is deliberate, and a corrupt value must\n * not be a way to reach an audience nobody declared.\n *\n * The five refusals, each of which would otherwise pool without a disclosure\n * or file one sender's records under another's key:\n *\n * - **no usable name** — cannot be shown on a form, so cannot be disclosed;\n * - **fewer than two sites** — not a pooling declaration, and a group of one\n * is what an undeclared site already gets;\n * - **over {@link MAX_CONSENT_GROUP_HOSTS}** — see that constant;\n * - **an id that is a site's id** — a site that declared nothing uses its\n * own id as its group id, so a group spelled the same way would share\n * that site's key: its grant stamps, its opt-out lookups and the records\n * the CRM files under the group id would read as the lone site's, and the\n * lone site's as the group's. Checked against every site the org holds\n * and every site any entry names, usable or not;\n * - **a site claimed by two groups** — two controllers claiming one site is\n * a contradiction, not a wider group, so BOTH claims are dropped and the\n * site falls back to being alone.\n */\nexport function readConsentGroups(\n org: Record<string, unknown> | null | undefined,\n): Record<string, StoredConsentGroup> {\n const raw = (org ?? {})[CONSENT_GROUPS_FIELD]\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return {}\n const siteIds = consentGroupSiteIds(org, raw as Record<string, unknown>)\n const usable: Record<string, StoredConsentGroup> = {}\n for (const [groupId, value] of Object.entries(raw as Record<string, unknown>)) {\n if (!groupId || !value || typeof value !== 'object') continue\n if (siteIds.has(groupId)) continue\n const group = value as Record<string, unknown>\n const name = typeof group['name'] === 'string' ? group['name'].trim() : ''\n if (!name) continue\n const hostIds = Array.isArray(group['hostIds'])\n ? [\n ...new Set(\n (group['hostIds'] as unknown[])\n .map((id) => String(id ?? '').trim())\n .filter(Boolean),\n ),\n ].sort()\n : []\n if (hostIds.length < 2 || hostIds.length > MAX_CONSENT_GROUP_HOSTS) continue\n usable[groupId] = { name, hostIds }\n }\n /*\n * The overlap pass, second because it needs every survivor of the first.\n * A site named by two groups is dropped from BOTH — the alternative is\n * picking one, and a coin flip is not a declaration.\n */\n const claims = new Map<string, number>()\n for (const group of Object.values(usable)) {\n for (const hostId of group.hostIds) {\n claims.set(hostId, (claims.get(hostId) ?? 0) + 1)\n }\n }\n const contested = new Set(\n [...claims.entries()].filter(([, count]) => count > 1).map(([id]) => id),\n )\n if (!contested.size) return usable\n const settled: Record<string, StoredConsentGroup> = {}\n for (const [groupId, group] of Object.entries(usable)) {\n if (group.hostIds.some((hostId) => contested.has(hostId))) continue\n settled[groupId] = group\n }\n return settled\n}\n\n/**\n * Every id that names a site as far as the declaration can tell: the org's\n * own `hosts` and every site any raw entry names, including the entries the\n * other refusals will drop.\n *\n * Wider than the usable set on purpose. A group id that collides with a\n * site the org has not linked yet, or with a site named only by a broken\n * entry, is the same collision one edit later, and refusing it now costs a\n * declaration nothing a valid one would have.\n */\nfunction consentGroupSiteIds(\n org: Record<string, unknown> | null | undefined,\n raw: Record<string, unknown>,\n): Set<string> {\n const ids = new Set<string>()\n const hosts = (org ?? {})['hosts']\n if (Array.isArray(hosts)) {\n for (const id of hosts) if (typeof id === 'string' && id) ids.add(id)\n } else if (hosts && typeof hosts === 'object') {\n for (const id of Object.keys(hosts)) if (id) ids.add(id)\n }\n for (const value of Object.values(raw)) {\n const hostIds = (value as { hostIds?: unknown } | null)?.hostIds\n if (!Array.isArray(hostIds)) continue\n for (const id of hostIds) {\n const trimmed = String(id ?? '').trim()\n if (trimmed) ids.add(trimmed)\n }\n }\n return ids\n}\n\n/**\n * The group `hostId` belongs to — the declared one, or the group of one.\n *\n * The ONLY way any caller learns what a site's consent covers. A caller that\n * built its own set from `org.hosts`, or from a sending domain, or from a\n * theme, would be inferring a controller, which is the thing this module\n * exists to make impossible.\n */\nexport function consentGroupForHost(\n org: Record<string, unknown> | null | undefined,\n hostId: string,\n): ConsentGroup {\n if (!hostId) {\n throw new Error('[consent-groups] a consent group must name a site')\n }\n for (const [groupId, group] of Object.entries(readConsentGroups(org))) {\n if (!group.hostIds.includes(hostId)) continue\n return {\n hostId,\n groupId,\n name: group.name,\n hostIds: [...group.hostIds],\n declared: true,\n awaitsConfirmation: consentGroupsAwaitConfirmation(org),\n }\n }\n return soloConsentGroup(hostId)\n}\n\n/**\n * The org field a running consent group change marks itself with:\n * `{ changeId, phase, hostIds, … }`, present from the moment the change\n * starts until it has finished. Only its `hostIds` is read here.\n */\nconst CONSENT_GROUP_CHANGE_MARKER_FIELD = 'consentGroupsChange'\n\n/** Why a site may not be deleted yet — see {@link consentGroupSiteHold}. */\nexport type ConsentGroupSiteHold =\n /** The site is one sender with other sites, under this group. */\n | { reason: 'grouped'; groupId: string; name: string }\n /** A consent group change naming the site has not finished. */\n | { reason: 'changing' }\n\n/**\n * Whether deleting `hostId` would take refusals its consent group still\n * needs, and why — or `null` when the site may go (AGL-3320).\n *\n * A site's opt-outs are stored on the site and read across its group, so\n * erasing a grouped site erases refusals its siblings honor today, and\n * leaves the declaration naming a site that no longer exists. A site named\n * by a change still in progress is the same hazard in motion: the change\n * may be copying its opt-outs out, or relying on them being there. So the\n * site leaves its group first — which carries its refusals to the sites\n * that stay — and is deleted after.\n *\n * Deleting the whole organization is the one exception, and is the caller's\n * to make: nothing is left to read the refusals.\n */\nexport function consentGroupSiteHold(\n org: Record<string, unknown> | null | undefined,\n hostId: string,\n): ConsentGroupSiteHold | null {\n if (!hostId) return null\n const group = consentGroupForHost(org, hostId)\n if (group.declared) {\n return { reason: 'grouped', groupId: group.groupId, name: group.name ?? '' }\n }\n const marker = (org ?? {})[CONSENT_GROUP_CHANGE_MARKER_FIELD]\n const changing = (marker as { hostIds?: unknown } | null | undefined)?.hostIds\n if (Array.isArray(changing) && changing.includes(hostId)) {\n return { reason: 'changing' }\n }\n return null\n}\n\n/**\n * The sites whose OPT-OUTS answer for mail sent by `group.hostId`, the\n * sending site first.\n *\n * Every site the group names, because a refusal filed against any of them is\n * a refusal of the sender — see \"OPT-OUT runs the other way\" above. The\n * sending site leads so a reader that treats its own record differently from\n * a sibling's (a sibling's pending confirmation holds the send only when the\n * group {@link ConsentGroup.awaitsConfirmation}) can tell them apart by\n * position. A group of one is the site alone.\n */\nexport function consentGroupOptOutHosts(\n group: Pick<ConsentGroup, 'hostId' | 'hostIds'>,\n): string[] {\n const siblings = (group.hostIds ?? []).filter(\n (id) => typeof id === 'string' && id && id !== group.hostId,\n )\n return [group.hostId, ...new Set(siblings)]\n}\n\n/**\n * The sending site's standing on one stream, read across its group.\n *\n * `states` are the stream's states on {@link consentGroupOptOutHosts}' sites,\n * in that order, so the sending site's comes first. A refusal on any of them\n * is a refusal of the sender. The sending site's own pending confirmation\n * holds its mail; a sibling's holds it only when the group\n * {@link ConsentGroup.awaitsConfirmation}. The precedence is the one\n * `readTopicSubscriptionState` keeps within one entry: a refusal outranks a\n * pending question, and an expired question is still pending.\n */\nexport function consentGroupTopicState(\n group: Pick<ConsentGroup, 'awaitsConfirmation'>,\n states: readonly TopicSubscriptionState[],\n): TopicSubscriptionState {\n const [own = 'subscribed', ...siblings] = states\n if (own === 'opted-out' || siblings.includes('opted-out')) return 'opted-out'\n if (own === 'pending') return 'pending'\n return group.awaitsConfirmation === true && siblings.includes('pending')\n ? 'pending'\n : 'subscribed'\n}\n\n/**\n * The `visibleTo` a resource captured by this group starts with.\n *\n * A separate function from anything above, called by the capture doors and\n * by nothing that decides mailability, because visibility and consent are\n * different questions with different answers. A group of one produces\n * `['host:{id}']` — the isolation an agency needs, arrived at without the\n * agency configuring anything.\n */\nexport function consentGroupScope(group: ConsentGroup): ScopeToken[] {\n return group.hostIds.map(hostScopeToken)\n}\n\n/**\n * The sentence a capture surface must show beside the checkbox, or `null`\n * when the site's own name already says it.\n *\n * Returned as text rather than a boolean so that the disclosure and the\n * grant come from ONE resolved group: a form that rendered its own wording\n * from a separate lookup could show one set of brands and record another,\n * and the recorded set is the one that decides who mails this person.\n */\nexport function consentGroupDisclosure(group: ConsentGroup): string | null {\n if (!group.declared || !group.name) return null\n return `You'll receive marketing email from ${group.name}, which covers ${group.hostIds.length} sites.`\n}\n\n/**\n * A short fingerprint of the disclosure a capture surface rendered, or\n * `null` when {@link consentGroupDisclosure} has nothing to render.\n *\n * The surface sends it back with the submission, and\n * {@link consentGroupForGrant} pools only when it still matches: a form\n * rendered before a rename, before a site joined, or before the group\n * existed at all carries a key that no longer does, and its grant narrows to\n * the site the person was on rather than reaching sites they were never told\n * about.\n *\n * Over the three things the sentence and the grant stand for — the group's\n * id, its name and its sites, sorted — serialized as JSON so no name can\n * run into an id. FNV-1a over the UTF-16 code units, which keeps it\n * synchronous and identical in the browser and on the server with nothing\n * to import. It is not a secret and proves no identity: it answers \"was it\n * THIS disclosure\", and a caller willing to lie could as easily lie about\n * the checkbox.\n */\nexport function consentGroupDisclosureKey(\n group: ConsentGroup,\n): string | null {\n if (!consentGroupDisclosure(group)) return null\n const text = JSON.stringify([\n group.groupId,\n group.name,\n [...group.hostIds].sort(),\n ])\n let hash = 0x811c9dc5\n for (let index = 0; index < text.length; index += 1) {\n hash ^= text.charCodeAt(index)\n hash = Math.imul(hash, 0x01000193)\n }\n return (hash >>> 0).toString(16).padStart(8, '0')\n}\n\n/**\n * The group a GRANT is recorded for: `group` itself only when it is declared\n * and `disclosureKey` is exactly its current {@link consentGroupDisclosureKey};\n * otherwise the capturing site alone.\n *\n * Called by every door that records a grant, with whatever key the capture\n * surface sent back — none, for every surface that does not render the\n * disclosure. Narrow is the failure direction on purpose: a grant that should\n * have pooled and did not withholds mail from a sibling site, where one that\n * pooled on a sentence nobody saw sends it.\n *\n * Only the grant narrows. The same capture still stamps `visibleTo` from the\n * whole group, and still reads refusals across it: who may SEE a person and\n * who may MAIL them are separate questions — see the module note.\n */\nexport function consentGroupForGrant(\n group: ConsentGroup,\n disclosureKey: string | null | undefined,\n): ConsentGroup {\n const expected = consentGroupDisclosureKey(group)\n if (expected !== null && disclosureKey === expected) return group\n return soloConsentGroup(group.hostId)\n}\n"],"names":["hostScopeToken","MAX_SCOPE_HOSTS","CONSENT_GROUPS_FIELD","CONSENT_GROUPS_AWAIT_CONFIRMATION_FIELD","MAX_CONSENT_GROUP_HOSTS","soloConsentGroup","hostId","Error","groupId","name","hostIds","declared","awaitsConfirmation","consentGroupsAwaitConfirmation","org","readConsentGroups","raw","Array","isArray","siteIds","consentGroupSiteIds","usable","value","Object","entries","has","group","trim","Set","map","id","String","filter","Boolean","sort","length","claims","Map","values","set","get","contested","count","size","settled","some","ids","hosts","add","keys","trimmed","consentGroupForHost","includes","CONSENT_GROUP_CHANGE_MARKER_FIELD","consentGroupSiteHold","reason","marker","changing","consentGroupOptOutHosts","siblings","consentGroupTopicState","states","own","consentGroupScope","consentGroupDisclosure","consentGroupDisclosureKey","text","JSON","stringify","hash","index","charCodeAt","Math","imul","toString","padStart","consentGroupForGrant","disclosureKey","expected"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/consent-groups.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\n/**\n * CONSENT GROUPS — the declared unit a marketing basis is given to.\n *\n * ## The two organizations this exists to serve at once\n *\n * An AGENCY runs twelve unrelated client brands out of one account. A person\n * who ticked a box on one client's form has agreed to hear from that client\n * and from nobody else, and mailing them on behalf of the other eleven is a\n * consent violation whatever the account structure says.\n *\n * A SINGLE BUSINESS runs three sites — a shop, a booking page, a blog — under\n * one name. It collects addresses on all three and mails them from any of\n * them, because there is one controller and one brand and the visitor knows\n * it. Refusing that is not caution, it is a product that cannot serve a\n * perfectly ordinary customer.\n *\n * The org boundary cannot tell them apart, and neither can anything else the\n * database already holds. So the difference is DECLARED: a group names the\n * sites that are one sender. Undeclared, every site is its own group of one,\n * which is the agency's answer and costs the agency nothing to get right.\n *\n * ## ⛔ NOTHING HERE IS INFERRED\n *\n * Not from shared ownership, not from a shared org, not from a shared sending\n * domain, not from a shared theme. Sharing an account is a billing fact.\n * There is exactly one input — {@link CONSENT_GROUPS_FIELD} on the org — and\n * a site that is not named in it is alone.\n *\n * ## A group must be DISCLOSABLE, so it must have a NAME\n *\n * Pooling is only legitimate if the person was told about it: the checkbox\n * has to say who they will hear from. A group with no display name cannot be\n * rendered on a capture surface, so it cannot be disclosed, so it does not\n * pool — {@link consentGroupForHost} returns the group of one for every site\n * in a nameless group rather than pooling silently. That refusal is the whole\n * mechanism by which \"declared\" is stronger than \"configured\".\n *\n * ## …and a grant pools only where the name was actually SHOWN\n *\n * Disclosable is not disclosed. A form rendered before the group existed, a\n * checkout that has never displayed the sentence, and a page cached across a\n * rename all capture under a declaration their visitor never read. So a\n * capture surface that renders {@link consentGroupDisclosure} sends back the\n * {@link consentGroupDisclosureKey} of what it rendered, and\n * {@link consentGroupForGrant} pools the grant only when that key matches the\n * group as it stands. Anything else — no key, a stale key, a surface that\n * never renders the sentence — records the capturing site alone. Visibility\n * is not narrowed with it: that axis follows the group, see below.\n *\n * ## Pooling applies FORWARD only\n *\n * A grant records the group AS DISCLOSED at the moment it was given, in the\n * entry itself, and every covered site gets its own entry. Adding a site to a\n * group therefore reaches captures made after the change and none made\n * before. An org-level switch that widened existing grants would be the leak\n * wearing a different hat: the people already on the list were told a\n * different thing. The same holds for a grant carried from one record to\n * another — a list enrollment, a lead becoming a contact: it is copied as it\n * was recorded, never re-derived from the group as it stands now.\n *\n * OPT-OUT runs the other way — read against the CURRENT group, so a site\n * joining a group inherits every refusal already standing against it. The\n * asymmetry is the same one the rest of this area keeps: a permissive fact is\n * written narrowly and read exactly, a restrictive fact is read as broadly as\n * it could possibly apply. Every opt-out the send paths consult is read this\n * way — the site suppression lists, the topic opt-outs and the recipient's\n * cadence (`email-suppression.ts`), and the consent refusal\n * (`marketing-consent.ts`) — and the preference pages lift one across the\n * group, since a person rejoining the sender has rejoined all of it.\n *\n * ## A pending CONFIRMATION is the asking site's, unless the org says the group waits\n *\n * A site that asks for a confirmation click holds its own mail on that stream\n * until the click comes. A pending question is not a refusal, so by default\n * the rule above does not carry it: the sibling sites go on mailing. An org\n * may turn {@link CONSENT_GROUPS_AWAIT_CONFIRMATION_FIELD} on, and then every\n * site of a declared group waits for the click the way the asking site does —\n * a pending entry on any of them holds the stream from all of them, read\n * across the CURRENT group exactly as a refusal is.\n *\n * The switch is resolved INTO the group ({@link ConsentGroup.awaitsConfirmation}),\n * so a send path already holding the group holds the answer and reads nothing\n * more for it. A group of one never waits on anybody: there is nobody else.\n * Off is the default and the absence, and an org that never set it reads\n * exactly the documents it read before the field existed.\n *\n * ⚠️ The one change that runs AGAINST reading is sites SEPARATING — a site\n * leaving a group, moving to another, or a group dissolving. A refusal is\n * stored on the site the person acted on, so once two sites stop being one\n * sender, each stops seeing the refusals filed against the other, and every\n * one of them was a refusal of the sender both sites were at the time. So a\n * change to this declaration carries them BOTH WAYS before it takes effect:\n * the sites that stay keep every opt-out filed on the site that leaves, and\n * the site that leaves keeps every opt-out filed on the sites it leaves —\n * the preference page promised the person that an opt-out from the group\n * covers all of it, and separating the sites must not quietly break that.\n * The consent group change (the pure plan in `consent-group-change.ts`, run\n * by the executor in `@aglyn/tenant-data-admin`, started from the Emails\n * hub's Consent groups section through `POST /api/orgs/consent-groups`) is\n * the one writer of this field, and it carries the site suppression rows,\n * the topic opt-outs, the pace and a contact's per-site refusal before it\n * flips the declaration.\n * A site that is in a group, or in a change still running, cannot be\n * deleted until it is out: deletion would destroy refusals a sibling still\n * reads.\n *\n * ## VISIBILITY IS A SEPARATE AXIS\n *\n * Seeing a contact is not permission to mail them. A group decides consent\n * and opt-out; `visibleTo` decides who can read the row, and an org may\n * widen that for reporting while consent stays where it was given. The two\n * are resolved by different functions on purpose, and neither reads the\n * other.\n */\n\nimport type { TopicSubscriptionState } from './subscription-topics'\nimport { hostScopeToken, MAX_SCOPE_HOSTS, type ScopeToken } from './scope-tokens'\n\n/**\n * The org field holding the declarations:\n * `{ [groupId]: { name, hostIds } }`.\n *\n * On the org rather than on each host, because a group is a statement ABOUT\n * a set and a set stored as N per-host pointers can disagree with itself —\n * site A naming a group that site B has left is a half-declared controller,\n * and there is no reading of that which is safe to pool on.\n */\nexport const CONSENT_GROUPS_FIELD = 'consentGroups'\n\n/**\n * The org field that makes a declared group's sites wait for each other's\n * confirmation click (AGL-3316): `true` is on; absent is off.\n *\n * One switch for the org beside {@link CONSENT_GROUPS_FIELD} rather than a\n * flag on each declaration, because a declaration decides WHICH sites are one\n * sender and this decides how the org treats a question one of them has put.\n * Written only by `/api/orgs/settings`; the rules deny both fields to every\n * client.\n */\nexport const CONSENT_GROUPS_AWAIT_CONFIRMATION_FIELD =\n 'consentGroupsAwaitConfirmation'\n\n/**\n * The most sites one group may name.\n *\n * The same ceiling `visibleTo` carries, and for a related reason: the send\n * path reads a group's opt-out lists in one `getAll`, so the group multiplies\n * that round trip's size. It is also a sanity bound on a disclosure — a\n * checkbox that has to name forty brands has not disclosed anything.\n */\nexport const MAX_CONSENT_GROUP_HOSTS = MAX_SCOPE_HOSTS\n\n/** One declared group, as stored. */\nexport interface StoredConsentGroup {\n /** Shown on the capture surface. Without it the group cannot pool. */\n name: string\n /** The sites that are one sender. */\n hostIds: string[]\n}\n\n/**\n * The group a host belongs to, resolved FOR that host.\n *\n * It carries the asking site as well as the set, which is what lets one value\n * be passed where both are needed. Every consent read needs both — the grant\n * is looked up under the asking site, while a refusal is honored across the\n * whole group — and two arguments that must agree is two arguments that can\n * disagree.\n */\nexport interface ConsentGroup {\n /** The site this was resolved for. Always a member of {@link hostIds}. */\n hostId: string\n /**\n * The group's id, or the host's own id for an undeclared site.\n *\n * Recorded on every grant so a stored basis says which controller it was\n * given to even after the declaration changes.\n */\n groupId: string\n /**\n * What the capture surface must display. `null` for a group of one, where\n * the site's own name is what a form already shows and inventing a second\n * one would put a name in front of a person that nothing else uses.\n */\n name: string | null\n /** Every site covered, sorted, always including the host asked about. */\n hostIds: string[]\n /** False for the implicit group of one. */\n declared: boolean\n /**\n * Whether a confirmation one site of the group is waiting on holds the\n * other sites' mail too — the org's\n * {@link CONSENT_GROUPS_AWAIT_CONFIRMATION_FIELD}, for a declared group.\n * Always `false` for a group of one, which has no other site to hold.\n */\n awaitsConfirmation: boolean\n}\n\n/**\n * The group of one — a site that has declared no pooling.\n *\n * Exported because it is a value callers state deliberately: a writer that\n * means \"this site only\" says so, rather than passing a host id and letting\n * a helper decide what it covers.\n */\nexport function soloConsentGroup(hostId: string): ConsentGroup {\n if (!hostId) {\n throw new Error('[consent-groups] a consent group must name a site')\n }\n return {\n hostId,\n groupId: hostId,\n name: null,\n hostIds: [hostId],\n declared: false,\n awaitsConfirmation: false,\n }\n}\n\n/**\n * Whether the org turned {@link CONSENT_GROUPS_AWAIT_CONFIRMATION_FIELD} on.\n *\n * Only a stored `true` is on. Absent, `false`, or anything a stray write left\n * is off — the state every org was in before the field existed.\n */\nexport function consentGroupsAwaitConfirmation(\n org: Record<string, unknown> | null | undefined,\n): boolean {\n return (org ?? {})[CONSENT_GROUPS_AWAIT_CONFIRMATION_FIELD] === true\n}\n\n/**\n * Reads the declared groups off an org document, dropping every entry that\n * cannot be honored.\n *\n * A malformed declaration reads as ABSENT, never as a wider group: the whole\n * value of this field is that pooling is deliberate, and a corrupt value must\n * not be a way to reach an audience nobody declared.\n *\n * The five refusals, each of which would otherwise pool without a disclosure\n * or file one sender's records under another's key:\n *\n * - **no usable name** — cannot be shown on a form, so cannot be disclosed;\n * - **fewer than two sites** — not a pooling declaration, and a group of one\n * is what an undeclared site already gets;\n * - **over {@link MAX_CONSENT_GROUP_HOSTS}** — see that constant;\n * - **an id that is a site's id** — a site that declared nothing uses its\n * own id as its group id, so a group spelled the same way would share\n * that site's key: its grant stamps, its opt-out lookups and the records\n * the CRM files under the group id would read as the lone site's, and the\n * lone site's as the group's. Checked against every site the org holds\n * and every site any entry names, usable or not;\n * - **a site claimed by two groups** — two controllers claiming one site is\n * a contradiction, not a wider group, so BOTH claims are dropped and the\n * site falls back to being alone.\n */\nexport function readConsentGroups(\n org: Record<string, unknown> | null | undefined,\n): Record<string, StoredConsentGroup> {\n const raw = (org ?? {})[CONSENT_GROUPS_FIELD]\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return {}\n const siteIds = consentGroupSiteIds(org, raw as Record<string, unknown>)\n const usable: Record<string, StoredConsentGroup> = {}\n for (const [groupId, value] of Object.entries(raw as Record<string, unknown>)) {\n if (!groupId || !value || typeof value !== 'object') continue\n if (siteIds.has(groupId)) continue\n const group = value as Record<string, unknown>\n const name = typeof group['name'] === 'string' ? group['name'].trim() : ''\n if (!name) continue\n const hostIds = Array.isArray(group['hostIds'])\n ? [\n ...new Set(\n (group['hostIds'] as unknown[])\n .map((id) => String(id ?? '').trim())\n .filter(Boolean),\n ),\n ].sort()\n : []\n if (hostIds.length < 2 || hostIds.length > MAX_CONSENT_GROUP_HOSTS) continue\n usable[groupId] = { name, hostIds }\n }\n /*\n * The overlap pass, second because it needs every survivor of the first.\n * A site named by two groups is dropped from BOTH — the alternative is\n * picking one, and a coin flip is not a declaration.\n */\n const claims = new Map<string, number>()\n for (const group of Object.values(usable)) {\n for (const hostId of group.hostIds) {\n claims.set(hostId, (claims.get(hostId) ?? 0) + 1)\n }\n }\n const contested = new Set(\n [...claims.entries()].filter(([, count]) => count > 1).map(([id]) => id),\n )\n if (!contested.size) return usable\n const settled: Record<string, StoredConsentGroup> = {}\n for (const [groupId, group] of Object.entries(usable)) {\n if (group.hostIds.some((hostId) => contested.has(hostId))) continue\n settled[groupId] = group\n }\n return settled\n}\n\n/**\n * Every id that names a site as far as the declaration can tell: the org's\n * own `hosts` and every site any raw entry names, including the entries the\n * other refusals will drop.\n *\n * Wider than the usable set on purpose. A group id that collides with a\n * site the org has not linked yet, or with a site named only by a broken\n * entry, is the same collision one edit later, and refusing it now costs a\n * declaration nothing a valid one would have.\n */\nfunction consentGroupSiteIds(\n org: Record<string, unknown> | null | undefined,\n raw: Record<string, unknown>,\n): Set<string> {\n const ids = new Set<string>()\n const hosts = (org ?? {})['hosts']\n if (Array.isArray(hosts)) {\n for (const id of hosts) if (typeof id === 'string' && id) ids.add(id)\n } else if (hosts && typeof hosts === 'object') {\n for (const id of Object.keys(hosts)) if (id) ids.add(id)\n }\n for (const value of Object.values(raw)) {\n const hostIds = (value as { hostIds?: unknown } | null)?.hostIds\n if (!Array.isArray(hostIds)) continue\n for (const id of hostIds) {\n const trimmed = String(id ?? '').trim()\n if (trimmed) ids.add(trimmed)\n }\n }\n return ids\n}\n\n/**\n * The group `hostId` belongs to — the declared one, or the group of one.\n *\n * The ONLY way any caller learns what a site's consent covers. A caller that\n * built its own set from `org.hosts`, or from a sending domain, or from a\n * theme, would be inferring a controller, which is the thing this module\n * exists to make impossible.\n */\nexport function consentGroupForHost(\n org: Record<string, unknown> | null | undefined,\n hostId: string,\n): ConsentGroup {\n if (!hostId) {\n throw new Error('[consent-groups] a consent group must name a site')\n }\n for (const [groupId, group] of Object.entries(readConsentGroups(org))) {\n if (!group.hostIds.includes(hostId)) continue\n return {\n hostId,\n groupId,\n name: group.name,\n hostIds: [...group.hostIds],\n declared: true,\n awaitsConfirmation: consentGroupsAwaitConfirmation(org),\n }\n }\n return soloConsentGroup(hostId)\n}\n\n/**\n * The org field a running consent group change marks itself with:\n * `{ changeId, phase, hostIds, … }`, present from the moment the change\n * starts until it has finished. Only its `hostIds` is read here.\n */\nconst CONSENT_GROUP_CHANGE_MARKER_FIELD = 'consentGroupsChange'\n\n/** Why a site may not be deleted yet — see {@link consentGroupSiteHold}. */\nexport type ConsentGroupSiteHold =\n /** The site is one sender with other sites, under this group. */\n | { reason: 'grouped'; groupId: string; name: string }\n /** A consent group change naming the site has not finished. */\n | { reason: 'changing' }\n\n/**\n * Whether deleting `hostId` would take refusals its consent group still\n * needs, and why — or `null` when the site may go (AGL-3320).\n *\n * A site's opt-outs are stored on the site and read across its group, so\n * erasing a grouped site erases refusals its siblings honor today, and\n * leaves the declaration naming a site that no longer exists. A site named\n * by a change still in progress is the same hazard in motion: the change\n * may be copying its opt-outs out, or relying on them being there. So the\n * site leaves its group first — which carries its refusals to the sites\n * that stay — and is deleted after.\n *\n * Deleting the whole organization is the one exception, and is the caller's\n * to make: nothing is left to read the refusals.\n */\nexport function consentGroupSiteHold(\n org: Record<string, unknown> | null | undefined,\n hostId: string,\n): ConsentGroupSiteHold | null {\n if (!hostId) return null\n const group = consentGroupForHost(org, hostId)\n if (group.declared) {\n return { reason: 'grouped', groupId: group.groupId, name: group.name ?? '' }\n }\n const marker = (org ?? {})[CONSENT_GROUP_CHANGE_MARKER_FIELD]\n const changing = (marker as { hostIds?: unknown } | null | undefined)?.hostIds\n if (Array.isArray(changing) && changing.includes(hostId)) {\n return { reason: 'changing' }\n }\n return null\n}\n\n/**\n * The sites whose OPT-OUTS answer for mail sent by `group.hostId`, the\n * sending site first.\n *\n * Every site the group names, because a refusal filed against any of them is\n * a refusal of the sender — see \"OPT-OUT runs the other way\" above. The\n * sending site leads so a reader that treats its own record differently from\n * a sibling's (a sibling's pending confirmation holds the send only when the\n * group {@link ConsentGroup.awaitsConfirmation}) can tell them apart by\n * position. A group of one is the site alone.\n */\nexport function consentGroupOptOutHosts(\n group: Pick<ConsentGroup, 'hostId' | 'hostIds'>,\n): string[] {\n const siblings = (group.hostIds ?? []).filter(\n (id) => typeof id === 'string' && id && id !== group.hostId,\n )\n return [group.hostId, ...new Set(siblings)]\n}\n\n/**\n * The sending site's standing on one stream, read across its group.\n *\n * `states` are the stream's states on {@link consentGroupOptOutHosts}' sites,\n * in that order, so the sending site's comes first. A refusal on any of them\n * is a refusal of the sender. The sending site's own pending confirmation\n * holds its mail; a sibling's holds it only when the group\n * {@link ConsentGroup.awaitsConfirmation}. The precedence is the one\n * `readTopicSubscriptionState` keeps within one entry: a refusal outranks a\n * pending question, and an expired question is still pending.\n */\nexport function consentGroupTopicState(\n group: Pick<ConsentGroup, 'awaitsConfirmation'>,\n states: readonly TopicSubscriptionState[],\n): TopicSubscriptionState {\n const [own = 'subscribed', ...siblings] = states\n if (own === 'opted-out' || siblings.includes('opted-out')) return 'opted-out'\n if (own === 'pending') return 'pending'\n return group.awaitsConfirmation === true && siblings.includes('pending')\n ? 'pending'\n : 'subscribed'\n}\n\n/**\n * The `visibleTo` a resource captured by this group starts with.\n *\n * A separate function from anything above, called by the capture doors and\n * by nothing that decides mailability, because visibility and consent are\n * different questions with different answers. A group of one produces\n * `['host:{id}']` — the isolation an agency needs, arrived at without the\n * agency configuring anything.\n */\nexport function consentGroupScope(group: ConsentGroup): ScopeToken[] {\n return group.hostIds.map(hostScopeToken)\n}\n\n/**\n * The sentence a capture surface must show beside the checkbox, or `null`\n * when the site's own name already says it.\n *\n * Returned as text rather than a boolean so that the disclosure and the\n * grant come from ONE resolved group: a form that rendered its own wording\n * from a separate lookup could show one set of brands and record another,\n * and the recorded set is the one that decides who mails this person.\n */\nexport function consentGroupDisclosure(group: ConsentGroup): string | null {\n if (!group.declared || !group.name) return null\n return `You'll receive marketing email from ${group.name}, which covers ${group.hostIds.length} sites.`\n}\n\n/**\n * A short fingerprint of the disclosure a capture surface rendered, or\n * `null` when {@link consentGroupDisclosure} has nothing to render.\n *\n * The surface sends it back with the submission, and\n * {@link consentGroupForGrant} pools only when it still matches: a form\n * rendered before a rename, before a site joined, or before the group\n * existed at all carries a key that no longer does, and its grant narrows to\n * the site the person was on rather than reaching sites they were never told\n * about.\n *\n * Over the three things the sentence and the grant stand for — the group's\n * id, its name and its sites, sorted — serialized as JSON so no name can\n * run into an id. FNV-1a over the UTF-16 code units, which keeps it\n * synchronous and identical in the browser and on the server with nothing\n * to import. It is not a secret and proves no identity: it answers \"was it\n * THIS disclosure\", and a caller willing to lie could as easily lie about\n * the checkbox.\n */\nexport function consentGroupDisclosureKey(\n group: ConsentGroup,\n): string | null {\n if (!consentGroupDisclosure(group)) return null\n const text = JSON.stringify([\n group.groupId,\n group.name,\n [...group.hostIds].sort(),\n ])\n let hash = 0x811c9dc5\n for (let index = 0; index < text.length; index += 1) {\n hash ^= text.charCodeAt(index)\n hash = Math.imul(hash, 0x01000193)\n }\n return (hash >>> 0).toString(16).padStart(8, '0')\n}\n\n/**\n * The group a GRANT is recorded for: `group` itself only when it is declared\n * and `disclosureKey` is exactly its current {@link consentGroupDisclosureKey};\n * otherwise the capturing site alone.\n *\n * Called by every door that records a grant, with whatever key the capture\n * surface sent back — none, for every surface that does not render the\n * disclosure. Narrow is the failure direction on purpose: a grant that should\n * have pooled and did not withholds mail from a sibling site, where one that\n * pooled on a sentence nobody saw sends it.\n *\n * Only the grant narrows. The same capture still stamps `visibleTo` from the\n * whole group, and still reads refusals across it: who may SEE a person and\n * who may MAIL them are separate questions — see the module note.\n */\nexport function consentGroupForGrant(\n group: ConsentGroup,\n disclosureKey: string | null | undefined,\n): ConsentGroup {\n const expected = consentGroupDisclosureKey(group)\n if (expected !== null && disclosureKey === expected) return group\n return soloConsentGroup(group.hostId)\n}\n"],"names":["hostScopeToken","MAX_SCOPE_HOSTS","CONSENT_GROUPS_FIELD","CONSENT_GROUPS_AWAIT_CONFIRMATION_FIELD","MAX_CONSENT_GROUP_HOSTS","soloConsentGroup","hostId","Error","groupId","name","hostIds","declared","awaitsConfirmation","consentGroupsAwaitConfirmation","org","readConsentGroups","raw","Array","isArray","siteIds","consentGroupSiteIds","usable","value","Object","entries","has","group","trim","Set","map","id","String","filter","Boolean","sort","length","claims","Map","values","set","get","contested","count","size","settled","some","ids","hosts","add","keys","trimmed","consentGroupForHost","includes","CONSENT_GROUP_CHANGE_MARKER_FIELD","consentGroupSiteHold","reason","marker","changing","consentGroupOptOutHosts","siblings","consentGroupTopicState","states","own","consentGroupScope","consentGroupDisclosure","consentGroupDisclosureKey","text","JSON","stringify","hash","index","charCodeAt","Math","imul","toString","padStart","consentGroupForGrant","disclosureKey","expected"],"mappings":"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"}
|
|
@@ -45,7 +45,6 @@ export declare enum Route {
|
|
|
45
45
|
ORG_HOME = "/[orgSlug]",
|
|
46
46
|
ORG_MEDIA = "/[orgSlug]/media",
|
|
47
47
|
ORG_DATA = "/[orgSlug]/data",
|
|
48
|
-
ORG_CONTACTS = "/[orgSlug]/contacts",
|
|
49
48
|
ORG_CRM = "/[orgSlug]/crm",
|
|
50
49
|
ORG_PLUGIN = "/[orgSlug]/[pluginSlug]",
|
|
51
50
|
ORG_PLUGINS = "/[orgSlug]/plugins",
|
|
@@ -114,7 +113,6 @@ export declare enum Route {
|
|
|
114
113
|
HOST_DASHBOARD = "/[orgSlug]/hosts/[host]",
|
|
115
114
|
HOST_PLUGIN = "/[orgSlug]/hosts/[host]/[pluginSlug]",
|
|
116
115
|
HOST_INBOX = "/[orgSlug]/hosts/[host]/inbox",
|
|
117
|
-
HOST_CONTACTS = "/[orgSlug]/hosts/[host]/contacts",
|
|
118
116
|
HOST_MEDIA = "/[orgSlug]/hosts/[host]/media",
|
|
119
117
|
HOST_SETUP = "/[orgSlug]/hosts/[host]/setup",
|
|
120
118
|
HOST_SETUP_DETAILS = "/[orgSlug]/hosts/[host]/setup/details",
|
|
@@ -134,22 +132,8 @@ export declare enum Route {
|
|
|
134
132
|
HOST_ADMIN_DANGER = "/[orgSlug]/hosts/[host]/admin/danger",
|
|
135
133
|
HOST_THEME = "/[orgSlug]/hosts/[host]/theme",
|
|
136
134
|
HOST_AUTOMATION = "/[orgSlug]/hosts/[host]/automation",
|
|
137
|
-
/**
|
|
138
|
-
* The address the automation section answered to before it had a name of
|
|
139
|
-
* its own. Nothing links here — `HOST_AUTOMATION` is what the nav, the
|
|
140
|
-
* search results and the activity feed build — but the console redirects it
|
|
141
|
-
* permanently, so a held bookmark still lands on the section (and keeps its
|
|
142
|
-
* `?tab=`, which never changed).
|
|
143
|
-
*
|
|
144
|
-
* Kept on the table rather than as a bare literal in the redirect
|
|
145
|
-
* because the redirect and the address it serves are one fact:
|
|
146
|
-
* `automation-section.spec.ts` reads this entry and fails if the rule for
|
|
147
|
-
* it goes missing.
|
|
148
|
-
*/
|
|
149
|
-
HOST_WORKFLOWS = "/[orgSlug]/hosts/[host]/workflows",
|
|
150
135
|
HOST_DATA = "/[orgSlug]/hosts/[host]/data",
|
|
151
136
|
HOST_LOGIC = "/[orgSlug]/hosts/[host]/logic",
|
|
152
|
-
HOST_PRODUCTS = "/[orgSlug]/hosts/[host]/products",
|
|
153
137
|
HOST_COMPONENTS = "/[orgSlug]/hosts/[host]/components",
|
|
154
138
|
COMPONENT_DETAILS = "/[orgSlug]/hosts/[host]/components/[componentId]",
|
|
155
139
|
HOST_FORMS = "/[orgSlug]/hosts/[host]/forms",
|
|
@@ -157,7 +141,6 @@ export declare enum Route {
|
|
|
157
141
|
HOST_TEMPLATES = "/[orgSlug]/hosts/[host]/templates",
|
|
158
142
|
TEMPLATE_DETAILS = "/[orgSlug]/hosts/[host]/templates/[templateId]",
|
|
159
143
|
HOST_MARKETING = "/[orgSlug]/hosts/[host]/marketing",
|
|
160
|
-
HOST_BOOKINGS = "/[orgSlug]/hosts/[host]/bookings",
|
|
161
144
|
HOST_REDIRECTS = "/[orgSlug]/hosts/[host]/redirects",
|
|
162
145
|
HOST_USERS = "/[orgSlug]/hosts/[host]/users",
|
|
163
146
|
HOST_ANALYTICS = "/[orgSlug]/hosts/[host]/analytics",
|
|
@@ -264,9 +247,6 @@ export interface RoutePayload {
|
|
|
264
247
|
[Route.ORG_DATA]: {
|
|
265
248
|
orgSlug: string;
|
|
266
249
|
};
|
|
267
|
-
[Route.ORG_CONTACTS]: {
|
|
268
|
-
orgSlug: string;
|
|
269
|
-
};
|
|
270
250
|
[Route.ORG_CRM]: {
|
|
271
251
|
orgSlug: string;
|
|
272
252
|
};
|
|
@@ -410,10 +390,6 @@ export interface RoutePayload {
|
|
|
410
390
|
orgSlug: string;
|
|
411
391
|
host: string;
|
|
412
392
|
};
|
|
413
|
-
[Route.HOST_CONTACTS]: {
|
|
414
|
-
orgSlug: string;
|
|
415
|
-
host: string;
|
|
416
|
-
};
|
|
417
393
|
[Route.HOST_SETUP]: {
|
|
418
394
|
orgSlug: string;
|
|
419
395
|
host: string;
|
|
@@ -496,10 +472,6 @@ export interface RoutePayload {
|
|
|
496
472
|
orgSlug: string;
|
|
497
473
|
host: string;
|
|
498
474
|
};
|
|
499
|
-
[Route.HOST_WORKFLOWS]: {
|
|
500
|
-
orgSlug: string;
|
|
501
|
-
host: string;
|
|
502
|
-
};
|
|
503
475
|
[Route.HOST_DATA]: {
|
|
504
476
|
orgSlug: string;
|
|
505
477
|
host: string;
|
|
@@ -508,10 +480,6 @@ export interface RoutePayload {
|
|
|
508
480
|
orgSlug: string;
|
|
509
481
|
host: string;
|
|
510
482
|
};
|
|
511
|
-
[Route.HOST_PRODUCTS]: {
|
|
512
|
-
orgSlug: string;
|
|
513
|
-
host: string;
|
|
514
|
-
};
|
|
515
483
|
[Route.HOST_COMPONENTS]: {
|
|
516
484
|
orgSlug: string;
|
|
517
485
|
host: string;
|
|
@@ -543,10 +511,6 @@ export interface RoutePayload {
|
|
|
543
511
|
orgSlug: string;
|
|
544
512
|
host: string;
|
|
545
513
|
};
|
|
546
|
-
[Route.HOST_BOOKINGS]: {
|
|
547
|
-
orgSlug: string;
|
|
548
|
-
host: string;
|
|
549
|
-
};
|
|
550
514
|
[Route.HOST_REDIRECTS]: {
|
|
551
515
|
orgSlug: string;
|
|
552
516
|
host: string;
|
|
@@ -108,27 +108,6 @@ export var Route = /*#__PURE__*/ function(Route) {
|
|
|
108
108
|
Route["ORG_MEDIA"] = "/[orgSlug]/media";
|
|
109
109
|
Route["ORG_DATA"] = "/[orgSlug]/data";
|
|
110
110
|
/*
|
|
111
|
-
* The organization's address book.
|
|
112
|
-
*
|
|
113
|
-
* Org-scoped rather than a section of a site, because it answers the one
|
|
114
|
-
* question a site page cannot: which of the organization's sites know a
|
|
115
|
-
* given person. A contact document is shared by every site in the org — one
|
|
116
|
-
* human who touched two of them is one row — and until this route there was
|
|
117
|
-
* no surface anywhere that showed the deduped person, so the dedupe the
|
|
118
|
-
* shared address book exists for was invisible and the billing unit
|
|
119
|
-
* (unique people per org) had nothing standing behind it.
|
|
120
|
-
*
|
|
121
|
-
* It shows identity, which sites hold the person, and consent per site. The
|
|
122
|
-
* per-site CRM — notes, tags, timeline, order history — stays on the site's
|
|
123
|
-
* own Contacts surface, because those are the holder's business records and
|
|
124
|
-
* this is the one route designed to cross between holders.
|
|
125
|
-
*
|
|
126
|
-
* A LEGACY ADDRESS since AGL-2630: the page behind it redirects, permanently,
|
|
127
|
-
* to the org-level CRM hub's contacts section. It stays declared so a link
|
|
128
|
-
* kept from before — a bookmark, a docs page, an email — still resolves,
|
|
129
|
-
* and so the redirect has a constant to be built from rather than a string.
|
|
130
|
-
*/ Route["ORG_CONTACTS"] = "/[orgSlug]/contacts";
|
|
131
|
-
/*
|
|
132
111
|
* The organization-level CRM hub (AGL-2630): the same sections the site hub
|
|
133
112
|
* has — `/[orgSlug]/crm/<section>[/<recordId>]` — mounted with an ORG scope,
|
|
134
113
|
* for an org-wide member who works every site's people at once. A bare
|
|
@@ -136,7 +115,8 @@ export var Route = /*#__PURE__*/ function(Route) {
|
|
|
136
115
|
* does. What this route adds over the site hub is the cross-site fact the
|
|
137
116
|
* old address book carried — which sites know a person — and what it takes
|
|
138
117
|
* from it is the site every create must name, since a record is always
|
|
139
|
-
* captured BY a site.
|
|
118
|
+
* captured BY a site. Served by `ORG_PLUGIN` below, from the CRM's own
|
|
119
|
+
* `orgNavItems`; the constant is the address its links are built from.
|
|
140
120
|
*/ Route["ORG_CRM"] = "/[orgSlug]/crm";
|
|
141
121
|
/*
|
|
142
122
|
* The generic ORGANIZATION-level plugin route (AGL-2974): every surface an
|
|
@@ -385,7 +365,6 @@ export var Route = /*#__PURE__*/ function(Route) {
|
|
|
385
365
|
// are open-ended, so this is the one route whose leaf segment is data.
|
|
386
366
|
Route["HOST_PLUGIN"] = "/[orgSlug]/hosts/[host]/[pluginSlug]";
|
|
387
367
|
Route["HOST_INBOX"] = "/[orgSlug]/hosts/[host]/inbox";
|
|
388
|
-
Route["HOST_CONTACTS"] = "/[orgSlug]/hosts/[host]/contacts";
|
|
389
368
|
Route["HOST_MEDIA"] = "/[orgSlug]/hosts/[host]/media";
|
|
390
369
|
Route["HOST_SETUP"] = "/[orgSlug]/hosts/[host]/setup";
|
|
391
370
|
/*
|
|
@@ -448,21 +427,8 @@ export var Route = /*#__PURE__*/ function(Route) {
|
|
|
448
427
|
* The parent is named for what the three have in common, so no tab shares a
|
|
449
428
|
* name with the section that contains it.
|
|
450
429
|
*/ Route["HOST_AUTOMATION"] = "/[orgSlug]/hosts/[host]/automation";
|
|
451
|
-
/**
|
|
452
|
-
* The address the automation section answered to before it had a name of
|
|
453
|
-
* its own. Nothing links here — `HOST_AUTOMATION` is what the nav, the
|
|
454
|
-
* search results and the activity feed build — but the console redirects it
|
|
455
|
-
* permanently, so a held bookmark still lands on the section (and keeps its
|
|
456
|
-
* `?tab=`, which never changed).
|
|
457
|
-
*
|
|
458
|
-
* Kept on the table rather than as a bare literal in the redirect
|
|
459
|
-
* because the redirect and the address it serves are one fact:
|
|
460
|
-
* `automation-section.spec.ts` reads this entry and fails if the rule for
|
|
461
|
-
* it goes missing.
|
|
462
|
-
*/ Route["HOST_WORKFLOWS"] = "/[orgSlug]/hosts/[host]/workflows";
|
|
463
430
|
Route["HOST_DATA"] = "/[orgSlug]/hosts/[host]/data";
|
|
464
431
|
Route["HOST_LOGIC"] = "/[orgSlug]/hosts/[host]/logic";
|
|
465
|
-
Route["HOST_PRODUCTS"] = "/[orgSlug]/hosts/[host]/products";
|
|
466
432
|
Route["HOST_COMPONENTS"] = "/[orgSlug]/hosts/[host]/components";
|
|
467
433
|
// Component detail (AGL-693): the listing links here, and the besigner is
|
|
468
434
|
// reached from here — matching SCREEN_DETAILS rather than jumping a row
|
|
@@ -481,7 +447,6 @@ export var Route = /*#__PURE__*/ function(Route) {
|
|
|
481
447
|
// publish, so there is no "current" pointer to route through.
|
|
482
448
|
Route["TEMPLATE_DETAILS"] = "/[orgSlug]/hosts/[host]/templates/[templateId]";
|
|
483
449
|
Route["HOST_MARKETING"] = "/[orgSlug]/hosts/[host]/marketing";
|
|
484
|
-
Route["HOST_BOOKINGS"] = "/[orgSlug]/hosts/[host]/bookings";
|
|
485
450
|
// Events now come from the events-calendar plugin, served by the generic
|
|
486
451
|
// `[orgSlug]/hosts/[host]/[pluginSlug]` route (AGL-394).
|
|
487
452
|
Route["HOST_REDIRECTS"] = "/[orgSlug]/hosts/[host]/redirects";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/console-routes.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2022 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\n// URL scheme (AGL-621): the org is a first-class path segment `[orgSlug]`\n// so the URL — not client-side precedence — is the source of truth for the\n// active workspace. Host routes nest under `/[orgSlug]/hosts/[host]`; the\n// org area (settings, team, billing, media, data, plugins, support,\n// marketplace) lives directly under `/[orgSlug]`. User-level `manage/*`,\n// staff `admin/*`, and `auth` routes are NOT org-scoped. Hosts stay keyed\n// by doc id here; AGL-622 swaps `[hostId]` for the subdomain slug.\nexport enum Route {\n ADMIN_ORGS = '/admin/orgs',\n ADMIN_ORG_DETAIL = '/admin/orgs/[orgId]',\n ADMIN_ORG_HOST_DETAIL = '/admin/orgs/[orgId]/host/[hostId]',\n // Every site on the platform, across organizations, and one site's staff\n // detail (AGL-3378). The org-nested host route above redirects here.\n ADMIN_SITES = '/admin/sites',\n ADMIN_SITE_DETAIL = '/admin/sites/[hostId]',\n // A site's screen, layout, component, template or form as its draft renders,\n // for staff who are not members of the site (AGL-3378). `kind` is a\n // `PreviewKind`; the version, when not the current one, rides `?version=`.\n ADMIN_SITE_PREVIEW = '/admin/sites/[hostId]/preview/[kind]/[docId]',\n ADMIN_OVERVIEW = '/admin/overview',\n // Staff coupon management (AGL-1105): create discount coupons + promotion\n // codes and read the live net-margin rating before committing to one.\n ADMIN_COUPONS = '/admin/coupons',\n // The panic button (AGL-1501): platform/org/host/user lockdown controls.\n ADMIN_LOCKDOWN = '/admin/lockdown',\n // The narrow lever beside it (AGL-1687): disable or release ONE uploaded\n // file. Its own page rather than a panel on Lockdown because the input is\n // an asset, not a scope — the two forms share no field but the reason.\n ADMIN_MEDIA_QUARANTINE = '/admin/media-quarantine',\n // The INPUT to the two levers above (AGL-1964). Lockdown and quarantine are\n // both things an operator does after learning there is a problem; until\n // this page there was no way for anyone outside the company to say there\n // was one. Reports arrive from the unauthenticated form the tenant runtime\n // serves at /api/report-abuse on every origin.\n ADMIN_ABUSE_REPORTS = '/admin/abuse-reports',\n // The same input, one collection over (AGL-2310). The marketplace's own\n // report button wrote `marketplaceReports` from the day it shipped and\n // nothing read it — every report filed, acknowledged, and unreachable. This\n // is that queue, deliberately shaped like the abuse one rather than as a\n // second vocabulary for the same act.\n // The probes, on a screen (AGL-1900). /api/health/{backups,rate-limits,\n // signups} and /api/admin/email-health each answered a bad-day question to\n // a curl and to nothing else; this is where an operator reads them.\n ADMIN_HEALTH = '/admin/health',\n // The maintenance jobs, on a screen (AGL-1949). audit-archive,\n // reap-plugin-artifacts and reverify-plugin-versions accepted the shared\n // cron secret and nothing else, so the only way to preview or run one was a\n // shell holding the production secret. Separate from Health because Health\n // reads and this acts.\n ADMIN_MAINTENANCE = '/admin/maintenance',\n // The Texas sales tax return (AGL-1900). The figures AGL-1811 computes had\n // only a curl to reach them; filing happens on a fixed quarterly calendar\n // from 2026-09-01, so the return needs a URL a person can bookmark.\n ADMIN_REVENUE = '/admin/revenue',\n // Realised band utilization and the margin that follows from it. Beside\n // Revenue because the two are the halves of one question: that page reports\n // what came in, this one what serving it cost.\n ADMIN_MARGIN_UTILIZATION = '/admin/margin-utilization',\n ADMIN_TAX_RETURN = '/admin/tax-return',\n ADMIN_AUDIT = '/admin/audit',\n ADMIN_USERS = '/admin/users',\n ADMIN_USER_DETAIL = '/admin/users/[uid]',\n ADMIN_FLAGS = '/admin/flags',\n // Platform-wide settings that are not a release flag (AGL-2486). The\n // free-workspace ceiling was the first of them and it opened the\n // Organizations LIST, above the table — a global lever wedged into the\n // screen for browsing individual orgs. Settings that describe the platform\n // rather than one org belong on their own tab, beside the flags.\n ADMIN_SETTINGS = '/admin/settings',\n // Which operator alerts email the operator, immediately or in the daily\n // digest, and whether the channels reach anybody (AGL-3377).\n ADMIN_OPERATOR_ALERTS = '/admin/operator-alerts',\n // One submission or listed plugin in full (AGL-959): the queue index is\n // for scanning, this is where a reviewer reads the manifest, weighs the\n // verifier findings and acts.\n // A page a plugin adds to the staff area (AGL-2939), rendered by the\n // console's generic staff route. The console's own staff routes win the\n // segments they use.\n ADMIN_STAFF_PAGE = '/admin/[staffPage]',\n // Staff support-ticket queue (AGL-849): the operator side of the\n // subscriber `MANAGE_SUPPORT_TICKETS` page — every org's tickets in one place.\n ADMIN_SUPPORT = '/admin/support',\n // Do-not-contact list (AGL-1592): the intake and the queue for Privacy\n // Policy v4 §11's marketing call/text opt-out, and for \"delete the phone\n // number you hold for me\".\n ADMIN_CONTACT_SUPPRESSIONS = '/admin/contact-suppressions',\n ADMIN_EMAILS = '/admin/emails',\n // The one besigner route with no host and no org in it (AGL-749). A system\n // email belongs to the platform, not to a workspace, so it is staff-scoped\n // like the rest of `/admin/*` rather than nested under `[orgSlug]`.\n ADMIN_EMAIL_BESIGNER = '/admin/emails/[templateKey]/versions/[versionId]/besigner',\n ORG_HOME = '/[orgSlug]',\n ORG_MEDIA = '/[orgSlug]/media',\n ORG_DATA = '/[orgSlug]/data',\n /*\n * The organization's address book.\n *\n * Org-scoped rather than a section of a site, because it answers the one\n * question a site page cannot: which of the organization's sites know a\n * given person. A contact document is shared by every site in the org — one\n * human who touched two of them is one row — and until this route there was\n * no surface anywhere that showed the deduped person, so the dedupe the\n * shared address book exists for was invisible and the billing unit\n * (unique people per org) had nothing standing behind it.\n *\n * It shows identity, which sites hold the person, and consent per site. The\n * per-site CRM — notes, tags, timeline, order history — stays on the site's\n * own Contacts surface, because those are the holder's business records and\n * this is the one route designed to cross between holders.\n *\n * A LEGACY ADDRESS since AGL-2630: the page behind it redirects, permanently,\n * to the org-level CRM hub's contacts section. It stays declared so a link\n * kept from before — a bookmark, a docs page, an email — still resolves,\n * and so the redirect has a constant to be built from rather than a string.\n */\n ORG_CONTACTS = '/[orgSlug]/contacts',\n /*\n * The organization-level CRM hub (AGL-2630): the same sections the site hub\n * has — `/[orgSlug]/crm/<section>[/<recordId>]` — mounted with an ORG scope,\n * for an org-wide member who works every site's people at once. A bare\n * `/crm` lands on the first section the reader may open, as the site hub's\n * does. What this route adds over the site hub is the cross-site fact the\n * old address book carried — which sites know a person — and what it takes\n * from it is the site every create must name, since a record is always\n * captured BY a site.\n */\n ORG_CRM = '/[orgSlug]/crm',\n /*\n * The generic ORGANIZATION-level plugin route (AGL-2974): every surface an\n * extension declares in `orgNavItems` is served here, at\n * `/[orgSlug]/<href>[/<section>…]`, by\n * `app/(app)/[orgSlug]/[...pluginSlug]/page.tsx`. The org twin of\n * `HOST_PLUGIN`, and like it the one org route whose leaf segment is data:\n * every named org route in this enum wins over it.\n */\n ORG_PLUGIN = '/[orgSlug]/[pluginSlug]',\n ORG_PLUGINS = '/[orgSlug]/plugins',\n // One plugin, as installed in this workspace (AGL-1007): scope, settings\n // and permissions in one place. The segment takes EITHER identifier\n // (AGL-1010) — a listing id for a marketplace install, a registry id for a\n // first-party plugin — hence `pluginRef` rather than `listingId`.\n ORG_PLUGIN_INSTALLATION = '/[orgSlug]/plugins/[pluginRef]',\n // Org-scope marketplace (AGL-772): the single place to browse, view and\n // install marketplace items. The per-site tab it replaced, and the retired\n // `/[orgSlug]/marketplace` seller area, were removed with the rest of the\n // `marketplace` naming in AGL-975 — the word is being freed for a public\n // forum, and a redirect stub sitting on `/marketplace` would have taken the\n // one path that feature wants.\n /*\n * The marketplace's own SECTIONS are not here any more (AGL-3080). Eight\n * of them — browse, licences, upload, the four seller cards and the\n * publish form — were hand-written console routes, and became an\n * `orgNavItems` declaration whose `sections` name those segments once, in\n * the plugin. Restating them here was a second copy of a list the plugin\n * owns, and every entry had zero callers the moment the routes went.\n *\n * What stays is what the CONSOLE still builds: the surface itself, a\n * listing, a publisher, and the installed tab that the org plugins card\n * links across to. A console page linking into a plugin's surface is the\n * ordinary case; a console page enumerating that surface's sections is\n * not.\n */\n ORG_MARKETPLACE = '/[orgSlug]/marketplace',\n /*\n * Marketplace SECTIONS are routes (AGL-2501), for the reason the settings\n * and account sections are: a panel that is not open should cost nothing,\n * and the seller half of this hub reads the organization's REVENUE.\n *\n * The four seller sections are the reason the layout gates rather than the\n * rail. As tabs they were simply not rendered for a member without\n * `publishToMarketplace`; as routes each one is reachable by typing its URL,\n * so the refusal has to sit above them all rather than in the list that\n * draws them.\n *\n * Every segment here is shorter than the ten characters `createResourceUid`\n * emits, so none of them can ever shadow a real `[listingId]` sitting at the\n * same level. `upload` rather than `publish` because `publish/plugin` is\n * already a route beneath this one, and two directories contributing the\n * same segment is a tree Next cannot resolve a layout for.\n */\n ORG_MARKETPLACE_INSTALLED = '/[orgSlug]/marketplace/installed',\n ORG_MARKETPLACE_LISTING = '/[orgSlug]/marketplace/[listingId]',\n ORG_MARKETPLACE_PUBLISHER = '/[orgSlug]/marketplace/publisher/[handle]',\n // Publishing a plugin is a page, not a modal (AGL-1078): the most\n // consequential thing a publisher does now has a URL, so it can be linked,\n // reloaded, and reached from a listing to ship an update.\n ORG_SETTINGS = '/[orgSlug]/settings',\n /*\n * Settings SECTIONS are routes (AGL-2501). `HubTabs` mounted every panel —\n * `keepMounted`, with `lazy` off by default and passed by nobody — so\n * opening General also mounted the API-keys, SSO and data-export cards and\n * ran every read in them. Routes mount one page and code-split per route.\n * `ORG_SETTINGS` itself redirects to General.\n */\n ORG_SETTINGS_GENERAL = '/[orgSlug]/settings/general',\n ORG_SETTINGS_PROFILE = '/[orgSlug]/settings/profile',\n ORG_SETTINGS_PLUGINS = '/[orgSlug]/settings/plugins',\n ORG_SETTINGS_API_KEYS = '/[orgSlug]/settings/api-keys',\n ORG_SETTINGS_BRANDING = '/[orgSlug]/settings/branding',\n ORG_SETTINGS_SSO = '/[orgSlug]/settings/sso',\n ORG_SETTINGS_OWNERSHIP = '/[orgSlug]/settings/ownership',\n /**\n * Export the people a workspace holds and file a person's erasure\n * (AGL-2839) — outside the CRM, so it is there on every plan.\n */\n ORG_SETTINGS_PRIVACY = '/[orgSlug]/settings/privacy',\n // Holds & reviews (AGL-3368): everything on the workspace that was held,\n // flagged, locked or paused, its status, and the way to request a review.\n // Where every risk notice's \"Request a review\" lands (`?notice=<id>`).\n ORG_SETTINGS_HOLDS = '/[orgSlug]/settings/holds',\n ORG_SETTINGS_DELETE = '/[orgSlug]/settings/delete',\n /**\n * Billing, section by section (AGL-2501).\n *\n * `MANAGE_BILLING` is unchanged and is the PLAN section — it resolves to a\n * page inside a route group, which adds no path segment. That is what makes\n * this split free: every link in the console, every hash anchor\n * (`#addons`, `#collaborator-seats`) and Stripe's own dunning mail all point\n * here already and none of them moves. No redirect, and no `?tab=` shim to\n * carry forever.\n */\n MANAGE_BILLING = '/[orgSlug]/billing',\n MANAGE_BILLING_USAGE = '/[orgSlug]/billing/usage',\n MANAGE_BILLING_INVOICES = '/[orgSlug]/billing/invoices',\n MANAGE_BILLING_SETTINGS = '/[orgSlug]/billing/settings',\n // The ONE billing URL that carries no org (AGL-2430).\n //\n // Everything else on this table is org-scoped, which is correct for a\n // console and wrong for the only caller that cannot template a URL:\n // Stripe's dunning emails. \"Payment method updates\" takes a single static\n // link for every customer in the account — there is no `{{org}}` to\n // interpolate — so a table with no org-agnostic billing entry means those\n // emails can only ever point at a page that is not billing. Today all four\n // of them point at the marketing homepage, which is how a failed card\n // becomes a cancelled subscription with no reachable way to fix it.\n //\n // This resolves the org from the SESSION instead of from the URL: one\n // workspace goes straight through, several offer a choice, none says so.\n // `billing` is a reserved subdomain (`RESERVED_SUBDOMAINS`), so no org can\n // hold this slug and the literal segment can never shadow a real\n // `/[orgSlug]`.\n BILLING_ENTRY = '/billing',\n MANAGE_USER_SETTINGS = '/manage/user',\n /*\n * Account SECTIONS are routes (AGL-2501). `HubTabs` mounted every panel —\n * `keepMounted`, with `lazy` off by default and passed by nobody — so\n * opening Account also mounted the email-addresses, passkeys, recent\n * sign-ins, data-export and close-account cards and ran every read in them.\n * Routes mount one page and code-split per route.\n *\n * The segments are the ids the panels carried, because a transactional\n * email already in people's inboxes links `/manage/user?tab=security`:\n * `MANAGE_USER_SETTINGS` forwards each of those ids to the section that\n * holds it, and the segment matching the id is what keeps that map a\n * one-liner rather than a second naming scheme.\n */\n MANAGE_USER_ACCOUNT = '/manage/user/account',\n MANAGE_USER_EMAILS = '/manage/user/emails',\n MANAGE_USER_PROFILE = '/manage/user/profile',\n MANAGE_USER_BASIC = '/manage/user/basic',\n MANAGE_USER_SECURITY = '/manage/user/security',\n MANAGE_USER_CLOSE = '/manage/user/close',\n MANAGE_NOTIFICATIONS = '/manage/notifications',\n /*\n * The preferences, off the feed (AGL-3226).\n *\n * They lived in the header of the feed itself, which was already the\n * tallest thing on that page before per-channel and per-scope settings\n * multiplied them — and it put an edit form above the list it edits, which\n * the console does nowhere else.\n *\n * A CHILD of the feed rather than a sibling under `/manage`, because the\n * relationship is exactly that: it configures what the page above it shows.\n * That also keeps `/manage/notifications` the address in the transactional\n * mail already in people's inboxes, which keeps working and now carries a\n * button to here.\n */\n MANAGE_NOTIFICATION_SETTINGS = '/manage/notifications/settings',\n /*\n * A URL for the defect channel (AGL-2486). \"Report an issue\" was reachable\n * only by opening the account menu, so nothing outside the console could\n * send anyone to it — the repo's README pointed at GitHub issues, which we\n * do not use, and every other \"tell us about it\" surface had the same\n * problem. User-level rather than org-scoped because the dialog is: it\n * needs no workspace, no role and no paid plan.\n */\n MANAGE_REPORT_ISSUE = '/manage/report-issue',\n AUTH_SIGN_IN = '/signin',\n AUTH_SIGN_OUT = '/signout',\n AUTH_SIGN_UP = '/signup',\n AUTH_VERIFY_EMAIL = '/verify-email',\n HOST_LIST = '/[orgSlug]/hosts',\n HOST_CONTENT = '/[orgSlug]/hosts/[host]/content',\n // The collection is a PATH SEGMENT, not `?collection=` (AGL-2498).\n //\n // It was a query parameter because the manager began as one page with a\n // Select on it, and the Select was the only thing that knew which\n // collection you were looking at. That is the wrong owner: which collection\n // is open is the page's IDENTITY, not a filter applied to it. As a\n // parameter it could be dropped by any link that rebuilt the query, it\n // sorted below `?tab=` in a URL nobody could read, and — the reason this\n // moved — it left the entry beneath it with nowhere to hang: an entry is\n // addressed `collection + entry`, and half of that address cannot live in\n // the path while the other half lives in the query.\n //\n // The segment is the collection's SLUG, not its document id, and that is a\n // correction rather than a preference. Collection document ids are not\n // uniform — the seeded ones were given readable ids (`blog`, `changelog`)\n // and everything created since gets a `createResourceUid`, so routing by id\n // produced `/content/changelog` beside `/content/QgXv7lU_rG` on ONE site.\n //\n // A slug is the right key on the merits too: it is unique per host and kind\n // (claimed in a transaction by /api/hosts/collections), it is the\n // collection's own public address, and it is the thing an author recognises.\n // A document id is still ACCEPTED and rewritten to the slug form, so no\n // existing link breaks.\n //\n // `?collection=` still resolves the same way (AGL-845's DAM deep links are\n // in the wild).\n HOST_CONTENT_COLLECTION = '/[orgSlug]/hosts/[host]/content/[collectionSlug]',\n // One entry's detail page (AGL-2498). `entries` is a LITERAL segment rather\n // than `content/[collectionId]/[entryId]`, and it is load-bearing twice\n // over: it mirrors the Firestore path the entry actually lives at\n // (`collections/{c}/entries/{e}`), and it keeps the id space clear so a\n // later sibling — a collection's own settings page, say — cannot be\n // shadowed by an entry that happens to be called the same thing.\n //\n // `[entryId]` also carries the sentinel `new`, which is a draft that has no\n // document yet. Entry ids come from `createResourceUid`, so nothing real\n // can collide with it.\n CONTENT_ENTRY_DETAILS = '/[orgSlug]/hosts/[host]/content/[collectionSlug]/entries/[entryId]',\n MANAGE_TEAM = '/[orgSlug]/team',\n /*\n * Team SECTIONS are routes, not tabs (AGL-2501). A section's bundle then\n * arrives when a reader opens it and never before, the section is linkable,\n * and the active state is a fact about the URL rather than state that has to\n * be kept in sync with it. `MANAGE_TEAM` itself redirects to members.\n */\n MANAGE_TEAM_MEMBERS = '/[orgSlug]/team/members',\n MANAGE_TEAM_ROLES = '/[orgSlug]/team/roles',\n MANAGE_TEAM_ACTIVITY = '/[orgSlug]/team/activity',\n MANAGE_TEAM_MEMBER = '/[orgSlug]/team/[uid]',\n // Support is an UMBRELLA, not a page (AGL-1158). The two channels beneath\n // it are separate features that were only ever one screen: they are gated\n // differently (tickets need a first-response commitment, from Pro; the\n // forum is open to every tier including Free) and they fail independently.\n // Sharing one route made them share a blast radius — AGL-1157 was one bad\n // line in the common loader that emptied BOTH lists at once.\n //\n // `MANAGE_SUPPORT` itself stays, and stays the nav tab's href: it now\n // forwards to whichever channel the org's tier makes primary, so a\n // forum-only workspace lands on a whole forum instead of a half-empty page\n // beside a ticket card it may not use.\n MANAGE_SUPPORT = '/[orgSlug]/support',\n MANAGE_SUPPORT_TICKETS = '/[orgSlug]/support/tickets',\n MANAGE_SUPPORT_FORUM = '/[orgSlug]/support/forum',\n /*\n * The ONE support URL that carries no org (AGL-3265), and the second member\n * of the apex category `BILLING_ENTRY` opened.\n *\n * Everything a person is handed when they need help — a receipt, a system\n * email footer, a Stripe customer portal, a self-host runbook — stores ONE\n * link for every customer, with no workspace to interpolate. `/[orgSlug]`\n * cannot be templated into any of them, so the only support address those\n * surfaces could carry was one outside the console entirely.\n *\n * This resolves the workspace from the SESSION instead: one workspace goes\n * straight through, several offer a choice, none says so. It forwards to\n * `MANAGE_SUPPORT`, which is itself an umbrella — so the tier decision about\n * tickets versus the forum stays in the one place that already makes it.\n *\n * `support` is a reserved subdomain (`RESERVED_SUBDOMAINS` in\n * `host-naming.ts`), so no org can hold this slug and the literal segment\n * can never shadow a real `/[orgSlug]`. That is the same guarantee\n * `BILLING_ENTRY` rests on, and it is a precondition rather than a\n * coincidence: an apex route whose segment an org could claim would take a\n * workspace's own address away from it.\n */\n SUPPORT_ENTRY = '/support',\n HOST_DASHBOARD = '/[orgSlug]/hosts/[host]',\n // The catch-all console page a plugin's own nav items resolve against\n // (`app/(app)/[orgSlug]/hosts/[host]/[pluginSlug]/page.tsx`). Plugin slugs\n // are open-ended, so this is the one route whose leaf segment is data.\n HOST_PLUGIN = '/[orgSlug]/hosts/[host]/[pluginSlug]',\n HOST_INBOX = '/[orgSlug]/hosts/[host]/inbox',\n HOST_CONTACTS = '/[orgSlug]/hosts/[host]/contacts',\n HOST_MEDIA = '/[orgSlug]/hosts/[host]/media',\n HOST_SETUP = '/[orgSlug]/hosts/[host]/setup',\n /*\n * Setup SECTIONS are routes (AGL-2501), so an unopened one costs neither a\n * read nor a byte. `HOST_SETUP` stays the nav tab's href and redirects to\n * Basic details, honouring the `?tab=` ids these sections were deep-linked\n * by — unlike the settings and marketplace hubs, links holding those ids are\n * demonstrably in the wild and two are built in this repo.\n */\n HOST_SETUP_DETAILS = '/[orgSlug]/hosts/[host]/setup/details',\n HOST_SETUP_SEO = '/[orgSlug]/hosts/[host]/setup/seo',\n HOST_SETUP_TRACKING = '/[orgSlug]/hosts/[host]/setup/tracking',\n HOST_SETUP_THEME = '/[orgSlug]/hosts/[host]/setup/theme',\n HOST_SETUP_EMAILS = '/[orgSlug]/hosts/[host]/setup/emails',\n // Host Admin area (AGL-1014): owner/admin-only controls — per-site plugin\n // enablement and the Danger zone — out of the Setup page collaborators\n // legitimately visit.\n HOST_ADMIN = '/[orgSlug]/hosts/[host]/admin',\n /*\n * Site-admin SECTIONS are routes (AGL-2501), so an unopened one costs neither\n * a read nor a byte. `HOST_ADMIN` redirects to Plugins and still honours the\n * `?tab=` ids these sections were deep-linked by.\n */\n /*\n * The site as an OBJECT: its name in the console and the subdomain it\n * answers to. First in the rail because an address and a name are what\n * identify the thing every other section governs.\n */\n HOST_ADMIN_GENERAL = '/[orgSlug]/hosts/[host]/admin/general',\n HOST_ADMIN_PLUGINS = '/[orgSlug]/hosts/[host]/admin/plugins',\n /*\n * One plugin, as installed on ONE site (AGL-428, AGL-1014) — the\n * site-scoped twin of `ORG_PLUGIN_INSTALLATION`, reached by clicking a row\n * on the site's plugin\n * list exactly as the workspace list opens its own detail page. The segment\n * is `[pluginRef]` and accepts either identifier for the same reason the\n * workspace route does (AGL-1010): a marketplace install is keyed by listing\n * id, a first-party plugin by its registry id, and an admin does not think\n * of the two as different things.\n */\n HOST_ADMIN_PLUGIN = '/[orgSlug]/hosts/[host]/admin/plugins/[pluginRef]',\n HOST_ADMIN_DOMAIN = '/[orgSlug]/hosts/[host]/admin/domain',\n HOST_ADMIN_SECURITY = '/[orgSlug]/hosts/[host]/admin/security',\n /*\n * The designed screen behind each status code, and the switch that shows the\n * 503 one at every address at once (AGL-3178).\n *\n * Beside Security because both answer what a visitor's browser is handed on\n * a page the site did not design for them, and above Activity because it is\n * a control rather than a record.\n */\n HOST_ADMIN_ERROR_PAGES = '/[orgSlug]/hosts/[host]/admin/error-pages',\n HOST_ADMIN_ACTIVITY = '/[orgSlug]/hosts/[host]/admin/activity',\n /*\n * Backup, restore and publishing the site as a template — moving the whole\n * site somewhere else, or bringing it back. Its own section rather than the\n * Danger zone: restore overwrites, but export and publishing a template do\n * not, and filing all three under a destructive heading mislabels two of\n * them.\n */\n HOST_ADMIN_BACKUP = '/[orgSlug]/hosts/[host]/admin/backup',\n HOST_ADMIN_DANGER = '/[orgSlug]/hosts/[host]/admin/danger',\n HOST_THEME = '/[orgSlug]/hosts/[host]/theme',\n /*\n * The automation section: Workflows, Actions and Webhooks under one parent.\n * The parent is named for what the three have in common, so no tab shares a\n * name with the section that contains it.\n */\n HOST_AUTOMATION = '/[orgSlug]/hosts/[host]/automation',\n /**\n * The address the automation section answered to before it had a name of\n * its own. Nothing links here — `HOST_AUTOMATION` is what the nav, the\n * search results and the activity feed build — but the console redirects it\n * permanently, so a held bookmark still lands on the section (and keeps its\n * `?tab=`, which never changed).\n *\n * Kept on the table rather than as a bare literal in the redirect\n * because the redirect and the address it serves are one fact:\n * `automation-section.spec.ts` reads this entry and fails if the rule for\n * it goes missing.\n */\n HOST_WORKFLOWS = '/[orgSlug]/hosts/[host]/workflows',\n HOST_DATA = '/[orgSlug]/hosts/[host]/data',\n HOST_LOGIC = '/[orgSlug]/hosts/[host]/logic',\n HOST_PRODUCTS = '/[orgSlug]/hosts/[host]/products',\n HOST_COMPONENTS = '/[orgSlug]/hosts/[host]/components',\n // Component detail (AGL-693): the listing links here, and the besigner is\n // reached from here — matching SCREEN_DETAILS rather than jumping a row\n // straight into the editor.\n COMPONENT_DETAILS = '/[orgSlug]/hosts/[host]/components/[componentId]',\n HOST_FORMS = '/[orgSlug]/hosts/[host]/forms',\n // A form is a designable document, so its surface is the component one:\n // a list, a detail page that owns the versions, and the besigner reached\n // from there. Deliberately NOT a row that jumps into the editor — a form's\n // routing and consent field are edited on the detail page, and those decide\n // what the design is then checked against.\n FORM_DETAILS = '/[orgSlug]/hosts/[host]/forms/[formId]',\n HOST_TEMPLATES = '/[orgSlug]/hosts/[host]/templates',\n // Template detail (AGL-694), the counterpart to COMPONENT_DETAILS. Note\n // TEMPLATE_BESIGNER carries no versionId — templates version but never\n // publish, so there is no \"current\" pointer to route through.\n TEMPLATE_DETAILS = '/[orgSlug]/hosts/[host]/templates/[templateId]',\n HOST_MARKETING = '/[orgSlug]/hosts/[host]/marketing',\n HOST_BOOKINGS = '/[orgSlug]/hosts/[host]/bookings',\n // Events now come from the events-calendar plugin, served by the generic\n // `[orgSlug]/hosts/[host]/[pluginSlug]` route (AGL-394).\n HOST_REDIRECTS = '/[orgSlug]/hosts/[host]/redirects',\n HOST_USERS = '/[orgSlug]/hosts/[host]/users',\n HOST_ANALYTICS = '/[orgSlug]/hosts/[host]/analytics',\n COMPONENT_BESIGNER = '/[orgSlug]/hosts/[host]/components/[componentId]/versions/[versionId]/besigner',\n /*\n * A document a plugin keeps under a site and authors in the besigner\n * (`plugin-manager/besigner-documents.ts`): the segment is the kind the\n * plugin declared, the details page is the plugin's own at\n * `<segment>/<docId>`, and the editor and preview are the console's.\n */\n PLUGIN_DOCUMENT_DETAILS = '/[orgSlug]/hosts/[host]/[documentSegment]/[docId]',\n PLUGIN_DOCUMENT_BESIGNER = '/[orgSlug]/hosts/[host]/[documentSegment]/[docId]/versions/[versionId]/besigner',\n TEMPLATE_BESIGNER = '/[orgSlug]/hosts/[host]/templates/[templateId]/besigner',\n LAYOUT_BESIGNER = '/[orgSlug]/hosts/[host]/layouts/[layoutId]/versions/[versionId]/besigner',\n // Draft preview for every besigner document kind (AGL-1203). Screens had\n // the only preview route; components, layouts and templates showed a\n // Preview button that did nothing. Each renders the same localStorage\n // snapshot through the site renderer, so they work on localhost with no\n // deployment. TEMPLATE_PREVIEW carries no versionId, matching\n // TEMPLATE_BESIGNER.\n COMPONENT_PREVIEW = '/[orgSlug]/hosts/[host]/components/[componentId]/versions/[versionId]/preview',\n PLUGIN_DOCUMENT_PREVIEW = '/[orgSlug]/hosts/[host]/[documentSegment]/[docId]/versions/[versionId]/preview',\n TEMPLATE_PREVIEW = '/[orgSlug]/hosts/[host]/templates/[templateId]/preview',\n LAYOUT_PREVIEW = '/[orgSlug]/hosts/[host]/layouts/[layoutId]/versions/[versionId]/preview',\n // The list sits at the bare path, like HOST_COMPONENTS. It used to be\n // `/layouts/list`: the pages router had a `layouts/index.tsx` that\n // redirected to `layouts/list.tsx`, and the App Router migration carried\n // the pair over verbatim. Components, added later, had no such legacy and\n // put its list at the bare path — which is the shape we want everywhere.\n // `/layouts/list` still serves the same page so old links survive — an\n // alias, not a redirect, because the bare path used to answer a cached 308\n // pointing at `/list` and redirecting back would loop. See the comment in\n // `app/(app)/[orgSlug]/hosts/[host]/screens/list/page.tsx`.\n HOST_LAYOUTS = '/[orgSlug]/hosts/[host]/layouts',\n // Layout detail (AGL-695), completing the list → detail → besigner shape\n // across screens, components, templates and layouts.\n LAYOUT_DETAILS = '/[orgSlug]/hosts/[host]/layouts/[layoutId]',\n SCREEN_BESIGNER = '/[orgSlug]/hosts/[host]/screens/[screenId]/versions/[versionId]/besigner',\n // Per-site email besigner (AGL-770): a site owner designs a transactional\n // email their site sends. The host-scoped sibling of the staff system-email\n // editor (ADMIN_EMAIL_BESIGNER).\n HOST_EMAIL_BESIGNER = '/[orgSlug]/hosts/[host]/emails/[templateKey]/versions/[versionId]/besigner',\n SCREEN_DETAILS = '/[orgSlug]/hosts/[host]/screens/[screenId]/versions/[versionId]/view',\n SCREEN_PREVIEW = '/[orgSlug]/hosts/[host]/screens/[screenId]/versions/[versionId]/preview',\n // Bare path, matching HOST_LAYOUTS and HOST_COMPONENTS — see the note on\n // HOST_LAYOUTS. `/screens/list` still serves the same page as an alias.\n HOST_SCREENS = '/[orgSlug]/hosts/[host]/screens',\n}\n\n/**\n * Params required to build each {@link Route}.\n *\n * Deliberately NOT extended from an index signature (AGL-685). It used to be\n * `extends Record<keyof any, any>`, which meant any `Route` missing an entry\n * here silently typed its payload as `any` — `Route.HOST_SETUP` was one of\n * them, and `buildRoute(Route.HOST_SETUP, { hostId })` compiled cleanly while\n * emitting `/<orgSlug?>/hosts/<host?>/setup`. Without the index signature a\n * new `Route` with no entry is a compile error, which is the whole point of\n * the payload being required in the first place.\n */\nexport interface RoutePayload {\n [Route.AUTH_SIGN_UP]: undefined\n [Route.AUTH_SIGN_IN]: undefined\n [Route.AUTH_SIGN_OUT]: undefined\n [Route.AUTH_VERIFY_EMAIL]: undefined\n [Route.SCREEN_BESIGNER]: {\n orgSlug: string\n host: string\n screenId: string\n versionId: string\n }\n [Route.ORG_HOME]: { orgSlug: string }\n [Route.HOST_DASHBOARD]: { orgSlug: string; host: string }\n [Route.ADMIN_ORGS]: undefined\n [Route.ADMIN_ORG_DETAIL]: { orgId: string }\n [Route.ADMIN_ORG_HOST_DETAIL]: { orgId: string; hostId: string }\n [Route.ADMIN_SITES]: undefined\n [Route.ADMIN_SITE_DETAIL]: { hostId: string }\n [Route.ADMIN_SITE_PREVIEW]: { hostId: string; kind: string; docId: string }\n [Route.ADMIN_OVERVIEW]: undefined\n [Route.ADMIN_COUPONS]: undefined\n [Route.ADMIN_LOCKDOWN]: undefined\n [Route.ADMIN_MEDIA_QUARANTINE]: undefined\n [Route.ADMIN_ABUSE_REPORTS]: undefined\n [Route.ADMIN_HEALTH]: undefined\n [Route.ADMIN_MAINTENANCE]: undefined\n [Route.ADMIN_REVENUE]: undefined\n [Route.ADMIN_MARGIN_UTILIZATION]: undefined\n [Route.ADMIN_TAX_RETURN]: undefined\n [Route.ADMIN_AUDIT]: undefined\n [Route.ADMIN_USERS]: undefined\n [Route.ADMIN_USER_DETAIL]: { uid: string }\n [Route.ADMIN_FLAGS]: undefined\n [Route.ADMIN_SETTINGS]: undefined\n [Route.ADMIN_OPERATOR_ALERTS]: undefined\n [Route.ADMIN_STAFF_PAGE]: { staffPage: string }\n [Route.ADMIN_SUPPORT]: undefined\n [Route.ADMIN_CONTACT_SUPPRESSIONS]: undefined\n [Route.ADMIN_EMAILS]: undefined\n [Route.ADMIN_EMAIL_BESIGNER]: { templateKey: string; versionId: string }\n [Route.HOST_EMAIL_BESIGNER]: {\n orgSlug: string\n host: string\n templateKey: string\n versionId: string\n }\n [Route.ORG_MEDIA]: { orgSlug: string }\n [Route.ORG_DATA]: { orgSlug: string }\n [Route.ORG_CONTACTS]: { orgSlug: string }\n [Route.ORG_CRM]: { orgSlug: string }\n [Route.ORG_PLUGIN]: { orgSlug: string; pluginSlug: string }\n [Route.ORG_PLUGINS]: { orgSlug: string }\n [Route.ORG_PLUGIN_INSTALLATION]: { orgSlug: string; pluginRef: string }\n [Route.ORG_MARKETPLACE]: { orgSlug: string }\n [Route.ORG_MARKETPLACE_INSTALLED]: { orgSlug: string }\n [Route.ORG_MARKETPLACE_LISTING]: { orgSlug: string; listingId: string }\n [Route.ORG_MARKETPLACE_PUBLISHER]: { orgSlug: string; handle: string }\n [Route.MANAGE_USER_SETTINGS]: undefined\n [Route.MANAGE_USER_ACCOUNT]: undefined\n [Route.MANAGE_USER_EMAILS]: undefined\n [Route.MANAGE_USER_PROFILE]: undefined\n [Route.MANAGE_USER_BASIC]: undefined\n [Route.MANAGE_USER_SECURITY]: undefined\n [Route.MANAGE_USER_CLOSE]: undefined\n [Route.MANAGE_NOTIFICATIONS]: undefined\n [Route.MANAGE_NOTIFICATION_SETTINGS]: undefined\n [Route.MANAGE_REPORT_ISSUE]: undefined\n [Route.ORG_SETTINGS]: { orgSlug: string }\n [Route.ORG_SETTINGS_GENERAL]: { orgSlug: string }\n [Route.ORG_SETTINGS_PROFILE]: { orgSlug: string }\n [Route.ORG_SETTINGS_PLUGINS]: { orgSlug: string }\n [Route.ORG_SETTINGS_API_KEYS]: { orgSlug: string }\n [Route.ORG_SETTINGS_BRANDING]: { orgSlug: string }\n [Route.ORG_SETTINGS_SSO]: { orgSlug: string }\n [Route.ORG_SETTINGS_OWNERSHIP]: { orgSlug: string }\n [Route.ORG_SETTINGS_PRIVACY]: { orgSlug: string }\n [Route.ORG_SETTINGS_HOLDS]: { orgSlug: string }\n [Route.ORG_SETTINGS_DELETE]: { orgSlug: string }\n [Route.HOST_LIST]: { orgSlug: string }\n [Route.HOST_CONTENT]: { orgSlug: string; host: string }\n [Route.HOST_CONTENT_COLLECTION]: {\n orgSlug: string\n host: string\n /** The collection's slug — see the route. A document id also resolves. */\n collectionSlug: string\n }\n [Route.CONTENT_ENTRY_DETAILS]: {\n orgSlug: string\n host: string\n collectionSlug: string\n /**\n * An entry id, or the literal `new` for a draft with no document yet.\n *\n * An ID here while the collection above is a SLUG, deliberately. An entry\n * slug is unique only WITHIN its collection and it is editable on the\n * detail page — so routing by it would move the console address out from\n * under someone in the middle of typing in the slug field, and a draft has\n * no slug at all to route by. The entry id is also what every activity\n * record and the DAM's \"Used on\" list already carry.\n */\n entryId: string\n }\n [Route.MANAGE_TEAM]: { orgSlug: string }\n [Route.MANAGE_TEAM_MEMBERS]: { orgSlug: string }\n [Route.MANAGE_TEAM_ROLES]: { orgSlug: string }\n [Route.MANAGE_TEAM_ACTIVITY]: { orgSlug: string }\n [Route.MANAGE_TEAM_MEMBER]: { orgSlug: string; uid: string }\n [Route.MANAGE_SUPPORT]: { orgSlug: string }\n [Route.MANAGE_SUPPORT_TICKETS]: { orgSlug: string }\n [Route.MANAGE_SUPPORT_FORUM]: { orgSlug: string }\n [Route.MANAGE_BILLING]: { orgSlug: string }\n [Route.MANAGE_BILLING_USAGE]: { orgSlug: string }\n [Route.MANAGE_BILLING_INVOICES]: { orgSlug: string }\n [Route.MANAGE_BILLING_SETTINGS]: { orgSlug: string }\n [Route.BILLING_ENTRY]: undefined\n [Route.SUPPORT_ENTRY]: undefined\n [Route.HOST_INBOX]: { orgSlug: string; host: string }\n [Route.HOST_CONTACTS]: { orgSlug: string; host: string }\n [Route.HOST_SETUP]: { orgSlug: string; host: string }\n [Route.HOST_SETUP_DETAILS]: { orgSlug: string; host: string }\n [Route.HOST_SETUP_SEO]: { orgSlug: string; host: string }\n [Route.HOST_SETUP_TRACKING]: { orgSlug: string; host: string }\n [Route.HOST_SETUP_THEME]: { orgSlug: string; host: string }\n [Route.HOST_SETUP_EMAILS]: { orgSlug: string; host: string }\n [Route.HOST_ADMIN]: { orgSlug: string; host: string }\n [Route.HOST_ADMIN_GENERAL]: { orgSlug: string; host: string }\n [Route.HOST_ADMIN_PLUGINS]: { orgSlug: string; host: string }\n [Route.HOST_ADMIN_PLUGIN]: { orgSlug: string; host: string; pluginRef: string }\n [Route.HOST_ADMIN_DOMAIN]: { orgSlug: string; host: string }\n [Route.HOST_ADMIN_SECURITY]: { orgSlug: string; host: string }\n [Route.HOST_ADMIN_ERROR_PAGES]: { orgSlug: string; host: string }\n [Route.HOST_ADMIN_ACTIVITY]: { orgSlug: string; host: string }\n [Route.HOST_ADMIN_BACKUP]: { orgSlug: string; host: string }\n [Route.HOST_ADMIN_DANGER]: { orgSlug: string; host: string }\n [Route.HOST_PLUGIN]: { orgSlug: string; host: string; pluginSlug: string }\n [Route.HOST_MEDIA]: { orgSlug: string; host: string }\n [Route.HOST_THEME]: { orgSlug: string; host: string }\n [Route.HOST_AUTOMATION]: { orgSlug: string; host: string }\n [Route.HOST_WORKFLOWS]: { orgSlug: string; host: string }\n [Route.HOST_DATA]: { orgSlug: string; host: string }\n [Route.HOST_LOGIC]: { orgSlug: string; host: string }\n [Route.HOST_PRODUCTS]: { orgSlug: string; host: string }\n [Route.HOST_COMPONENTS]: { orgSlug: string; host: string }\n [Route.COMPONENT_DETAILS]: {\n orgSlug: string\n host: string\n componentId: string\n }\n [Route.HOST_FORMS]: { orgSlug: string; host: string }\n [Route.FORM_DETAILS]: {\n orgSlug: string\n host: string\n formId: string\n }\n [Route.HOST_TEMPLATES]: { orgSlug: string; host: string }\n [Route.TEMPLATE_DETAILS]: {\n orgSlug: string\n host: string\n templateId: string\n }\n [Route.HOST_MARKETING]: { orgSlug: string; host: string }\n [Route.HOST_BOOKINGS]: { orgSlug: string; host: string }\n [Route.HOST_REDIRECTS]: { orgSlug: string; host: string }\n [Route.HOST_USERS]: { orgSlug: string; host: string }\n [Route.HOST_ANALYTICS]: { orgSlug: string; host: string }\n [Route.COMPONENT_BESIGNER]: {\n orgSlug: string\n host: string\n componentId: string\n versionId: string\n }\n [Route.PLUGIN_DOCUMENT_DETAILS]: {\n orgSlug: string\n host: string\n documentSegment: string\n docId: string\n }\n [Route.PLUGIN_DOCUMENT_BESIGNER]: {\n orgSlug: string\n host: string\n documentSegment: string\n docId: string\n versionId: string\n }\n [Route.TEMPLATE_BESIGNER]: {\n orgSlug: string\n host: string\n templateId: string\n }\n [Route.LAYOUT_BESIGNER]: {\n orgSlug: string\n host: string\n layoutId: string\n versionId: string\n }\n [Route.COMPONENT_PREVIEW]: {\n orgSlug: string\n host: string\n componentId: string\n versionId: string\n }\n [Route.PLUGIN_DOCUMENT_PREVIEW]: {\n orgSlug: string\n host: string\n documentSegment: string\n docId: string\n versionId: string\n }\n [Route.TEMPLATE_PREVIEW]: {\n orgSlug: string\n host: string\n templateId: string\n }\n [Route.LAYOUT_PREVIEW]: {\n orgSlug: string\n host: string\n layoutId: string\n versionId: string\n }\n [Route.HOST_LAYOUTS]: { orgSlug: string; host: string }\n [Route.LAYOUT_DETAILS]: { orgSlug: string; host: string; layoutId: string }\n [Route.SCREEN_DETAILS]: {\n orgSlug: string\n host: string\n screenId: string\n versionId: string\n }\n [Route.SCREEN_PREVIEW]: {\n orgSlug: string\n host: string\n screenId: string\n versionId: string\n }\n [Route.HOST_SCREENS]: { orgSlug: string; host: string }\n}\n\nexport const routeReplacePattern = /\\[([^\\]]+)\\]/g\n\n/**\n * Builds a concrete path from a {@link Route} template. The payload is\n * REQUIRED whenever the route declares params (e.g. `orgSlug`, `hostId`) and\n * omitted only for param-less routes — so a forgotten `orgSlug` is a compile\n * error, not a `/<orgSlug?>/…` link that breaks at runtime (AGL-621).\n */\nexport function buildRoute<Tmpl extends Route>(\n template: Tmpl,\n ...[payload]: RoutePayload[Tmpl] extends undefined\n ? [payload?: undefined]\n : [payload: RoutePayload[Tmpl]]\n) {\n return template.replace(routeReplacePattern, (match, key) => {\n const value = (payload as Record<string, unknown> | undefined)?.[key]\n return value != null ? String(value) : `<${key}?>`\n })\n}\n"],"names":["Route","routeReplacePattern","buildRoute","template","payload","replace","match","key","value","String"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/console-routes.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2022 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\n// URL scheme (AGL-621): the org is a first-class path segment `[orgSlug]`\n// so the URL — not client-side precedence — is the source of truth for the\n// active workspace. Host routes nest under `/[orgSlug]/hosts/[host]`; the\n// org area (settings, team, billing, media, data, plugins, support,\n// marketplace) lives directly under `/[orgSlug]`. User-level `manage/*`,\n// staff `admin/*`, and `auth` routes are NOT org-scoped. Hosts stay keyed\n// by doc id here; AGL-622 swaps `[hostId]` for the subdomain slug.\nexport enum Route {\n ADMIN_ORGS = '/admin/orgs',\n ADMIN_ORG_DETAIL = '/admin/orgs/[orgId]',\n ADMIN_ORG_HOST_DETAIL = '/admin/orgs/[orgId]/host/[hostId]',\n // Every site on the platform, across organizations, and one site's staff\n // detail (AGL-3378). The org-nested host route above redirects here.\n ADMIN_SITES = '/admin/sites',\n ADMIN_SITE_DETAIL = '/admin/sites/[hostId]',\n // A site's screen, layout, component, template or form as its draft renders,\n // for staff who are not members of the site (AGL-3378). `kind` is a\n // `PreviewKind`; the version, when not the current one, rides `?version=`.\n ADMIN_SITE_PREVIEW = '/admin/sites/[hostId]/preview/[kind]/[docId]',\n ADMIN_OVERVIEW = '/admin/overview',\n // Staff coupon management (AGL-1105): create discount coupons + promotion\n // codes and read the live net-margin rating before committing to one.\n ADMIN_COUPONS = '/admin/coupons',\n // The panic button (AGL-1501): platform/org/host/user lockdown controls.\n ADMIN_LOCKDOWN = '/admin/lockdown',\n // The narrow lever beside it (AGL-1687): disable or release ONE uploaded\n // file. Its own page rather than a panel on Lockdown because the input is\n // an asset, not a scope — the two forms share no field but the reason.\n ADMIN_MEDIA_QUARANTINE = '/admin/media-quarantine',\n // The INPUT to the two levers above (AGL-1964). Lockdown and quarantine are\n // both things an operator does after learning there is a problem; until\n // this page there was no way for anyone outside the company to say there\n // was one. Reports arrive from the unauthenticated form the tenant runtime\n // serves at /api/report-abuse on every origin.\n ADMIN_ABUSE_REPORTS = '/admin/abuse-reports',\n // The same input, one collection over (AGL-2310). The marketplace's own\n // report button wrote `marketplaceReports` from the day it shipped and\n // nothing read it — every report filed, acknowledged, and unreachable. This\n // is that queue, deliberately shaped like the abuse one rather than as a\n // second vocabulary for the same act.\n // The probes, on a screen (AGL-1900). /api/health/{backups,rate-limits,\n // signups} and /api/admin/email-health each answered a bad-day question to\n // a curl and to nothing else; this is where an operator reads them.\n ADMIN_HEALTH = '/admin/health',\n // The maintenance jobs, on a screen (AGL-1949). audit-archive,\n // reap-plugin-artifacts and reverify-plugin-versions accepted the shared\n // cron secret and nothing else, so the only way to preview or run one was a\n // shell holding the production secret. Separate from Health because Health\n // reads and this acts.\n ADMIN_MAINTENANCE = '/admin/maintenance',\n // The Texas sales tax return (AGL-1900). The figures AGL-1811 computes had\n // only a curl to reach them; filing happens on a fixed quarterly calendar\n // from 2026-09-01, so the return needs a URL a person can bookmark.\n ADMIN_REVENUE = '/admin/revenue',\n // Realised band utilization and the margin that follows from it. Beside\n // Revenue because the two are the halves of one question: that page reports\n // what came in, this one what serving it cost.\n ADMIN_MARGIN_UTILIZATION = '/admin/margin-utilization',\n ADMIN_TAX_RETURN = '/admin/tax-return',\n ADMIN_AUDIT = '/admin/audit',\n ADMIN_USERS = '/admin/users',\n ADMIN_USER_DETAIL = '/admin/users/[uid]',\n ADMIN_FLAGS = '/admin/flags',\n // Platform-wide settings that are not a release flag (AGL-2486). The\n // free-workspace ceiling was the first of them and it opened the\n // Organizations LIST, above the table — a global lever wedged into the\n // screen for browsing individual orgs. Settings that describe the platform\n // rather than one org belong on their own tab, beside the flags.\n ADMIN_SETTINGS = '/admin/settings',\n // Which operator alerts email the operator, immediately or in the daily\n // digest, and whether the channels reach anybody (AGL-3377).\n ADMIN_OPERATOR_ALERTS = '/admin/operator-alerts',\n // One submission or listed plugin in full (AGL-959): the queue index is\n // for scanning, this is where a reviewer reads the manifest, weighs the\n // verifier findings and acts.\n // A page a plugin adds to the staff area (AGL-2939), rendered by the\n // console's generic staff route. The console's own staff routes win the\n // segments they use.\n ADMIN_STAFF_PAGE = '/admin/[staffPage]',\n // Staff support-ticket queue (AGL-849): the operator side of the\n // subscriber `MANAGE_SUPPORT_TICKETS` page — every org's tickets in one place.\n ADMIN_SUPPORT = '/admin/support',\n // Do-not-contact list (AGL-1592): the intake and the queue for Privacy\n // Policy v4 §11's marketing call/text opt-out, and for \"delete the phone\n // number you hold for me\".\n ADMIN_CONTACT_SUPPRESSIONS = '/admin/contact-suppressions',\n ADMIN_EMAILS = '/admin/emails',\n // The one besigner route with no host and no org in it (AGL-749). A system\n // email belongs to the platform, not to a workspace, so it is staff-scoped\n // like the rest of `/admin/*` rather than nested under `[orgSlug]`.\n ADMIN_EMAIL_BESIGNER = '/admin/emails/[templateKey]/versions/[versionId]/besigner',\n ORG_HOME = '/[orgSlug]',\n ORG_MEDIA = '/[orgSlug]/media',\n ORG_DATA = '/[orgSlug]/data',\n /*\n * The organization-level CRM hub (AGL-2630): the same sections the site hub\n * has — `/[orgSlug]/crm/<section>[/<recordId>]` — mounted with an ORG scope,\n * for an org-wide member who works every site's people at once. A bare\n * `/crm` lands on the first section the reader may open, as the site hub's\n * does. What this route adds over the site hub is the cross-site fact the\n * old address book carried — which sites know a person — and what it takes\n * from it is the site every create must name, since a record is always\n * captured BY a site. Served by `ORG_PLUGIN` below, from the CRM's own\n * `orgNavItems`; the constant is the address its links are built from.\n */\n ORG_CRM = '/[orgSlug]/crm',\n /*\n * The generic ORGANIZATION-level plugin route (AGL-2974): every surface an\n * extension declares in `orgNavItems` is served here, at\n * `/[orgSlug]/<href>[/<section>…]`, by\n * `app/(app)/[orgSlug]/[...pluginSlug]/page.tsx`. The org twin of\n * `HOST_PLUGIN`, and like it the one org route whose leaf segment is data:\n * every named org route in this enum wins over it.\n */\n ORG_PLUGIN = '/[orgSlug]/[pluginSlug]',\n ORG_PLUGINS = '/[orgSlug]/plugins',\n // One plugin, as installed in this workspace (AGL-1007): scope, settings\n // and permissions in one place. The segment takes EITHER identifier\n // (AGL-1010) — a listing id for a marketplace install, a registry id for a\n // first-party plugin — hence `pluginRef` rather than `listingId`.\n ORG_PLUGIN_INSTALLATION = '/[orgSlug]/plugins/[pluginRef]',\n // Org-scope marketplace (AGL-772): the single place to browse, view and\n // install marketplace items. The per-site tab it replaced, and the retired\n // `/[orgSlug]/marketplace` seller area, were removed with the rest of the\n // `marketplace` naming in AGL-975 — the word is being freed for a public\n // forum, and a redirect stub sitting on `/marketplace` would have taken the\n // one path that feature wants.\n /*\n * The marketplace's own SECTIONS are not here any more (AGL-3080). Eight\n * of them — browse, licences, upload, the four seller cards and the\n * publish form — were hand-written console routes, and became an\n * `orgNavItems` declaration whose `sections` name those segments once, in\n * the plugin. Restating them here was a second copy of a list the plugin\n * owns, and every entry had zero callers the moment the routes went.\n *\n * What stays is what the CONSOLE still builds: the surface itself, a\n * listing, a publisher, and the installed tab that the org plugins card\n * links across to. A console page linking into a plugin's surface is the\n * ordinary case; a console page enumerating that surface's sections is\n * not.\n */\n ORG_MARKETPLACE = '/[orgSlug]/marketplace',\n /*\n * Marketplace SECTIONS are routes (AGL-2501), for the reason the settings\n * and account sections are: a panel that is not open should cost nothing,\n * and the seller half of this hub reads the organization's REVENUE.\n *\n * The four seller sections are the reason the layout gates rather than the\n * rail. As tabs they were simply not rendered for a member without\n * `publishToMarketplace`; as routes each one is reachable by typing its URL,\n * so the refusal has to sit above them all rather than in the list that\n * draws them.\n *\n * Every segment here is shorter than the ten characters `createResourceUid`\n * emits, so none of them can ever shadow a real `[listingId]` sitting at the\n * same level. `upload` rather than `publish` because `publish/plugin` is\n * already a route beneath this one, and two directories contributing the\n * same segment is a tree Next cannot resolve a layout for.\n */\n ORG_MARKETPLACE_INSTALLED = '/[orgSlug]/marketplace/installed',\n ORG_MARKETPLACE_LISTING = '/[orgSlug]/marketplace/[listingId]',\n ORG_MARKETPLACE_PUBLISHER = '/[orgSlug]/marketplace/publisher/[handle]',\n // Publishing a plugin is a page, not a modal (AGL-1078): the most\n // consequential thing a publisher does now has a URL, so it can be linked,\n // reloaded, and reached from a listing to ship an update.\n ORG_SETTINGS = '/[orgSlug]/settings',\n /*\n * Settings SECTIONS are routes (AGL-2501). `HubTabs` mounted every panel —\n * `keepMounted`, with `lazy` off by default and passed by nobody — so\n * opening General also mounted the API-keys, SSO and data-export cards and\n * ran every read in them. Routes mount one page and code-split per route.\n * `ORG_SETTINGS` itself redirects to General.\n */\n ORG_SETTINGS_GENERAL = '/[orgSlug]/settings/general',\n ORG_SETTINGS_PROFILE = '/[orgSlug]/settings/profile',\n ORG_SETTINGS_PLUGINS = '/[orgSlug]/settings/plugins',\n ORG_SETTINGS_API_KEYS = '/[orgSlug]/settings/api-keys',\n ORG_SETTINGS_BRANDING = '/[orgSlug]/settings/branding',\n ORG_SETTINGS_SSO = '/[orgSlug]/settings/sso',\n ORG_SETTINGS_OWNERSHIP = '/[orgSlug]/settings/ownership',\n /**\n * Export the people a workspace holds and file a person's erasure\n * (AGL-2839) — outside the CRM, so it is there on every plan.\n */\n ORG_SETTINGS_PRIVACY = '/[orgSlug]/settings/privacy',\n // Holds & reviews (AGL-3368): everything on the workspace that was held,\n // flagged, locked or paused, its status, and the way to request a review.\n // Where every risk notice's \"Request a review\" lands (`?notice=<id>`).\n ORG_SETTINGS_HOLDS = '/[orgSlug]/settings/holds',\n ORG_SETTINGS_DELETE = '/[orgSlug]/settings/delete',\n /**\n * Billing, section by section (AGL-2501).\n *\n * `MANAGE_BILLING` is unchanged and is the PLAN section — it resolves to a\n * page inside a route group, which adds no path segment. That is what makes\n * this split free: every link in the console, every hash anchor\n * (`#addons`, `#collaborator-seats`) and Stripe's own dunning mail all point\n * here already and none of them moves. No redirect, and no `?tab=` shim to\n * carry forever.\n */\n MANAGE_BILLING = '/[orgSlug]/billing',\n MANAGE_BILLING_USAGE = '/[orgSlug]/billing/usage',\n MANAGE_BILLING_INVOICES = '/[orgSlug]/billing/invoices',\n MANAGE_BILLING_SETTINGS = '/[orgSlug]/billing/settings',\n // The ONE billing URL that carries no org (AGL-2430).\n //\n // Everything else on this table is org-scoped, which is correct for a\n // console and wrong for the only caller that cannot template a URL:\n // Stripe's dunning emails. \"Payment method updates\" takes a single static\n // link for every customer in the account — there is no `{{org}}` to\n // interpolate — so a table with no org-agnostic billing entry means those\n // emails can only ever point at a page that is not billing. Today all four\n // of them point at the marketing homepage, which is how a failed card\n // becomes a cancelled subscription with no reachable way to fix it.\n //\n // This resolves the org from the SESSION instead of from the URL: one\n // workspace goes straight through, several offer a choice, none says so.\n // `billing` is a reserved subdomain (`RESERVED_SUBDOMAINS`), so no org can\n // hold this slug and the literal segment can never shadow a real\n // `/[orgSlug]`.\n BILLING_ENTRY = '/billing',\n MANAGE_USER_SETTINGS = '/manage/user',\n /*\n * Account SECTIONS are routes (AGL-2501). `HubTabs` mounted every panel —\n * `keepMounted`, with `lazy` off by default and passed by nobody — so\n * opening Account also mounted the email-addresses, passkeys, recent\n * sign-ins, data-export and close-account cards and ran every read in them.\n * Routes mount one page and code-split per route.\n *\n * The segments are the ids the panels carried, because a transactional\n * email already in people's inboxes links `/manage/user?tab=security`:\n * `MANAGE_USER_SETTINGS` forwards each of those ids to the section that\n * holds it, and the segment matching the id is what keeps that map a\n * one-liner rather than a second naming scheme.\n */\n MANAGE_USER_ACCOUNT = '/manage/user/account',\n MANAGE_USER_EMAILS = '/manage/user/emails',\n MANAGE_USER_PROFILE = '/manage/user/profile',\n MANAGE_USER_BASIC = '/manage/user/basic',\n MANAGE_USER_SECURITY = '/manage/user/security',\n MANAGE_USER_CLOSE = '/manage/user/close',\n MANAGE_NOTIFICATIONS = '/manage/notifications',\n /*\n * The preferences, off the feed (AGL-3226).\n *\n * They lived in the header of the feed itself, which was already the\n * tallest thing on that page before per-channel and per-scope settings\n * multiplied them — and it put an edit form above the list it edits, which\n * the console does nowhere else.\n *\n * A CHILD of the feed rather than a sibling under `/manage`, because the\n * relationship is exactly that: it configures what the page above it shows.\n * That also keeps `/manage/notifications` the address in the transactional\n * mail already in people's inboxes, which keeps working and now carries a\n * button to here.\n */\n MANAGE_NOTIFICATION_SETTINGS = '/manage/notifications/settings',\n /*\n * A URL for the defect channel (AGL-2486). \"Report an issue\" was reachable\n * only by opening the account menu, so nothing outside the console could\n * send anyone to it — the repo's README pointed at GitHub issues, which we\n * do not use, and every other \"tell us about it\" surface had the same\n * problem. User-level rather than org-scoped because the dialog is: it\n * needs no workspace, no role and no paid plan.\n */\n MANAGE_REPORT_ISSUE = '/manage/report-issue',\n AUTH_SIGN_IN = '/signin',\n AUTH_SIGN_OUT = '/signout',\n AUTH_SIGN_UP = '/signup',\n AUTH_VERIFY_EMAIL = '/verify-email',\n HOST_LIST = '/[orgSlug]/hosts',\n HOST_CONTENT = '/[orgSlug]/hosts/[host]/content',\n // The collection is a PATH SEGMENT, not `?collection=` (AGL-2498).\n //\n // It was a query parameter because the manager began as one page with a\n // Select on it, and the Select was the only thing that knew which\n // collection you were looking at. That is the wrong owner: which collection\n // is open is the page's IDENTITY, not a filter applied to it. As a\n // parameter it could be dropped by any link that rebuilt the query, it\n // sorted below `?tab=` in a URL nobody could read, and — the reason this\n // moved — it left the entry beneath it with nowhere to hang: an entry is\n // addressed `collection + entry`, and half of that address cannot live in\n // the path while the other half lives in the query.\n //\n // The segment is the collection's SLUG, not its document id, and that is a\n // correction rather than a preference. Collection document ids are not\n // uniform — the seeded ones were given readable ids (`blog`, `changelog`)\n // and everything created since gets a `createResourceUid`, so routing by id\n // produced `/content/changelog` beside `/content/QgXv7lU_rG` on ONE site.\n //\n // A slug is the right key on the merits too: it is unique per host and kind\n // (claimed in a transaction by /api/hosts/collections), it is the\n // collection's own public address, and it is the thing an author recognises.\n // A document id is still ACCEPTED and rewritten to the slug form, so no\n // existing link breaks.\n //\n // `?collection=` still resolves the same way (AGL-845's DAM deep links are\n // in the wild).\n HOST_CONTENT_COLLECTION = '/[orgSlug]/hosts/[host]/content/[collectionSlug]',\n // One entry's detail page (AGL-2498). `entries` is a LITERAL segment rather\n // than `content/[collectionId]/[entryId]`, and it is load-bearing twice\n // over: it mirrors the Firestore path the entry actually lives at\n // (`collections/{c}/entries/{e}`), and it keeps the id space clear so a\n // later sibling — a collection's own settings page, say — cannot be\n // shadowed by an entry that happens to be called the same thing.\n //\n // `[entryId]` also carries the sentinel `new`, which is a draft that has no\n // document yet. Entry ids come from `createResourceUid`, so nothing real\n // can collide with it.\n CONTENT_ENTRY_DETAILS = '/[orgSlug]/hosts/[host]/content/[collectionSlug]/entries/[entryId]',\n MANAGE_TEAM = '/[orgSlug]/team',\n /*\n * Team SECTIONS are routes, not tabs (AGL-2501). A section's bundle then\n * arrives when a reader opens it and never before, the section is linkable,\n * and the active state is a fact about the URL rather than state that has to\n * be kept in sync with it. `MANAGE_TEAM` itself redirects to members.\n */\n MANAGE_TEAM_MEMBERS = '/[orgSlug]/team/members',\n MANAGE_TEAM_ROLES = '/[orgSlug]/team/roles',\n MANAGE_TEAM_ACTIVITY = '/[orgSlug]/team/activity',\n MANAGE_TEAM_MEMBER = '/[orgSlug]/team/[uid]',\n // Support is an UMBRELLA, not a page (AGL-1158). The two channels beneath\n // it are separate features that were only ever one screen: they are gated\n // differently (tickets need a first-response commitment, from Pro; the\n // forum is open to every tier including Free) and they fail independently.\n // Sharing one route made them share a blast radius — AGL-1157 was one bad\n // line in the common loader that emptied BOTH lists at once.\n //\n // `MANAGE_SUPPORT` itself stays, and stays the nav tab's href: it now\n // forwards to whichever channel the org's tier makes primary, so a\n // forum-only workspace lands on a whole forum instead of a half-empty page\n // beside a ticket card it may not use.\n MANAGE_SUPPORT = '/[orgSlug]/support',\n MANAGE_SUPPORT_TICKETS = '/[orgSlug]/support/tickets',\n MANAGE_SUPPORT_FORUM = '/[orgSlug]/support/forum',\n /*\n * The ONE support URL that carries no org (AGL-3265), and the second member\n * of the apex category `BILLING_ENTRY` opened.\n *\n * Everything a person is handed when they need help — a receipt, a system\n * email footer, a Stripe customer portal, a self-host runbook — stores ONE\n * link for every customer, with no workspace to interpolate. `/[orgSlug]`\n * cannot be templated into any of them, so the only support address those\n * surfaces could carry was one outside the console entirely.\n *\n * This resolves the workspace from the SESSION instead: one workspace goes\n * straight through, several offer a choice, none says so. It forwards to\n * `MANAGE_SUPPORT`, which is itself an umbrella — so the tier decision about\n * tickets versus the forum stays in the one place that already makes it.\n *\n * `support` is a reserved subdomain (`RESERVED_SUBDOMAINS` in\n * `host-naming.ts`), so no org can hold this slug and the literal segment\n * can never shadow a real `/[orgSlug]`. That is the same guarantee\n * `BILLING_ENTRY` rests on, and it is a precondition rather than a\n * coincidence: an apex route whose segment an org could claim would take a\n * workspace's own address away from it.\n */\n SUPPORT_ENTRY = '/support',\n HOST_DASHBOARD = '/[orgSlug]/hosts/[host]',\n // The catch-all console page a plugin's own nav items resolve against\n // (`app/(app)/[orgSlug]/hosts/[host]/[pluginSlug]/page.tsx`). Plugin slugs\n // are open-ended, so this is the one route whose leaf segment is data.\n HOST_PLUGIN = '/[orgSlug]/hosts/[host]/[pluginSlug]',\n HOST_INBOX = '/[orgSlug]/hosts/[host]/inbox',\n HOST_MEDIA = '/[orgSlug]/hosts/[host]/media',\n HOST_SETUP = '/[orgSlug]/hosts/[host]/setup',\n /*\n * Setup SECTIONS are routes (AGL-2501), so an unopened one costs neither a\n * read nor a byte. `HOST_SETUP` stays the nav tab's href and redirects to\n * Basic details, honouring the `?tab=` ids these sections were deep-linked\n * by — unlike the settings and marketplace hubs, links holding those ids are\n * demonstrably in the wild and two are built in this repo.\n */\n HOST_SETUP_DETAILS = '/[orgSlug]/hosts/[host]/setup/details',\n HOST_SETUP_SEO = '/[orgSlug]/hosts/[host]/setup/seo',\n HOST_SETUP_TRACKING = '/[orgSlug]/hosts/[host]/setup/tracking',\n HOST_SETUP_THEME = '/[orgSlug]/hosts/[host]/setup/theme',\n HOST_SETUP_EMAILS = '/[orgSlug]/hosts/[host]/setup/emails',\n // Host Admin area (AGL-1014): owner/admin-only controls — per-site plugin\n // enablement and the Danger zone — out of the Setup page collaborators\n // legitimately visit.\n HOST_ADMIN = '/[orgSlug]/hosts/[host]/admin',\n /*\n * Site-admin SECTIONS are routes (AGL-2501), so an unopened one costs neither\n * a read nor a byte. `HOST_ADMIN` redirects to Plugins and still honours the\n * `?tab=` ids these sections were deep-linked by.\n */\n /*\n * The site as an OBJECT: its name in the console and the subdomain it\n * answers to. First in the rail because an address and a name are what\n * identify the thing every other section governs.\n */\n HOST_ADMIN_GENERAL = '/[orgSlug]/hosts/[host]/admin/general',\n HOST_ADMIN_PLUGINS = '/[orgSlug]/hosts/[host]/admin/plugins',\n /*\n * One plugin, as installed on ONE site (AGL-428, AGL-1014) — the\n * site-scoped twin of `ORG_PLUGIN_INSTALLATION`, reached by clicking a row\n * on the site's plugin\n * list exactly as the workspace list opens its own detail page. The segment\n * is `[pluginRef]` and accepts either identifier for the same reason the\n * workspace route does (AGL-1010): a marketplace install is keyed by listing\n * id, a first-party plugin by its registry id, and an admin does not think\n * of the two as different things.\n */\n HOST_ADMIN_PLUGIN = '/[orgSlug]/hosts/[host]/admin/plugins/[pluginRef]',\n HOST_ADMIN_DOMAIN = '/[orgSlug]/hosts/[host]/admin/domain',\n HOST_ADMIN_SECURITY = '/[orgSlug]/hosts/[host]/admin/security',\n /*\n * The designed screen behind each status code, and the switch that shows the\n * 503 one at every address at once (AGL-3178).\n *\n * Beside Security because both answer what a visitor's browser is handed on\n * a page the site did not design for them, and above Activity because it is\n * a control rather than a record.\n */\n HOST_ADMIN_ERROR_PAGES = '/[orgSlug]/hosts/[host]/admin/error-pages',\n HOST_ADMIN_ACTIVITY = '/[orgSlug]/hosts/[host]/admin/activity',\n /*\n * Backup, restore and publishing the site as a template — moving the whole\n * site somewhere else, or bringing it back. Its own section rather than the\n * Danger zone: restore overwrites, but export and publishing a template do\n * not, and filing all three under a destructive heading mislabels two of\n * them.\n */\n HOST_ADMIN_BACKUP = '/[orgSlug]/hosts/[host]/admin/backup',\n HOST_ADMIN_DANGER = '/[orgSlug]/hosts/[host]/admin/danger',\n HOST_THEME = '/[orgSlug]/hosts/[host]/theme',\n /*\n * The automation section: Workflows, Actions and Webhooks under one parent.\n * The parent is named for what the three have in common, so no tab shares a\n * name with the section that contains it.\n */\n HOST_AUTOMATION = '/[orgSlug]/hosts/[host]/automation',\n HOST_DATA = '/[orgSlug]/hosts/[host]/data',\n HOST_LOGIC = '/[orgSlug]/hosts/[host]/logic',\n HOST_COMPONENTS = '/[orgSlug]/hosts/[host]/components',\n // Component detail (AGL-693): the listing links here, and the besigner is\n // reached from here — matching SCREEN_DETAILS rather than jumping a row\n // straight into the editor.\n COMPONENT_DETAILS = '/[orgSlug]/hosts/[host]/components/[componentId]',\n HOST_FORMS = '/[orgSlug]/hosts/[host]/forms',\n // A form is a designable document, so its surface is the component one:\n // a list, a detail page that owns the versions, and the besigner reached\n // from there. Deliberately NOT a row that jumps into the editor — a form's\n // routing and consent field are edited on the detail page, and those decide\n // what the design is then checked against.\n FORM_DETAILS = '/[orgSlug]/hosts/[host]/forms/[formId]',\n HOST_TEMPLATES = '/[orgSlug]/hosts/[host]/templates',\n // Template detail (AGL-694), the counterpart to COMPONENT_DETAILS. Note\n // TEMPLATE_BESIGNER carries no versionId — templates version but never\n // publish, so there is no \"current\" pointer to route through.\n TEMPLATE_DETAILS = '/[orgSlug]/hosts/[host]/templates/[templateId]',\n HOST_MARKETING = '/[orgSlug]/hosts/[host]/marketing',\n // Events now come from the events-calendar plugin, served by the generic\n // `[orgSlug]/hosts/[host]/[pluginSlug]` route (AGL-394).\n HOST_REDIRECTS = '/[orgSlug]/hosts/[host]/redirects',\n HOST_USERS = '/[orgSlug]/hosts/[host]/users',\n HOST_ANALYTICS = '/[orgSlug]/hosts/[host]/analytics',\n COMPONENT_BESIGNER = '/[orgSlug]/hosts/[host]/components/[componentId]/versions/[versionId]/besigner',\n /*\n * A document a plugin keeps under a site and authors in the besigner\n * (`plugin-manager/besigner-documents.ts`): the segment is the kind the\n * plugin declared, the details page is the plugin's own at\n * `<segment>/<docId>`, and the editor and preview are the console's.\n */\n PLUGIN_DOCUMENT_DETAILS = '/[orgSlug]/hosts/[host]/[documentSegment]/[docId]',\n PLUGIN_DOCUMENT_BESIGNER = '/[orgSlug]/hosts/[host]/[documentSegment]/[docId]/versions/[versionId]/besigner',\n TEMPLATE_BESIGNER = '/[orgSlug]/hosts/[host]/templates/[templateId]/besigner',\n LAYOUT_BESIGNER = '/[orgSlug]/hosts/[host]/layouts/[layoutId]/versions/[versionId]/besigner',\n // Draft preview for every besigner document kind (AGL-1203). Screens had\n // the only preview route; components, layouts and templates showed a\n // Preview button that did nothing. Each renders the same localStorage\n // snapshot through the site renderer, so they work on localhost with no\n // deployment. TEMPLATE_PREVIEW carries no versionId, matching\n // TEMPLATE_BESIGNER.\n COMPONENT_PREVIEW = '/[orgSlug]/hosts/[host]/components/[componentId]/versions/[versionId]/preview',\n PLUGIN_DOCUMENT_PREVIEW = '/[orgSlug]/hosts/[host]/[documentSegment]/[docId]/versions/[versionId]/preview',\n TEMPLATE_PREVIEW = '/[orgSlug]/hosts/[host]/templates/[templateId]/preview',\n LAYOUT_PREVIEW = '/[orgSlug]/hosts/[host]/layouts/[layoutId]/versions/[versionId]/preview',\n // The list sits at the bare path, like HOST_COMPONENTS. It used to be\n // `/layouts/list`: the pages router had a `layouts/index.tsx` that\n // redirected to `layouts/list.tsx`, and the App Router migration carried\n // the pair over verbatim. Components, added later, had no such legacy and\n // put its list at the bare path — which is the shape we want everywhere.\n // `/layouts/list` still serves the same page so old links survive — an\n // alias, not a redirect, because the bare path used to answer a cached 308\n // pointing at `/list` and redirecting back would loop. See the comment in\n // `app/(app)/[orgSlug]/hosts/[host]/screens/list/page.tsx`.\n HOST_LAYOUTS = '/[orgSlug]/hosts/[host]/layouts',\n // Layout detail (AGL-695), completing the list → detail → besigner shape\n // across screens, components, templates and layouts.\n LAYOUT_DETAILS = '/[orgSlug]/hosts/[host]/layouts/[layoutId]',\n SCREEN_BESIGNER = '/[orgSlug]/hosts/[host]/screens/[screenId]/versions/[versionId]/besigner',\n // Per-site email besigner (AGL-770): a site owner designs a transactional\n // email their site sends. The host-scoped sibling of the staff system-email\n // editor (ADMIN_EMAIL_BESIGNER).\n HOST_EMAIL_BESIGNER = '/[orgSlug]/hosts/[host]/emails/[templateKey]/versions/[versionId]/besigner',\n SCREEN_DETAILS = '/[orgSlug]/hosts/[host]/screens/[screenId]/versions/[versionId]/view',\n SCREEN_PREVIEW = '/[orgSlug]/hosts/[host]/screens/[screenId]/versions/[versionId]/preview',\n // Bare path, matching HOST_LAYOUTS and HOST_COMPONENTS — see the note on\n // HOST_LAYOUTS. `/screens/list` still serves the same page as an alias.\n HOST_SCREENS = '/[orgSlug]/hosts/[host]/screens',\n}\n\n/**\n * Params required to build each {@link Route}.\n *\n * Deliberately NOT extended from an index signature (AGL-685). It used to be\n * `extends Record<keyof any, any>`, which meant any `Route` missing an entry\n * here silently typed its payload as `any` — `Route.HOST_SETUP` was one of\n * them, and `buildRoute(Route.HOST_SETUP, { hostId })` compiled cleanly while\n * emitting `/<orgSlug?>/hosts/<host?>/setup`. Without the index signature a\n * new `Route` with no entry is a compile error, which is the whole point of\n * the payload being required in the first place.\n */\nexport interface RoutePayload {\n [Route.AUTH_SIGN_UP]: undefined\n [Route.AUTH_SIGN_IN]: undefined\n [Route.AUTH_SIGN_OUT]: undefined\n [Route.AUTH_VERIFY_EMAIL]: undefined\n [Route.SCREEN_BESIGNER]: {\n orgSlug: string\n host: string\n screenId: string\n versionId: string\n }\n [Route.ORG_HOME]: { orgSlug: string }\n [Route.HOST_DASHBOARD]: { orgSlug: string; host: string }\n [Route.ADMIN_ORGS]: undefined\n [Route.ADMIN_ORG_DETAIL]: { orgId: string }\n [Route.ADMIN_ORG_HOST_DETAIL]: { orgId: string; hostId: string }\n [Route.ADMIN_SITES]: undefined\n [Route.ADMIN_SITE_DETAIL]: { hostId: string }\n [Route.ADMIN_SITE_PREVIEW]: { hostId: string; kind: string; docId: string }\n [Route.ADMIN_OVERVIEW]: undefined\n [Route.ADMIN_COUPONS]: undefined\n [Route.ADMIN_LOCKDOWN]: undefined\n [Route.ADMIN_MEDIA_QUARANTINE]: undefined\n [Route.ADMIN_ABUSE_REPORTS]: undefined\n [Route.ADMIN_HEALTH]: undefined\n [Route.ADMIN_MAINTENANCE]: undefined\n [Route.ADMIN_REVENUE]: undefined\n [Route.ADMIN_MARGIN_UTILIZATION]: undefined\n [Route.ADMIN_TAX_RETURN]: undefined\n [Route.ADMIN_AUDIT]: undefined\n [Route.ADMIN_USERS]: undefined\n [Route.ADMIN_USER_DETAIL]: { uid: string }\n [Route.ADMIN_FLAGS]: undefined\n [Route.ADMIN_SETTINGS]: undefined\n [Route.ADMIN_OPERATOR_ALERTS]: undefined\n [Route.ADMIN_STAFF_PAGE]: { staffPage: string }\n [Route.ADMIN_SUPPORT]: undefined\n [Route.ADMIN_CONTACT_SUPPRESSIONS]: undefined\n [Route.ADMIN_EMAILS]: undefined\n [Route.ADMIN_EMAIL_BESIGNER]: { templateKey: string; versionId: string }\n [Route.HOST_EMAIL_BESIGNER]: {\n orgSlug: string\n host: string\n templateKey: string\n versionId: string\n }\n [Route.ORG_MEDIA]: { orgSlug: string }\n [Route.ORG_DATA]: { orgSlug: string }\n [Route.ORG_CRM]: { orgSlug: string }\n [Route.ORG_PLUGIN]: { orgSlug: string; pluginSlug: string }\n [Route.ORG_PLUGINS]: { orgSlug: string }\n [Route.ORG_PLUGIN_INSTALLATION]: { orgSlug: string; pluginRef: string }\n [Route.ORG_MARKETPLACE]: { orgSlug: string }\n [Route.ORG_MARKETPLACE_INSTALLED]: { orgSlug: string }\n [Route.ORG_MARKETPLACE_LISTING]: { orgSlug: string; listingId: string }\n [Route.ORG_MARKETPLACE_PUBLISHER]: { orgSlug: string; handle: string }\n [Route.MANAGE_USER_SETTINGS]: undefined\n [Route.MANAGE_USER_ACCOUNT]: undefined\n [Route.MANAGE_USER_EMAILS]: undefined\n [Route.MANAGE_USER_PROFILE]: undefined\n [Route.MANAGE_USER_BASIC]: undefined\n [Route.MANAGE_USER_SECURITY]: undefined\n [Route.MANAGE_USER_CLOSE]: undefined\n [Route.MANAGE_NOTIFICATIONS]: undefined\n [Route.MANAGE_NOTIFICATION_SETTINGS]: undefined\n [Route.MANAGE_REPORT_ISSUE]: undefined\n [Route.ORG_SETTINGS]: { orgSlug: string }\n [Route.ORG_SETTINGS_GENERAL]: { orgSlug: string }\n [Route.ORG_SETTINGS_PROFILE]: { orgSlug: string }\n [Route.ORG_SETTINGS_PLUGINS]: { orgSlug: string }\n [Route.ORG_SETTINGS_API_KEYS]: { orgSlug: string }\n [Route.ORG_SETTINGS_BRANDING]: { orgSlug: string }\n [Route.ORG_SETTINGS_SSO]: { orgSlug: string }\n [Route.ORG_SETTINGS_OWNERSHIP]: { orgSlug: string }\n [Route.ORG_SETTINGS_PRIVACY]: { orgSlug: string }\n [Route.ORG_SETTINGS_HOLDS]: { orgSlug: string }\n [Route.ORG_SETTINGS_DELETE]: { orgSlug: string }\n [Route.HOST_LIST]: { orgSlug: string }\n [Route.HOST_CONTENT]: { orgSlug: string; host: string }\n [Route.HOST_CONTENT_COLLECTION]: {\n orgSlug: string\n host: string\n /** The collection's slug — see the route. A document id also resolves. */\n collectionSlug: string\n }\n [Route.CONTENT_ENTRY_DETAILS]: {\n orgSlug: string\n host: string\n collectionSlug: string\n /**\n * An entry id, or the literal `new` for a draft with no document yet.\n *\n * An ID here while the collection above is a SLUG, deliberately. An entry\n * slug is unique only WITHIN its collection and it is editable on the\n * detail page — so routing by it would move the console address out from\n * under someone in the middle of typing in the slug field, and a draft has\n * no slug at all to route by. The entry id is also what every activity\n * record and the DAM's \"Used on\" list already carry.\n */\n entryId: string\n }\n [Route.MANAGE_TEAM]: { orgSlug: string }\n [Route.MANAGE_TEAM_MEMBERS]: { orgSlug: string }\n [Route.MANAGE_TEAM_ROLES]: { orgSlug: string }\n [Route.MANAGE_TEAM_ACTIVITY]: { orgSlug: string }\n [Route.MANAGE_TEAM_MEMBER]: { orgSlug: string; uid: string }\n [Route.MANAGE_SUPPORT]: { orgSlug: string }\n [Route.MANAGE_SUPPORT_TICKETS]: { orgSlug: string }\n [Route.MANAGE_SUPPORT_FORUM]: { orgSlug: string }\n [Route.MANAGE_BILLING]: { orgSlug: string }\n [Route.MANAGE_BILLING_USAGE]: { orgSlug: string }\n [Route.MANAGE_BILLING_INVOICES]: { orgSlug: string }\n [Route.MANAGE_BILLING_SETTINGS]: { orgSlug: string }\n [Route.BILLING_ENTRY]: undefined\n [Route.SUPPORT_ENTRY]: undefined\n [Route.HOST_INBOX]: { orgSlug: string; host: string }\n [Route.HOST_SETUP]: { orgSlug: string; host: string }\n [Route.HOST_SETUP_DETAILS]: { orgSlug: string; host: string }\n [Route.HOST_SETUP_SEO]: { orgSlug: string; host: string }\n [Route.HOST_SETUP_TRACKING]: { orgSlug: string; host: string }\n [Route.HOST_SETUP_THEME]: { orgSlug: string; host: string }\n [Route.HOST_SETUP_EMAILS]: { orgSlug: string; host: string }\n [Route.HOST_ADMIN]: { orgSlug: string; host: string }\n [Route.HOST_ADMIN_GENERAL]: { orgSlug: string; host: string }\n [Route.HOST_ADMIN_PLUGINS]: { orgSlug: string; host: string }\n [Route.HOST_ADMIN_PLUGIN]: { orgSlug: string; host: string; pluginRef: string }\n [Route.HOST_ADMIN_DOMAIN]: { orgSlug: string; host: string }\n [Route.HOST_ADMIN_SECURITY]: { orgSlug: string; host: string }\n [Route.HOST_ADMIN_ERROR_PAGES]: { orgSlug: string; host: string }\n [Route.HOST_ADMIN_ACTIVITY]: { orgSlug: string; host: string }\n [Route.HOST_ADMIN_BACKUP]: { orgSlug: string; host: string }\n [Route.HOST_ADMIN_DANGER]: { orgSlug: string; host: string }\n [Route.HOST_PLUGIN]: { orgSlug: string; host: string; pluginSlug: string }\n [Route.HOST_MEDIA]: { orgSlug: string; host: string }\n [Route.HOST_THEME]: { orgSlug: string; host: string }\n [Route.HOST_AUTOMATION]: { orgSlug: string; host: string }\n [Route.HOST_DATA]: { orgSlug: string; host: string }\n [Route.HOST_LOGIC]: { orgSlug: string; host: string }\n [Route.HOST_COMPONENTS]: { orgSlug: string; host: string }\n [Route.COMPONENT_DETAILS]: {\n orgSlug: string\n host: string\n componentId: string\n }\n [Route.HOST_FORMS]: { orgSlug: string; host: string }\n [Route.FORM_DETAILS]: {\n orgSlug: string\n host: string\n formId: string\n }\n [Route.HOST_TEMPLATES]: { orgSlug: string; host: string }\n [Route.TEMPLATE_DETAILS]: {\n orgSlug: string\n host: string\n templateId: string\n }\n [Route.HOST_MARKETING]: { orgSlug: string; host: string }\n [Route.HOST_REDIRECTS]: { orgSlug: string; host: string }\n [Route.HOST_USERS]: { orgSlug: string; host: string }\n [Route.HOST_ANALYTICS]: { orgSlug: string; host: string }\n [Route.COMPONENT_BESIGNER]: {\n orgSlug: string\n host: string\n componentId: string\n versionId: string\n }\n [Route.PLUGIN_DOCUMENT_DETAILS]: {\n orgSlug: string\n host: string\n documentSegment: string\n docId: string\n }\n [Route.PLUGIN_DOCUMENT_BESIGNER]: {\n orgSlug: string\n host: string\n documentSegment: string\n docId: string\n versionId: string\n }\n [Route.TEMPLATE_BESIGNER]: {\n orgSlug: string\n host: string\n templateId: string\n }\n [Route.LAYOUT_BESIGNER]: {\n orgSlug: string\n host: string\n layoutId: string\n versionId: string\n }\n [Route.COMPONENT_PREVIEW]: {\n orgSlug: string\n host: string\n componentId: string\n versionId: string\n }\n [Route.PLUGIN_DOCUMENT_PREVIEW]: {\n orgSlug: string\n host: string\n documentSegment: string\n docId: string\n versionId: string\n }\n [Route.TEMPLATE_PREVIEW]: {\n orgSlug: string\n host: string\n templateId: string\n }\n [Route.LAYOUT_PREVIEW]: {\n orgSlug: string\n host: string\n layoutId: string\n versionId: string\n }\n [Route.HOST_LAYOUTS]: { orgSlug: string; host: string }\n [Route.LAYOUT_DETAILS]: { orgSlug: string; host: string; layoutId: string }\n [Route.SCREEN_DETAILS]: {\n orgSlug: string\n host: string\n screenId: string\n versionId: string\n }\n [Route.SCREEN_PREVIEW]: {\n orgSlug: string\n host: string\n screenId: string\n versionId: string\n }\n [Route.HOST_SCREENS]: { orgSlug: string; host: string }\n}\n\nexport const routeReplacePattern = /\\[([^\\]]+)\\]/g\n\n/**\n * Builds a concrete path from a {@link Route} template. The payload is\n * REQUIRED whenever the route declares params (e.g. `orgSlug`, `hostId`) and\n * omitted only for param-less routes — so a forgotten `orgSlug` is a compile\n * error, not a `/<orgSlug?>/…` link that breaks at runtime (AGL-621).\n */\nexport function buildRoute<Tmpl extends Route>(\n template: Tmpl,\n ...[payload]: RoutePayload[Tmpl] extends undefined\n ? [payload?: undefined]\n : [payload: RoutePayload[Tmpl]]\n) {\n return template.replace(routeReplacePattern, (match, key) => {\n const value = (payload as Record<string, unknown> | undefined)?.[key]\n return value != null ? String(value) : `<${key}?>`\n })\n}\n"],"names":["Route","routeReplacePattern","buildRoute","template","payload","replace","match","key","value","String"],"mappings":"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"}
|
|
@@ -306,7 +306,7 @@ export interface ContactFacet {
|
|
|
306
306
|
*
|
|
307
307
|
* Per-holder for the reason the notes are: two unrelated businesses sharing
|
|
308
308
|
* one row must not read each other's segmentation of a person they both
|
|
309
|
-
* know. See `
|
|
309
|
+
* know. See `container-membership.ts` for the field and the path.
|
|
310
310
|
*/
|
|
311
311
|
campaignIds?: string[];
|
|
312
312
|
/** Gross of fees and refunds — see `upsertHostContact`. */
|