@aglyn/plugins-redirects 1.0.0-beta.284 → 1.0.0-beta.287
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 +11 -11
- package/src/lib/components/redirects-console-page.js +1 -1
- package/src/lib/components/redirects-console-page.js.map +1 -1
- package/src/lib/server/redirect-ai-writer.d.ts +2 -1
- package/src/lib/server/redirect-ai-writer.js +5 -6
- package/src/lib/server/redirect-ai-writer.js.map +1 -1
- package/src/lib/server/redirect-check.js +5 -4
- package/src/lib/server/redirect-check.js.map +1 -1
- package/src/lib/transfer/redirects-transfer.js +10 -10
- package/src/lib/transfer/redirects-transfer.js.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aglyn/plugins-redirects",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.287",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"homepage": "https://aglyn.com",
|
|
6
6
|
"repository": {
|
|
@@ -25,16 +25,16 @@
|
|
|
25
25
|
"./package.json": "./package.json"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@aglyn/aglyn": "1.0.0-beta.
|
|
29
|
-
"@aglyn/shared-data-mdi": "1.0.0-beta.
|
|
30
|
-
"@aglyn/shared-ui-jsx": "1.0.0-beta.
|
|
31
|
-
"@aglyn/shared-ui-snackstack": "1.0.0-beta.
|
|
32
|
-
"@aglyn/shared-util-email": "1.0.0-beta.
|
|
33
|
-
"@aglyn/shared-util-http": "1.0.0-beta.
|
|
34
|
-
"@aglyn/shared-util-timestamp": "1.0.0-beta.
|
|
35
|
-
"@aglyn/shared-util-tools": "1.0.0-beta.
|
|
36
|
-
"@aglyn/tenant-data-admin": "1.0.0-beta.
|
|
37
|
-
"@aglyn/tenant-feature-instance": "1.0.0-beta.
|
|
28
|
+
"@aglyn/aglyn": "1.0.0-beta.287",
|
|
29
|
+
"@aglyn/shared-data-mdi": "1.0.0-beta.287",
|
|
30
|
+
"@aglyn/shared-ui-jsx": "1.0.0-beta.287",
|
|
31
|
+
"@aglyn/shared-ui-snackstack": "1.0.0-beta.287",
|
|
32
|
+
"@aglyn/shared-util-email": "1.0.0-beta.287",
|
|
33
|
+
"@aglyn/shared-util-http": "1.0.0-beta.287",
|
|
34
|
+
"@aglyn/shared-util-timestamp": "1.0.0-beta.287",
|
|
35
|
+
"@aglyn/shared-util-tools": "1.0.0-beta.287",
|
|
36
|
+
"@aglyn/tenant-data-admin": "1.0.0-beta.287",
|
|
37
|
+
"@aglyn/tenant-feature-instance": "1.0.0-beta.287",
|
|
38
38
|
"@swc/helpers": "0.5.23"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
@@ -591,7 +591,7 @@ import { ceilingedWindow, collectionCeiling, useFirestore, useFirestoreCollectio
|
|
|
591
591
|
/*#__PURE__*/ _jsx(Typography, {
|
|
592
592
|
variant: "body2",
|
|
593
593
|
color: "text.secondary",
|
|
594
|
-
children: 'Exact-path rules; 302 while testing, promote to 301 when ' + 'sure (browsers cache 301 aggressively). Changes go live ' + 'within ~30 seconds.'
|
|
594
|
+
children: 'Exact-path rules; 302 while testing, promote to 301 when ' + 'sure (browsers cache 301 aggressively). Changes go live ' + 'within ~30 seconds. A visitor’s query string is passed on: ' + 'its parameters are added to the destination’s, and the ' + 'destination’s own win.'
|
|
595
595
|
}),
|
|
596
596
|
hits && totalHits > 0 ? /*#__PURE__*/ _jsx(Typography, {
|
|
597
597
|
variant: "caption",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/redirects/src/lib/components/redirects-console-page.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { TransferResumeImport } from '@aglyn/aglyn/app-utils/transfer-resume-import'\nimport { checkQuota, pluginDocsHelp, useTransferLauncher } from '@aglyn/aglyn'\nimport { type ConsolePluginPageProps } from '@aglyn/aglyn'\nimport { TENANT_APEX } from '@aglyn/aglyn/app-utils/tenant-apex'\nimport { findDuplicateRedirect, isExternalRedirectDestination, isSelfRedirect, matchRedirect, normalizeRedirectDestination, normalizeRedirectSource, REDIRECT_DEFAULT_PRIORITY, redirectHitKey, redirectSourceIsLivePage, validateRedirectRule, REDIRECT_STATUS_CODES, walkRedirectChain } from '../model'\nimport { CardDisplay, useConfirmationContext } from '@aglyn/shared-ui-jsx'\nimport { REDIRECTS_TRANSFER_KEY } from '../transfer/redirects-transfer-fields'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport QuotaReadoutComponent from '@aglyn/shared-ui-jsx/components/quota-readout.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { Timestamp } from '@aglyn/shared-util-timestamp'\nimport {\n Alert,\n Button,\n Chip,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n MenuItem,\n Stack,\n Switch,\n TextField,\n Typography,\n} from '@mui/material'\nimport {\n collection,\n deleteField,\n doc,\n getDoc,\n setDoc,\n updateDoc,\n} from 'firebase/firestore'\nimport { getLiveCountFromServer } from '@aglyn/aglyn/app-utils/live-document-count.client'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport { TABLE_PAGE_SIZE_DEFAULT } from '@aglyn/shared-ui-jsx/const/table-pagination'\nimport {\n ceilingedWindow,\n collectionCeiling,\n useFirestore,\n useFirestoreCollection,\n useFirestoreDoc,\n useHostResourceApi,\n useUser,\n writeGuardedBySeed,\n} from '@aglyn/tenant-feature-instance'\n\n/**\n * How many redirect documents this page reads.\n *\n * A CEILING, not a page size. Everything the page decides is computed over\n * the WHOLE rule set: the duplicate-source check, the chain-loop walk that\n * follows internal destinations through the rules, and the inline tester that\n * runs the enforcement matcher. A server page would give each of them a\n * tenth of the rules to reason about, and the duplicate check would then\n * create exactly the collision it exists to prevent.\n */\nconst REDIRECT_CEILING = 200\n\ninterface RedirectDraft {\n id: string | null\n source: string\n destination: string\n statusCode: number\n /** Match mode (AGL-375): exact | prefix | regex. */\n kind: string\n /** Lower fires first. */\n priority: number\n /**\n * The rule's on/off state, carried through the editor rather than reset by\n * it (AGL-1372). The save used to hardcode `true`, so changing a **disabled**\n * rule's target turned it back on and rerouted the site without the author\n * asking. A new rule seeds this `true`; an edit seeds it from the stored\n * rule, which is also where the row's switch reads from.\n */\n enabled: boolean\n}\n\n/**\n * Redirect manager (AGL-156): CRUD over `hosts/{hostId}/redirects` with\n * the shared AGL-154 validation (normalized sources, https-or-internal\n * destinations, self-redirect refusal, duplicate-source check). Paid\n * (`redirects` flag + `redirectsPerHost` quota); rules take effect on the\n * site within ~30 seconds (AGL-155 ISR window).\n *\n * The plan gate is the SHELL's, and it runs before this component exists\n * (AGL-2484): the console resolves `redirects` from the org billing doc and\n * renders its own refusal notice in place of the surface, so this card is\n * only ever mounted for an entitled org and states no plan terms of its own.\n * Redirects registers no `upgradeNotice` override because it is a genuine\n * upgrade — free denies it, Starter and above grant it — which makes the\n * shell's plan-derived sentence the accurate one already.\n *\n * `entitled` is still read below, where it is not redundant: it holds back\n * the thirty analytics day-doc reads behind the hit counts, and refuses the\n * add path ahead of the server that enforces it.\n */\nexport function RedirectsConsolePage(props: ConsolePluginPageProps) {\n const { hostId, entitled, org } = props\n const firestore = useFirestore()\n const createHostResource = useHostResourceApi()\n const { data: currentUser } = useUser()\n const { enqueueSnackbar } = useSnackbar()\n const { confirm } = useConfirmationContext()\n\n const {\n data: redirectDocs,\n status: redirectsStatus,\n /**\n * The rows this editor is seeded from are unconfirmed by the server\n * (AGL-1358). Editing copies every field the form owns out of the stored\n * rule and writes all of them back, so `merge: true` protects only the\n * fields written elsewhere (`lastHitAt`, `deletedAt`). This is the site\n * where a rollback is hardest to undo: a reverted `destination` on a\n * **301** is cached by every browser that has already followed it, so the\n * stale target outlives the fix in the console. `kind` reverting from\n * exact to prefix silently widens a rule over a whole subtree, and\n * `priority` reorders which rule fires when several match.\n */\n fromCache: redirectsFromCache,\n } = useFirestoreCollection<any>(\n /*\n * ORDERED AND CEILINGED, with a probe (AGL-2501).\n *\n * `limit(200)` alone is answered in DOCUMENT-ID order and the ids are\n * generated, so the window was a pseudo-random two hundred of the site's\n * rules — sorted by `source` in the browser afterwards, which is what\n * made it invisible: the rules on screen run alphabetically and are\n * simply the wrong rules, and the ones missing leave no gap to notice.\n *\n * `orderBy('source')` is the tempting fix and the dangerous one. It\n * matches only documents that HAVE the field, and nothing validates\n * `source` for presence — `/api/hosts/resources` stores an allow-list of\n * fields and checks none of them — so a rule saved without one would\n * vanish from this page while still redirecting live traffic. The\n * document NAME cannot be absent, so the walk is total: every rule is\n * reachable, and the alphabetical order stays where it belongs, in the\n * sort below over a window the page holds whole.\n */\n () =>\n collectionCeiling(\n collection(firestore, 'hosts', hostId, 'redirects'),\n REDIRECT_CEILING,\n ),\n [firestore, hostId],\n { idField: '$id' },\n )\n const { rows: readRedirects, truncated: redirectsTruncated } =\n ceilingedWindow<any>(redirectDocs, REDIRECT_CEILING)\n const redirects = [...readRedirects]\n .filter((redirect: any) => !redirect.deletedAt)\n .sort((a: any, b: any) =>\n String(a.source ?? '').localeCompare(String(b.source ?? '')),\n )\n\n /*\n * The page is a SLICE of the ceiling, which is what the three whole-set\n * consumers above require. The count is the rules this page HOLDS, after\n * soft-deleted ones are dropped — not the quota, which is a server count of\n * every document including the soft-deleted, and is read separately below.\n */\n const [page, setPage] = useState(0)\n const [pageSize, setPageSize] = useState(TABLE_PAGE_SIZE_DEFAULT)\n const visibleRedirects = useMemo(\n () => redirects.slice(page * pageSize, page * pageSize + pageSize),\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [redirectDocs, page, pageSize],\n )\n\n /**\n * What the ENFORCING route counts, which is not what this page was counting.\n *\n * `hosts/{id}/redirects` is read here as an unordered `limit(200)` window\n * and then filtered to drop soft-deleted rows, and that filtered length was\n * feeding both `checkQuota('redirectsPerHost')` and the readout beside it.\n * The route in `app/api/hosts/resources` counted the collection plainly,\n * soft-deleted rules included, so the two disagreed on both counts at\n * once: a rule deleted in this console still occupied a slot on the\n * server, and a site past 200 rules has rows the window never saw.\n *\n * The visible effect is the worst kind: the page shows room, the author\n * fills in the form, and the save is refused by a server counting something\n * else. Same shape as AGL-1716.\n *\n * The RULE is now that a deleted rule frees its slot and a disabled one\n * keeps it (AGL-3918), on the route and here alike: every document less\n * the tombstones (`getLiveCountFromServer`), two aggregation queries that\n * cost one read unit per thousand documents rather than reading them.\n * Re-run when the LIVE set read here changes, which covers an add and a\n * delete alike.\n */\n const [enforcedCount, setEnforcedCount] = useState<number | null>(null)\n const liveRedirectDocCount = (redirectDocs ?? []).filter(\n (redirect: any) => !redirect.deletedAt,\n ).length\n\n /**\n * `null` when the count cannot be taken, never a number.\n *\n * `try`, not `.catch()`: `getCountFromServer` validates its reference and\n * THROWS synchronously on a bad one, so a rejection handler alone leaves the\n * error to escape the effect and take the page down with it. Unreadable is\n * also not zero — answering zero would tell an author at their cap that the\n * whole allowance is free.\n */\n const countRedirects = useCallback(async (): Promise<number | null> => {\n try {\n return await getLiveCountFromServer(\n collection(firestore, 'hosts', hostId, 'redirects'),\n )\n } catch {\n return null\n }\n }, [firestore, hostId])\n\n useEffect(() => {\n let active = true\n void countRedirects().then((count) => {\n if (active && count != null) setEnforcedCount(count)\n })\n return () => {\n active = false\n }\n }, [countRedirects, liveRedirectDocCount])\n\n const [draft, setDraft] = useState<RedirectDraft | null>(null)\n const [testPath, setTestPath] = useState('')\n\n // Host routing map for the screen-collision warning (AGL-156 spec).\n const { data: host } = useFirestoreDoc<any>(\n () => doc(firestore, 'hosts', hostId),\n [firestore, hostId],\n { idField: '$id' },\n )\n\n // Sampled hit counts (AGL-157): summed from the last 30 analytics day\n // docs' `redirects` maps written by enforcement. Counts are sampled —\n // one per ISR revalidation window with traffic, not per request.\n const [hits, setHits] = useState<Record<string, number> | null>(null)\n useEffect(() => {\n if (!entitled) return\n let active = true\n const ids = Array.from({ length: 30 }, (_, index) => {\n const date = new Date()\n date.setDate(date.getDate() - index)\n return date.toISOString().slice(0, 10)\n })\n void Promise.all(\n ids.map((id) =>\n getDoc(doc(firestore, 'hosts', hostId, 'analytics', id))\n .then((snapshot) => (snapshot.get('redirects') ?? {}) as Record<string, number>)\n .catch(() => ({}) as Record<string, number>),\n ),\n ).then((days) => {\n if (!active) return\n const totals: Record<string, number> = {}\n for (const dayMap of days) {\n for (const [redirectId, count] of Object.entries(dayMap)) {\n totals[redirectId] = (totals[redirectId] ?? 0) + Number(count)\n }\n }\n setHits(totals)\n })\n return () => {\n active = false\n }\n }, [entitled, firestore, hostId])\n const totalHits = Object.values(hits ?? {}).reduce(\n (sum, count) => sum + count,\n 0,\n )\n\n const handleAdd = useCallback(() => {\n if (!entitled) {\n return void enqueueSnackbar(\n 'URL redirects require a Starter plan — see Billing to upgrade',\n { variant: 'warning', persist: false },\n )\n }\n /*\n * The SERVER's count, not the rows on screen — the whole point of this\n * fix. Read from the polled value rather than re-counted on the click:\n * this gate is a courtesy that spares the author a form they are about to\n * be refused, the route is the actual enforcer, and making the button\n * await an aggregation would delay the dialog on every press to sharpen a\n * number the server re-checks anyway.\n *\n * `null` means the count could not be read, and that lets the add through:\n * refusing on a number we do not have would block an author who may well\n * be inside their cap.\n */\n const quota = checkQuota(org, 'redirectsPerHost', enforcedCount ?? 0)\n if (!quota.allowed) {\n return void enqueueSnackbar(\n `Redirect limit reached (${quota.limit}) — upgrade in Billing`,\n { variant: 'warning', persist: false },\n )\n }\n setDraft({\n id: null,\n source: '',\n destination: '',\n statusCode: 302,\n kind: 'exact',\n priority: REDIRECT_DEFAULT_PRIORITY,\n // A NEW rule is on: the author is adding it to make it fire, and\n // enforcement queries `where('enabled', '==', true)`, so a rule created\n // without the field would never resolve (AGL-1372).\n enabled: true,\n })\n }, [entitled, org, enforcedCount, enqueueSnackbar])\n\n /**\n * Tell the tenant a rule changed (best effort, never awaited into the save\n * outcome). Rule writes are client Firestore writes with no publish step,\n * and the tenant's rules list sits behind the hour-long\n * `tenant-data:{hostId}` backstop — without this call a new rule waits out\n * that TTL while the snackbar above promises ~30 seconds. The route busts\n * the host tag and drops the source path's cached HTML; a regex pattern has\n * no single literal path, so it announces `/` for the tag bust alone and\n * the matching pages catch up on their own ISR window.\n */\n const announceRuleChange = useCallback(\n (source: string | undefined) => {\n const redirectPath =\n typeof source === 'string' && source.startsWith('/') ? source : '/'\n void authorizedFetch(currentUser, '/api/screens/revalidate', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ hostId, redirectPath }),\n }).catch(() => undefined)\n },\n [currentUser, hostId],\n )\n\n const handleSave = useCallback(async () => {\n if (!draft) return\n const kind = draft.kind || 'exact'\n // Shared v2 validation (AGL-375): regex patterns must compile; path\n // kinds keep the v1 normalization. A destination that wears another\n // brand is refused (AGL-3447); the site's own hosts never do.\n const problem = validateRedirectRule(\n {\n kind,\n source: draft.source,\n destination: draft.destination,\n },\n {\n ownDomains: [\n typeof host?.cname === 'string' ? host.cname : null,\n typeof host?.subdomain === 'string' && host.subdomain\n ? `${host.subdomain}.${TENANT_APEX}`\n : null,\n ],\n },\n )\n if (problem) {\n return void enqueueSnackbar(problem, {\n variant: 'warning',\n persist: false,\n })\n }\n const source =\n kind === 'regex'\n ? draft.source.trim()\n : (normalizeRedirectSource(draft.source) as string)\n const destination = normalizeRedirectDestination(\n draft.destination,\n ) as string\n if (kind !== 'regex' && isSelfRedirect({ source, destination })) {\n return void enqueueSnackbar('That would redirect the path to itself', {\n variant: 'warning',\n persist: false,\n })\n }\n const duplicate = findDuplicateRedirect(redirects, {\n $id: draft.id,\n source,\n kind,\n })\n if (duplicate) {\n return void enqueueSnackbar(`A rule for ${source} already exists`, {\n variant: 'warning',\n persist: false,\n })\n }\n // Chain-loop detection (AGL-156): follow internal destinations through\n // the rule set; a walk that returns to this source can never execute.\n if (walkRedirectChain(redirects, { $id: draft.id, source, destination }).loop) {\n return void enqueueSnackbar(\n 'That destination chains back to this rule — a redirect loop',\n { variant: 'warning', persist: false },\n )\n }\n // Screen-route collision: shadowing a live page may be intentional\n // (moved pages) — warn, don't block (decision per the issue).\n if (redirectSourceIsLivePage(host?.screens, source)) {\n enqueueSnackbar(\n `${source} is a published page — the redirect takes precedence`,\n { variant: 'info', persist: false },\n )\n }\n const fields = {\n source,\n destination,\n statusCode: (REDIRECT_STATUS_CODES as readonly number[]).includes(\n draft.statusCode,\n )\n ? draft.statusCode\n : 302,\n kind,\n priority: Number.isFinite(Number(draft.priority))\n ? Number(draft.priority)\n : REDIRECT_DEFAULT_PRIORITY,\n /**\n * Whatever the rule already was (AGL-1372). This was `true`, so editing\n * a **disabled** rule's target silently re-enabled it — a deliberate\n * \"off\" (a retired campaign, a rule that broke something) undone by a\n * routine save, changing site routing without the author asking.\n *\n * `!== false` rather than the raw value, because the switch below reads\n * the stored field the same way: absent means on. Enforcement's query is\n * `where('enabled', '==', true)`, so a rule missing the field is not\n * served, and normalising it here makes the console's claim true.\n *\n * The create default lives in `handleAdd` — a new rule seeds the draft\n * `true`, so both defaults hold without either one reading the other.\n */\n enabled: draft.enabled !== false,\n }\n /**\n * The publisher's stamp on an external destination (AGL-1881).\n *\n * `matchRedirect` will not send traffic off the platform without it, and\n * `cloud/firebase-firestore.rules` admits no write to this collection from\n * anyone but `canPublishHostContent` — so reaching this line at all is the\n * approval, and saving is how a rule written before that fix starts firing\n * again.\n *\n * Written on EDIT only. A create rides /api/hosts/resources, which stamps\n * the same field from the id token's verified uid; sending it from here\n * would be the client supplying its own provenance, and that route drops\n * it for exactly that reason.\n *\n * Cleared with `deleteField()` when the destination is internal, rather\n * than left standing. `merge: true` never removes a key, so an edit from\n * `https://elsewhere.example` back to `/pricing` would otherwise leave a\n * stamp behind that a LATER edit back to an external target would inherit\n * — approval carried across a destination nobody approved.\n */\n const approval = isExternalRedirectDestination(destination)\n ? { externalDestinationApprovedBy: currentUser?.uid ?? null }\n : { externalDestinationApprovedBy: deleteField() }\n try {\n if (draft.id) {\n /**\n * Edit stays client-direct (no quota consumed) — and is refused when\n * its seed was never confirmed by the server (AGL-1358); the eight\n * validation refusals above have all already returned by here.\n *\n * `merge: true` is load-bearing, contrary to what this said before\n * (AGL-1372): `fields` is every key the FORM owns, not every key the\n * document holds. `lastHitAt` is written by enforcement\n * (`resolve-redirect`), and `deletedAt` by the delete below — a\n * replacing write would erase both.\n *\n * The guard WRAPS the write. An early return is a shape you can keep\n * while losing the protection; here the write is only reachable\n * through the verdict.\n */\n const verdict = await writeGuardedBySeed(\n {\n subject: 'redirect',\n unreadable: redirectsStatus === 'error',\n fromCache: redirectsFromCache,\n },\n async () => {\n await setDoc(\n doc(firestore, 'hosts', hostId, 'redirects', draft.id),\n { ...fields, ...approval, updatedAt: Timestamp.now() },\n { merge: true },\n )\n },\n )\n // Before `setDraft(null)`, so a refusal keeps the dialog open with\n // what was typed, in the same warning vocabulary as the validation\n // refusals above it.\n if (!verdict.ok) {\n return void enqueueSnackbar(verdict.message, {\n variant: 'warning',\n persist: false,\n })\n }\n } else {\n // New redirect rides the quota-enforcing resources API (AGL-473) —\n // it also re-checks the `redirects` entitlement server-side.\n await createHostResource({ hostId, resource: 'redirect', data: fields })\n }\n // The changed rule, and — when an edit moved the source — the path\n // that just STOPPED redirecting, so its page refreshes too.\n announceRuleChange(source)\n if (draft.id) {\n const previous = (redirects ?? []).find(\n (rule: any) => rule.$id === draft.id,\n )?.source\n if (previous && previous !== source) announceRuleChange(previous)\n }\n setDraft(null)\n enqueueSnackbar('Redirect saved — live within ~30 seconds', {\n variant: 'success',\n persist: false,\n })\n } catch (error: any) {\n console.error(error)\n enqueueSnackbar(error?.message ?? 'An error has occurred', {\n variant: 'error',\n allowDuplicate: true,\n })\n }\n }, [\n draft,\n redirects,\n host,\n firestore,\n hostId,\n createHostResource,\n currentUser,\n enqueueSnackbar,\n redirectsStatus,\n redirectsFromCache,\n announceRuleChange,\n ])\n\n /**\n * A refusal has to be visible (AGL-1881).\n *\n * These two writes were bare `await`s. The rules now gate this collection on\n * a publishing role, so an `author` reaching either gets a\n * permission-denied — and an unhandled rejection renders as a control that\n * flips back with no error, which is the worst way to say no. `handleSave`\n * has always ended in a snackbar; these now match it.\n *\n * The card is not role-gated on purpose: it is entitlement-gated, and the\n * rules leave READ to the catch-all so an author can still see the rules\n * that affect the pages they write.\n */\n const reportWriteFailure = useCallback(\n (error: any) => {\n enqueueSnackbar(\n error?.code === 'permission-denied'\n ? 'Changing a redirect needs a publishing role — ask an editor or admin'\n : (error?.message ?? 'An error has occurred'),\n { variant: 'warning', persist: false },\n )\n },\n [enqueueSnackbar],\n )\n\n const handleToggle = useCallback(\n (redirect: any) => async (event: { target: { checked: boolean } }) => {\n try {\n await updateDoc(\n doc(firestore, 'hosts', hostId, 'redirects', redirect.$id),\n { enabled: event.target.checked, updatedAt: Timestamp.now() },\n )\n announceRuleChange(redirect.source)\n } catch (error: any) {\n reportWriteFailure(error)\n }\n },\n [firestore, hostId, reportWriteFailure, announceRuleChange],\n )\n\n const handleDelete = useCallback(\n (redirect: any) => async () => {\n const confirmed = await confirm({\n title: 'Delete this redirect?',\n description: `${redirect.source} stops redirecting.`,\n confirmationText: 'Delete',\n confirmationButtonProps: { color: 'error' },\n })\n .then(() => true)\n .catch(() => false)\n if (!confirmed) return\n try {\n await updateDoc(\n doc(firestore, 'hosts', hostId, 'redirects', redirect.$id),\n { deletedAt: Timestamp.now(), enabled: false },\n )\n announceRuleChange(redirect.source)\n } catch (error: any) {\n reportWriteFailure(error)\n }\n },\n [confirm, firestore, hostId, reportWriteFailure, announceRuleChange],\n )\n\n /*\n * Import and Export, through the console's transfer launcher (`null`\n * outside the console shell, where there is no workspace to move rules in\n * or out of). The list above is a live listener, so an import's rules show\n * up without a refresh.\n */\n const transfer = useTransferLauncher()\n const transferTarget = { resource: REDIRECTS_TRANSFER_KEY, scope: 'host' as const, hostId }\n // Import for those who may write the records, Export for those who may read them.\n const canImport = Boolean(transfer?.can('import', transferTarget))\n const canExport = Boolean(transfer?.can('export', transferTarget))\n const transferActions =\n transfer && (canImport || canExport) ? (\n <Stack direction=\"row\" spacing={1}>\n {/* An import left unfinished, reopened where it stopped (AGL-3549). */}\n <TransferResumeImport target={transferTarget} />\n {canImport && (\n <Button size=\"small\" onClick={() => transfer.openImport(transferTarget)}>\n {'Import'}\n </Button>\n )}\n {canExport && (\n <Button\n size=\"small\"\n onClick={() =>\n transfer.openExport(transferTarget)\n }\n >\n {'Export'}\n </Button>\n )}\n </Stack>\n ) : null\n\n return (\n <CardDisplay\n header={'URL redirects'}\n help={pluginDocsHelp('redirects', { anchor: '#manage-redirects' })}\n HeaderProps={transferActions ? { action: transferActions } : undefined}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={1}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Exact-path rules; 302 while testing, promote to 301 when ' +\n 'sure (browsers cache 301 aggressively). Changes go live ' +\n 'within ~30 seconds.'}\n </Typography>\n {hits && totalHits > 0 ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`${totalHits} redirect hit${totalHits === 1 ? '' : 's'} in ` +\n 'the last 30 days (sampled — one per cache window with ' +\n 'traffic).'}\n </Typography>\n ) : null}\n {visibleRedirects.map((redirect: any) => (\n <Stack\n key={redirect.$id}\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center' }}\n >\n <Switch\n size=\"small\"\n checked={redirect.enabled !== false}\n onChange={handleToggle(redirect)}\n />\n <Chip size=\"small\" label={redirect.statusCode ?? 302} />\n {(redirect.kind ?? 'exact') !== 'exact' ? (\n <Chip\n size=\"small\"\n variant=\"outlined\"\n label={redirect.kind}\n />\n ) : null}\n {/* An external rule with no publisher stamp is not being served\n (AGL-1881) — `matchRedirect` skips it. Say so on the row\n rather than letting it read as live: a rule that silently\n stops firing is the failure mode a fail-closed gate owes an\n explanation for, and pressing Edit → Save is the whole fix.\n Shown for external destinations only, since that is the only\n case the gate applies to. */}\n {isExternalRedirectDestination(redirect.destination) &&\n !redirect.externalDestinationApprovedBy ? (\n <Chip\n size=\"small\"\n color=\"warning\"\n variant=\"outlined\"\n label=\"not serving\"\n title={\n 'This rule sends visitors to another site. Open it and ' +\n 'save to confirm the destination — until then it is ' +\n 'skipped.'\n }\n />\n ) : null}\n <Stack sx={{ flex: 1, minWidth: 0 }}>\n <Typography variant=\"body2\" noWrap>\n {`${redirect.source} → ${redirect.destination}`}\n </Typography>\n {hits || redirect.lastHitAt ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {[\n hits\n ? `${hits[redirectHitKey(redirect.$id)] ?? 0} hits (30d, sampled)`\n : null,\n redirect.lastHitAt\n ? `last ${redirect.lastHitAt\n .toDate()\n .toLocaleString()}`\n : null,\n ]\n .filter(Boolean)\n .join(' · ')}\n </Typography>\n ) : null}\n </Stack>\n <Button\n size=\"small\"\n onClick={() =>\n setDraft({\n id: redirect.$id,\n source: redirect.source ?? '',\n destination: redirect.destination ?? '',\n statusCode: redirect.statusCode ?? 302,\n kind: redirect.kind ?? 'exact',\n priority: redirect.priority ?? REDIRECT_DEFAULT_PRIORITY,\n // Read exactly as the switch beside it does (AGL-1372):\n // an edit must not be a way to turn a rule back on.\n enabled: redirect.enabled !== false,\n })\n }\n >\n {'Edit'}\n </Button>\n <Button\n size=\"small\"\n color=\"error\"\n onClick={handleDelete(redirect)}\n >\n {'Delete'}\n </Button>\n </Stack>\n ))}\n {redirects.length === 0 ? null : (\n <ListPagination\n page={page}\n pageSize={pageSize}\n rowCount={visibleRedirects.length}\n // The rules this page HOLDS, after the soft-deleted ones are\n // dropped. Not the quota figure below it: that is a server\n // count over every document, soft-deleted rows included,\n // because a deleted rule still occupies a slot.\n count={redirects.length}\n onPageChange={setPage}\n onPageSizeChange={setPageSize}\n />\n )}\n {redirectsTruncated ? (\n <Alert severity=\"info\">\n {`Showing ${REDIRECT_CEILING} rules, ordered by id. This site ` +\n 'has more — the duplicate check and the tester below only ' +\n 'cover the ones listed here.'}\n </Alert>\n ) : null}\n <Button\n size=\"small\"\n color=\"primary\"\n sx={{ alignSelf: 'flex-start' }}\n onClick={handleAdd}\n >\n {'Add redirect'}\n </Button>\n {/* The cap, standing rather than only on refusal (AGL-2113).\n `enforcedCount` is what the enforcing route counts and what\n `handleAdd` checks against, so the readout, the gate and the\n server cannot disagree. It is deliberately NOT the number of rows\n on screen: the list is a capped window, and the live count past\n it is what the server refuses at. */}\n <QuotaReadoutComponent\n ready={org != null && enforcedCount != null}\n used={enforcedCount ?? 0}\n limit={\n checkQuota(org, 'redirectsPerHost', enforcedCount ?? 0).limit\n }\n noun=\"redirect\"\n />\n {/* Inline tester (AGL-375): same matcher as enforcement. */}\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n <TextField\n size=\"small\"\n label=\"Test a path\"\n placeholder=\"/old-page\"\n value={testPath}\n onChange={(event) => setTestPath(event.target.value)}\n sx={{ maxWidth: 260 }}\n />\n {testPath.trim() ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {(() => {\n const normalized =\n normalizeRedirectSource(testPath) ?? testPath.trim()\n const result = matchRedirect(redirects as any, normalized)\n return result\n ? `→ ${result.destination} (${result.statusCode})`\n : 'No rule matches'\n })()}\n </Typography>\n ) : null}\n </Stack>\n </Stack>\n\n <Dialog\n open={Boolean(draft)}\n onClose={() => setDraft(null)}\n maxWidth=\"xs\"\n fullWidth\n >\n <DialogTitle>{draft?.id ? 'Edit redirect' : 'Add redirect'}</DialogTitle>\n <DialogContent\n sx={{ display: 'flex', flexDirection: 'column', gap: 1.5, pt: 1 }}\n >\n <TextField\n select\n label=\"Match mode\"\n value={draft?.kind ?? 'exact'}\n onChange={(event) =>\n setDraft((prev) =>\n prev ? { ...prev, kind: event.target.value } : prev,\n )\n }\n size=\"small\"\n sx={{ mt: 1 }}\n >\n <MenuItem value=\"exact\">{'Exact path'}</MenuItem>\n <MenuItem value=\"prefix\">{'Path prefix'}</MenuItem>\n <MenuItem value=\"regex\">{'Regular expression'}</MenuItem>\n </TextField>\n <TextField\n label={draft?.kind === 'regex' ? 'Pattern' : 'From path'}\n placeholder={\n draft?.kind === 'regex' ? '/product/(\\\\d+)' : '/old-page'\n }\n helperText={\n draft?.kind === 'regex'\n ? 'Anchored to the whole path; use $1, $2 in the destination'\n : draft?.kind === 'prefix'\n ? 'Matches the path and everything under it'\n : undefined\n }\n value={draft?.source ?? ''}\n onChange={(event) =>\n setDraft((prev) =>\n prev ? { ...prev, source: event.target.value } : prev,\n )\n }\n size=\"small\"\n autoFocus\n />\n <TextField\n label=\"To\"\n placeholder=\"/new-page or https://example.com\"\n value={draft?.destination ?? ''}\n onChange={(event) =>\n setDraft((prev) =>\n prev ? { ...prev, destination: event.target.value } : prev,\n )\n }\n size=\"small\"\n />\n <TextField\n select\n label=\"Status code\"\n value={draft?.statusCode ?? 302}\n onChange={(event) =>\n setDraft((prev) =>\n prev\n ? { ...prev, statusCode: Number(event.target.value) }\n : prev,\n )\n }\n size=\"small\"\n >\n <MenuItem value={302}>{'302 — temporary (default)'}</MenuItem>\n <MenuItem value={301}>{'301 — permanent'}</MenuItem>\n <MenuItem value={307}>{'307 — temporary, keep method'}</MenuItem>\n <MenuItem value={308}>{'308 — permanent, keep method'}</MenuItem>\n </TextField>\n <TextField\n label=\"Priority\"\n type=\"number\"\n helperText=\"Lower fires first when several rules match\"\n value={draft?.priority ?? REDIRECT_DEFAULT_PRIORITY}\n onChange={(event) =>\n setDraft((prev) =>\n prev\n ? { ...prev, priority: Number(event.target.value) }\n : prev,\n )\n }\n size=\"small\"\n />\n </DialogContent>\n <DialogActions>\n <Button onClick={() => setDraft(null)}>{'Cancel'}</Button>\n <Button\n variant=\"contained\"\n color=\"primary\"\n disabled={!draft?.source.trim() || !draft?.destination.trim()}\n onClick={handleSave}\n >\n {'Save redirect'}\n </Button>\n </DialogActions>\n </Dialog>\n </CardDisplay>\n )\n}\nRedirectsConsolePage.displayName = 'RedirectsConsolePage'\n\nexport default RedirectsConsolePage\n"],"names":["TransferResumeImport","checkQuota","pluginDocsHelp","useTransferLauncher","TENANT_APEX","findDuplicateRedirect","isExternalRedirectDestination","isSelfRedirect","matchRedirect","normalizeRedirectDestination","normalizeRedirectSource","REDIRECT_DEFAULT_PRIORITY","redirectHitKey","redirectSourceIsLivePage","validateRedirectRule","REDIRECT_STATUS_CODES","walkRedirectChain","CardDisplay","useConfirmationContext","REDIRECTS_TRANSFER_KEY","authorizedFetch","QuotaReadoutComponent","useSnackbar","Timestamp","Alert","Button","Chip","Dialog","DialogActions","DialogContent","DialogTitle","MenuItem","Stack","Switch","TextField","Typography","collection","deleteField","doc","getDoc","setDoc","updateDoc","getLiveCountFromServer","useCallback","useEffect","useMemo","useState","ListPagination","TABLE_PAGE_SIZE_DEFAULT","ceilingedWindow","collectionCeiling","useFirestore","useFirestoreCollection","useFirestoreDoc","useHostResourceApi","useUser","writeGuardedBySeed","REDIRECT_CEILING","RedirectsConsolePage","props","hostId","entitled","org","firestore","createHostResource","data","currentUser","enqueueSnackbar","confirm","redirectDocs","status","redirectsStatus","fromCache","redirectsFromCache","idField","rows","readRedirects","truncated","redirectsTruncated","redirects","filter","redirect","deletedAt","sort","a","b","String","source","localeCompare","page","setPage","pageSize","setPageSize","visibleRedirects","slice","enforcedCount","setEnforcedCount","liveRedirectDocCount","length","countRedirects","active","then","count","draft","setDraft","testPath","setTestPath","host","hits","setHits","ids","Array","from","_","index","date","Date","setDate","getDate","toISOString","Promise","all","map","id","snapshot","get","catch","days","totals","dayMap","redirectId","Object","entries","Number","totalHits","values","reduce","sum","handleAdd","variant","persist","quota","allowed","limit","destination","statusCode","kind","priority","enabled","announceRuleChange","redirectPath","startsWith","method","headers","body","JSON","stringify","undefined","handleSave","problem","ownDomains","cname","subdomain","trim","duplicate","$id","loop","screens","fields","includes","isFinite","approval","externalDestinationApprovedBy","uid","verdict","subject","unreadable","updatedAt","now","merge","ok","message","resource","previous","find","rule","error","console","allowDuplicate","reportWriteFailure","code","handleToggle","event","target","checked","handleDelete","confirmed","title","description","confirmationText","confirmationButtonProps","color","transfer","transferTarget","scope","canImport","Boolean","can","canExport","transferActions","direction","spacing","size","onClick","openImport","openExport","header","help","anchor","HeaderProps","action","contentGutterX","contentGutterY","sx","alignItems","onChange","label","flex","minWidth","noWrap","lastHitAt","toDate","toLocaleString","join","rowCount","onPageChange","onPageSizeChange","severity","alignSelf","ready","used","noun","placeholder","value","maxWidth","normalized","result","open","onClose","fullWidth","display","flexDirection","gap","pt","select","prev","mt","helperText","autoFocus","type","disabled","displayName"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/redirects/src/lib/components/redirects-console-page.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { TransferResumeImport } from '@aglyn/aglyn/app-utils/transfer-resume-import'\nimport { checkQuota, pluginDocsHelp, useTransferLauncher } from '@aglyn/aglyn'\nimport { type ConsolePluginPageProps } from '@aglyn/aglyn'\nimport { TENANT_APEX } from '@aglyn/aglyn/app-utils/tenant-apex'\nimport { findDuplicateRedirect, isExternalRedirectDestination, isSelfRedirect, matchRedirect, normalizeRedirectDestination, normalizeRedirectSource, REDIRECT_DEFAULT_PRIORITY, redirectHitKey, redirectSourceIsLivePage, validateRedirectRule, REDIRECT_STATUS_CODES, walkRedirectChain } from '../model'\nimport { CardDisplay, useConfirmationContext } from '@aglyn/shared-ui-jsx'\nimport { REDIRECTS_TRANSFER_KEY } from '../transfer/redirects-transfer-fields'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport QuotaReadoutComponent from '@aglyn/shared-ui-jsx/components/quota-readout.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { Timestamp } from '@aglyn/shared-util-timestamp'\nimport {\n Alert,\n Button,\n Chip,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n MenuItem,\n Stack,\n Switch,\n TextField,\n Typography,\n} from '@mui/material'\nimport {\n collection,\n deleteField,\n doc,\n getDoc,\n setDoc,\n updateDoc,\n} from 'firebase/firestore'\nimport { getLiveCountFromServer } from '@aglyn/aglyn/app-utils/live-document-count.client'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport { TABLE_PAGE_SIZE_DEFAULT } from '@aglyn/shared-ui-jsx/const/table-pagination'\nimport {\n ceilingedWindow,\n collectionCeiling,\n useFirestore,\n useFirestoreCollection,\n useFirestoreDoc,\n useHostResourceApi,\n useUser,\n writeGuardedBySeed,\n} from '@aglyn/tenant-feature-instance'\n\n/**\n * How many redirect documents this page reads.\n *\n * A CEILING, not a page size. Everything the page decides is computed over\n * the WHOLE rule set: the duplicate-source check, the chain-loop walk that\n * follows internal destinations through the rules, and the inline tester that\n * runs the enforcement matcher. A server page would give each of them a\n * tenth of the rules to reason about, and the duplicate check would then\n * create exactly the collision it exists to prevent.\n */\nconst REDIRECT_CEILING = 200\n\ninterface RedirectDraft {\n id: string | null\n source: string\n destination: string\n statusCode: number\n /** Match mode (AGL-375): exact | prefix | regex. */\n kind: string\n /** Lower fires first. */\n priority: number\n /**\n * The rule's on/off state, carried through the editor rather than reset by\n * it (AGL-1372). The save used to hardcode `true`, so changing a **disabled**\n * rule's target turned it back on and rerouted the site without the author\n * asking. A new rule seeds this `true`; an edit seeds it from the stored\n * rule, which is also where the row's switch reads from.\n */\n enabled: boolean\n}\n\n/**\n * Redirect manager (AGL-156): CRUD over `hosts/{hostId}/redirects` with\n * the shared AGL-154 validation (normalized sources, https-or-internal\n * destinations, self-redirect refusal, duplicate-source check). Paid\n * (`redirects` flag + `redirectsPerHost` quota); rules take effect on the\n * site within ~30 seconds (AGL-155 ISR window).\n *\n * The plan gate is the SHELL's, and it runs before this component exists\n * (AGL-2484): the console resolves `redirects` from the org billing doc and\n * renders its own refusal notice in place of the surface, so this card is\n * only ever mounted for an entitled org and states no plan terms of its own.\n * Redirects registers no `upgradeNotice` override because it is a genuine\n * upgrade — free denies it, Starter and above grant it — which makes the\n * shell's plan-derived sentence the accurate one already.\n *\n * `entitled` is still read below, where it is not redundant: it holds back\n * the thirty analytics day-doc reads behind the hit counts, and refuses the\n * add path ahead of the server that enforces it.\n */\nexport function RedirectsConsolePage(props: ConsolePluginPageProps) {\n const { hostId, entitled, org } = props\n const firestore = useFirestore()\n const createHostResource = useHostResourceApi()\n const { data: currentUser } = useUser()\n const { enqueueSnackbar } = useSnackbar()\n const { confirm } = useConfirmationContext()\n\n const {\n data: redirectDocs,\n status: redirectsStatus,\n /**\n * The rows this editor is seeded from are unconfirmed by the server\n * (AGL-1358). Editing copies every field the form owns out of the stored\n * rule and writes all of them back, so `merge: true` protects only the\n * fields written elsewhere (`lastHitAt`, `deletedAt`). This is the site\n * where a rollback is hardest to undo: a reverted `destination` on a\n * **301** is cached by every browser that has already followed it, so the\n * stale target outlives the fix in the console. `kind` reverting from\n * exact to prefix silently widens a rule over a whole subtree, and\n * `priority` reorders which rule fires when several match.\n */\n fromCache: redirectsFromCache,\n } = useFirestoreCollection<any>(\n /*\n * ORDERED AND CEILINGED, with a probe (AGL-2501).\n *\n * `limit(200)` alone is answered in DOCUMENT-ID order and the ids are\n * generated, so the window was a pseudo-random two hundred of the site's\n * rules — sorted by `source` in the browser afterwards, which is what\n * made it invisible: the rules on screen run alphabetically and are\n * simply the wrong rules, and the ones missing leave no gap to notice.\n *\n * `orderBy('source')` is the tempting fix and the dangerous one. It\n * matches only documents that HAVE the field, and nothing validates\n * `source` for presence — `/api/hosts/resources` stores an allow-list of\n * fields and checks none of them — so a rule saved without one would\n * vanish from this page while still redirecting live traffic. The\n * document NAME cannot be absent, so the walk is total: every rule is\n * reachable, and the alphabetical order stays where it belongs, in the\n * sort below over a window the page holds whole.\n */\n () =>\n collectionCeiling(\n collection(firestore, 'hosts', hostId, 'redirects'),\n REDIRECT_CEILING,\n ),\n [firestore, hostId],\n { idField: '$id' },\n )\n const { rows: readRedirects, truncated: redirectsTruncated } =\n ceilingedWindow<any>(redirectDocs, REDIRECT_CEILING)\n const redirects = [...readRedirects]\n .filter((redirect: any) => !redirect.deletedAt)\n .sort((a: any, b: any) =>\n String(a.source ?? '').localeCompare(String(b.source ?? '')),\n )\n\n /*\n * The page is a SLICE of the ceiling, which is what the three whole-set\n * consumers above require. The count is the rules this page HOLDS, after\n * soft-deleted ones are dropped — not the quota, which is a server count of\n * every document including the soft-deleted, and is read separately below.\n */\n const [page, setPage] = useState(0)\n const [pageSize, setPageSize] = useState(TABLE_PAGE_SIZE_DEFAULT)\n const visibleRedirects = useMemo(\n () => redirects.slice(page * pageSize, page * pageSize + pageSize),\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [redirectDocs, page, pageSize],\n )\n\n /**\n * What the ENFORCING route counts, which is not what this page was counting.\n *\n * `hosts/{id}/redirects` is read here as an unordered `limit(200)` window\n * and then filtered to drop soft-deleted rows, and that filtered length was\n * feeding both `checkQuota('redirectsPerHost')` and the readout beside it.\n * The route in `app/api/hosts/resources` counted the collection plainly,\n * soft-deleted rules included, so the two disagreed on both counts at\n * once: a rule deleted in this console still occupied a slot on the\n * server, and a site past 200 rules has rows the window never saw.\n *\n * The visible effect is the worst kind: the page shows room, the author\n * fills in the form, and the save is refused by a server counting something\n * else. Same shape as AGL-1716.\n *\n * The RULE is now that a deleted rule frees its slot and a disabled one\n * keeps it (AGL-3918), on the route and here alike: every document less\n * the tombstones (`getLiveCountFromServer`), two aggregation queries that\n * cost one read unit per thousand documents rather than reading them.\n * Re-run when the LIVE set read here changes, which covers an add and a\n * delete alike.\n */\n const [enforcedCount, setEnforcedCount] = useState<number | null>(null)\n const liveRedirectDocCount = (redirectDocs ?? []).filter(\n (redirect: any) => !redirect.deletedAt,\n ).length\n\n /**\n * `null` when the count cannot be taken, never a number.\n *\n * `try`, not `.catch()`: `getCountFromServer` validates its reference and\n * THROWS synchronously on a bad one, so a rejection handler alone leaves the\n * error to escape the effect and take the page down with it. Unreadable is\n * also not zero — answering zero would tell an author at their cap that the\n * whole allowance is free.\n */\n const countRedirects = useCallback(async (): Promise<number | null> => {\n try {\n return await getLiveCountFromServer(\n collection(firestore, 'hosts', hostId, 'redirects'),\n )\n } catch {\n return null\n }\n }, [firestore, hostId])\n\n useEffect(() => {\n let active = true\n void countRedirects().then((count) => {\n if (active && count != null) setEnforcedCount(count)\n })\n return () => {\n active = false\n }\n }, [countRedirects, liveRedirectDocCount])\n\n const [draft, setDraft] = useState<RedirectDraft | null>(null)\n const [testPath, setTestPath] = useState('')\n\n // Host routing map for the screen-collision warning (AGL-156 spec).\n const { data: host } = useFirestoreDoc<any>(\n () => doc(firestore, 'hosts', hostId),\n [firestore, hostId],\n { idField: '$id' },\n )\n\n // Sampled hit counts (AGL-157): summed from the last 30 analytics day\n // docs' `redirects` maps written by enforcement. Counts are sampled —\n // one per ISR revalidation window with traffic, not per request.\n const [hits, setHits] = useState<Record<string, number> | null>(null)\n useEffect(() => {\n if (!entitled) return\n let active = true\n const ids = Array.from({ length: 30 }, (_, index) => {\n const date = new Date()\n date.setDate(date.getDate() - index)\n return date.toISOString().slice(0, 10)\n })\n void Promise.all(\n ids.map((id) =>\n getDoc(doc(firestore, 'hosts', hostId, 'analytics', id))\n .then((snapshot) => (snapshot.get('redirects') ?? {}) as Record<string, number>)\n .catch(() => ({}) as Record<string, number>),\n ),\n ).then((days) => {\n if (!active) return\n const totals: Record<string, number> = {}\n for (const dayMap of days) {\n for (const [redirectId, count] of Object.entries(dayMap)) {\n totals[redirectId] = (totals[redirectId] ?? 0) + Number(count)\n }\n }\n setHits(totals)\n })\n return () => {\n active = false\n }\n }, [entitled, firestore, hostId])\n const totalHits = Object.values(hits ?? {}).reduce(\n (sum, count) => sum + count,\n 0,\n )\n\n const handleAdd = useCallback(() => {\n if (!entitled) {\n return void enqueueSnackbar(\n 'URL redirects require a Starter plan — see Billing to upgrade',\n { variant: 'warning', persist: false },\n )\n }\n /*\n * The SERVER's count, not the rows on screen — the whole point of this\n * fix. Read from the polled value rather than re-counted on the click:\n * this gate is a courtesy that spares the author a form they are about to\n * be refused, the route is the actual enforcer, and making the button\n * await an aggregation would delay the dialog on every press to sharpen a\n * number the server re-checks anyway.\n *\n * `null` means the count could not be read, and that lets the add through:\n * refusing on a number we do not have would block an author who may well\n * be inside their cap.\n */\n const quota = checkQuota(org, 'redirectsPerHost', enforcedCount ?? 0)\n if (!quota.allowed) {\n return void enqueueSnackbar(\n `Redirect limit reached (${quota.limit}) — upgrade in Billing`,\n { variant: 'warning', persist: false },\n )\n }\n setDraft({\n id: null,\n source: '',\n destination: '',\n statusCode: 302,\n kind: 'exact',\n priority: REDIRECT_DEFAULT_PRIORITY,\n // A NEW rule is on: the author is adding it to make it fire, and\n // enforcement queries `where('enabled', '==', true)`, so a rule created\n // without the field would never resolve (AGL-1372).\n enabled: true,\n })\n }, [entitled, org, enforcedCount, enqueueSnackbar])\n\n /**\n * Tell the tenant a rule changed (best effort, never awaited into the save\n * outcome). Rule writes are client Firestore writes with no publish step,\n * and the tenant's rules list sits behind the hour-long\n * `tenant-data:{hostId}` backstop — without this call a new rule waits out\n * that TTL while the snackbar above promises ~30 seconds. The route busts\n * the host tag and drops the source path's cached HTML; a regex pattern has\n * no single literal path, so it announces `/` for the tag bust alone and\n * the matching pages catch up on their own ISR window.\n */\n const announceRuleChange = useCallback(\n (source: string | undefined) => {\n const redirectPath =\n typeof source === 'string' && source.startsWith('/') ? source : '/'\n void authorizedFetch(currentUser, '/api/screens/revalidate', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ hostId, redirectPath }),\n }).catch(() => undefined)\n },\n [currentUser, hostId],\n )\n\n const handleSave = useCallback(async () => {\n if (!draft) return\n const kind = draft.kind || 'exact'\n // Shared v2 validation (AGL-375): regex patterns must compile; path\n // kinds keep the v1 normalization. A destination that wears another\n // brand is refused (AGL-3447); the site's own hosts never do.\n const problem = validateRedirectRule(\n {\n kind,\n source: draft.source,\n destination: draft.destination,\n },\n {\n ownDomains: [\n typeof host?.cname === 'string' ? host.cname : null,\n typeof host?.subdomain === 'string' && host.subdomain\n ? `${host.subdomain}.${TENANT_APEX}`\n : null,\n ],\n },\n )\n if (problem) {\n return void enqueueSnackbar(problem, {\n variant: 'warning',\n persist: false,\n })\n }\n const source =\n kind === 'regex'\n ? draft.source.trim()\n : (normalizeRedirectSource(draft.source) as string)\n const destination = normalizeRedirectDestination(\n draft.destination,\n ) as string\n if (kind !== 'regex' && isSelfRedirect({ source, destination })) {\n return void enqueueSnackbar('That would redirect the path to itself', {\n variant: 'warning',\n persist: false,\n })\n }\n const duplicate = findDuplicateRedirect(redirects, {\n $id: draft.id,\n source,\n kind,\n })\n if (duplicate) {\n return void enqueueSnackbar(`A rule for ${source} already exists`, {\n variant: 'warning',\n persist: false,\n })\n }\n // Chain-loop detection (AGL-156): follow internal destinations through\n // the rule set; a walk that returns to this source can never execute.\n if (walkRedirectChain(redirects, { $id: draft.id, source, destination }).loop) {\n return void enqueueSnackbar(\n 'That destination chains back to this rule — a redirect loop',\n { variant: 'warning', persist: false },\n )\n }\n // Screen-route collision: shadowing a live page may be intentional\n // (moved pages) — warn, don't block (decision per the issue).\n if (redirectSourceIsLivePage(host?.screens, source)) {\n enqueueSnackbar(\n `${source} is a published page — the redirect takes precedence`,\n { variant: 'info', persist: false },\n )\n }\n const fields = {\n source,\n destination,\n statusCode: (REDIRECT_STATUS_CODES as readonly number[]).includes(\n draft.statusCode,\n )\n ? draft.statusCode\n : 302,\n kind,\n priority: Number.isFinite(Number(draft.priority))\n ? Number(draft.priority)\n : REDIRECT_DEFAULT_PRIORITY,\n /**\n * Whatever the rule already was (AGL-1372). This was `true`, so editing\n * a **disabled** rule's target silently re-enabled it — a deliberate\n * \"off\" (a retired campaign, a rule that broke something) undone by a\n * routine save, changing site routing without the author asking.\n *\n * `!== false` rather than the raw value, because the switch below reads\n * the stored field the same way: absent means on. Enforcement's query is\n * `where('enabled', '==', true)`, so a rule missing the field is not\n * served, and normalising it here makes the console's claim true.\n *\n * The create default lives in `handleAdd` — a new rule seeds the draft\n * `true`, so both defaults hold without either one reading the other.\n */\n enabled: draft.enabled !== false,\n }\n /**\n * The publisher's stamp on an external destination (AGL-1881).\n *\n * `matchRedirect` will not send traffic off the platform without it, and\n * `cloud/firebase-firestore.rules` admits no write to this collection from\n * anyone but `canPublishHostContent` — so reaching this line at all is the\n * approval, and saving is how a rule written before that fix starts firing\n * again.\n *\n * Written on EDIT only. A create rides /api/hosts/resources, which stamps\n * the same field from the id token's verified uid; sending it from here\n * would be the client supplying its own provenance, and that route drops\n * it for exactly that reason.\n *\n * Cleared with `deleteField()` when the destination is internal, rather\n * than left standing. `merge: true` never removes a key, so an edit from\n * `https://elsewhere.example` back to `/pricing` would otherwise leave a\n * stamp behind that a LATER edit back to an external target would inherit\n * — approval carried across a destination nobody approved.\n */\n const approval = isExternalRedirectDestination(destination)\n ? { externalDestinationApprovedBy: currentUser?.uid ?? null }\n : { externalDestinationApprovedBy: deleteField() }\n try {\n if (draft.id) {\n /**\n * Edit stays client-direct (no quota consumed) — and is refused when\n * its seed was never confirmed by the server (AGL-1358); the eight\n * validation refusals above have all already returned by here.\n *\n * `merge: true` is load-bearing, contrary to what this said before\n * (AGL-1372): `fields` is every key the FORM owns, not every key the\n * document holds. `lastHitAt` is written by enforcement\n * (`resolve-redirect`), and `deletedAt` by the delete below — a\n * replacing write would erase both.\n *\n * The guard WRAPS the write. An early return is a shape you can keep\n * while losing the protection; here the write is only reachable\n * through the verdict.\n */\n const verdict = await writeGuardedBySeed(\n {\n subject: 'redirect',\n unreadable: redirectsStatus === 'error',\n fromCache: redirectsFromCache,\n },\n async () => {\n await setDoc(\n doc(firestore, 'hosts', hostId, 'redirects', draft.id),\n { ...fields, ...approval, updatedAt: Timestamp.now() },\n { merge: true },\n )\n },\n )\n // Before `setDraft(null)`, so a refusal keeps the dialog open with\n // what was typed, in the same warning vocabulary as the validation\n // refusals above it.\n if (!verdict.ok) {\n return void enqueueSnackbar(verdict.message, {\n variant: 'warning',\n persist: false,\n })\n }\n } else {\n // New redirect rides the quota-enforcing resources API (AGL-473) —\n // it also re-checks the `redirects` entitlement server-side.\n await createHostResource({ hostId, resource: 'redirect', data: fields })\n }\n // The changed rule, and — when an edit moved the source — the path\n // that just STOPPED redirecting, so its page refreshes too.\n announceRuleChange(source)\n if (draft.id) {\n const previous = (redirects ?? []).find(\n (rule: any) => rule.$id === draft.id,\n )?.source\n if (previous && previous !== source) announceRuleChange(previous)\n }\n setDraft(null)\n enqueueSnackbar('Redirect saved — live within ~30 seconds', {\n variant: 'success',\n persist: false,\n })\n } catch (error: any) {\n console.error(error)\n enqueueSnackbar(error?.message ?? 'An error has occurred', {\n variant: 'error',\n allowDuplicate: true,\n })\n }\n }, [\n draft,\n redirects,\n host,\n firestore,\n hostId,\n createHostResource,\n currentUser,\n enqueueSnackbar,\n redirectsStatus,\n redirectsFromCache,\n announceRuleChange,\n ])\n\n /**\n * A refusal has to be visible (AGL-1881).\n *\n * These two writes were bare `await`s. The rules now gate this collection on\n * a publishing role, so an `author` reaching either gets a\n * permission-denied — and an unhandled rejection renders as a control that\n * flips back with no error, which is the worst way to say no. `handleSave`\n * has always ended in a snackbar; these now match it.\n *\n * The card is not role-gated on purpose: it is entitlement-gated, and the\n * rules leave READ to the catch-all so an author can still see the rules\n * that affect the pages they write.\n */\n const reportWriteFailure = useCallback(\n (error: any) => {\n enqueueSnackbar(\n error?.code === 'permission-denied'\n ? 'Changing a redirect needs a publishing role — ask an editor or admin'\n : (error?.message ?? 'An error has occurred'),\n { variant: 'warning', persist: false },\n )\n },\n [enqueueSnackbar],\n )\n\n const handleToggle = useCallback(\n (redirect: any) => async (event: { target: { checked: boolean } }) => {\n try {\n await updateDoc(\n doc(firestore, 'hosts', hostId, 'redirects', redirect.$id),\n { enabled: event.target.checked, updatedAt: Timestamp.now() },\n )\n announceRuleChange(redirect.source)\n } catch (error: any) {\n reportWriteFailure(error)\n }\n },\n [firestore, hostId, reportWriteFailure, announceRuleChange],\n )\n\n const handleDelete = useCallback(\n (redirect: any) => async () => {\n const confirmed = await confirm({\n title: 'Delete this redirect?',\n description: `${redirect.source} stops redirecting.`,\n confirmationText: 'Delete',\n confirmationButtonProps: { color: 'error' },\n })\n .then(() => true)\n .catch(() => false)\n if (!confirmed) return\n try {\n await updateDoc(\n doc(firestore, 'hosts', hostId, 'redirects', redirect.$id),\n { deletedAt: Timestamp.now(), enabled: false },\n )\n announceRuleChange(redirect.source)\n } catch (error: any) {\n reportWriteFailure(error)\n }\n },\n [confirm, firestore, hostId, reportWriteFailure, announceRuleChange],\n )\n\n /*\n * Import and Export, through the console's transfer launcher (`null`\n * outside the console shell, where there is no workspace to move rules in\n * or out of). The list above is a live listener, so an import's rules show\n * up without a refresh.\n */\n const transfer = useTransferLauncher()\n const transferTarget = { resource: REDIRECTS_TRANSFER_KEY, scope: 'host' as const, hostId }\n // Import for those who may write the records, Export for those who may read them.\n const canImport = Boolean(transfer?.can('import', transferTarget))\n const canExport = Boolean(transfer?.can('export', transferTarget))\n const transferActions =\n transfer && (canImport || canExport) ? (\n <Stack direction=\"row\" spacing={1}>\n {/* An import left unfinished, reopened where it stopped (AGL-3549). */}\n <TransferResumeImport target={transferTarget} />\n {canImport && (\n <Button size=\"small\" onClick={() => transfer.openImport(transferTarget)}>\n {'Import'}\n </Button>\n )}\n {canExport && (\n <Button\n size=\"small\"\n onClick={() =>\n transfer.openExport(transferTarget)\n }\n >\n {'Export'}\n </Button>\n )}\n </Stack>\n ) : null\n\n return (\n <CardDisplay\n header={'URL redirects'}\n help={pluginDocsHelp('redirects', { anchor: '#manage-redirects' })}\n HeaderProps={transferActions ? { action: transferActions } : undefined}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={1}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Exact-path rules; 302 while testing, promote to 301 when ' +\n 'sure (browsers cache 301 aggressively). Changes go live ' +\n 'within ~30 seconds. A visitor’s query string is passed on: ' +\n 'its parameters are added to the destination’s, and the ' +\n 'destination’s own win.'}\n </Typography>\n {hits && totalHits > 0 ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`${totalHits} redirect hit${totalHits === 1 ? '' : 's'} in ` +\n 'the last 30 days (sampled — one per cache window with ' +\n 'traffic).'}\n </Typography>\n ) : null}\n {visibleRedirects.map((redirect: any) => (\n <Stack\n key={redirect.$id}\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center' }}\n >\n <Switch\n size=\"small\"\n checked={redirect.enabled !== false}\n onChange={handleToggle(redirect)}\n />\n <Chip size=\"small\" label={redirect.statusCode ?? 302} />\n {(redirect.kind ?? 'exact') !== 'exact' ? (\n <Chip\n size=\"small\"\n variant=\"outlined\"\n label={redirect.kind}\n />\n ) : null}\n {/* An external rule with no publisher stamp is not being served\n (AGL-1881) — `matchRedirect` skips it. Say so on the row\n rather than letting it read as live: a rule that silently\n stops firing is the failure mode a fail-closed gate owes an\n explanation for, and pressing Edit → Save is the whole fix.\n Shown for external destinations only, since that is the only\n case the gate applies to. */}\n {isExternalRedirectDestination(redirect.destination) &&\n !redirect.externalDestinationApprovedBy ? (\n <Chip\n size=\"small\"\n color=\"warning\"\n variant=\"outlined\"\n label=\"not serving\"\n title={\n 'This rule sends visitors to another site. Open it and ' +\n 'save to confirm the destination — until then it is ' +\n 'skipped.'\n }\n />\n ) : null}\n <Stack sx={{ flex: 1, minWidth: 0 }}>\n <Typography variant=\"body2\" noWrap>\n {`${redirect.source} → ${redirect.destination}`}\n </Typography>\n {hits || redirect.lastHitAt ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {[\n hits\n ? `${hits[redirectHitKey(redirect.$id)] ?? 0} hits (30d, sampled)`\n : null,\n redirect.lastHitAt\n ? `last ${redirect.lastHitAt\n .toDate()\n .toLocaleString()}`\n : null,\n ]\n .filter(Boolean)\n .join(' · ')}\n </Typography>\n ) : null}\n </Stack>\n <Button\n size=\"small\"\n onClick={() =>\n setDraft({\n id: redirect.$id,\n source: redirect.source ?? '',\n destination: redirect.destination ?? '',\n statusCode: redirect.statusCode ?? 302,\n kind: redirect.kind ?? 'exact',\n priority: redirect.priority ?? REDIRECT_DEFAULT_PRIORITY,\n // Read exactly as the switch beside it does (AGL-1372):\n // an edit must not be a way to turn a rule back on.\n enabled: redirect.enabled !== false,\n })\n }\n >\n {'Edit'}\n </Button>\n <Button\n size=\"small\"\n color=\"error\"\n onClick={handleDelete(redirect)}\n >\n {'Delete'}\n </Button>\n </Stack>\n ))}\n {redirects.length === 0 ? null : (\n <ListPagination\n page={page}\n pageSize={pageSize}\n rowCount={visibleRedirects.length}\n // The rules this page HOLDS, after the soft-deleted ones are\n // dropped. Not the quota figure below it: that is a server\n // count over every document, soft-deleted rows included,\n // because a deleted rule still occupies a slot.\n count={redirects.length}\n onPageChange={setPage}\n onPageSizeChange={setPageSize}\n />\n )}\n {redirectsTruncated ? (\n <Alert severity=\"info\">\n {`Showing ${REDIRECT_CEILING} rules, ordered by id. This site ` +\n 'has more — the duplicate check and the tester below only ' +\n 'cover the ones listed here.'}\n </Alert>\n ) : null}\n <Button\n size=\"small\"\n color=\"primary\"\n sx={{ alignSelf: 'flex-start' }}\n onClick={handleAdd}\n >\n {'Add redirect'}\n </Button>\n {/* The cap, standing rather than only on refusal (AGL-2113).\n `enforcedCount` is what the enforcing route counts and what\n `handleAdd` checks against, so the readout, the gate and the\n server cannot disagree. It is deliberately NOT the number of rows\n on screen: the list is a capped window, and the live count past\n it is what the server refuses at. */}\n <QuotaReadoutComponent\n ready={org != null && enforcedCount != null}\n used={enforcedCount ?? 0}\n limit={\n checkQuota(org, 'redirectsPerHost', enforcedCount ?? 0).limit\n }\n noun=\"redirect\"\n />\n {/* Inline tester (AGL-375): same matcher as enforcement. */}\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n <TextField\n size=\"small\"\n label=\"Test a path\"\n placeholder=\"/old-page\"\n value={testPath}\n onChange={(event) => setTestPath(event.target.value)}\n sx={{ maxWidth: 260 }}\n />\n {testPath.trim() ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {(() => {\n const normalized =\n normalizeRedirectSource(testPath) ?? testPath.trim()\n const result = matchRedirect(redirects as any, normalized)\n return result\n ? `→ ${result.destination} (${result.statusCode})`\n : 'No rule matches'\n })()}\n </Typography>\n ) : null}\n </Stack>\n </Stack>\n\n <Dialog\n open={Boolean(draft)}\n onClose={() => setDraft(null)}\n maxWidth=\"xs\"\n fullWidth\n >\n <DialogTitle>{draft?.id ? 'Edit redirect' : 'Add redirect'}</DialogTitle>\n <DialogContent\n sx={{ display: 'flex', flexDirection: 'column', gap: 1.5, pt: 1 }}\n >\n <TextField\n select\n label=\"Match mode\"\n value={draft?.kind ?? 'exact'}\n onChange={(event) =>\n setDraft((prev) =>\n prev ? { ...prev, kind: event.target.value } : prev,\n )\n }\n size=\"small\"\n sx={{ mt: 1 }}\n >\n <MenuItem value=\"exact\">{'Exact path'}</MenuItem>\n <MenuItem value=\"prefix\">{'Path prefix'}</MenuItem>\n <MenuItem value=\"regex\">{'Regular expression'}</MenuItem>\n </TextField>\n <TextField\n label={draft?.kind === 'regex' ? 'Pattern' : 'From path'}\n placeholder={\n draft?.kind === 'regex' ? '/product/(\\\\d+)' : '/old-page'\n }\n helperText={\n draft?.kind === 'regex'\n ? 'Anchored to the whole path; use $1, $2 in the destination'\n : draft?.kind === 'prefix'\n ? 'Matches the path and everything under it'\n : undefined\n }\n value={draft?.source ?? ''}\n onChange={(event) =>\n setDraft((prev) =>\n prev ? { ...prev, source: event.target.value } : prev,\n )\n }\n size=\"small\"\n autoFocus\n />\n <TextField\n label=\"To\"\n placeholder=\"/new-page or https://example.com\"\n value={draft?.destination ?? ''}\n onChange={(event) =>\n setDraft((prev) =>\n prev ? { ...prev, destination: event.target.value } : prev,\n )\n }\n size=\"small\"\n />\n <TextField\n select\n label=\"Status code\"\n value={draft?.statusCode ?? 302}\n onChange={(event) =>\n setDraft((prev) =>\n prev\n ? { ...prev, statusCode: Number(event.target.value) }\n : prev,\n )\n }\n size=\"small\"\n >\n <MenuItem value={302}>{'302 — temporary (default)'}</MenuItem>\n <MenuItem value={301}>{'301 — permanent'}</MenuItem>\n <MenuItem value={307}>{'307 — temporary, keep method'}</MenuItem>\n <MenuItem value={308}>{'308 — permanent, keep method'}</MenuItem>\n </TextField>\n <TextField\n label=\"Priority\"\n type=\"number\"\n helperText=\"Lower fires first when several rules match\"\n value={draft?.priority ?? REDIRECT_DEFAULT_PRIORITY}\n onChange={(event) =>\n setDraft((prev) =>\n prev\n ? { ...prev, priority: Number(event.target.value) }\n : prev,\n )\n }\n size=\"small\"\n />\n </DialogContent>\n <DialogActions>\n <Button onClick={() => setDraft(null)}>{'Cancel'}</Button>\n <Button\n variant=\"contained\"\n color=\"primary\"\n disabled={!draft?.source.trim() || !draft?.destination.trim()}\n onClick={handleSave}\n >\n {'Save redirect'}\n </Button>\n </DialogActions>\n </Dialog>\n </CardDisplay>\n )\n}\nRedirectsConsolePage.displayName = 'RedirectsConsolePage'\n\nexport default RedirectsConsolePage\n"],"names":["TransferResumeImport","checkQuota","pluginDocsHelp","useTransferLauncher","TENANT_APEX","findDuplicateRedirect","isExternalRedirectDestination","isSelfRedirect","matchRedirect","normalizeRedirectDestination","normalizeRedirectSource","REDIRECT_DEFAULT_PRIORITY","redirectHitKey","redirectSourceIsLivePage","validateRedirectRule","REDIRECT_STATUS_CODES","walkRedirectChain","CardDisplay","useConfirmationContext","REDIRECTS_TRANSFER_KEY","authorizedFetch","QuotaReadoutComponent","useSnackbar","Timestamp","Alert","Button","Chip","Dialog","DialogActions","DialogContent","DialogTitle","MenuItem","Stack","Switch","TextField","Typography","collection","deleteField","doc","getDoc","setDoc","updateDoc","getLiveCountFromServer","useCallback","useEffect","useMemo","useState","ListPagination","TABLE_PAGE_SIZE_DEFAULT","ceilingedWindow","collectionCeiling","useFirestore","useFirestoreCollection","useFirestoreDoc","useHostResourceApi","useUser","writeGuardedBySeed","REDIRECT_CEILING","RedirectsConsolePage","props","hostId","entitled","org","firestore","createHostResource","data","currentUser","enqueueSnackbar","confirm","redirectDocs","status","redirectsStatus","fromCache","redirectsFromCache","idField","rows","readRedirects","truncated","redirectsTruncated","redirects","filter","redirect","deletedAt","sort","a","b","String","source","localeCompare","page","setPage","pageSize","setPageSize","visibleRedirects","slice","enforcedCount","setEnforcedCount","liveRedirectDocCount","length","countRedirects","active","then","count","draft","setDraft","testPath","setTestPath","host","hits","setHits","ids","Array","from","_","index","date","Date","setDate","getDate","toISOString","Promise","all","map","id","snapshot","get","catch","days","totals","dayMap","redirectId","Object","entries","Number","totalHits","values","reduce","sum","handleAdd","variant","persist","quota","allowed","limit","destination","statusCode","kind","priority","enabled","announceRuleChange","redirectPath","startsWith","method","headers","body","JSON","stringify","undefined","handleSave","problem","ownDomains","cname","subdomain","trim","duplicate","$id","loop","screens","fields","includes","isFinite","approval","externalDestinationApprovedBy","uid","verdict","subject","unreadable","updatedAt","now","merge","ok","message","resource","previous","find","rule","error","console","allowDuplicate","reportWriteFailure","code","handleToggle","event","target","checked","handleDelete","confirmed","title","description","confirmationText","confirmationButtonProps","color","transfer","transferTarget","scope","canImport","Boolean","can","canExport","transferActions","direction","spacing","size","onClick","openImport","openExport","header","help","anchor","HeaderProps","action","contentGutterX","contentGutterY","sx","alignItems","onChange","label","flex","minWidth","noWrap","lastHitAt","toDate","toLocaleString","join","rowCount","onPageChange","onPageSizeChange","severity","alignSelf","ready","used","noun","placeholder","value","maxWidth","normalized","result","open","onClose","fullWidth","display","flexDirection","gap","pt","select","prev","mt","helperText","autoFocus","type","disabled","displayName"],"mappings":"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"}
|
|
@@ -19,7 +19,8 @@ import type { PluginDraftContext, PluginResourceDraftWriter } from '@aglyn/aglyn
|
|
|
19
19
|
type Firestore = FirebaseFirestore.Firestore;
|
|
20
20
|
type StoredRule = Record<string, unknown>;
|
|
21
21
|
/** The page's refusal for a member without the publishing role. */
|
|
22
|
-
|
|
22
|
+
/** Names "Publish site content" (`site.publish`, AGL-4047), never a role. */
|
|
23
|
+
export declare const REDIRECT_AI_ROLE_REFUSAL: string;
|
|
23
24
|
/** The page's refusal for a plan without redirects. */
|
|
24
25
|
export declare const REDIRECT_AI_PLAN_REFUSAL = "URL redirects require a Starter plan \u2014 see Billing to upgrade";
|
|
25
26
|
/** What a member is told while a lockdown refuses writes to the site. */
|
|
@@ -14,15 +14,15 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
14
14
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
15
|
* See the License for the specific language governing permissions and
|
|
16
16
|
* limitations under the License.
|
|
17
|
-
*/ import {
|
|
18
|
-
import {
|
|
17
|
+
*/ import { checkEntitlement, checkQuota } from "@aglyn/aglyn/app-utils/plan-entitlements";
|
|
18
|
+
import { holdsSiteKey, siteKeyRefusal } from "@aglyn/aglyn/app-utils/host-permissions";
|
|
19
19
|
import { countLiveDocuments } from "@aglyn/aglyn/app-utils/live-document-count";
|
|
20
20
|
import { FieldValue, Timestamp } from "firebase-admin/firestore";
|
|
21
21
|
import { checkRedirectDraftContent, readRedirectDraftContent } from "../model/redirect-draft-content.js";
|
|
22
22
|
import { REDIRECT_DEFAULT_PRIORITY, findDuplicateRedirect, isExternalRedirectDestination, normalizeRedirectSource, redirectSourceIsLivePage, walkRedirectChain } from "../model/redirects.js";
|
|
23
23
|
import { announcedPath, redirectDeclaration, redirectRuleValues, ruleProblem, storedFields, withDefaults } from "../transfer/redirects-transfer.js";
|
|
24
24
|
import { REDIRECT_FIELD } from "../transfer/redirects-transfer-fields.js";
|
|
25
|
-
/** The page's refusal for a member without the publishing role. */
|
|
25
|
+
/** The page's refusal for a member without the publishing role. */ /** Names "Publish site content" (`site.publish`, AGL-4047), never a role. */ export const REDIRECT_AI_ROLE_REFUSAL = siteKeyRefusal('site.publish');
|
|
26
26
|
/** The page's refusal for a plan without redirects. */ export const REDIRECT_AI_PLAN_REFUSAL = 'URL redirects require a Starter plan — see Billing to upgrade';
|
|
27
27
|
/** What a member is told while a lockdown refuses writes to the site. */ export const REDIRECT_AI_LOCKED_REFUSAL = 'This site is not accepting changes right now.';
|
|
28
28
|
/** The create route's refusal at the plan's allowance. */ export function redirectAiLimitRefusal(limit) {
|
|
@@ -63,14 +63,13 @@ export function createRedirectAiWriter(deps) {
|
|
|
63
63
|
const hostRef = (hostId)=>deps.firestore().collection('hosts').doc(hostId);
|
|
64
64
|
const rulesOf = (hostId)=>hostRef(hostId).collection('redirects');
|
|
65
65
|
async function refusalFor(host, context) {
|
|
66
|
-
var _host_data,
|
|
66
|
+
var _host_data, _context_org;
|
|
67
67
|
if (!host.exists) return {
|
|
68
68
|
status: 404,
|
|
69
69
|
error: 'Unknown site'
|
|
70
70
|
};
|
|
71
71
|
const data = (_host_data = host.data()) != null ? _host_data : {};
|
|
72
|
-
|
|
73
|
-
if (!hostRoleCanPublish(role)) return {
|
|
72
|
+
if (!holdsSiteKey(data, context.uid, 'site.publish')) return {
|
|
74
73
|
status: 403,
|
|
75
74
|
error: REDIRECT_AI_ROLE_REFUSAL
|
|
76
75
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/redirects/src/lib/server/redirect-ai-writer.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { hostRoleCanPublish } from '@aglyn/aglyn/app-utils/organizations'\nimport { checkEntitlement, checkQuota } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport { countLiveDocuments } from '@aglyn/aglyn/app-utils/live-document-count'\nimport type { ResolvedPluginHostResource } from '@aglyn/aglyn/plugin-manager/plugin-host-resources'\nimport type {\n PluginDraftContext,\n PluginDraftRecord,\n PluginDraftRefusal,\n PluginDraftWrite,\n PluginResourceDraftWriter,\n} from '@aglyn/aglyn/plugin-manager/plugin-resource-drafts'\nimport { FieldValue, Timestamp } from 'firebase-admin/firestore'\nimport {\n checkRedirectDraftContent,\n readRedirectDraftContent,\n type RedirectDraftContent,\n} from '../model/redirect-draft-content'\nimport {\n REDIRECT_DEFAULT_PRIORITY,\n findDuplicateRedirect,\n isExternalRedirectDestination,\n normalizeRedirectSource,\n redirectSourceIsLivePage,\n walkRedirectChain,\n type RedirectRuleRow,\n} from '../model/redirects'\nimport {\n announcedPath,\n redirectDeclaration,\n redirectRuleValues,\n ruleProblem,\n storedFields,\n withDefaults,\n} from '../transfer/redirects-transfer'\nimport { REDIRECT_FIELD } from '../transfer/redirects-transfer-fields'\n\n/*==========================================\n * A REDIRECT RULE AN AI BUILD CHANGES (AGL-3745)\n *\n * The writer the redirects plugin registers for the `redirect` resource on the\n * core's resource-drafts seam, which the `redirect` build operation\n * (`redirect-ai-capability.ts`) executes. Every rule is this plugin's, made\n * with the functions the redirects page's save and the importer already use:\n *\n * - who: the publishing role on the site (`hostRoleCanPublish`, the axis the\n * Firestore rules gate the collection on), the plan's `redirects` feature,\n * and no lockdown refusing a write to the site, its workspace or the member;\n * - what: the page's normalization and defaults (exact, 302, priority 100,\n * on), the create route's allow-list of fields, and `validateRedirectRule`\n * with the site's own domains, so a lookalike destination, a pattern the\n * matcher cannot run and a path redirected to itself are refused;\n * - against what: the page's duplicate check and chain walk over the site's\n * live rules. A walk that comes back to the rule is the page's loop and is\n * refused; one that leads into a circle other rules make is refused too,\n * which the page only warns about — an AI change never sends visitors into\n * a loop. A from-path the site publishes a page at is refused unless the\n * item says the person asked for it (`replacesLivePage`): the page warns,\n * because replacing a moved page is what a redirect is for, but a build\n * must not take a live page away on its own reading of a request;\n * - how many: a create is counted and written in one transaction against\n * `redirectsPerHost`, as the create route does. An update or a remove\n * takes no slot, so the allowance never refuses one;\n * - provenance: an off-site destination carries the member's uid, the stamp\n * the serve path requires and the page writes; an on-site one has it\n * removed;\n * - afterwards: a created rule gets `createdAt`, `updatedAt`, `createdBy` and\n * the \"Created redirect\" activity line; every path a change touches is\n * announced to the site cache, as the page announces a save.\n *\n * A redirect has no draft state: the console writes a rule live, and so does\n * this. The build's plan card is where the person agrees to it, and the job\n * reports the change as live (the capability's `live`). A remove is the page's\n * Delete: the rule is marked deleted and off.\n *\n * Asked again for the same change, it answers with what it already did: a\n * create finds the rule written under the request's id, an update whose values\n * already hold changes nothing, and a remove of a rule already deleted is done.\n *=========================================*/\n\ntype Firestore = FirebaseFirestore.Firestore\ntype StoredRule = Record<string, unknown>\n\n/** The page's refusal for a member without the publishing role. */\nexport const REDIRECT_AI_ROLE_REFUSAL = 'Changing a redirect needs a publishing role — ask an editor or admin'\n/** The page's refusal for a plan without redirects. */\nexport const REDIRECT_AI_PLAN_REFUSAL = 'URL redirects require a Starter plan — see Billing to upgrade'\n/** What a member is told while a lockdown refuses writes to the site. */\nexport const REDIRECT_AI_LOCKED_REFUSAL = 'This site is not accepting changes right now.'\n\n/** The create route's refusal at the plan's allowance. */\nexport function redirectAiLimitRefusal(limit: number): string {\n return `Your plan includes ${limit} redirects — upgrade in Billing for more`\n}\n\n/** Whether a lockdown refuses this member's write to this site; `true` refuses. */\nexport type RedirectAiLockdownCheck = (input: {\n uid: string\n org: PluginDraftContext['org']\n host: StoredRule\n}) => Promise<boolean>\n\nexport interface RedirectAiWriterDeps {\n firestore: () => Firestore\n lockdown: RedirectAiLockdownCheck\n /** The `redirect` kind's declaration; the create route's otherwise. */\n declaration?: () => ResolvedPluginHostResource\n /** Drops the site's cached pages at these paths, best effort. */\n announcePaths?(hostId: string, paths: readonly string[]): Promise<void>\n /** Writes the site activity line for a created rule, best effort. */\n logCreated?(hostId: string, actorUid: string, ruleId: string, declared: ResolvedPluginHostResource): Promise<void>\n}\n\n/** What a caller is told about a rule: what it is, and that it is live. */\nfunction factsOf(action: RedirectDraftContent['action'], data: StoredRule): Record<string, unknown> {\n const values = redirectRuleValues('', data)\n return {\n action,\n source: values['source'],\n destination: values['destination'],\n kind: values['kind'],\n statusCode: values['statusCode'],\n priority: values['priority'],\n enabled: action === 'remove' ? false : values['enabled'],\n removed: action === 'remove',\n live: true,\n }\n}\n\nfunction recordOf(id: string, action: RedirectDraftContent['action'], data: StoredRule): PluginDraftRecord {\n return {\n id,\n name: `${String(data['source'] ?? '')} → ${String(data['destination'] ?? '')}`,\n versionId: null,\n facts: factsOf(action, data),\n }\n}\n\nfunction isLive(data: StoredRule | undefined): data is StoredRule {\n return Boolean(data) && !data?.['deletedAt']\n}\n\n/** A from-path as a rule of `kind` stores it: a pattern trimmed, a path normalized. */\nfunction storedSourceOf(kind: string, source: string): string | null {\n return kind === 'regex' ? source.trim() || null : normalizeRedirectSource(source)\n}\n\nexport function createRedirectAiWriter(deps: RedirectAiWriterDeps): PluginResourceDraftWriter {\n const declaration = deps.declaration ?? redirectDeclaration\n const hostRef = (hostId: string) => deps.firestore().collection('hosts').doc(hostId)\n const rulesOf = (hostId: string) => hostRef(hostId).collection('redirects')\n\n async function refusalFor(\n host: FirebaseFirestore.DocumentSnapshot,\n context: Pick<PluginDraftContext, 'uid' | 'org'>,\n ): Promise<PluginDraftRefusal | null> {\n if (!host.exists) return { status: 404, error: 'Unknown site' }\n const data = (host.data() ?? {}) as StoredRule\n const role = ((data['memberRoles'] ?? {}) as Record<string, unknown>)[context.uid]\n if (!hostRoleCanPublish(role)) return { status: 403, error: REDIRECT_AI_ROLE_REFUSAL }\n const declared = declaration()\n if (declared.entitlement && !checkEntitlement((context.org ?? {}) as never, declared.entitlement)) {\n return { status: 403, error: REDIRECT_AI_PLAN_REFUSAL }\n }\n if (await deps.lockdown({ uid: context.uid, org: context.org, host: data })) {\n return { status: 403, error: REDIRECT_AI_LOCKED_REFUSAL }\n }\n return null\n }\n\n /** Every rule document of the site, soft-deleted ones included. */\n async function readAllRules(hostId: string): Promise<Array<{ id: string; data: StoredRule }>> {\n const snapshot = await rulesOf(hostId).get()\n return snapshot.docs.map((doc) => ({ id: doc.id, data: (doc.data() ?? {}) as StoredRule }))\n }\n\n /** The live rules at a from-path, of `kind` when it is given. */\n function rulesAt(\n all: ReadonlyArray<{ id: string; data: StoredRule }>,\n source: string,\n kind: string | undefined,\n ): Array<{ id: string; data: StoredRule }> {\n return all.filter(({ data }) => {\n const ruleKind = typeof data['kind'] === 'string' && data['kind'] ? (data['kind'] as string) : 'exact'\n if (kind && ruleKind !== kind) return false\n return data['source'] === storedSourceOf(ruleKind, source)\n })\n }\n\n /** Why the rule a change leaves cannot be saved among the site's live rules, or `null`. */\n function setProblem(\n rows: readonly RedirectRuleRow[],\n rule: { $id: string; source: string; destination: string; kind: string },\n ): string | null {\n if (findDuplicateRedirect(rows, rule)) return `A rule for ${rule.source} already exists`\n const walk = walkRedirectChain(rows, rule)\n if (walk.loop) return 'That destination chains back to this rule — a redirect loop'\n if (walk.cycle) return 'That destination leads into a redirect loop other rules already make'\n return null\n }\n\n function livePageProblem(host: StoredRule, source: string, kind: string, asked: boolean | undefined): string | null {\n if (asked || kind === 'regex' || !redirectSourceIsLivePage(host['screens'] as Record<string, unknown>, source)) {\n return null\n }\n return `${source} is a published page, and a redirect there would take its place. Redirect it only when the person asks for that page to send visitors elsewhere.`\n }\n\n async function announce(hostId: string, paths: Array<string | null>): Promise<void> {\n const unique = [...new Set(paths.filter((path): path is string => Boolean(path)))]\n if (unique.length && deps.announcePaths) await deps.announcePaths(hostId, unique).catch(() => undefined)\n }\n\n const refuse = (status: PluginDraftRefusal['status'], error: string): PluginDraftWrite => ({ ok: false, status, error })\n\n return {\n refusal: async (context) => refusalFor(await hostRef(context.hostId).get(), context),\n\n check: (content) => checkRedirectDraftContent(content),\n\n read: async ({ hostId, id }) => {\n const snapshot = await rulesOf(hostId).doc(id).get()\n if (!snapshot.exists) return null\n const data = (snapshot.data() ?? {}) as StoredRule\n return recordOf(id, isLive(data) ? 'create' : 'remove', data)\n },\n\n write: async (request): Promise<PluginDraftWrite> => {\n const read = readRedirectDraftContent(request.content)\n if (read.ok === false) return refuse(400, read.problems[0] as string)\n const change = read.value\n const hostSnapshot = await hostRef(request.hostId).get()\n const refusal = await refusalFor(hostSnapshot, request)\n if (refusal) return { ok: false, ...refusal }\n const host = (hostSnapshot.data() ?? {}) as StoredRule\n const declared = declaration()\n const approvalField = declared.externalDestination?.approvedByField ?? REDIRECT_FIELD.approvedBy\n const stampFor = (destination: unknown): string | null =>\n declared.externalDestination && isExternalRedirectDestination(String(destination ?? '')) ? request.uid : null\n const at = Timestamp.fromDate(request.now)\n const all = await readAllRules(request.hostId)\n const live = all.filter(({ data }) => isLive(data))\n const rows: RedirectRuleRow[] = live.map(({ id, data }) => ({\n $id: id,\n source: data['source'] as string,\n destination: data['destination'] as string,\n kind: data['kind'] as string,\n }))\n\n if (change.action === 'create') {\n const ref = rulesOf(request.hostId).doc(request.id)\n const before = all.find((one) => one.id === request.id)\n if (before) return { ok: true, replayed: true, ...recordOf(request.id, 'create', before.data) }\n const rule = withDefaults({\n source: change.source,\n destination: change.destination,\n kind: change.kind ?? 'exact',\n statusCode: change.statusCode ?? 302,\n priority: change.priority ?? REDIRECT_DEFAULT_PRIORITY,\n enabled: change.enabled ?? true,\n })\n const problem = ruleProblem(rule, host)\n if (problem) return refuse(400, problem.detail)\n const fields = storedFields(rule, declared.fields)\n const source = String(fields['source'])\n const kind = String(fields['kind'] ?? 'exact')\n const shadow = livePageProblem(host, source, kind, change.replacesLivePage)\n if (shadow) return refuse(409, shadow)\n const clash = setProblem(rows, { $id: request.id, source, destination: String(fields['destination']), kind })\n if (clash) return refuse(409, clash)\n const stamp = stampFor(fields['destination'])\n const written = { ...fields, ...(stamp ? { [approvalField]: stamp } : {}) }\n const db = deps.firestore()\n const outcome = await db.runTransaction(async (tx): Promise<{ refusal?: string; existed?: StoredRule }> => {\n const existing = await tx.get(ref)\n if (existing.exists) return { existed: (existing.data() ?? {}) as StoredRule }\n if (declared.quotaKey) {\n // Live rules only, as the create route counts: a deleted rule frees its slot (AGL-3918).\n const used = await countLiveDocuments(rulesOf(request.hostId), (aggregate) => tx.get(aggregate))\n const quota = checkQuota((request.org ?? {}) as never, declared.quotaKey as never, Number(used) || 0)\n if (!quota.allowed) return { refusal: redirectAiLimitRefusal(quota.limit) }\n }\n tx.create(ref, { ...written, createdAt: at, updatedAt: at, createdBy: request.uid })\n return {}\n })\n if (outcome.refusal) return refuse(403, outcome.refusal)\n if (outcome.existed) return { ok: true, replayed: true, ...recordOf(request.id, 'create', outcome.existed) }\n if (deps.logCreated) {\n await deps.logCreated(request.hostId, request.uid, ref.id, declared).catch(() => undefined)\n }\n await announce(request.hostId, [announcedPath(fields)])\n return { ok: true, replayed: false, ...recordOf(ref.id, 'create', written) }\n }\n\n // An update or a remove names a rule the site has.\n const found = rulesAt(live, change.source, change.kind)\n if (found.length > 1) {\n return refuse(\n 409,\n `The site has more than one redirect at ${change.source}; say which match mode (exact, prefix or regex) to change`,\n )\n }\n const target = found[0]\n if (!target) {\n if (change.action === 'remove') {\n const gone = rulesAt(all, change.source, change.kind)[0]\n if (gone) return { ok: true, replayed: true, ...recordOf(gone.id, 'remove', gone.data) }\n }\n return refuse(404, `The site has no redirect from ${change.source}`)\n }\n const ref = rulesOf(request.hostId).doc(target.id)\n\n if (change.action === 'remove') {\n // The page's Delete: marked deleted and off, so the serve path drops it.\n await ref.set({ deletedAt: at, enabled: false }, { merge: true })\n await announce(request.hostId, [announcedPath(target.data as { kind?: string; source?: string })])\n return { ok: true, replayed: false, ...recordOf(target.id, 'remove', target.data) }\n }\n\n const current = redirectRuleValues(target.id, target.data)\n const rule = withDefaults({\n ...current,\n ...(change.newSource !== undefined ? { source: change.newSource } : {}),\n ...(change.destination !== undefined ? { destination: change.destination } : {}),\n ...(change.statusCode !== undefined ? { statusCode: change.statusCode } : {}),\n ...(change.priority !== undefined ? { priority: change.priority } : {}),\n ...(change.enabled !== undefined ? { enabled: change.enabled } : {}),\n })\n const problem = ruleProblem(rule, host)\n if (problem) return refuse(400, problem.detail)\n const fields = storedFields(rule, declared.fields)\n const source = String(fields['source'])\n const kind = String(fields['kind'] ?? 'exact')\n if (source !== current['source']) {\n const shadow = livePageProblem(host, source, kind, change.replacesLivePage)\n if (shadow) return refuse(409, shadow)\n }\n const clash = setProblem(rows, { $id: target.id, source, destination: String(fields['destination']), kind })\n if (clash) return refuse(409, clash)\n const stamp = stampFor(fields['destination'])\n const unchanged =\n Object.entries(fields).every(([key, value]) => current[key] === value) &&\n (current[approvalField] ?? null) === stamp\n const after = { ...target.data, ...fields, ...(stamp ? { [approvalField]: stamp } : {}) }\n if (unchanged) return { ok: true, replayed: true, ...recordOf(target.id, 'update', after) }\n // The page's edit: every field the form owns, the stamp set or removed,\n // `updatedAt`, merged so `lastHitAt` stays.\n await ref.set({ ...fields, [approvalField]: stamp ?? FieldValue.delete(), updatedAt: at }, { merge: true })\n await announce(request.hostId, [announcedPath(fields), announcedPath(target.data as { kind?: string; source?: string })])\n return { ok: true, replayed: false, ...recordOf(target.id, 'update', after) }\n },\n }\n}\n"],"names":["hostRoleCanPublish","checkEntitlement","checkQuota","countLiveDocuments","FieldValue","Timestamp","checkRedirectDraftContent","readRedirectDraftContent","REDIRECT_DEFAULT_PRIORITY","findDuplicateRedirect","isExternalRedirectDestination","normalizeRedirectSource","redirectSourceIsLivePage","walkRedirectChain","announcedPath","redirectDeclaration","redirectRuleValues","ruleProblem","storedFields","withDefaults","REDIRECT_FIELD","REDIRECT_AI_ROLE_REFUSAL","REDIRECT_AI_PLAN_REFUSAL","REDIRECT_AI_LOCKED_REFUSAL","redirectAiLimitRefusal","limit","factsOf","action","data","values","source","destination","kind","statusCode","priority","enabled","removed","live","recordOf","id","name","String","versionId","facts","isLive","Boolean","storedSourceOf","trim","createRedirectAiWriter","deps","declaration","hostRef","hostId","firestore","collection","doc","rulesOf","refusalFor","host","context","exists","status","error","role","uid","declared","entitlement","org","lockdown","readAllRules","snapshot","get","docs","map","rulesAt","all","filter","ruleKind","setProblem","rows","rule","walk","loop","cycle","livePageProblem","asked","announce","paths","unique","Set","path","length","announcePaths","catch","undefined","refuse","ok","refusal","check","content","read","write","request","hostSnapshot","fields","current","problems","change","value","approvalField","externalDestination","approvedByField","approvedBy","stampFor","at","fromDate","now","$id","ref","before","find","one","replayed","problem","detail","shadow","replacesLivePage","clash","stamp","written","db","outcome","runTransaction","tx","existing","existed","quotaKey","used","aggregate","quota","Number","allowed","create","createdAt","updatedAt","createdBy","logCreated","found","target","gone","set","deletedAt","merge","newSource","unchanged","Object","entries","every","key","after","delete"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/redirects/src/lib/server/redirect-ai-writer.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { checkEntitlement, checkQuota } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport { holdsSiteKey, siteKeyRefusal } from '@aglyn/aglyn/app-utils/host-permissions'\nimport { countLiveDocuments } from '@aglyn/aglyn/app-utils/live-document-count'\nimport type { ResolvedPluginHostResource } from '@aglyn/aglyn/plugin-manager/plugin-host-resources'\nimport type {\n PluginDraftContext,\n PluginDraftRecord,\n PluginDraftRefusal,\n PluginDraftWrite,\n PluginResourceDraftWriter,\n} from '@aglyn/aglyn/plugin-manager/plugin-resource-drafts'\nimport { FieldValue, Timestamp } from 'firebase-admin/firestore'\nimport {\n checkRedirectDraftContent,\n readRedirectDraftContent,\n type RedirectDraftContent,\n} from '../model/redirect-draft-content'\nimport {\n REDIRECT_DEFAULT_PRIORITY,\n findDuplicateRedirect,\n isExternalRedirectDestination,\n normalizeRedirectSource,\n redirectSourceIsLivePage,\n walkRedirectChain,\n type RedirectRuleRow,\n} from '../model/redirects'\nimport {\n announcedPath,\n redirectDeclaration,\n redirectRuleValues,\n ruleProblem,\n storedFields,\n withDefaults,\n} from '../transfer/redirects-transfer'\nimport { REDIRECT_FIELD } from '../transfer/redirects-transfer-fields'\n\n/*==========================================\n * A REDIRECT RULE AN AI BUILD CHANGES (AGL-3745)\n *\n * The writer the redirects plugin registers for the `redirect` resource on the\n * core's resource-drafts seam, which the `redirect` build operation\n * (`redirect-ai-capability.ts`) executes. Every rule is this plugin's, made\n * with the functions the redirects page's save and the importer already use:\n *\n * - who: the publishing role on the site (`hostRoleCanPublish`, the axis the\n * Firestore rules gate the collection on), the plan's `redirects` feature,\n * and no lockdown refusing a write to the site, its workspace or the member;\n * - what: the page's normalization and defaults (exact, 302, priority 100,\n * on), the create route's allow-list of fields, and `validateRedirectRule`\n * with the site's own domains, so a lookalike destination, a pattern the\n * matcher cannot run and a path redirected to itself are refused;\n * - against what: the page's duplicate check and chain walk over the site's\n * live rules. A walk that comes back to the rule is the page's loop and is\n * refused; one that leads into a circle other rules make is refused too,\n * which the page only warns about — an AI change never sends visitors into\n * a loop. A from-path the site publishes a page at is refused unless the\n * item says the person asked for it (`replacesLivePage`): the page warns,\n * because replacing a moved page is what a redirect is for, but a build\n * must not take a live page away on its own reading of a request;\n * - how many: a create is counted and written in one transaction against\n * `redirectsPerHost`, as the create route does. An update or a remove\n * takes no slot, so the allowance never refuses one;\n * - provenance: an off-site destination carries the member's uid, the stamp\n * the serve path requires and the page writes; an on-site one has it\n * removed;\n * - afterwards: a created rule gets `createdAt`, `updatedAt`, `createdBy` and\n * the \"Created redirect\" activity line; every path a change touches is\n * announced to the site cache, as the page announces a save.\n *\n * A redirect has no draft state: the console writes a rule live, and so does\n * this. The build's plan card is where the person agrees to it, and the job\n * reports the change as live (the capability's `live`). A remove is the page's\n * Delete: the rule is marked deleted and off.\n *\n * Asked again for the same change, it answers with what it already did: a\n * create finds the rule written under the request's id, an update whose values\n * already hold changes nothing, and a remove of a rule already deleted is done.\n *=========================================*/\n\ntype Firestore = FirebaseFirestore.Firestore\ntype StoredRule = Record<string, unknown>\n\n/** The page's refusal for a member without the publishing role. */\n/** Names \"Publish site content\" (`site.publish`, AGL-4047), never a role. */\nexport const REDIRECT_AI_ROLE_REFUSAL = siteKeyRefusal('site.publish')\n/** The page's refusal for a plan without redirects. */\nexport const REDIRECT_AI_PLAN_REFUSAL = 'URL redirects require a Starter plan — see Billing to upgrade'\n/** What a member is told while a lockdown refuses writes to the site. */\nexport const REDIRECT_AI_LOCKED_REFUSAL = 'This site is not accepting changes right now.'\n\n/** The create route's refusal at the plan's allowance. */\nexport function redirectAiLimitRefusal(limit: number): string {\n return `Your plan includes ${limit} redirects — upgrade in Billing for more`\n}\n\n/** Whether a lockdown refuses this member's write to this site; `true` refuses. */\nexport type RedirectAiLockdownCheck = (input: {\n uid: string\n org: PluginDraftContext['org']\n host: StoredRule\n}) => Promise<boolean>\n\nexport interface RedirectAiWriterDeps {\n firestore: () => Firestore\n lockdown: RedirectAiLockdownCheck\n /** The `redirect` kind's declaration; the create route's otherwise. */\n declaration?: () => ResolvedPluginHostResource\n /** Drops the site's cached pages at these paths, best effort. */\n announcePaths?(hostId: string, paths: readonly string[]): Promise<void>\n /** Writes the site activity line for a created rule, best effort. */\n logCreated?(hostId: string, actorUid: string, ruleId: string, declared: ResolvedPluginHostResource): Promise<void>\n}\n\n/** What a caller is told about a rule: what it is, and that it is live. */\nfunction factsOf(action: RedirectDraftContent['action'], data: StoredRule): Record<string, unknown> {\n const values = redirectRuleValues('', data)\n return {\n action,\n source: values['source'],\n destination: values['destination'],\n kind: values['kind'],\n statusCode: values['statusCode'],\n priority: values['priority'],\n enabled: action === 'remove' ? false : values['enabled'],\n removed: action === 'remove',\n live: true,\n }\n}\n\nfunction recordOf(id: string, action: RedirectDraftContent['action'], data: StoredRule): PluginDraftRecord {\n return {\n id,\n name: `${String(data['source'] ?? '')} → ${String(data['destination'] ?? '')}`,\n versionId: null,\n facts: factsOf(action, data),\n }\n}\n\nfunction isLive(data: StoredRule | undefined): data is StoredRule {\n return Boolean(data) && !data?.['deletedAt']\n}\n\n/** A from-path as a rule of `kind` stores it: a pattern trimmed, a path normalized. */\nfunction storedSourceOf(kind: string, source: string): string | null {\n return kind === 'regex' ? source.trim() || null : normalizeRedirectSource(source)\n}\n\nexport function createRedirectAiWriter(deps: RedirectAiWriterDeps): PluginResourceDraftWriter {\n const declaration = deps.declaration ?? redirectDeclaration\n const hostRef = (hostId: string) => deps.firestore().collection('hosts').doc(hostId)\n const rulesOf = (hostId: string) => hostRef(hostId).collection('redirects')\n\n async function refusalFor(\n host: FirebaseFirestore.DocumentSnapshot,\n context: Pick<PluginDraftContext, 'uid' | 'org'>,\n ): Promise<PluginDraftRefusal | null> {\n if (!host.exists) return { status: 404, error: 'Unknown site' }\n const data = (host.data() ?? {}) as StoredRule\n if (!holdsSiteKey(data, context.uid, 'site.publish')) return { status: 403, error: REDIRECT_AI_ROLE_REFUSAL }\n const declared = declaration()\n if (declared.entitlement && !checkEntitlement((context.org ?? {}) as never, declared.entitlement)) {\n return { status: 403, error: REDIRECT_AI_PLAN_REFUSAL }\n }\n if (await deps.lockdown({ uid: context.uid, org: context.org, host: data })) {\n return { status: 403, error: REDIRECT_AI_LOCKED_REFUSAL }\n }\n return null\n }\n\n /** Every rule document of the site, soft-deleted ones included. */\n async function readAllRules(hostId: string): Promise<Array<{ id: string; data: StoredRule }>> {\n const snapshot = await rulesOf(hostId).get()\n return snapshot.docs.map((doc) => ({ id: doc.id, data: (doc.data() ?? {}) as StoredRule }))\n }\n\n /** The live rules at a from-path, of `kind` when it is given. */\n function rulesAt(\n all: ReadonlyArray<{ id: string; data: StoredRule }>,\n source: string,\n kind: string | undefined,\n ): Array<{ id: string; data: StoredRule }> {\n return all.filter(({ data }) => {\n const ruleKind = typeof data['kind'] === 'string' && data['kind'] ? (data['kind'] as string) : 'exact'\n if (kind && ruleKind !== kind) return false\n return data['source'] === storedSourceOf(ruleKind, source)\n })\n }\n\n /** Why the rule a change leaves cannot be saved among the site's live rules, or `null`. */\n function setProblem(\n rows: readonly RedirectRuleRow[],\n rule: { $id: string; source: string; destination: string; kind: string },\n ): string | null {\n if (findDuplicateRedirect(rows, rule)) return `A rule for ${rule.source} already exists`\n const walk = walkRedirectChain(rows, rule)\n if (walk.loop) return 'That destination chains back to this rule — a redirect loop'\n if (walk.cycle) return 'That destination leads into a redirect loop other rules already make'\n return null\n }\n\n function livePageProblem(host: StoredRule, source: string, kind: string, asked: boolean | undefined): string | null {\n if (asked || kind === 'regex' || !redirectSourceIsLivePage(host['screens'] as Record<string, unknown>, source)) {\n return null\n }\n return `${source} is a published page, and a redirect there would take its place. Redirect it only when the person asks for that page to send visitors elsewhere.`\n }\n\n async function announce(hostId: string, paths: Array<string | null>): Promise<void> {\n const unique = [...new Set(paths.filter((path): path is string => Boolean(path)))]\n if (unique.length && deps.announcePaths) await deps.announcePaths(hostId, unique).catch(() => undefined)\n }\n\n const refuse = (status: PluginDraftRefusal['status'], error: string): PluginDraftWrite => ({ ok: false, status, error })\n\n return {\n refusal: async (context) => refusalFor(await hostRef(context.hostId).get(), context),\n\n check: (content) => checkRedirectDraftContent(content),\n\n read: async ({ hostId, id }) => {\n const snapshot = await rulesOf(hostId).doc(id).get()\n if (!snapshot.exists) return null\n const data = (snapshot.data() ?? {}) as StoredRule\n return recordOf(id, isLive(data) ? 'create' : 'remove', data)\n },\n\n write: async (request): Promise<PluginDraftWrite> => {\n const read = readRedirectDraftContent(request.content)\n if (read.ok === false) return refuse(400, read.problems[0] as string)\n const change = read.value\n const hostSnapshot = await hostRef(request.hostId).get()\n const refusal = await refusalFor(hostSnapshot, request)\n if (refusal) return { ok: false, ...refusal }\n const host = (hostSnapshot.data() ?? {}) as StoredRule\n const declared = declaration()\n const approvalField = declared.externalDestination?.approvedByField ?? REDIRECT_FIELD.approvedBy\n const stampFor = (destination: unknown): string | null =>\n declared.externalDestination && isExternalRedirectDestination(String(destination ?? '')) ? request.uid : null\n const at = Timestamp.fromDate(request.now)\n const all = await readAllRules(request.hostId)\n const live = all.filter(({ data }) => isLive(data))\n const rows: RedirectRuleRow[] = live.map(({ id, data }) => ({\n $id: id,\n source: data['source'] as string,\n destination: data['destination'] as string,\n kind: data['kind'] as string,\n }))\n\n if (change.action === 'create') {\n const ref = rulesOf(request.hostId).doc(request.id)\n const before = all.find((one) => one.id === request.id)\n if (before) return { ok: true, replayed: true, ...recordOf(request.id, 'create', before.data) }\n const rule = withDefaults({\n source: change.source,\n destination: change.destination,\n kind: change.kind ?? 'exact',\n statusCode: change.statusCode ?? 302,\n priority: change.priority ?? REDIRECT_DEFAULT_PRIORITY,\n enabled: change.enabled ?? true,\n })\n const problem = ruleProblem(rule, host)\n if (problem) return refuse(400, problem.detail)\n const fields = storedFields(rule, declared.fields)\n const source = String(fields['source'])\n const kind = String(fields['kind'] ?? 'exact')\n const shadow = livePageProblem(host, source, kind, change.replacesLivePage)\n if (shadow) return refuse(409, shadow)\n const clash = setProblem(rows, { $id: request.id, source, destination: String(fields['destination']), kind })\n if (clash) return refuse(409, clash)\n const stamp = stampFor(fields['destination'])\n const written = { ...fields, ...(stamp ? { [approvalField]: stamp } : {}) }\n const db = deps.firestore()\n const outcome = await db.runTransaction(async (tx): Promise<{ refusal?: string; existed?: StoredRule }> => {\n const existing = await tx.get(ref)\n if (existing.exists) return { existed: (existing.data() ?? {}) as StoredRule }\n if (declared.quotaKey) {\n // Live rules only, as the create route counts: a deleted rule frees its slot (AGL-3918).\n const used = await countLiveDocuments(rulesOf(request.hostId), (aggregate) => tx.get(aggregate))\n const quota = checkQuota((request.org ?? {}) as never, declared.quotaKey as never, Number(used) || 0)\n if (!quota.allowed) return { refusal: redirectAiLimitRefusal(quota.limit) }\n }\n tx.create(ref, { ...written, createdAt: at, updatedAt: at, createdBy: request.uid })\n return {}\n })\n if (outcome.refusal) return refuse(403, outcome.refusal)\n if (outcome.existed) return { ok: true, replayed: true, ...recordOf(request.id, 'create', outcome.existed) }\n if (deps.logCreated) {\n await deps.logCreated(request.hostId, request.uid, ref.id, declared).catch(() => undefined)\n }\n await announce(request.hostId, [announcedPath(fields)])\n return { ok: true, replayed: false, ...recordOf(ref.id, 'create', written) }\n }\n\n // An update or a remove names a rule the site has.\n const found = rulesAt(live, change.source, change.kind)\n if (found.length > 1) {\n return refuse(\n 409,\n `The site has more than one redirect at ${change.source}; say which match mode (exact, prefix or regex) to change`,\n )\n }\n const target = found[0]\n if (!target) {\n if (change.action === 'remove') {\n const gone = rulesAt(all, change.source, change.kind)[0]\n if (gone) return { ok: true, replayed: true, ...recordOf(gone.id, 'remove', gone.data) }\n }\n return refuse(404, `The site has no redirect from ${change.source}`)\n }\n const ref = rulesOf(request.hostId).doc(target.id)\n\n if (change.action === 'remove') {\n // The page's Delete: marked deleted and off, so the serve path drops it.\n await ref.set({ deletedAt: at, enabled: false }, { merge: true })\n await announce(request.hostId, [announcedPath(target.data as { kind?: string; source?: string })])\n return { ok: true, replayed: false, ...recordOf(target.id, 'remove', target.data) }\n }\n\n const current = redirectRuleValues(target.id, target.data)\n const rule = withDefaults({\n ...current,\n ...(change.newSource !== undefined ? { source: change.newSource } : {}),\n ...(change.destination !== undefined ? { destination: change.destination } : {}),\n ...(change.statusCode !== undefined ? { statusCode: change.statusCode } : {}),\n ...(change.priority !== undefined ? { priority: change.priority } : {}),\n ...(change.enabled !== undefined ? { enabled: change.enabled } : {}),\n })\n const problem = ruleProblem(rule, host)\n if (problem) return refuse(400, problem.detail)\n const fields = storedFields(rule, declared.fields)\n const source = String(fields['source'])\n const kind = String(fields['kind'] ?? 'exact')\n if (source !== current['source']) {\n const shadow = livePageProblem(host, source, kind, change.replacesLivePage)\n if (shadow) return refuse(409, shadow)\n }\n const clash = setProblem(rows, { $id: target.id, source, destination: String(fields['destination']), kind })\n if (clash) return refuse(409, clash)\n const stamp = stampFor(fields['destination'])\n const unchanged =\n Object.entries(fields).every(([key, value]) => current[key] === value) &&\n (current[approvalField] ?? null) === stamp\n const after = { ...target.data, ...fields, ...(stamp ? { [approvalField]: stamp } : {}) }\n if (unchanged) return { ok: true, replayed: true, ...recordOf(target.id, 'update', after) }\n // The page's edit: every field the form owns, the stamp set or removed,\n // `updatedAt`, merged so `lastHitAt` stays.\n await ref.set({ ...fields, [approvalField]: stamp ?? FieldValue.delete(), updatedAt: at }, { merge: true })\n await announce(request.hostId, [announcedPath(fields), announcedPath(target.data as { kind?: string; source?: string })])\n return { ok: true, replayed: false, ...recordOf(target.id, 'update', after) }\n },\n }\n}\n"],"names":["checkEntitlement","checkQuota","holdsSiteKey","siteKeyRefusal","countLiveDocuments","FieldValue","Timestamp","checkRedirectDraftContent","readRedirectDraftContent","REDIRECT_DEFAULT_PRIORITY","findDuplicateRedirect","isExternalRedirectDestination","normalizeRedirectSource","redirectSourceIsLivePage","walkRedirectChain","announcedPath","redirectDeclaration","redirectRuleValues","ruleProblem","storedFields","withDefaults","REDIRECT_FIELD","REDIRECT_AI_ROLE_REFUSAL","REDIRECT_AI_PLAN_REFUSAL","REDIRECT_AI_LOCKED_REFUSAL","redirectAiLimitRefusal","limit","factsOf","action","data","values","source","destination","kind","statusCode","priority","enabled","removed","live","recordOf","id","name","String","versionId","facts","isLive","Boolean","storedSourceOf","trim","createRedirectAiWriter","deps","declaration","hostRef","hostId","firestore","collection","doc","rulesOf","refusalFor","host","context","exists","status","error","uid","declared","entitlement","org","lockdown","readAllRules","snapshot","get","docs","map","rulesAt","all","filter","ruleKind","setProblem","rows","rule","walk","loop","cycle","livePageProblem","asked","announce","paths","unique","Set","path","length","announcePaths","catch","undefined","refuse","ok","refusal","check","content","read","write","request","hostSnapshot","fields","current","problems","change","value","approvalField","externalDestination","approvedByField","approvedBy","stampFor","at","fromDate","now","$id","ref","before","find","one","replayed","problem","detail","shadow","replacesLivePage","clash","stamp","written","db","outcome","runTransaction","tx","existing","existed","quotaKey","used","aggregate","quota","Number","allowed","create","createdAt","updatedAt","createdBy","logCreated","found","target","gone","set","deletedAt","merge","newSource","unchanged","Object","entries","every","key","after","delete"],"mappings":"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"}
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
17
|
+
import { holdsSiteKey, siteKeyRefusal } from "@aglyn/aglyn/app-utils/host-permissions";
|
|
17
18
|
import { firebaseAdmin } from "@aglyn/tenant-data-admin";
|
|
18
19
|
import { TENANT_APEX } from "@aglyn/aglyn/app-utils/tenant-apex";
|
|
19
20
|
import { findDuplicateRedirect, isSelfRedirect, normalizeRedirectDestination, normalizeRedirectSource, redirectSourceIsLivePage, REDIRECT_STATUS_CODES, validateRedirectRule, walkRedirectChain } from "../model/redirects.js";
|
|
@@ -113,17 +114,17 @@ import { findDuplicateRedirect, isSelfRedirect, normalizeRedirectDestination, no
|
|
|
113
114
|
statusCode: Number(body.statusCode)
|
|
114
115
|
};
|
|
115
116
|
try {
|
|
116
|
-
var
|
|
117
|
+
var _host_data;
|
|
117
118
|
const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken);
|
|
118
119
|
const hostRef = firebaseAdmin.app().firestore().collection('hosts').doc(hostId);
|
|
119
120
|
const host = await hostRef.get();
|
|
120
121
|
if (!host.exists) return res.status(404).json({
|
|
121
122
|
error: 'Unknown site'
|
|
122
123
|
});
|
|
123
|
-
|
|
124
|
-
if (
|
|
124
|
+
// "Publish site content" (`site.publish`, AGL-4047), as the rules ask it of a redirect.
|
|
125
|
+
if (!holdsSiteKey(host, decoded.uid, 'site.publish')) {
|
|
125
126
|
return res.status(403).json({
|
|
126
|
-
error: '
|
|
127
|
+
error: siteKeyRefusal('site.publish')
|
|
127
128
|
});
|
|
128
129
|
}
|
|
129
130
|
const snapshot = await hostRef.collection('redirects').limit(RULES_WINDOW).get();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/redirects/src/lib/server/redirect-check.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { type PluginApiHandler } from '@aglyn/aglyn/server'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport { TENANT_APEX } from '@aglyn/aglyn/app-utils/tenant-apex'\nimport {\n findDuplicateRedirect,\n isSelfRedirect,\n normalizeRedirectDestination,\n normalizeRedirectSource,\n redirectSourceIsLivePage,\n REDIRECT_STATUS_CODES,\n validateRedirectRule,\n walkRedirectChain,\n} from '../model/redirects'\n\n/** What a save is asked to write. */\nexport interface RedirectSaveDraft {\n /** The rule being edited; absent for a new rule. */\n id?: string\n kind?: string\n source: string\n destination: string\n statusCode?: number\n}\n\n/** The answer: the page's refusal in its own words, or the rule as the page would store it. */\nexport type RedirectSaveCheck =\n | { ok: false; problem: string }\n | { ok: true; source: string; destination: string; statusCode: number; kind: string; notice: string | null }\n\ntype Rule = { $id: string; source?: string; destination?: string; kind?: string; deletedAt?: unknown }\n\n/**\n * The Redirects page's save checks, in its order and its words\n * (`redirects-console-page.tsx` `handleSave`): the shared rule validation\n * (match mode, from-path or pattern, destination, another company's address),\n * a path redirected to itself, a duplicate rule, a chain that loops back, and\n * the published-page notice that warns without refusing. One source of truth\n * for every client that saves a rule: the native apps ask it here rather than\n * carry their own copy of the phishing screen and the pattern engine.\n */\nexport function checkRedirectSave(\n draft: RedirectSaveDraft,\n host: { cname?: unknown; subdomain?: unknown; screens?: unknown },\n rules: readonly Rule[],\n): RedirectSaveCheck {\n const kind = draft.kind || 'exact'\n const problem = validateRedirectRule(\n { kind, source: draft.source, destination: draft.destination },\n {\n ownDomains: [\n typeof host.cname === 'string' ? host.cname : null,\n typeof host.subdomain === 'string' && host.subdomain ? `${host.subdomain}.${TENANT_APEX}` : null,\n ],\n },\n )\n if (problem) return { ok: false, problem }\n const source = kind === 'regex' ? draft.source.trim() : (normalizeRedirectSource(draft.source) as string)\n const destination = normalizeRedirectDestination(draft.destination) as string\n if (kind !== 'regex' && isSelfRedirect({ source, destination })) {\n return { ok: false, problem: 'That would redirect the path to itself' }\n }\n const live = rules.filter((rule) => !rule.deletedAt)\n if (findDuplicateRedirect(live as never, { $id: draft.id, source, kind })) {\n return { ok: false, problem: `A rule for ${source} already exists` }\n }\n if (walkRedirectChain(live as never, { $id: draft.id, source, destination }).loop) {\n return { ok: false, problem: 'That destination chains back to this rule — a redirect loop' }\n }\n const statusCode = (REDIRECT_STATUS_CODES as readonly number[]).includes(Number(draft.statusCode))\n ? Number(draft.statusCode)\n : 302\n const notice = redirectSourceIsLivePage(host.screens as never, source)\n ? `${source} is a published page — the redirect takes precedence`\n : null\n return { ok: true, source, destination, statusCode, kind, notice }\n}\n\n/** Rules a check reads, past the page's 200-rule window, so a duplicate or loop beyond it is still caught. */\nconst RULES_WINDOW = 500\n\n/**\n * `POST /api/redirects/check`: runs {@link checkRedirectSave} for a member\n * who may publish on the site (the role the Firestore rules gate redirect\n * writes on). It reads and answers; it writes nothing, so the save itself\n * stays the same write the page makes, under the same rules.\n */\nexport const redirectCheckHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') return res.status(405).json({ error: 'Method not allowed' })\n const authorization = String(req.headers.authorization ?? '')\n const idToken = authorization.startsWith('Bearer ') ? authorization.slice('Bearer '.length) : ''\n if (!idToken) return res.status(401).json({ error: 'Unauthenticated' })\n const body = typeof req.body === 'string' ? JSON.parse(req.body || '{}') : (req.body ?? {})\n const hostId = String(body.hostId ?? '')\n if (!hostId || /^__.*__$/.test(hostId) || hostId.includes('/')) return res.status(400).json({ error: 'Missing hostId' })\n const draft: RedirectSaveDraft = {\n id: typeof body.id === 'string' && body.id ? body.id : undefined,\n kind: typeof body.kind === 'string' ? body.kind : undefined,\n source: String(body.source ?? '').slice(0, 2000),\n destination: String(body.destination ?? '').slice(0, 2000),\n statusCode: Number(body.statusCode),\n }\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n const hostRef = firebaseAdmin.app().firestore().collection('hosts').doc(hostId)\n const host = await hostRef.get()\n if (!host.exists) return res.status(404).json({ error: 'Unknown site' })\n const role = (host.get('memberRoles') ?? {})[decoded.uid]\n if (role !== 'admin' && role !== 'editor') {\n return res.status(403).json({ error: 'Changing a redirect needs a publishing role — ask an editor or admin' })\n }\n const snapshot = await hostRef.collection('redirects').limit(RULES_WINDOW).get()\n const rules = snapshot.docs.map((doc: { id: string; data: () => Record<string, unknown> }) => ({ $id: doc.id, ...doc.data() }))\n return res.status(200).json(checkRedirectSave(draft, host.data() ?? {}, rules))\n } catch (error) {\n console.error('[redirects/check]', error)\n return res.status(500).json({ error: 'The redirect could not be checked' })\n }\n}\n"],"names":["firebaseAdmin","TENANT_APEX","findDuplicateRedirect","isSelfRedirect","normalizeRedirectDestination","normalizeRedirectSource","redirectSourceIsLivePage","REDIRECT_STATUS_CODES","validateRedirectRule","walkRedirectChain","checkRedirectSave","draft","host","rules","kind","problem","source","destination","ownDomains","cname","subdomain","ok","trim","live","filter","rule","deletedAt","$id","id","loop","statusCode","includes","Number","notice","screens","RULES_WINDOW","redirectCheckHandler","req","res","body","method","status","json","error","authorization","String","headers","idToken","startsWith","slice","length","JSON","parse","hostId","test","undefined","decoded","app","auth","verifyIdToken","hostRef","firestore","collection","doc","get","exists","role","uid","snapshot","limit","docs","map","data","console"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/redirects/src/lib/server/redirect-check.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { type PluginApiHandler } from '@aglyn/aglyn/server'\nimport { holdsSiteKey, siteKeyRefusal } from '@aglyn/aglyn/app-utils/host-permissions'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport { TENANT_APEX } from '@aglyn/aglyn/app-utils/tenant-apex'\nimport {\n findDuplicateRedirect,\n isSelfRedirect,\n normalizeRedirectDestination,\n normalizeRedirectSource,\n redirectSourceIsLivePage,\n REDIRECT_STATUS_CODES,\n validateRedirectRule,\n walkRedirectChain,\n} from '../model/redirects'\n\n/** What a save is asked to write. */\nexport interface RedirectSaveDraft {\n /** The rule being edited; absent for a new rule. */\n id?: string\n kind?: string\n source: string\n destination: string\n statusCode?: number\n}\n\n/** The answer: the page's refusal in its own words, or the rule as the page would store it. */\nexport type RedirectSaveCheck =\n | { ok: false; problem: string }\n | { ok: true; source: string; destination: string; statusCode: number; kind: string; notice: string | null }\n\ntype Rule = { $id: string; source?: string; destination?: string; kind?: string; deletedAt?: unknown }\n\n/**\n * The Redirects page's save checks, in its order and its words\n * (`redirects-console-page.tsx` `handleSave`): the shared rule validation\n * (match mode, from-path or pattern, destination, another company's address),\n * a path redirected to itself, a duplicate rule, a chain that loops back, and\n * the published-page notice that warns without refusing. One source of truth\n * for every client that saves a rule: the native apps ask it here rather than\n * carry their own copy of the phishing screen and the pattern engine.\n */\nexport function checkRedirectSave(\n draft: RedirectSaveDraft,\n host: { cname?: unknown; subdomain?: unknown; screens?: unknown },\n rules: readonly Rule[],\n): RedirectSaveCheck {\n const kind = draft.kind || 'exact'\n const problem = validateRedirectRule(\n { kind, source: draft.source, destination: draft.destination },\n {\n ownDomains: [\n typeof host.cname === 'string' ? host.cname : null,\n typeof host.subdomain === 'string' && host.subdomain ? `${host.subdomain}.${TENANT_APEX}` : null,\n ],\n },\n )\n if (problem) return { ok: false, problem }\n const source = kind === 'regex' ? draft.source.trim() : (normalizeRedirectSource(draft.source) as string)\n const destination = normalizeRedirectDestination(draft.destination) as string\n if (kind !== 'regex' && isSelfRedirect({ source, destination })) {\n return { ok: false, problem: 'That would redirect the path to itself' }\n }\n const live = rules.filter((rule) => !rule.deletedAt)\n if (findDuplicateRedirect(live as never, { $id: draft.id, source, kind })) {\n return { ok: false, problem: `A rule for ${source} already exists` }\n }\n if (walkRedirectChain(live as never, { $id: draft.id, source, destination }).loop) {\n return { ok: false, problem: 'That destination chains back to this rule — a redirect loop' }\n }\n const statusCode = (REDIRECT_STATUS_CODES as readonly number[]).includes(Number(draft.statusCode))\n ? Number(draft.statusCode)\n : 302\n const notice = redirectSourceIsLivePage(host.screens as never, source)\n ? `${source} is a published page — the redirect takes precedence`\n : null\n return { ok: true, source, destination, statusCode, kind, notice }\n}\n\n/** Rules a check reads, past the page's 200-rule window, so a duplicate or loop beyond it is still caught. */\nconst RULES_WINDOW = 500\n\n/**\n * `POST /api/redirects/check`: runs {@link checkRedirectSave} for a member\n * who may publish on the site (the role the Firestore rules gate redirect\n * writes on). It reads and answers; it writes nothing, so the save itself\n * stays the same write the page makes, under the same rules.\n */\nexport const redirectCheckHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') return res.status(405).json({ error: 'Method not allowed' })\n const authorization = String(req.headers.authorization ?? '')\n const idToken = authorization.startsWith('Bearer ') ? authorization.slice('Bearer '.length) : ''\n if (!idToken) return res.status(401).json({ error: 'Unauthenticated' })\n const body = typeof req.body === 'string' ? JSON.parse(req.body || '{}') : (req.body ?? {})\n const hostId = String(body.hostId ?? '')\n if (!hostId || /^__.*__$/.test(hostId) || hostId.includes('/')) return res.status(400).json({ error: 'Missing hostId' })\n const draft: RedirectSaveDraft = {\n id: typeof body.id === 'string' && body.id ? body.id : undefined,\n kind: typeof body.kind === 'string' ? body.kind : undefined,\n source: String(body.source ?? '').slice(0, 2000),\n destination: String(body.destination ?? '').slice(0, 2000),\n statusCode: Number(body.statusCode),\n }\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n const hostRef = firebaseAdmin.app().firestore().collection('hosts').doc(hostId)\n const host = await hostRef.get()\n if (!host.exists) return res.status(404).json({ error: 'Unknown site' })\n // \"Publish site content\" (`site.publish`, AGL-4047), as the rules ask it of a redirect.\n if (!holdsSiteKey(host, decoded.uid, 'site.publish')) {\n return res.status(403).json({ error: siteKeyRefusal('site.publish') })\n }\n const snapshot = await hostRef.collection('redirects').limit(RULES_WINDOW).get()\n const rules = snapshot.docs.map((doc: { id: string; data: () => Record<string, unknown> }) => ({ $id: doc.id, ...doc.data() }))\n return res.status(200).json(checkRedirectSave(draft, host.data() ?? {}, rules))\n } catch (error) {\n console.error('[redirects/check]', error)\n return res.status(500).json({ error: 'The redirect could not be checked' })\n }\n}\n"],"names":["holdsSiteKey","siteKeyRefusal","firebaseAdmin","TENANT_APEX","findDuplicateRedirect","isSelfRedirect","normalizeRedirectDestination","normalizeRedirectSource","redirectSourceIsLivePage","REDIRECT_STATUS_CODES","validateRedirectRule","walkRedirectChain","checkRedirectSave","draft","host","rules","kind","problem","source","destination","ownDomains","cname","subdomain","ok","trim","live","filter","rule","deletedAt","$id","id","loop","statusCode","includes","Number","notice","screens","RULES_WINDOW","redirectCheckHandler","req","res","body","method","status","json","error","authorization","String","headers","idToken","startsWith","slice","length","JSON","parse","hostId","test","undefined","decoded","app","auth","verifyIdToken","hostRef","firestore","collection","doc","get","exists","uid","snapshot","limit","docs","map","data","console"],"mappings":"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"}
|
|
@@ -15,8 +15,8 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
15
15
|
* See the License for the specific language governing permissions and
|
|
16
16
|
* limitations under the License.
|
|
17
17
|
*/ import { createResourceUid } from "@aglyn/aglyn/app-utils/create-resource-uid";
|
|
18
|
+
import { holdsSiteKey, siteKeyRefusal } from "@aglyn/aglyn/app-utils/host-permissions";
|
|
18
19
|
import { TENANT_APEX } from "@aglyn/aglyn/app-utils/tenant-apex";
|
|
19
|
-
import { hostRoleCanPublish, hostRoleCanWrite } from "@aglyn/aglyn/app-utils/organizations";
|
|
20
20
|
import { checkEntitlement, checkQuota } from "@aglyn/aglyn/app-utils/plan-entitlements";
|
|
21
21
|
import { countLiveDocuments } from "@aglyn/aglyn/app-utils/live-document-count";
|
|
22
22
|
import { ACKNOWLEDGED_WARNING_CLASSES, TRANSFER_ID_FIELD, TRANSFER_WARNING_CLASSES, buildTransferPlan, isBlankTransferValue, matchLookupKey, planTransferUndo, transferValuesEqual, withTransferResourceFindings } from "@aglyn/aglyn/data-transfer";
|
|
@@ -352,7 +352,7 @@ function requireHost(ctx) {
|
|
|
352
352
|
const hostRef = (hostId)=>firestore.collection('hosts').doc(hostId);
|
|
353
353
|
const rulesOf = (hostId)=>hostRef(hostId).collection('redirects');
|
|
354
354
|
async function readSite(ctx) {
|
|
355
|
-
var _ref
|
|
355
|
+
var _ref;
|
|
356
356
|
const hostId = requireHost(ctx);
|
|
357
357
|
const [snapshot, org] = await Promise.all([
|
|
358
358
|
hostRef(hostId).get(),
|
|
@@ -360,15 +360,16 @@ function requireHost(ctx) {
|
|
|
360
360
|
]);
|
|
361
361
|
const host = (_ref = snapshot.exists ? snapshot.data() : null) != null ? _ref : {};
|
|
362
362
|
const declared = redirectDeclaration();
|
|
363
|
-
const role = ctx.actorUid ? ((_host_memberRoles = host['memberRoles']) != null ? _host_memberRoles : {})[ctx.actorUid] : undefined;
|
|
364
363
|
// The route's question, on the route's axis: a kind that is live the
|
|
365
|
-
// moment it exists needs
|
|
366
|
-
|
|
364
|
+
// moment it exists needs "Publish site content", never just "Edit site
|
|
365
|
+
// content" (AGL-4047).
|
|
366
|
+
const roleKey = declared.requiresPublishRole ? 'site.publish' : 'site.edit';
|
|
367
|
+
const roleOk = Boolean(ctx.actorUid) && holdsSiteKey(host, ctx.actorUid, roleKey);
|
|
367
368
|
// A plan-less workspace resolves as the free plan, which has no redirects.
|
|
368
369
|
const billing = org != null ? org : {};
|
|
369
370
|
let refusal = null;
|
|
370
371
|
if (!snapshot.exists) refusal = 'This site no longer exists.';
|
|
371
|
-
else if (!roleOk) refusal =
|
|
372
|
+
else if (!roleOk) refusal = siteKeyRefusal(roleKey);
|
|
372
373
|
else if (declared.entitlement && !checkEntitlement(billing, declared.entitlement)) {
|
|
373
374
|
refusal = 'URL redirects are not included in this workspace’s plan — see Billing';
|
|
374
375
|
}
|
|
@@ -910,14 +911,13 @@ function requireHost(ctx) {
|
|
|
910
911
|
};
|
|
911
912
|
},
|
|
912
913
|
async revert (ctx, snapshot, decisions) {
|
|
913
|
-
var
|
|
914
|
+
var _ref;
|
|
914
915
|
var _site_declared_externalDestination;
|
|
915
916
|
const site = await readSite(ctx);
|
|
916
917
|
// Undo writes rules too, so it needs the same role; a lapsed plan does
|
|
917
918
|
// not stop it, since a rule the plan no longer covers is not served.
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
throw new Error('Undoing a redirect import needs a publishing role on this site — ask an editor or admin');
|
|
919
|
+
if (!ctx.actorUid || !holdsSiteKey(site.host, ctx.actorUid, 'site.publish')) {
|
|
920
|
+
throw new Error(siteKeyRefusal('site.publish'));
|
|
921
921
|
}
|
|
922
922
|
const approvalField = (_ref = (_site_declared_externalDestination = site.declared.externalDestination) == null ? void 0 : _site_declared_externalDestination.approvedByField) != null ? _ref : REDIRECT_FIELD.approvedBy;
|
|
923
923
|
const done = [];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/redirects/src/lib/transfer/redirects-transfer.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { createResourceUid } from '@aglyn/aglyn/app-utils/create-resource-uid'\nimport { TENANT_APEX } from '@aglyn/aglyn/app-utils/tenant-apex'\nimport { hostRoleCanPublish, hostRoleCanWrite } from '@aglyn/aglyn/app-utils/organizations'\nimport { checkEntitlement, checkQuota } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport { countLiveDocuments } from '@aglyn/aglyn/app-utils/live-document-count'\nimport {\n ACKNOWLEDGED_WARNING_CLASSES,\n TRANSFER_ID_FIELD,\n TRANSFER_WARNING_CLASSES,\n buildTransferPlan,\n isBlankTransferValue,\n matchLookupKey,\n planTransferUndo,\n transferValuesEqual,\n withTransferResourceFindings,\n type BuildTransferPlanInput,\n type PlannedTransferRow,\n type RowMatchOutcome,\n type TransferFieldChange,\n type TransferPlan,\n type TransferPlanRow,\n type TransferPlanSummary,\n type TransferResourceFinding,\n type TransferRowResult,\n type TransferUndoEntry,\n type TransferUndoStep,\n type TransferWarning,\n} from '@aglyn/aglyn/data-transfer'\nimport {\n pluginHostResource,\n type ResolvedPluginHostResource,\n} from '@aglyn/aglyn/plugin-manager/plugin-host-resources'\nimport { dropPluginSiteCache } from '@aglyn/aglyn/plugin-manager/plugin-site-cache'\nimport type {\n PluginTransferResource,\n TransferApplyResult,\n TransferApplyWriter,\n TransferLookupResult,\n TransferMatchAnswer,\n TransferReadOptions,\n TransferReadPage,\n TransferResourceContext,\n TransferRevertDecisions,\n TransferRevertResult,\n} from '@aglyn/aglyn/plugin-manager/plugin-transfer-resources'\nimport { FieldPath, FieldValue, Timestamp } from 'firebase-admin/firestore'\nimport {\n REDIRECT_DEFAULT_PRIORITY,\n REDIRECT_STATUS_CODES,\n findDuplicateRedirect,\n isExternalRedirectDestination,\n isSelfRedirect,\n normalizeRedirectDestination,\n normalizeRedirectSource,\n redirectHitKey,\n redirectSourceIsLivePage,\n validateRedirectRule,\n walkRedirectChain,\n type RedirectRuleRow,\n} from '../model/redirects'\nimport {\n REDIRECTS_ALIAS_DICTIONARIES,\n REDIRECTS_DEFAULT_POLICY,\n REDIRECTS_MATCH_KEYS,\n REDIRECT_FIELD,\n REDIRECT_HOST_RESOURCE_KIND,\n REDIRECT_WRITABLE_FIELDS,\n canonicalRedirectKind,\n redirectValuesEqual,\n redirectsTransferCatalog,\n} from './redirects-transfer-fields'\n\n/*==========================================\n * THE REDIRECTS RESOURCE — a site's redirect rules, in and out of a file.\n *\n * A rule is a routing statement over the live site the moment it is written,\n * so an import writes nothing the redirects page or the create route would\n * not. Every write here makes the checks those paths make, in the same words:\n *\n * - who: the publishing role on the site (`hostRoleCanPublish`, the axis the\n * Firestore rules gate the collection on) and the plan's `redirects`\n * feature — read from the create route's own declaration of the kind, not\n * restated. Without either, every row is refused with the reason.\n * - what: the declaration's allow-list of fields, the page's normalization\n * of the from-path and the destination, its defaults (exact, 302, priority\n * 100, on), and `validateRedirectRule` — so a lookalike destination, a\n * pattern the matcher cannot run and a path that redirects to itself are\n * refused, as the page refuses them.\n * - against what: the page's duplicate check (`findDuplicateRedirect`) and\n * chain-loop walk (`walkRedirectChain`) over the site's rules as the\n * import leaves them, row by row in file order, so a row is refused\n * exactly when saving it after the rows above would be refused.\n * - how many: a new rule is counted and created in one transaction against\n * the plan's `redirectsPerHost`, as the create route does; the dry run\n * holds the creates past what the plan leaves.\n * - provenance: `externalDestinationApprovedBy` is never read from a file.\n * A written rule whose destination is off the site carries the importing\n * member's uid, the same stamp the page and the route write and the serve\n * path requires; one whose destination is on the site has it removed.\n * The dry run says so for every such row, naming the host.\n * - afterwards: a created rule gets the route's `createdAt`, `updatedAt`,\n * `createdBy` and its \"Created redirect\" activity line; every written\n * from-path (and one an update moved away from) is announced to the site\n * cache, `/` for a regex rule, as the page announces a save.\n *\n * The dry run also warns, without refusing, where the page warns or a\n * person would want to know: a from-path that is a published page, a\n * destination that is itself redirected (a chain), and one that leads into a\n * loop other rules make.\n *\n * Reads skip soft-deleted rules (`deletedAt`), as the page does. Undo\n * deletes a rule the import created — outright rather than soft-deleted:\n * the rule did not exist before the import, and a soft-deleted document\n * would keep occupying a slot the plan's counter counts — and restores the\n * fields an update changed, stamp included.\n *=========================================*/\n\n/** The billing document a plan's entitlement and counter are read from. */\ntype OrgBilling = Parameters<typeof checkEntitlement>[0]\n\nexport interface RedirectsTransferDeps {\n firestore: FirebaseFirestore.Firestore\n /** Defaults to `Date.now`. */\n now?: () => number\n /** The site's owning workspace's billing document (`getOrgForHost`); `null` reads as the free plan. */\n loadOrg(hostId: string): Promise<OrgBilling | null>\n /**\n * Drops the site's cached pages at these addresses, best effort. Defaults\n * to the platform's site cache (`dropPluginSiteCache`), the announcement\n * the create route makes for a new rule.\n */\n announcePaths?(hostId: string, paths: readonly string[]): Promise<void>\n /** Writes the site activity line for a created rule, best effort. */\n logCreated?(hostId: string, actorUid: string, ruleId: string, resource: ResolvedPluginHostResource): Promise<void>\n}\n\n/** How long before the chunk's budget runs out a row is not started. */\nconst ROW_RESERVE_MS = 2_000\n\n/** The most rows one export page reads. */\nconst PAGE_SIZE_DEFAULT = 500\nconst PAGE_SIZE_MAX = 1_000\n\n/** Values one `in` query may name. */\nconst IN_QUERY_MAX = 30\n\n/** Documents one `getAll` reads. */\nconst GET_ALL_MAX = 100\n\n/** Days the hit count sums, as the redirects page does. */\nconst HIT_DAYS = 30\n\nconst DAY_MS = 24 * 60 * 60 * 1000\n\n/*==========================================\n * A RULE AS THE TRANSFER READS IT\n *=========================================*/\n\ntype StoredRule = Record<string, unknown>\n\n/** The rule's six writable fields, as written. */\nexport interface RuleFields {\n source: string\n kind: string\n destination: string\n statusCode: number\n priority: number\n enabled: boolean\n}\n\nfunction text(value: unknown): string {\n return typeof value === 'string' ? value : ''\n}\n\n/** A stored time as ISO text, or `null`. */\nfunction isoOf(value: unknown): string | null {\n if (value === null || value === undefined) return null\n if (typeof (value as { toMillis?: unknown }).toMillis === 'function') {\n return new Date((value as { toMillis(): number }).toMillis()).toISOString()\n }\n if (value instanceof Date) return value.toISOString()\n if (typeof value === 'number' && Number.isFinite(value)) return new Date(value).toISOString()\n return null\n}\n\nfunction validStatusCode(value: unknown): value is RuleFields['statusCode'] {\n return (REDIRECT_STATUS_CODES as readonly number[]).includes(value as number)\n}\n\n/**\n * A stored rule's fields as the redirects page reads them into its editor: a\n * missing mode is exact, a missing priority 100, a status code outside the\n * four is 302 (as the serve path reads it), and a rule is on unless it says\n * `false`. The export, the dry run's \"before\" and undo all read this, so a\n * file exported and imported back is unchanged.\n */\nexport function redirectRuleValues(id: string, data: StoredRule): Record<string, unknown> {\n const priority = data['priority']\n return {\n [TRANSFER_ID_FIELD]: id,\n source: text(data['source']),\n kind: typeof data['kind'] === 'string' && data['kind'] ? data['kind'] : 'exact',\n destination: text(data['destination']),\n statusCode: validStatusCode(data['statusCode']) ? data['statusCode'] : 302,\n priority:\n priority !== null && priority !== undefined && Number.isFinite(Number(priority))\n ? Number(priority)\n : REDIRECT_DEFAULT_PRIORITY,\n enabled: data['enabled'] !== false,\n [REDIRECT_FIELD.approvedBy]:\n typeof data[REDIRECT_FIELD.approvedBy] === 'string' && data[REDIRECT_FIELD.approvedBy]\n ? data[REDIRECT_FIELD.approvedBy]\n : null,\n createdAt: isoOf(data['createdAt']),\n updatedAt: isoOf(data['updatedAt']),\n createdBy: typeof data['createdBy'] === 'string' ? data['createdBy'] : null,\n lastHitAt: isoOf(data['lastHitAt']),\n }\n}\n\n/** A rule as the whole-set checks read it: as stored, like the page's rows. */\nfunction ruleRow(id: string, data: StoredRule): RedirectRuleRow {\n return {\n $id: id,\n source: data['source'] as string,\n destination: data['destination'] as string,\n kind: data['kind'] as string,\n }\n}\n\nfunction isLive(data: StoredRule | undefined): data is StoredRule {\n return Boolean(data) && !data?.['deletedAt']\n}\n\n/**\n * The from-path as a rule of `kind` stores it: a pattern trimmed, a path\n * normalized (lowercase, no trailing slash, no query); `null` when a path\n * cannot be one.\n */\nfunction storedSource(kind: string, source: string): string | null {\n if (kind === 'regex') return source.trim() || null\n return normalizeRedirectSource(source)\n}\n\n/** What makes two rules one: their mode and their from-path, as stored. */\nfunction ruleKey(kind: string, source: string): string | null {\n const stored = storedSource(kind, source)\n return stored === null ? null : `${kind}\\u0000${stored}`\n}\n\n/** The address a rule answers at, for the site cache: a pattern names no one path, so `/`. */\nexport function announcedPath(rule: { kind?: string; source?: string }): string | null {\n const source = text(rule.source)\n if ((rule.kind ?? 'exact') === 'regex') return '/'\n return source.startsWith('/') ? source : null\n}\n\n/** The rule a row leaves: what the record held (or the defaults), with the row's changes. */\nfunction finalRule(\n before: Readonly<Record<string, unknown>> | null | undefined,\n diff: readonly TransferFieldChange[],\n): RuleFields {\n const values: Record<string, unknown> = { ...(before ?? {}) }\n for (const change of diff) values[change.fieldId] = change.after\n return withDefaults(values)\n}\n\n/**\n * The six fields with the page's defaults where a value is missing:\n * exact, 302, priority 100, on.\n */\nexport function withDefaults(values: Readonly<Record<string, unknown>>): RuleFields {\n const priority = values['priority']\n const kind = canonicalRedirectKind(values['kind'])\n return {\n source: text(values['source']),\n kind: isBlankTransferValue(values['kind']) ? 'exact' : (kind ?? String(values['kind'])),\n destination: text(values['destination']),\n statusCode: isBlankTransferValue(values['statusCode'])\n ? 302\n : (Number(values['statusCode']) as RuleFields['statusCode']),\n priority:\n priority !== null && priority !== undefined && Number.isFinite(Number(priority))\n ? Number(priority)\n : REDIRECT_DEFAULT_PRIORITY,\n enabled: values['enabled'] !== false,\n }\n}\n\n/** The site's own addresses, which a destination may always name. */\nfunction ownDomains(host: StoredRule): Array<string | null> {\n return [\n typeof host['cname'] === 'string' ? host['cname'] : null,\n typeof host['subdomain'] === 'string' && host['subdomain'] ? `${host['subdomain']}.${TENANT_APEX}` : null,\n ]\n}\n\n/**\n * Why the page would refuse to save this rule, or `null`: an unknown mode, a\n * from-path or pattern it cannot use, a destination it cannot use or that\n * wears another company's address, a status code it does not offer, or a\n * path redirected to itself.\n */\nexport function ruleProblem(rule: RuleFields, host: StoredRule): { detail: string; fieldId: string; value: string } | null {\n const problem = validateRedirectRule(\n { kind: rule.kind, source: rule.source, destination: rule.destination },\n { ownDomains: ownDomains(host) },\n )\n if (problem) {\n const fieldId =\n problem === 'Unknown match mode'\n ? REDIRECT_FIELD.kind\n : /destination/i.test(problem)\n ? REDIRECT_FIELD.destination\n : REDIRECT_FIELD.source\n return { detail: problem, fieldId, value: String((rule as unknown as Record<string, unknown>)[fieldId] ?? '') }\n }\n if (!validStatusCode(rule.statusCode)) {\n return {\n detail: 'Status codes are 301, 302, 307 or 308',\n fieldId: REDIRECT_FIELD.statusCode,\n value: String(rule.statusCode),\n }\n }\n if (rule.kind !== 'regex' && isSelfRedirect({ source: rule.source, destination: rule.destination })) {\n return { detail: 'That would redirect the path to itself', fieldId: REDIRECT_FIELD.destination, value: rule.destination }\n }\n return null\n}\n\n/** The rule's fields as stored: the page's normalization, and only what the create route's allow-list names. */\nexport function storedFields(rule: RuleFields, allowed: readonly string[]): Record<string, unknown> {\n const normalized: Record<string, unknown> = {\n source: rule.kind === 'regex' ? rule.source.trim() : (normalizeRedirectSource(rule.source) ?? rule.source),\n destination: normalizeRedirectDestination(rule.destination) ?? rule.destination,\n statusCode: rule.statusCode,\n kind: rule.kind,\n priority: rule.priority,\n enabled: rule.enabled,\n }\n return Object.fromEntries(Object.entries(normalized).filter(([key]) => allowed.includes(key)))\n}\n\nfunction hostnameOf(destination: string): string {\n try {\n return new URL(destination).hostname || destination\n } catch {\n return destination\n }\n}\n\n/*==========================================\n * THE SITE, AS A WRITE IS DECIDED AGAINST IT\n *=========================================*/\n\ninterface SiteState {\n hostId: string\n host: StoredRule\n org: OrgBilling | null\n declared: ResolvedPluginHostResource\n /** Why nothing may be written, or `null`. */\n refusal: string | null\n /** The plan's cap on rules on the site, `Infinity` for none. */\n limit: number\n}\n\nfunction requireHost(ctx: TransferResourceContext): string {\n if (!ctx.hostId) throw new Error('Redirects belong to a site; open the import from the site.')\n return ctx.hostId\n}\n\n/** The redirect kind's declaration, which the create route reads too. */\nexport function redirectDeclaration(): ResolvedPluginHostResource {\n const declared = pluginHostResource(REDIRECT_HOST_RESOURCE_KIND)\n if (!declared) throw new Error(`The \"${REDIRECT_HOST_RESOURCE_KIND}\" kind is not declared.`)\n return declared\n}\n\n/** Field ids → values, holding only `fieldIds`. */\nfunction pick(values: Readonly<Record<string, unknown>>, fieldIds: readonly string[]): Record<string, unknown> {\n const row: Record<string, unknown> = {}\n for (const fieldId of fieldIds) row[fieldId] = values[fieldId] ?? null\n return row\n}\n\n/** Each `size` items of `items`. */\nfunction slices<T>(items: readonly T[], size: number): T[][] {\n const out: T[][] = []\n for (let at = 0; at < items.length; at += size) out.push(items.slice(at, at + size))\n return out\n}\n\n/**\n * The plan with every create past `remaining` failed as `planLimit`, counted\n * after the rows the plugin refused — a refused row takes no slot.\n */\nfunction withCreateLimit(plan: TransferPlan, remaining: number, limit: number): TransferPlan {\n if (!Number.isFinite(remaining)) return plan\n let creates = 0\n const held: PlannedTransferRow[] = []\n const rows = plan.rows.map((row) => {\n if (row.verdict !== 'create') return row\n if (creates < remaining) {\n creates += 1\n return row\n }\n const failed: PlannedTransferRow = {\n ...row,\n verdict: 'fail',\n reason: 'planLimit',\n diff: [],\n warnings: row.warnings.includes('planLimit') ? row.warnings : [...row.warnings, 'planLimit'],\n }\n held.push(failed)\n return failed\n })\n if (!held.length) return plan\n const summary: TransferPlanSummary = { create: 0, update: 0, unchanged: 0, skip: 0, fail: 0, total: rows.length }\n for (const row of rows) summary[row.verdict] += 1\n const detail = `Your plan includes ${limit} redirects on this site`\n const prior = plan.warnings.find((entry) => entry.class === 'planLimit')\n const raised: TransferWarning = {\n class: 'planLimit',\n count: (prior?.count ?? 0) + held.length,\n rows: (prior?.rows ?? 0) + held.length,\n fieldIds: prior?.fieldIds ?? [],\n samples: [...(prior?.samples ?? []), ...held.map((row) => ({ row: row.index, detail }))].slice(0, 5),\n requiresAcknowledgement: ACKNOWLEDGED_WARNING_CLASSES.includes('planLimit'),\n }\n const warnings = TRANSFER_WARNING_CLASSES.flatMap((entry) =>\n entry === 'planLimit' ? [raised] : plan.warnings.filter((warning) => warning.class === entry),\n )\n return {\n rows,\n summary,\n warnings,\n acknowledgementsRequired: warnings.filter((entry) => entry.requiresAcknowledgement).map((entry) => entry.class),\n }\n}\n\n/*==========================================\n * MATCHING, DEFAULTS AND SAMENESS (AGL-3548)\n *=========================================*/\n\n/**\n * Each row's rule, as the page tells rules apart: a row the engine found by\n * its id keeps that rule; any other row is the rule of its mode (exact when\n * the file gives none) at its from-path as stored — so `/blog` names the\n * exact rule or the prefix rule beside it by the row's mode, never both.\n * Two rows naming one rule are both matched to it here, and the plan refuses\n * the later one, saying which row it repeats, rather than holding it back as\n * a duplicate. `existing` holds every live rule's values by id; running this\n * over its own outcomes changes nothing.\n */\nexport function redirectMatchOutcomes(\n rows: ReadonlyArray<Readonly<Record<string, unknown>>>,\n engine: readonly RowMatchOutcome[],\n existing: ReadonlyMap<string, Readonly<Record<string, unknown>>>,\n live: ReadonlyArray<{ id: string; data: StoredRule }>,\n): RowMatchOutcome[] {\n // Each live rule under its mode and stored from-path.\n const byKey = new Map<string, string[]>()\n for (const { id, data } of live) {\n const key = ruleKey(text(data['kind']) || 'exact', text(data['source']))\n if (key) byKey.set(key, [...(byKey.get(key) ?? []), id])\n }\n return rows.map((values, at): RowMatchOutcome => {\n const found = engine[at] ?? { kind: 'new' as const }\n const byId = found.kind !== 'new' && found.via.fieldId === TRANSFER_ID_FIELD\n const record = found.kind === 'matched' && byId ? existing.get(found.recordId) : undefined\n const rowKind = isBlankTransferValue(values['kind'])\n ? text(record?.['kind']) || 'exact'\n : canonicalRedirectKind(values['kind'])\n const source = isBlankTransferValue(values['source']) ? text(record?.['source']) : text(values['source'])\n const key = rowKind && source ? ruleKey(rowKind, source) : null\n if (byId || !key || !rowKind) return found\n const candidates = byKey.get(key) ?? []\n const via = { fieldId: REDIRECT_FIELD.source, value: storedSource(rowKind, source) as string }\n if (!candidates.length) return { kind: 'new' }\n return candidates.length === 1\n ? { kind: 'matched', recordId: candidates[0] as string, via }\n : { kind: 'ambiguous', recordIds: candidates, via }\n })\n}\n\n/*==========================================\n * THE RESOURCE\n *=========================================*/\n\n/**\n * The id a rule an import creates is written under: the job's row, so a\n * write retried after it landed and before the ledger recorded it finds its\n * own rule instead of making a second one. Without a job, a fresh resource id.\n */\nexport function importedRedirectId(jobId: string | undefined, row: number): string {\n return jobId ? `${jobId}-${row}` : createResourceUid()\n}\n\n/**\n * The redirects resource over `deps.firestore`: `hosts/{hostId}/redirects`,\n * with every hook the job engine and the export route call. See the block\n * above for what each write checks.\n */\nexport function createRedirectsTransferResource(deps: RedirectsTransferDeps): PluginTransferResource {\n const now = deps.now ?? (() => Date.now())\n const { firestore } = deps\n const announce =\n deps.announcePaths ??\n (async (hostId: string, paths: readonly string[]) => {\n await dropPluginSiteCache({\n hostIds: [hostId],\n paths: { [hostId]: [...paths] },\n reason: 'redirect rules were imported',\n })\n })\n\n const hostRef = (hostId: string) => firestore.collection('hosts').doc(hostId)\n const rulesOf = (hostId: string) => hostRef(hostId).collection('redirects')\n\n async function readSite(ctx: TransferResourceContext): Promise<SiteState> {\n const hostId = requireHost(ctx)\n const [snapshot, org] = await Promise.all([hostRef(hostId).get(), deps.loadOrg(hostId)])\n const host = (snapshot.exists ? snapshot.data() : null) ?? {}\n const declared = redirectDeclaration()\n const role = ctx.actorUid ? ((host['memberRoles'] ?? {}) as Record<string, unknown>)[ctx.actorUid] : undefined\n // The route's question, on the route's axis: a kind that is live the\n // moment it exists needs the publishing role, never just the write role.\n const roleOk = declared.requiresPublishRole ? hostRoleCanPublish(role) : hostRoleCanWrite(role)\n // A plan-less workspace resolves as the free plan, which has no redirects.\n const billing = org ?? {}\n let refusal: string | null = null\n if (!snapshot.exists) refusal = 'This site no longer exists.'\n else if (!roleOk) refusal = 'Importing redirects needs a publishing role on this site — ask an editor or admin'\n else if (declared.entitlement && !checkEntitlement(billing, declared.entitlement)) {\n refusal = 'URL redirects are not included in this workspace’s plan — see Billing'\n }\n const limit = declared.quotaKey\n ? checkQuota(billing, declared.quotaKey as Parameters<typeof checkQuota>[1], 0).limit\n : Number.POSITIVE_INFINITY\n return { hostId, host, org: billing, declared, refusal, limit }\n }\n\n /** Every rule document of the site, soft-deleted ones included. */\n async function readAllRules(hostId: string): Promise<Array<{ id: string; data: StoredRule }>> {\n const snapshot = await rulesOf(hostId).get()\n return snapshot.docs.map((doc) => ({ id: doc.id, data: doc.data() as StoredRule }))\n }\n\n async function readByIds(hostId: string, ids: readonly string[]): Promise<Map<string, StoredRule>> {\n const found = new Map<string, StoredRule>()\n for (const slice of slices([...new Set(ids)], GET_ALL_MAX)) {\n if (!slice.length) continue\n const snapshots = await firestore.getAll(...slice.map((id) => rulesOf(hostId).doc(id)))\n for (const snapshot of snapshots) {\n const data = snapshot.exists ? (snapshot.data() as StoredRule) : undefined\n if (isLive(data)) found.set(snapshot.id, data)\n }\n }\n return found\n }\n\n /** Each rule's sampled hits over the last thirty days, by its hit key. */\n async function readHits(hostId: string): Promise<Record<string, number>> {\n const today = now()\n const refs = Array.from({ length: HIT_DAYS }, (_, index) =>\n hostRef(hostId)\n .collection('analytics')\n .doc(new Date(today - index * DAY_MS).toISOString().slice(0, 10)),\n )\n const totals: Record<string, number> = {}\n for (const snapshot of await firestore.getAll(...refs)) {\n if (!snapshot.exists) continue\n const day = ((snapshot.data() as StoredRule)['redirects'] ?? {}) as Record<string, unknown>\n for (const [key, count] of Object.entries(day)) {\n const value = Number(count)\n if (Number.isFinite(value)) totals[key] = (totals[key] ?? 0) + value\n }\n }\n return totals\n }\n\n function exportRow(\n id: string,\n data: StoredRule,\n fieldIds: readonly string[],\n hits: Record<string, number> | null,\n ): Record<string, unknown> {\n const values = redirectRuleValues(id, data)\n if (hits) values[REDIRECT_FIELD.hits] = hits[redirectHitKey(id)] ?? 0\n return pick(values, fieldIds)\n }\n\n const resource: PluginTransferResource = {\n fields: () => redirectsTransferCatalog(),\n matchKeys: REDIRECTS_MATCH_KEYS,\n aliases: REDIRECTS_ALIAS_DICTIONARIES,\n defaultPolicy: REDIRECTS_DEFAULT_POLICY,\n valuesEqual: redirectValuesEqual,\n\n /*\n * Rows matched as the page tells rules apart (`redirectMatchOutcomes`),\n * over every live rule of the site — so the Matching step and the\n * Conflicts step show the rule the dry run will update.\n */\n async match(ctx, input): Promise<TransferMatchAnswer> {\n const hostId = requireHost(ctx)\n const live = (await readAllRules(hostId)).filter((entry) => isLive(entry.data))\n const existing = new Map(input.records)\n for (const { id, data } of live) existing.set(id, redirectRuleValues(id, data))\n const outcomes = redirectMatchOutcomes(input.rows, input.outcomes, existing, live)\n const records = new Map<string, Readonly<Record<string, unknown>>>()\n for (const outcome of outcomes) {\n const ids = outcome.kind === 'matched' ? [outcome.recordId] : outcome.kind === 'ambiguous' ? outcome.recordIds : []\n for (const id of ids) {\n const values = existing.get(id)\n if (values && !input.records.has(id)) records.set(id, values)\n }\n }\n return { outcomes, records }\n },\n\n /*\n * `filter` is not read: the redirects page lists every rule and has no\n * filter to export by. `scopeTokens` does not narrow either: rules are a\n * site's, carry no `visibleTo`, and the export route admits a reader to\n * a site's records only when they reach the site.\n */\n async count(ctx: TransferResourceContext, options: TransferReadOptions): Promise<number> {\n const hostId = requireHost(ctx)\n if (options.ids) return (await readByIds(hostId, options.ids)).size\n return countLiveDocuments(rulesOf(hostId))\n },\n\n async readPage(\n ctx: TransferResourceContext,\n cursor: string | null,\n fieldIds: readonly string[],\n options: TransferReadOptions = {},\n ): Promise<TransferReadPage> {\n const hostId = requireHost(ctx)\n const size = Math.min(PAGE_SIZE_MAX, Math.max(1, Math.floor(options.pageSize ?? PAGE_SIZE_DEFAULT)))\n const hits = fieldIds.includes(REDIRECT_FIELD.hits) ? await readHits(hostId) : null\n if (options.ids) {\n // The selection, in the order it was made; the cursor is how far into it.\n const start = Math.max(0, Number(cursor ?? 0) || 0)\n const ids = options.ids.slice(start, start + size)\n const found = await readByIds(hostId, ids)\n const rows = ids.flatMap((id) => {\n const data = found.get(id)\n return data ? [exportRow(id, data, fieldIds, hits)] : []\n })\n return { rows, next: start + size < options.ids.length ? String(start + size) : null }\n }\n // Ordered by document id, the one order every rule has: a rule with no\n // `source` still exports (the page sorts by source, in the browser).\n let query = rulesOf(hostId).orderBy(FieldPath.documentId()).limit(size)\n if (cursor) query = query.startAfter(cursor)\n const snapshot = await query.get()\n const rows = snapshot.docs.flatMap((doc) => {\n const data = doc.data() as StoredRule\n return isLive(data) ? [exportRow(doc.id, data, fieldIds, hits)] : []\n })\n const last = snapshot.docs[snapshot.docs.length - 1]\n return { rows, next: snapshot.docs.length === size && last ? last.id : null }\n },\n\n /*\n * By id, and by from-path: each value is read as the stored form of a\n * path (normalized) and of a pattern (trimmed), and a rule answers for it\n * under its own mode. The plan narrows a row's candidates to the row's\n * mode; this answers every rule the value could mean.\n */\n async lookup(ctx, requests): Promise<TransferLookupResult> {\n const hostId = requireHost(ctx)\n const lookup = new Map<string, string[]>()\n const records = new Map<string, Readonly<Record<string, unknown>>>()\n const file = (fieldId: string, value: string, id: string) => {\n const key = matchLookupKey(fieldId, value)\n const ids = lookup.get(key) ?? []\n if (!ids.includes(id)) ids.push(id)\n lookup.set(key, ids)\n }\n for (const request of requests) {\n if (request.fieldId === TRANSFER_ID_FIELD) {\n const found = await readByIds(hostId, request.values)\n for (const [id, data] of found) {\n file(TRANSFER_ID_FIELD, id, id)\n records.set(id, redirectRuleValues(id, data))\n }\n continue\n }\n if (request.fieldId !== REDIRECT_FIELD.source) continue\n const stored = new Set<string>()\n for (const value of request.values) {\n for (const form of [normalizeRedirectSource(value), value.trim()]) if (form) stored.add(form)\n }\n for (const slice of slices([...stored], IN_QUERY_MAX)) {\n const snapshot = await rulesOf(hostId).where('source', 'in', slice).get()\n for (const doc of snapshot.docs) {\n const data = doc.data() as StoredRule\n if (!isLive(data)) continue\n const kind = typeof data['kind'] === 'string' && data['kind'] ? data['kind'] : 'exact'\n const held = storedSource(kind, text(data['source']))\n for (const value of request.values) {\n if (held !== null && storedSource(kind, value) === held) {\n file(REDIRECT_FIELD.source, value, doc.id)\n records.set(doc.id, redirectRuleValues(doc.id, data))\n }\n }\n }\n }\n }\n return { lookup, records }\n },\n\n async plan(ctx, input: BuildTransferPlanInput): Promise<TransferPlan> {\n const site = await readSite(ctx)\n const documents = await readAllRules(site.hostId)\n const live = documents.filter((entry) => isLive(entry.data))\n const existing = new Map(input.existing)\n for (const { id, data } of live) existing.set(id, redirectRuleValues(id, data))\n\n // The resource's own matching (also the engine's `match` hook, so the\n // Matching and Conflicts steps showed these outcomes): run again over\n // the site as read here, which leaves outcomes it made unchanged.\n const matches = redirectMatchOutcomes(\n input.rows.map((row) => row.values),\n input.matches,\n existing,\n live,\n )\n\n // The row's values as the page would store them, so the review's\n // before → after compares like with like, and a new rule shows the\n // defaults it is created with.\n const rows = input.rows.map((row, at): TransferPlanRow => {\n const match = matches[at] as RowMatchOutcome\n const record = match.kind === 'matched' ? existing.get(match.recordId) : undefined\n const values: Record<string, unknown> = { ...row.values }\n const kind = canonicalRedirectKind(values['kind'])\n if (kind) values['kind'] = kind\n const sourceKind = kind ?? (isBlankTransferValue(values['kind']) ? text(record?.['kind']) || 'exact' : null)\n if (typeof values['source'] === 'string' && sourceKind) {\n values['source'] = storedSource(sourceKind, values['source']) ?? values['source'].trim()\n }\n if (typeof values['destination'] === 'string') {\n values['destination'] = normalizeRedirectDestination(values['destination']) ?? values['destination'].trim()\n }\n if (match.kind === 'new') {\n const defaults: Record<string, unknown> = {\n kind: 'exact',\n statusCode: 302,\n priority: REDIRECT_DEFAULT_PRIORITY,\n enabled: true,\n }\n for (const [fieldId, value] of Object.entries(defaults)) {\n if (isBlankTransferValue(values[fieldId])) values[fieldId] = value\n }\n }\n return { ...row, values }\n })\n\n const plan = buildTransferPlan({ ...input, rows, matches, existing })\n const findings: TransferResourceFinding[] = []\n const writing = plan.rows.filter((row) => row.verdict === 'create' || row.verdict === 'update')\n\n if (site.refusal) {\n for (const row of writing) findings.push({ row: row.index, detail: site.refusal, refuse: true })\n return withTransferResourceFindings(plan, findings)\n }\n\n // What the page refuses about a rule on its own.\n const accepted: Array<{ row: PlannedTransferRow; rule: RuleFields }> = []\n for (const row of writing) {\n const rule = finalRule(row.recordId ? existing.get(row.recordId) : null, row.diff)\n const problem = ruleProblem(rule, site.host)\n if (problem) {\n findings.push({ row: row.index, ...problem, refuse: true })\n continue\n }\n accepted.push({ row, rule })\n }\n\n // What the page refuses against the other rules, row by row in file\n // order over the site as the rows above leave it.\n const liveRows = new Map(live.map(({ id, data }) => [id, ruleRow(id, data)]))\n const touched = new Set(accepted.flatMap(({ row }) => (row.recordId ? [row.recordId] : [])))\n const ruleset: RedirectRuleRow[] = [...liveRows.values()].filter((rule) => !touched.has(rule.$id as string))\n const kept: Array<{ row: PlannedTransferRow; rule: RuleFields; candidate: Required<RedirectRuleRow> }> = []\n const keptByKey = new Map<string, number>()\n const keptByRecord = new Map<string, number>()\n for (const { row, rule } of accepted) {\n const candidate = {\n $id: row.recordId ?? `row:${row.index}`,\n source: rule.source,\n destination: rule.destination,\n kind: rule.kind,\n }\n const key = ruleKey(rule.kind, rule.source) as string\n const repeated = keptByKey.get(key) ?? (row.recordId ? keptByRecord.get(row.recordId) : undefined)\n let detail: string | null = null\n if (repeated !== undefined) detail = `Row ${repeated + 1} already redirects ${rule.source}`\n else if (findDuplicateRedirect(ruleset, candidate)) detail = `A rule for ${rule.source} already exists`\n else if (walkRedirectChain(ruleset, candidate).loop) {\n detail = 'That destination chains back to this rule — a redirect loop'\n }\n if (detail) {\n const fieldId = repeated !== undefined ? REDIRECT_FIELD.source : REDIRECT_FIELD.destination\n findings.push({ row: row.index, detail, fieldId, value: String(rule[fieldId]), refuse: true })\n // The rule it would have changed stays as it is.\n const current = row.recordId ? liveRows.get(row.recordId) : undefined\n if (current) ruleset.push(current)\n continue\n }\n ruleset.push(candidate)\n kept.push({ row, rule, candidate })\n keptByKey.set(key, row.index)\n if (row.recordId) keptByRecord.set(row.recordId, row.index)\n }\n\n // What a person should know about the rules that will be written, read\n // over the site as the whole import leaves it.\n for (const { row, rule, candidate } of kept) {\n const walk = walkRedirectChain(ruleset, candidate)\n const path = [rule.destination, ...walk.hops].join(' → ')\n if (walk.loop || walk.cycle) {\n findings.push({\n row: row.index,\n detail: `Leads into a redirect loop: ${path}`,\n fieldId: REDIRECT_FIELD.destination,\n value: rule.destination,\n })\n } else if (walk.hops.length) {\n findings.push({\n row: row.index,\n detail: `Chains through another rule: ${path}`,\n fieldId: REDIRECT_FIELD.destination,\n value: rule.destination,\n })\n }\n if (isExternalRedirectDestination(rule.destination)) {\n findings.push({\n row: row.index,\n detail: `Sends visitors to ${hostnameOf(rule.destination)}; importing approves it in your name`,\n fieldId: REDIRECT_FIELD.destination,\n value: rule.destination,\n })\n }\n if (redirectSourceIsLivePage((site.host as { screens?: Record<string, unknown> }).screens, rule.source)) {\n findings.push({\n row: row.index,\n detail: `${rule.source} is a published page — the redirect takes precedence`,\n fieldId: REDIRECT_FIELD.source,\n value: rule.source,\n })\n }\n }\n\n // The plan's counter counts the LIVE rules, as the create route counts\n // them: a deleted rule frees its slot, a disabled one keeps it (AGL-3918).\n const remaining = Math.max(0, site.limit - live.length)\n return withCreateLimit(withTransferResourceFindings(plan, findings), remaining, site.limit)\n },\n\n async apply(ctx, chunk, writer: TransferApplyWriter): Promise<TransferApplyResult> {\n const site = await readSite(ctx)\n const actor = ctx.actorUid as string\n const allowed = site.declared.fields\n const results: TransferRowResult[] = []\n const undo: TransferUndoEntry[] = []\n const paths: string[] = []\n\n const fail = (row: PlannedTransferRow, message: string, reason?: TransferRowResult['reason']) => ({\n result: {\n row: row.index,\n outcome: 'failed' as const,\n ...(row.recordId ? { recordId: row.recordId } : {}),\n ...(reason ? { reason } : {}),\n message,\n },\n entry: undefined,\n })\n\n const approvalField = site.declared.externalDestination?.approvedByField ?? REDIRECT_FIELD.approvedBy\n const stampFor = (destination: unknown): string | null =>\n site.declared.externalDestination && isExternalRedirectDestination(String(destination ?? '')) ? actor : null\n\n const create = async (row: PlannedTransferRow) => {\n const rule = finalRule(null, row.diff)\n const problem = ruleProblem(rule, site.host)\n if (problem) return fail(row, problem.detail, 'resourceRule')\n const fields = storedFields(rule, allowed)\n const stamp = stampFor(fields['destination'])\n const ref = rulesOf(site.hostId).doc(importedRedirectId(ctx.jobId, row.index))\n const at = Timestamp.fromMillis(now())\n // Counted and created in one transaction, as the create route does,\n // so two imports (or an import and the page) cannot both take the\n // last slot.\n const outcome = await firestore.runTransaction(async (tx): Promise<{ refusal?: string; existed?: boolean }> => {\n if ((await tx.get(ref)).exists) return { existed: true }\n if (site.declared.quotaKey && Number.isFinite(site.limit)) {\n // Live rules only, as the create route counts (AGL-3918).\n const used = await countLiveDocuments(rulesOf(site.hostId), (aggregate) => tx.get(aggregate))\n const quota = checkQuota(site.org, site.declared.quotaKey as Parameters<typeof checkQuota>[1], used)\n if (!quota.allowed) {\n return { refusal: `Your plan includes ${quota.limit} ${site.declared.label} — upgrade in Billing for more` }\n }\n }\n tx.create(ref, {\n ...fields,\n ...(stamp ? { [approvalField]: stamp } : {}),\n createdAt: at,\n updatedAt: at,\n createdBy: actor,\n })\n return {}\n })\n if (outcome.refusal) return fail(row, outcome.refusal, 'planLimit')\n const written: Record<string, unknown> = { ...fields, ...(stamp ? { [approvalField]: stamp } : {}) }\n if (!outcome.existed && deps.logCreated) {\n await deps.logCreated(site.hostId, actor, ref.id, site.declared).catch(() => undefined)\n }\n const path = announcedPath(fields)\n if (path) paths.push(path)\n const result: TransferRowResult = { row: row.index, outcome: 'created', recordId: ref.id }\n const entry: TransferUndoEntry = { row: row.index, recordId: ref.id, action: 'created', written }\n return { result, entry }\n }\n\n const update = async (row: PlannedTransferRow) => {\n const id = row.recordId as string\n const ref = rulesOf(site.hostId).doc(id)\n const snapshot = await ref.get()\n const data = snapshot.exists ? (snapshot.data() as StoredRule) : undefined\n if (!isLive(data)) return fail(row, 'This redirect was deleted after the dry run.', 'matchedRecordMissing')\n const current = redirectRuleValues(id, data)\n const rule = finalRule(current, row.diff)\n const problem = ruleProblem(rule, site.host)\n if (problem) return fail(row, problem.detail, 'resourceRule')\n const fields = storedFields(rule, allowed)\n const stamp = stampFor(fields['destination'])\n const previous: Record<string, unknown> = {}\n const written: Record<string, unknown> = {}\n for (const fieldId of REDIRECT_WRITABLE_FIELDS) {\n if (!(fieldId in fields) || transferValuesEqual(current[fieldId], fields[fieldId])) continue\n previous[fieldId] = current[fieldId] ?? null\n written[fieldId] = fields[fieldId]\n }\n if (!transferValuesEqual(current[approvalField], stamp)) {\n previous[approvalField] = current[approvalField] ?? null\n written[approvalField] = stamp\n }\n if (!Object.keys(written).length) {\n return { result: { row: row.index, outcome: 'unchanged' as const, recordId: id }, entry: undefined }\n }\n // The page's edit: every field the form owns, the stamp set or\n // removed, `updatedAt`, merged so `lastHitAt` and `deletedAt` stay.\n await ref.set(\n { ...fields, [approvalField]: stamp ?? FieldValue.delete(), updatedAt: Timestamp.fromMillis(now()) },\n { merge: true },\n )\n for (const rulePath of [announcedPath(fields), announcedPath({ kind: text(data['kind']), source: text(data['source']) })]) {\n if (rulePath) paths.push(rulePath)\n }\n const result: TransferRowResult = { row: row.index, outcome: 'updated', recordId: id }\n const entry: TransferUndoEntry = { row: row.index, recordId: id, action: 'updated', previous, written }\n return { result, entry }\n }\n\n for (const row of chunk.rows) {\n const before = await writer.alreadyApplied(row.index)\n if (before) {\n results.push(before)\n continue\n }\n if (writer.timeLeftMs() < ROW_RESERVE_MS) break\n const outcome = site.refusal\n ? fail(row, site.refusal, 'resourceRule')\n : row.verdict === 'create'\n ? await create(row)\n : await update(row)\n await writer.markApplied(outcome.result, outcome.entry)\n results.push(outcome.result)\n if (outcome.entry) undo.push(outcome.entry)\n }\n\n if (paths.length) await announce(site.hostId, [...new Set(paths)]).catch(() => undefined)\n return { results, undo }\n },\n\n async revert(ctx, snapshot, decisions?: TransferRevertDecisions): Promise<TransferRevertResult> {\n const site = await readSite(ctx)\n // Undo writes rules too, so it needs the same role; a lapsed plan does\n // not stop it, since a rule the plan no longer covers is not served.\n const role = ctx.actorUid ? ((site.host['memberRoles'] ?? {}) as Record<string, unknown>)[ctx.actorUid] : undefined\n if (!hostRoleCanPublish(role)) {\n throw new Error('Undoing a redirect import needs a publishing role on this site — ask an editor or admin')\n }\n const approvalField = site.declared.externalDestination?.approvedByField ?? REDIRECT_FIELD.approvedBy\n const done: TransferUndoStep[] = []\n const conflicts: TransferUndoStep[] = []\n const paths: string[] = []\n for (const entry of snapshot.entries) {\n const ref = rulesOf(site.hostId).doc(entry.recordId)\n const stored = await ref.get()\n const data = stored.exists ? (stored.data() as StoredRule) : undefined\n const current = isLive(data) ? redirectRuleValues(entry.recordId, data) : null\n const step = planTransferUndo(entry, current)\n if (step.action === 'conflict' && decisions?.[entry.recordId] !== 'revert') {\n if (decisions?.[entry.recordId] === 'keep') {\n done.push({ action: 'nothing', recordId: entry.recordId, why: 'alreadyReverted' })\n } else conflicts.push(step)\n continue\n }\n const before = current ? announcedPath({ kind: String(current['kind']), source: String(current['source']) }) : null\n if (step.action === 'delete' || (step.action === 'conflict' && entry.action === 'created')) {\n await ref.delete()\n if (before) paths.push(before)\n } else if ((step.action === 'restore' || step.action === 'conflict') && current) {\n const restored = { ...current, ...step.values }\n const patch: Record<string, unknown> = {}\n for (const [fieldId, value] of Object.entries(step.values)) {\n patch[fieldId] = value === null || value === undefined ? FieldValue.delete() : value\n }\n // A destination on the site never keeps an approval.\n if (!isExternalRedirectDestination(text(restored['destination']))) patch[approvalField] = FieldValue.delete()\n patch['updatedAt'] = Timestamp.fromMillis(now())\n await ref.set(patch, { merge: true })\n const after = announcedPath({ kind: String(restored['kind']), source: String(restored['source']) })\n for (const path of [before, after]) if (path) paths.push(path)\n }\n done.push(step)\n }\n if (paths.length) await announce(site.hostId, [...new Set(paths)]).catch(() => undefined)\n return { done, conflicts }\n },\n }\n return resource\n}\n"],"names":["createResourceUid","TENANT_APEX","hostRoleCanPublish","hostRoleCanWrite","checkEntitlement","checkQuota","countLiveDocuments","ACKNOWLEDGED_WARNING_CLASSES","TRANSFER_ID_FIELD","TRANSFER_WARNING_CLASSES","buildTransferPlan","isBlankTransferValue","matchLookupKey","planTransferUndo","transferValuesEqual","withTransferResourceFindings","pluginHostResource","dropPluginSiteCache","FieldPath","FieldValue","Timestamp","REDIRECT_DEFAULT_PRIORITY","REDIRECT_STATUS_CODES","findDuplicateRedirect","isExternalRedirectDestination","isSelfRedirect","normalizeRedirectDestination","normalizeRedirectSource","redirectHitKey","redirectSourceIsLivePage","validateRedirectRule","walkRedirectChain","REDIRECTS_ALIAS_DICTIONARIES","REDIRECTS_DEFAULT_POLICY","REDIRECTS_MATCH_KEYS","REDIRECT_FIELD","REDIRECT_HOST_RESOURCE_KIND","REDIRECT_WRITABLE_FIELDS","canonicalRedirectKind","redirectValuesEqual","redirectsTransferCatalog","ROW_RESERVE_MS","PAGE_SIZE_DEFAULT","PAGE_SIZE_MAX","IN_QUERY_MAX","GET_ALL_MAX","HIT_DAYS","DAY_MS","text","value","isoOf","undefined","toMillis","Date","toISOString","Number","isFinite","validStatusCode","includes","redirectRuleValues","id","data","priority","source","kind","destination","statusCode","enabled","approvedBy","createdAt","updatedAt","createdBy","lastHitAt","ruleRow","$id","isLive","Boolean","storedSource","trim","ruleKey","stored","announcedPath","rule","startsWith","finalRule","before","diff","values","change","fieldId","after","withDefaults","String","ownDomains","host","ruleProblem","problem","test","detail","storedFields","allowed","normalized","Object","fromEntries","entries","filter","key","hostnameOf","URL","hostname","requireHost","ctx","hostId","Error","redirectDeclaration","declared","pick","fieldIds","row","slices","items","size","out","at","length","push","slice","withCreateLimit","plan","remaining","limit","creates","held","rows","map","verdict","failed","reason","warnings","summary","create","update","unchanged","skip","fail","total","prior","find","entry","class","raised","count","samples","index","requiresAcknowledgement","flatMap","warning","acknowledgementsRequired","redirectMatchOutcomes","engine","existing","live","byKey","Map","set","get","found","byId","via","record","recordId","rowKind","candidates","recordIds","importedRedirectId","jobId","createRedirectsTransferResource","deps","now","firestore","announce","announcePaths","paths","hostIds","hostRef","collection","doc","rulesOf","readSite","snapshot","org","Promise","all","loadOrg","exists","role","actorUid","roleOk","requiresPublishRole","billing","refusal","entitlement","quotaKey","POSITIVE_INFINITY","readAllRules","docs","readByIds","ids","Set","snapshots","getAll","readHits","today","refs","Array","from","_","totals","day","exportRow","hits","resource","fields","matchKeys","aliases","defaultPolicy","valuesEqual","match","input","records","outcomes","outcome","has","options","readPage","cursor","Math","min","max","floor","pageSize","start","next","query","orderBy","documentId","startAfter","last","lookup","requests","file","request","form","add","where","site","documents","matches","sourceKind","defaults","findings","writing","refuse","accepted","liveRows","touched","ruleset","kept","keptByKey","keptByRecord","candidate","repeated","loop","current","walk","path","hops","join","cycle","screens","apply","chunk","writer","actor","results","undo","message","result","approvalField","externalDestination","approvedByField","stampFor","stamp","ref","fromMillis","runTransaction","tx","existed","used","aggregate","quota","label","written","logCreated","catch","action","previous","keys","delete","merge","rulePath","alreadyApplied","timeLeftMs","markApplied","revert","decisions","done","conflicts","step","why","restored","patch"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/redirects/src/lib/transfer/redirects-transfer.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { createResourceUid } from '@aglyn/aglyn/app-utils/create-resource-uid'\nimport { holdsSiteKey, siteKeyRefusal } from '@aglyn/aglyn/app-utils/host-permissions'\nimport { TENANT_APEX } from '@aglyn/aglyn/app-utils/tenant-apex'\nimport { checkEntitlement, checkQuota } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport { countLiveDocuments } from '@aglyn/aglyn/app-utils/live-document-count'\nimport {\n ACKNOWLEDGED_WARNING_CLASSES,\n TRANSFER_ID_FIELD,\n TRANSFER_WARNING_CLASSES,\n buildTransferPlan,\n isBlankTransferValue,\n matchLookupKey,\n planTransferUndo,\n transferValuesEqual,\n withTransferResourceFindings,\n type BuildTransferPlanInput,\n type PlannedTransferRow,\n type RowMatchOutcome,\n type TransferFieldChange,\n type TransferPlan,\n type TransferPlanRow,\n type TransferPlanSummary,\n type TransferResourceFinding,\n type TransferRowResult,\n type TransferUndoEntry,\n type TransferUndoStep,\n type TransferWarning,\n} from '@aglyn/aglyn/data-transfer'\nimport {\n pluginHostResource,\n type ResolvedPluginHostResource,\n} from '@aglyn/aglyn/plugin-manager/plugin-host-resources'\nimport { dropPluginSiteCache } from '@aglyn/aglyn/plugin-manager/plugin-site-cache'\nimport type {\n PluginTransferResource,\n TransferApplyResult,\n TransferApplyWriter,\n TransferLookupResult,\n TransferMatchAnswer,\n TransferReadOptions,\n TransferReadPage,\n TransferResourceContext,\n TransferRevertDecisions,\n TransferRevertResult,\n} from '@aglyn/aglyn/plugin-manager/plugin-transfer-resources'\nimport { FieldPath, FieldValue, Timestamp } from 'firebase-admin/firestore'\nimport {\n REDIRECT_DEFAULT_PRIORITY,\n REDIRECT_STATUS_CODES,\n findDuplicateRedirect,\n isExternalRedirectDestination,\n isSelfRedirect,\n normalizeRedirectDestination,\n normalizeRedirectSource,\n redirectHitKey,\n redirectSourceIsLivePage,\n validateRedirectRule,\n walkRedirectChain,\n type RedirectRuleRow,\n} from '../model/redirects'\nimport {\n REDIRECTS_ALIAS_DICTIONARIES,\n REDIRECTS_DEFAULT_POLICY,\n REDIRECTS_MATCH_KEYS,\n REDIRECT_FIELD,\n REDIRECT_HOST_RESOURCE_KIND,\n REDIRECT_WRITABLE_FIELDS,\n canonicalRedirectKind,\n redirectValuesEqual,\n redirectsTransferCatalog,\n} from './redirects-transfer-fields'\n\n/*==========================================\n * THE REDIRECTS RESOURCE — a site's redirect rules, in and out of a file.\n *\n * A rule is a routing statement over the live site the moment it is written,\n * so an import writes nothing the redirects page or the create route would\n * not. Every write here makes the checks those paths make, in the same words:\n *\n * - who: the publishing role on the site (`hostRoleCanPublish`, the axis the\n * Firestore rules gate the collection on) and the plan's `redirects`\n * feature — read from the create route's own declaration of the kind, not\n * restated. Without either, every row is refused with the reason.\n * - what: the declaration's allow-list of fields, the page's normalization\n * of the from-path and the destination, its defaults (exact, 302, priority\n * 100, on), and `validateRedirectRule` — so a lookalike destination, a\n * pattern the matcher cannot run and a path that redirects to itself are\n * refused, as the page refuses them.\n * - against what: the page's duplicate check (`findDuplicateRedirect`) and\n * chain-loop walk (`walkRedirectChain`) over the site's rules as the\n * import leaves them, row by row in file order, so a row is refused\n * exactly when saving it after the rows above would be refused.\n * - how many: a new rule is counted and created in one transaction against\n * the plan's `redirectsPerHost`, as the create route does; the dry run\n * holds the creates past what the plan leaves.\n * - provenance: `externalDestinationApprovedBy` is never read from a file.\n * A written rule whose destination is off the site carries the importing\n * member's uid, the same stamp the page and the route write and the serve\n * path requires; one whose destination is on the site has it removed.\n * The dry run says so for every such row, naming the host.\n * - afterwards: a created rule gets the route's `createdAt`, `updatedAt`,\n * `createdBy` and its \"Created redirect\" activity line; every written\n * from-path (and one an update moved away from) is announced to the site\n * cache, `/` for a regex rule, as the page announces a save.\n *\n * The dry run also warns, without refusing, where the page warns or a\n * person would want to know: a from-path that is a published page, a\n * destination that is itself redirected (a chain), and one that leads into a\n * loop other rules make.\n *\n * Reads skip soft-deleted rules (`deletedAt`), as the page does. Undo\n * deletes a rule the import created — outright rather than soft-deleted:\n * the rule did not exist before the import, and a soft-deleted document\n * would keep occupying a slot the plan's counter counts — and restores the\n * fields an update changed, stamp included.\n *=========================================*/\n\n/** The billing document a plan's entitlement and counter are read from. */\ntype OrgBilling = Parameters<typeof checkEntitlement>[0]\n\nexport interface RedirectsTransferDeps {\n firestore: FirebaseFirestore.Firestore\n /** Defaults to `Date.now`. */\n now?: () => number\n /** The site's owning workspace's billing document (`getOrgForHost`); `null` reads as the free plan. */\n loadOrg(hostId: string): Promise<OrgBilling | null>\n /**\n * Drops the site's cached pages at these addresses, best effort. Defaults\n * to the platform's site cache (`dropPluginSiteCache`), the announcement\n * the create route makes for a new rule.\n */\n announcePaths?(hostId: string, paths: readonly string[]): Promise<void>\n /** Writes the site activity line for a created rule, best effort. */\n logCreated?(hostId: string, actorUid: string, ruleId: string, resource: ResolvedPluginHostResource): Promise<void>\n}\n\n/** How long before the chunk's budget runs out a row is not started. */\nconst ROW_RESERVE_MS = 2_000\n\n/** The most rows one export page reads. */\nconst PAGE_SIZE_DEFAULT = 500\nconst PAGE_SIZE_MAX = 1_000\n\n/** Values one `in` query may name. */\nconst IN_QUERY_MAX = 30\n\n/** Documents one `getAll` reads. */\nconst GET_ALL_MAX = 100\n\n/** Days the hit count sums, as the redirects page does. */\nconst HIT_DAYS = 30\n\nconst DAY_MS = 24 * 60 * 60 * 1000\n\n/*==========================================\n * A RULE AS THE TRANSFER READS IT\n *=========================================*/\n\ntype StoredRule = Record<string, unknown>\n\n/** The rule's six writable fields, as written. */\nexport interface RuleFields {\n source: string\n kind: string\n destination: string\n statusCode: number\n priority: number\n enabled: boolean\n}\n\nfunction text(value: unknown): string {\n return typeof value === 'string' ? value : ''\n}\n\n/** A stored time as ISO text, or `null`. */\nfunction isoOf(value: unknown): string | null {\n if (value === null || value === undefined) return null\n if (typeof (value as { toMillis?: unknown }).toMillis === 'function') {\n return new Date((value as { toMillis(): number }).toMillis()).toISOString()\n }\n if (value instanceof Date) return value.toISOString()\n if (typeof value === 'number' && Number.isFinite(value)) return new Date(value).toISOString()\n return null\n}\n\nfunction validStatusCode(value: unknown): value is RuleFields['statusCode'] {\n return (REDIRECT_STATUS_CODES as readonly number[]).includes(value as number)\n}\n\n/**\n * A stored rule's fields as the redirects page reads them into its editor: a\n * missing mode is exact, a missing priority 100, a status code outside the\n * four is 302 (as the serve path reads it), and a rule is on unless it says\n * `false`. The export, the dry run's \"before\" and undo all read this, so a\n * file exported and imported back is unchanged.\n */\nexport function redirectRuleValues(id: string, data: StoredRule): Record<string, unknown> {\n const priority = data['priority']\n return {\n [TRANSFER_ID_FIELD]: id,\n source: text(data['source']),\n kind: typeof data['kind'] === 'string' && data['kind'] ? data['kind'] : 'exact',\n destination: text(data['destination']),\n statusCode: validStatusCode(data['statusCode']) ? data['statusCode'] : 302,\n priority:\n priority !== null && priority !== undefined && Number.isFinite(Number(priority))\n ? Number(priority)\n : REDIRECT_DEFAULT_PRIORITY,\n enabled: data['enabled'] !== false,\n [REDIRECT_FIELD.approvedBy]:\n typeof data[REDIRECT_FIELD.approvedBy] === 'string' && data[REDIRECT_FIELD.approvedBy]\n ? data[REDIRECT_FIELD.approvedBy]\n : null,\n createdAt: isoOf(data['createdAt']),\n updatedAt: isoOf(data['updatedAt']),\n createdBy: typeof data['createdBy'] === 'string' ? data['createdBy'] : null,\n lastHitAt: isoOf(data['lastHitAt']),\n }\n}\n\n/** A rule as the whole-set checks read it: as stored, like the page's rows. */\nfunction ruleRow(id: string, data: StoredRule): RedirectRuleRow {\n return {\n $id: id,\n source: data['source'] as string,\n destination: data['destination'] as string,\n kind: data['kind'] as string,\n }\n}\n\nfunction isLive(data: StoredRule | undefined): data is StoredRule {\n return Boolean(data) && !data?.['deletedAt']\n}\n\n/**\n * The from-path as a rule of `kind` stores it: a pattern trimmed, a path\n * normalized (lowercase, no trailing slash, no query); `null` when a path\n * cannot be one.\n */\nfunction storedSource(kind: string, source: string): string | null {\n if (kind === 'regex') return source.trim() || null\n return normalizeRedirectSource(source)\n}\n\n/** What makes two rules one: their mode and their from-path, as stored. */\nfunction ruleKey(kind: string, source: string): string | null {\n const stored = storedSource(kind, source)\n return stored === null ? null : `${kind}\\u0000${stored}`\n}\n\n/** The address a rule answers at, for the site cache: a pattern names no one path, so `/`. */\nexport function announcedPath(rule: { kind?: string; source?: string }): string | null {\n const source = text(rule.source)\n if ((rule.kind ?? 'exact') === 'regex') return '/'\n return source.startsWith('/') ? source : null\n}\n\n/** The rule a row leaves: what the record held (or the defaults), with the row's changes. */\nfunction finalRule(\n before: Readonly<Record<string, unknown>> | null | undefined,\n diff: readonly TransferFieldChange[],\n): RuleFields {\n const values: Record<string, unknown> = { ...(before ?? {}) }\n for (const change of diff) values[change.fieldId] = change.after\n return withDefaults(values)\n}\n\n/**\n * The six fields with the page's defaults where a value is missing:\n * exact, 302, priority 100, on.\n */\nexport function withDefaults(values: Readonly<Record<string, unknown>>): RuleFields {\n const priority = values['priority']\n const kind = canonicalRedirectKind(values['kind'])\n return {\n source: text(values['source']),\n kind: isBlankTransferValue(values['kind']) ? 'exact' : (kind ?? String(values['kind'])),\n destination: text(values['destination']),\n statusCode: isBlankTransferValue(values['statusCode'])\n ? 302\n : (Number(values['statusCode']) as RuleFields['statusCode']),\n priority:\n priority !== null && priority !== undefined && Number.isFinite(Number(priority))\n ? Number(priority)\n : REDIRECT_DEFAULT_PRIORITY,\n enabled: values['enabled'] !== false,\n }\n}\n\n/** The site's own addresses, which a destination may always name. */\nfunction ownDomains(host: StoredRule): Array<string | null> {\n return [\n typeof host['cname'] === 'string' ? host['cname'] : null,\n typeof host['subdomain'] === 'string' && host['subdomain'] ? `${host['subdomain']}.${TENANT_APEX}` : null,\n ]\n}\n\n/**\n * Why the page would refuse to save this rule, or `null`: an unknown mode, a\n * from-path or pattern it cannot use, a destination it cannot use or that\n * wears another company's address, a status code it does not offer, or a\n * path redirected to itself.\n */\nexport function ruleProblem(rule: RuleFields, host: StoredRule): { detail: string; fieldId: string; value: string } | null {\n const problem = validateRedirectRule(\n { kind: rule.kind, source: rule.source, destination: rule.destination },\n { ownDomains: ownDomains(host) },\n )\n if (problem) {\n const fieldId =\n problem === 'Unknown match mode'\n ? REDIRECT_FIELD.kind\n : /destination/i.test(problem)\n ? REDIRECT_FIELD.destination\n : REDIRECT_FIELD.source\n return { detail: problem, fieldId, value: String((rule as unknown as Record<string, unknown>)[fieldId] ?? '') }\n }\n if (!validStatusCode(rule.statusCode)) {\n return {\n detail: 'Status codes are 301, 302, 307 or 308',\n fieldId: REDIRECT_FIELD.statusCode,\n value: String(rule.statusCode),\n }\n }\n if (rule.kind !== 'regex' && isSelfRedirect({ source: rule.source, destination: rule.destination })) {\n return { detail: 'That would redirect the path to itself', fieldId: REDIRECT_FIELD.destination, value: rule.destination }\n }\n return null\n}\n\n/** The rule's fields as stored: the page's normalization, and only what the create route's allow-list names. */\nexport function storedFields(rule: RuleFields, allowed: readonly string[]): Record<string, unknown> {\n const normalized: Record<string, unknown> = {\n source: rule.kind === 'regex' ? rule.source.trim() : (normalizeRedirectSource(rule.source) ?? rule.source),\n destination: normalizeRedirectDestination(rule.destination) ?? rule.destination,\n statusCode: rule.statusCode,\n kind: rule.kind,\n priority: rule.priority,\n enabled: rule.enabled,\n }\n return Object.fromEntries(Object.entries(normalized).filter(([key]) => allowed.includes(key)))\n}\n\nfunction hostnameOf(destination: string): string {\n try {\n return new URL(destination).hostname || destination\n } catch {\n return destination\n }\n}\n\n/*==========================================\n * THE SITE, AS A WRITE IS DECIDED AGAINST IT\n *=========================================*/\n\ninterface SiteState {\n hostId: string\n host: StoredRule\n org: OrgBilling | null\n declared: ResolvedPluginHostResource\n /** Why nothing may be written, or `null`. */\n refusal: string | null\n /** The plan's cap on rules on the site, `Infinity` for none. */\n limit: number\n}\n\nfunction requireHost(ctx: TransferResourceContext): string {\n if (!ctx.hostId) throw new Error('Redirects belong to a site; open the import from the site.')\n return ctx.hostId\n}\n\n/** The redirect kind's declaration, which the create route reads too. */\nexport function redirectDeclaration(): ResolvedPluginHostResource {\n const declared = pluginHostResource(REDIRECT_HOST_RESOURCE_KIND)\n if (!declared) throw new Error(`The \"${REDIRECT_HOST_RESOURCE_KIND}\" kind is not declared.`)\n return declared\n}\n\n/** Field ids → values, holding only `fieldIds`. */\nfunction pick(values: Readonly<Record<string, unknown>>, fieldIds: readonly string[]): Record<string, unknown> {\n const row: Record<string, unknown> = {}\n for (const fieldId of fieldIds) row[fieldId] = values[fieldId] ?? null\n return row\n}\n\n/** Each `size` items of `items`. */\nfunction slices<T>(items: readonly T[], size: number): T[][] {\n const out: T[][] = []\n for (let at = 0; at < items.length; at += size) out.push(items.slice(at, at + size))\n return out\n}\n\n/**\n * The plan with every create past `remaining` failed as `planLimit`, counted\n * after the rows the plugin refused — a refused row takes no slot.\n */\nfunction withCreateLimit(plan: TransferPlan, remaining: number, limit: number): TransferPlan {\n if (!Number.isFinite(remaining)) return plan\n let creates = 0\n const held: PlannedTransferRow[] = []\n const rows = plan.rows.map((row) => {\n if (row.verdict !== 'create') return row\n if (creates < remaining) {\n creates += 1\n return row\n }\n const failed: PlannedTransferRow = {\n ...row,\n verdict: 'fail',\n reason: 'planLimit',\n diff: [],\n warnings: row.warnings.includes('planLimit') ? row.warnings : [...row.warnings, 'planLimit'],\n }\n held.push(failed)\n return failed\n })\n if (!held.length) return plan\n const summary: TransferPlanSummary = { create: 0, update: 0, unchanged: 0, skip: 0, fail: 0, total: rows.length }\n for (const row of rows) summary[row.verdict] += 1\n const detail = `Your plan includes ${limit} redirects on this site`\n const prior = plan.warnings.find((entry) => entry.class === 'planLimit')\n const raised: TransferWarning = {\n class: 'planLimit',\n count: (prior?.count ?? 0) + held.length,\n rows: (prior?.rows ?? 0) + held.length,\n fieldIds: prior?.fieldIds ?? [],\n samples: [...(prior?.samples ?? []), ...held.map((row) => ({ row: row.index, detail }))].slice(0, 5),\n requiresAcknowledgement: ACKNOWLEDGED_WARNING_CLASSES.includes('planLimit'),\n }\n const warnings = TRANSFER_WARNING_CLASSES.flatMap((entry) =>\n entry === 'planLimit' ? [raised] : plan.warnings.filter((warning) => warning.class === entry),\n )\n return {\n rows,\n summary,\n warnings,\n acknowledgementsRequired: warnings.filter((entry) => entry.requiresAcknowledgement).map((entry) => entry.class),\n }\n}\n\n/*==========================================\n * MATCHING, DEFAULTS AND SAMENESS (AGL-3548)\n *=========================================*/\n\n/**\n * Each row's rule, as the page tells rules apart: a row the engine found by\n * its id keeps that rule; any other row is the rule of its mode (exact when\n * the file gives none) at its from-path as stored — so `/blog` names the\n * exact rule or the prefix rule beside it by the row's mode, never both.\n * Two rows naming one rule are both matched to it here, and the plan refuses\n * the later one, saying which row it repeats, rather than holding it back as\n * a duplicate. `existing` holds every live rule's values by id; running this\n * over its own outcomes changes nothing.\n */\nexport function redirectMatchOutcomes(\n rows: ReadonlyArray<Readonly<Record<string, unknown>>>,\n engine: readonly RowMatchOutcome[],\n existing: ReadonlyMap<string, Readonly<Record<string, unknown>>>,\n live: ReadonlyArray<{ id: string; data: StoredRule }>,\n): RowMatchOutcome[] {\n // Each live rule under its mode and stored from-path.\n const byKey = new Map<string, string[]>()\n for (const { id, data } of live) {\n const key = ruleKey(text(data['kind']) || 'exact', text(data['source']))\n if (key) byKey.set(key, [...(byKey.get(key) ?? []), id])\n }\n return rows.map((values, at): RowMatchOutcome => {\n const found = engine[at] ?? { kind: 'new' as const }\n const byId = found.kind !== 'new' && found.via.fieldId === TRANSFER_ID_FIELD\n const record = found.kind === 'matched' && byId ? existing.get(found.recordId) : undefined\n const rowKind = isBlankTransferValue(values['kind'])\n ? text(record?.['kind']) || 'exact'\n : canonicalRedirectKind(values['kind'])\n const source = isBlankTransferValue(values['source']) ? text(record?.['source']) : text(values['source'])\n const key = rowKind && source ? ruleKey(rowKind, source) : null\n if (byId || !key || !rowKind) return found\n const candidates = byKey.get(key) ?? []\n const via = { fieldId: REDIRECT_FIELD.source, value: storedSource(rowKind, source) as string }\n if (!candidates.length) return { kind: 'new' }\n return candidates.length === 1\n ? { kind: 'matched', recordId: candidates[0] as string, via }\n : { kind: 'ambiguous', recordIds: candidates, via }\n })\n}\n\n/*==========================================\n * THE RESOURCE\n *=========================================*/\n\n/**\n * The id a rule an import creates is written under: the job's row, so a\n * write retried after it landed and before the ledger recorded it finds its\n * own rule instead of making a second one. Without a job, a fresh resource id.\n */\nexport function importedRedirectId(jobId: string | undefined, row: number): string {\n return jobId ? `${jobId}-${row}` : createResourceUid()\n}\n\n/**\n * The redirects resource over `deps.firestore`: `hosts/{hostId}/redirects`,\n * with every hook the job engine and the export route call. See the block\n * above for what each write checks.\n */\nexport function createRedirectsTransferResource(deps: RedirectsTransferDeps): PluginTransferResource {\n const now = deps.now ?? (() => Date.now())\n const { firestore } = deps\n const announce =\n deps.announcePaths ??\n (async (hostId: string, paths: readonly string[]) => {\n await dropPluginSiteCache({\n hostIds: [hostId],\n paths: { [hostId]: [...paths] },\n reason: 'redirect rules were imported',\n })\n })\n\n const hostRef = (hostId: string) => firestore.collection('hosts').doc(hostId)\n const rulesOf = (hostId: string) => hostRef(hostId).collection('redirects')\n\n async function readSite(ctx: TransferResourceContext): Promise<SiteState> {\n const hostId = requireHost(ctx)\n const [snapshot, org] = await Promise.all([hostRef(hostId).get(), deps.loadOrg(hostId)])\n const host = (snapshot.exists ? snapshot.data() : null) ?? {}\n const declared = redirectDeclaration()\n // The route's question, on the route's axis: a kind that is live the\n // moment it exists needs \"Publish site content\", never just \"Edit site\n // content\" (AGL-4047).\n const roleKey = declared.requiresPublishRole ? 'site.publish' : 'site.edit'\n const roleOk = Boolean(ctx.actorUid) && holdsSiteKey(host, ctx.actorUid as string, roleKey)\n // A plan-less workspace resolves as the free plan, which has no redirects.\n const billing = org ?? {}\n let refusal: string | null = null\n if (!snapshot.exists) refusal = 'This site no longer exists.'\n else if (!roleOk) refusal = siteKeyRefusal(roleKey)\n else if (declared.entitlement && !checkEntitlement(billing, declared.entitlement)) {\n refusal = 'URL redirects are not included in this workspace’s plan — see Billing'\n }\n const limit = declared.quotaKey\n ? checkQuota(billing, declared.quotaKey as Parameters<typeof checkQuota>[1], 0).limit\n : Number.POSITIVE_INFINITY\n return { hostId, host, org: billing, declared, refusal, limit }\n }\n\n /** Every rule document of the site, soft-deleted ones included. */\n async function readAllRules(hostId: string): Promise<Array<{ id: string; data: StoredRule }>> {\n const snapshot = await rulesOf(hostId).get()\n return snapshot.docs.map((doc) => ({ id: doc.id, data: doc.data() as StoredRule }))\n }\n\n async function readByIds(hostId: string, ids: readonly string[]): Promise<Map<string, StoredRule>> {\n const found = new Map<string, StoredRule>()\n for (const slice of slices([...new Set(ids)], GET_ALL_MAX)) {\n if (!slice.length) continue\n const snapshots = await firestore.getAll(...slice.map((id) => rulesOf(hostId).doc(id)))\n for (const snapshot of snapshots) {\n const data = snapshot.exists ? (snapshot.data() as StoredRule) : undefined\n if (isLive(data)) found.set(snapshot.id, data)\n }\n }\n return found\n }\n\n /** Each rule's sampled hits over the last thirty days, by its hit key. */\n async function readHits(hostId: string): Promise<Record<string, number>> {\n const today = now()\n const refs = Array.from({ length: HIT_DAYS }, (_, index) =>\n hostRef(hostId)\n .collection('analytics')\n .doc(new Date(today - index * DAY_MS).toISOString().slice(0, 10)),\n )\n const totals: Record<string, number> = {}\n for (const snapshot of await firestore.getAll(...refs)) {\n if (!snapshot.exists) continue\n const day = ((snapshot.data() as StoredRule)['redirects'] ?? {}) as Record<string, unknown>\n for (const [key, count] of Object.entries(day)) {\n const value = Number(count)\n if (Number.isFinite(value)) totals[key] = (totals[key] ?? 0) + value\n }\n }\n return totals\n }\n\n function exportRow(\n id: string,\n data: StoredRule,\n fieldIds: readonly string[],\n hits: Record<string, number> | null,\n ): Record<string, unknown> {\n const values = redirectRuleValues(id, data)\n if (hits) values[REDIRECT_FIELD.hits] = hits[redirectHitKey(id)] ?? 0\n return pick(values, fieldIds)\n }\n\n const resource: PluginTransferResource = {\n fields: () => redirectsTransferCatalog(),\n matchKeys: REDIRECTS_MATCH_KEYS,\n aliases: REDIRECTS_ALIAS_DICTIONARIES,\n defaultPolicy: REDIRECTS_DEFAULT_POLICY,\n valuesEqual: redirectValuesEqual,\n\n /*\n * Rows matched as the page tells rules apart (`redirectMatchOutcomes`),\n * over every live rule of the site — so the Matching step and the\n * Conflicts step show the rule the dry run will update.\n */\n async match(ctx, input): Promise<TransferMatchAnswer> {\n const hostId = requireHost(ctx)\n const live = (await readAllRules(hostId)).filter((entry) => isLive(entry.data))\n const existing = new Map(input.records)\n for (const { id, data } of live) existing.set(id, redirectRuleValues(id, data))\n const outcomes = redirectMatchOutcomes(input.rows, input.outcomes, existing, live)\n const records = new Map<string, Readonly<Record<string, unknown>>>()\n for (const outcome of outcomes) {\n const ids = outcome.kind === 'matched' ? [outcome.recordId] : outcome.kind === 'ambiguous' ? outcome.recordIds : []\n for (const id of ids) {\n const values = existing.get(id)\n if (values && !input.records.has(id)) records.set(id, values)\n }\n }\n return { outcomes, records }\n },\n\n /*\n * `filter` is not read: the redirects page lists every rule and has no\n * filter to export by. `scopeTokens` does not narrow either: rules are a\n * site's, carry no `visibleTo`, and the export route admits a reader to\n * a site's records only when they reach the site.\n */\n async count(ctx: TransferResourceContext, options: TransferReadOptions): Promise<number> {\n const hostId = requireHost(ctx)\n if (options.ids) return (await readByIds(hostId, options.ids)).size\n return countLiveDocuments(rulesOf(hostId))\n },\n\n async readPage(\n ctx: TransferResourceContext,\n cursor: string | null,\n fieldIds: readonly string[],\n options: TransferReadOptions = {},\n ): Promise<TransferReadPage> {\n const hostId = requireHost(ctx)\n const size = Math.min(PAGE_SIZE_MAX, Math.max(1, Math.floor(options.pageSize ?? PAGE_SIZE_DEFAULT)))\n const hits = fieldIds.includes(REDIRECT_FIELD.hits) ? await readHits(hostId) : null\n if (options.ids) {\n // The selection, in the order it was made; the cursor is how far into it.\n const start = Math.max(0, Number(cursor ?? 0) || 0)\n const ids = options.ids.slice(start, start + size)\n const found = await readByIds(hostId, ids)\n const rows = ids.flatMap((id) => {\n const data = found.get(id)\n return data ? [exportRow(id, data, fieldIds, hits)] : []\n })\n return { rows, next: start + size < options.ids.length ? String(start + size) : null }\n }\n // Ordered by document id, the one order every rule has: a rule with no\n // `source` still exports (the page sorts by source, in the browser).\n let query = rulesOf(hostId).orderBy(FieldPath.documentId()).limit(size)\n if (cursor) query = query.startAfter(cursor)\n const snapshot = await query.get()\n const rows = snapshot.docs.flatMap((doc) => {\n const data = doc.data() as StoredRule\n return isLive(data) ? [exportRow(doc.id, data, fieldIds, hits)] : []\n })\n const last = snapshot.docs[snapshot.docs.length - 1]\n return { rows, next: snapshot.docs.length === size && last ? last.id : null }\n },\n\n /*\n * By id, and by from-path: each value is read as the stored form of a\n * path (normalized) and of a pattern (trimmed), and a rule answers for it\n * under its own mode. The plan narrows a row's candidates to the row's\n * mode; this answers every rule the value could mean.\n */\n async lookup(ctx, requests): Promise<TransferLookupResult> {\n const hostId = requireHost(ctx)\n const lookup = new Map<string, string[]>()\n const records = new Map<string, Readonly<Record<string, unknown>>>()\n const file = (fieldId: string, value: string, id: string) => {\n const key = matchLookupKey(fieldId, value)\n const ids = lookup.get(key) ?? []\n if (!ids.includes(id)) ids.push(id)\n lookup.set(key, ids)\n }\n for (const request of requests) {\n if (request.fieldId === TRANSFER_ID_FIELD) {\n const found = await readByIds(hostId, request.values)\n for (const [id, data] of found) {\n file(TRANSFER_ID_FIELD, id, id)\n records.set(id, redirectRuleValues(id, data))\n }\n continue\n }\n if (request.fieldId !== REDIRECT_FIELD.source) continue\n const stored = new Set<string>()\n for (const value of request.values) {\n for (const form of [normalizeRedirectSource(value), value.trim()]) if (form) stored.add(form)\n }\n for (const slice of slices([...stored], IN_QUERY_MAX)) {\n const snapshot = await rulesOf(hostId).where('source', 'in', slice).get()\n for (const doc of snapshot.docs) {\n const data = doc.data() as StoredRule\n if (!isLive(data)) continue\n const kind = typeof data['kind'] === 'string' && data['kind'] ? data['kind'] : 'exact'\n const held = storedSource(kind, text(data['source']))\n for (const value of request.values) {\n if (held !== null && storedSource(kind, value) === held) {\n file(REDIRECT_FIELD.source, value, doc.id)\n records.set(doc.id, redirectRuleValues(doc.id, data))\n }\n }\n }\n }\n }\n return { lookup, records }\n },\n\n async plan(ctx, input: BuildTransferPlanInput): Promise<TransferPlan> {\n const site = await readSite(ctx)\n const documents = await readAllRules(site.hostId)\n const live = documents.filter((entry) => isLive(entry.data))\n const existing = new Map(input.existing)\n for (const { id, data } of live) existing.set(id, redirectRuleValues(id, data))\n\n // The resource's own matching (also the engine's `match` hook, so the\n // Matching and Conflicts steps showed these outcomes): run again over\n // the site as read here, which leaves outcomes it made unchanged.\n const matches = redirectMatchOutcomes(\n input.rows.map((row) => row.values),\n input.matches,\n existing,\n live,\n )\n\n // The row's values as the page would store them, so the review's\n // before → after compares like with like, and a new rule shows the\n // defaults it is created with.\n const rows = input.rows.map((row, at): TransferPlanRow => {\n const match = matches[at] as RowMatchOutcome\n const record = match.kind === 'matched' ? existing.get(match.recordId) : undefined\n const values: Record<string, unknown> = { ...row.values }\n const kind = canonicalRedirectKind(values['kind'])\n if (kind) values['kind'] = kind\n const sourceKind = kind ?? (isBlankTransferValue(values['kind']) ? text(record?.['kind']) || 'exact' : null)\n if (typeof values['source'] === 'string' && sourceKind) {\n values['source'] = storedSource(sourceKind, values['source']) ?? values['source'].trim()\n }\n if (typeof values['destination'] === 'string') {\n values['destination'] = normalizeRedirectDestination(values['destination']) ?? values['destination'].trim()\n }\n if (match.kind === 'new') {\n const defaults: Record<string, unknown> = {\n kind: 'exact',\n statusCode: 302,\n priority: REDIRECT_DEFAULT_PRIORITY,\n enabled: true,\n }\n for (const [fieldId, value] of Object.entries(defaults)) {\n if (isBlankTransferValue(values[fieldId])) values[fieldId] = value\n }\n }\n return { ...row, values }\n })\n\n const plan = buildTransferPlan({ ...input, rows, matches, existing })\n const findings: TransferResourceFinding[] = []\n const writing = plan.rows.filter((row) => row.verdict === 'create' || row.verdict === 'update')\n\n if (site.refusal) {\n for (const row of writing) findings.push({ row: row.index, detail: site.refusal, refuse: true })\n return withTransferResourceFindings(plan, findings)\n }\n\n // What the page refuses about a rule on its own.\n const accepted: Array<{ row: PlannedTransferRow; rule: RuleFields }> = []\n for (const row of writing) {\n const rule = finalRule(row.recordId ? existing.get(row.recordId) : null, row.diff)\n const problem = ruleProblem(rule, site.host)\n if (problem) {\n findings.push({ row: row.index, ...problem, refuse: true })\n continue\n }\n accepted.push({ row, rule })\n }\n\n // What the page refuses against the other rules, row by row in file\n // order over the site as the rows above leave it.\n const liveRows = new Map(live.map(({ id, data }) => [id, ruleRow(id, data)]))\n const touched = new Set(accepted.flatMap(({ row }) => (row.recordId ? [row.recordId] : [])))\n const ruleset: RedirectRuleRow[] = [...liveRows.values()].filter((rule) => !touched.has(rule.$id as string))\n const kept: Array<{ row: PlannedTransferRow; rule: RuleFields; candidate: Required<RedirectRuleRow> }> = []\n const keptByKey = new Map<string, number>()\n const keptByRecord = new Map<string, number>()\n for (const { row, rule } of accepted) {\n const candidate = {\n $id: row.recordId ?? `row:${row.index}`,\n source: rule.source,\n destination: rule.destination,\n kind: rule.kind,\n }\n const key = ruleKey(rule.kind, rule.source) as string\n const repeated = keptByKey.get(key) ?? (row.recordId ? keptByRecord.get(row.recordId) : undefined)\n let detail: string | null = null\n if (repeated !== undefined) detail = `Row ${repeated + 1} already redirects ${rule.source}`\n else if (findDuplicateRedirect(ruleset, candidate)) detail = `A rule for ${rule.source} already exists`\n else if (walkRedirectChain(ruleset, candidate).loop) {\n detail = 'That destination chains back to this rule — a redirect loop'\n }\n if (detail) {\n const fieldId = repeated !== undefined ? REDIRECT_FIELD.source : REDIRECT_FIELD.destination\n findings.push({ row: row.index, detail, fieldId, value: String(rule[fieldId]), refuse: true })\n // The rule it would have changed stays as it is.\n const current = row.recordId ? liveRows.get(row.recordId) : undefined\n if (current) ruleset.push(current)\n continue\n }\n ruleset.push(candidate)\n kept.push({ row, rule, candidate })\n keptByKey.set(key, row.index)\n if (row.recordId) keptByRecord.set(row.recordId, row.index)\n }\n\n // What a person should know about the rules that will be written, read\n // over the site as the whole import leaves it.\n for (const { row, rule, candidate } of kept) {\n const walk = walkRedirectChain(ruleset, candidate)\n const path = [rule.destination, ...walk.hops].join(' → ')\n if (walk.loop || walk.cycle) {\n findings.push({\n row: row.index,\n detail: `Leads into a redirect loop: ${path}`,\n fieldId: REDIRECT_FIELD.destination,\n value: rule.destination,\n })\n } else if (walk.hops.length) {\n findings.push({\n row: row.index,\n detail: `Chains through another rule: ${path}`,\n fieldId: REDIRECT_FIELD.destination,\n value: rule.destination,\n })\n }\n if (isExternalRedirectDestination(rule.destination)) {\n findings.push({\n row: row.index,\n detail: `Sends visitors to ${hostnameOf(rule.destination)}; importing approves it in your name`,\n fieldId: REDIRECT_FIELD.destination,\n value: rule.destination,\n })\n }\n if (redirectSourceIsLivePage((site.host as { screens?: Record<string, unknown> }).screens, rule.source)) {\n findings.push({\n row: row.index,\n detail: `${rule.source} is a published page — the redirect takes precedence`,\n fieldId: REDIRECT_FIELD.source,\n value: rule.source,\n })\n }\n }\n\n // The plan's counter counts the LIVE rules, as the create route counts\n // them: a deleted rule frees its slot, a disabled one keeps it (AGL-3918).\n const remaining = Math.max(0, site.limit - live.length)\n return withCreateLimit(withTransferResourceFindings(plan, findings), remaining, site.limit)\n },\n\n async apply(ctx, chunk, writer: TransferApplyWriter): Promise<TransferApplyResult> {\n const site = await readSite(ctx)\n const actor = ctx.actorUid as string\n const allowed = site.declared.fields\n const results: TransferRowResult[] = []\n const undo: TransferUndoEntry[] = []\n const paths: string[] = []\n\n const fail = (row: PlannedTransferRow, message: string, reason?: TransferRowResult['reason']) => ({\n result: {\n row: row.index,\n outcome: 'failed' as const,\n ...(row.recordId ? { recordId: row.recordId } : {}),\n ...(reason ? { reason } : {}),\n message,\n },\n entry: undefined,\n })\n\n const approvalField = site.declared.externalDestination?.approvedByField ?? REDIRECT_FIELD.approvedBy\n const stampFor = (destination: unknown): string | null =>\n site.declared.externalDestination && isExternalRedirectDestination(String(destination ?? '')) ? actor : null\n\n const create = async (row: PlannedTransferRow) => {\n const rule = finalRule(null, row.diff)\n const problem = ruleProblem(rule, site.host)\n if (problem) return fail(row, problem.detail, 'resourceRule')\n const fields = storedFields(rule, allowed)\n const stamp = stampFor(fields['destination'])\n const ref = rulesOf(site.hostId).doc(importedRedirectId(ctx.jobId, row.index))\n const at = Timestamp.fromMillis(now())\n // Counted and created in one transaction, as the create route does,\n // so two imports (or an import and the page) cannot both take the\n // last slot.\n const outcome = await firestore.runTransaction(async (tx): Promise<{ refusal?: string; existed?: boolean }> => {\n if ((await tx.get(ref)).exists) return { existed: true }\n if (site.declared.quotaKey && Number.isFinite(site.limit)) {\n // Live rules only, as the create route counts (AGL-3918).\n const used = await countLiveDocuments(rulesOf(site.hostId), (aggregate) => tx.get(aggregate))\n const quota = checkQuota(site.org, site.declared.quotaKey as Parameters<typeof checkQuota>[1], used)\n if (!quota.allowed) {\n return { refusal: `Your plan includes ${quota.limit} ${site.declared.label} — upgrade in Billing for more` }\n }\n }\n tx.create(ref, {\n ...fields,\n ...(stamp ? { [approvalField]: stamp } : {}),\n createdAt: at,\n updatedAt: at,\n createdBy: actor,\n })\n return {}\n })\n if (outcome.refusal) return fail(row, outcome.refusal, 'planLimit')\n const written: Record<string, unknown> = { ...fields, ...(stamp ? { [approvalField]: stamp } : {}) }\n if (!outcome.existed && deps.logCreated) {\n await deps.logCreated(site.hostId, actor, ref.id, site.declared).catch(() => undefined)\n }\n const path = announcedPath(fields)\n if (path) paths.push(path)\n const result: TransferRowResult = { row: row.index, outcome: 'created', recordId: ref.id }\n const entry: TransferUndoEntry = { row: row.index, recordId: ref.id, action: 'created', written }\n return { result, entry }\n }\n\n const update = async (row: PlannedTransferRow) => {\n const id = row.recordId as string\n const ref = rulesOf(site.hostId).doc(id)\n const snapshot = await ref.get()\n const data = snapshot.exists ? (snapshot.data() as StoredRule) : undefined\n if (!isLive(data)) return fail(row, 'This redirect was deleted after the dry run.', 'matchedRecordMissing')\n const current = redirectRuleValues(id, data)\n const rule = finalRule(current, row.diff)\n const problem = ruleProblem(rule, site.host)\n if (problem) return fail(row, problem.detail, 'resourceRule')\n const fields = storedFields(rule, allowed)\n const stamp = stampFor(fields['destination'])\n const previous: Record<string, unknown> = {}\n const written: Record<string, unknown> = {}\n for (const fieldId of REDIRECT_WRITABLE_FIELDS) {\n if (!(fieldId in fields) || transferValuesEqual(current[fieldId], fields[fieldId])) continue\n previous[fieldId] = current[fieldId] ?? null\n written[fieldId] = fields[fieldId]\n }\n if (!transferValuesEqual(current[approvalField], stamp)) {\n previous[approvalField] = current[approvalField] ?? null\n written[approvalField] = stamp\n }\n if (!Object.keys(written).length) {\n return { result: { row: row.index, outcome: 'unchanged' as const, recordId: id }, entry: undefined }\n }\n // The page's edit: every field the form owns, the stamp set or\n // removed, `updatedAt`, merged so `lastHitAt` and `deletedAt` stay.\n await ref.set(\n { ...fields, [approvalField]: stamp ?? FieldValue.delete(), updatedAt: Timestamp.fromMillis(now()) },\n { merge: true },\n )\n for (const rulePath of [announcedPath(fields), announcedPath({ kind: text(data['kind']), source: text(data['source']) })]) {\n if (rulePath) paths.push(rulePath)\n }\n const result: TransferRowResult = { row: row.index, outcome: 'updated', recordId: id }\n const entry: TransferUndoEntry = { row: row.index, recordId: id, action: 'updated', previous, written }\n return { result, entry }\n }\n\n for (const row of chunk.rows) {\n const before = await writer.alreadyApplied(row.index)\n if (before) {\n results.push(before)\n continue\n }\n if (writer.timeLeftMs() < ROW_RESERVE_MS) break\n const outcome = site.refusal\n ? fail(row, site.refusal, 'resourceRule')\n : row.verdict === 'create'\n ? await create(row)\n : await update(row)\n await writer.markApplied(outcome.result, outcome.entry)\n results.push(outcome.result)\n if (outcome.entry) undo.push(outcome.entry)\n }\n\n if (paths.length) await announce(site.hostId, [...new Set(paths)]).catch(() => undefined)\n return { results, undo }\n },\n\n async revert(ctx, snapshot, decisions?: TransferRevertDecisions): Promise<TransferRevertResult> {\n const site = await readSite(ctx)\n // Undo writes rules too, so it needs the same role; a lapsed plan does\n // not stop it, since a rule the plan no longer covers is not served.\n if (!ctx.actorUid || !holdsSiteKey(site.host, ctx.actorUid, 'site.publish')) {\n throw new Error(siteKeyRefusal('site.publish'))\n }\n const approvalField = site.declared.externalDestination?.approvedByField ?? REDIRECT_FIELD.approvedBy\n const done: TransferUndoStep[] = []\n const conflicts: TransferUndoStep[] = []\n const paths: string[] = []\n for (const entry of snapshot.entries) {\n const ref = rulesOf(site.hostId).doc(entry.recordId)\n const stored = await ref.get()\n const data = stored.exists ? (stored.data() as StoredRule) : undefined\n const current = isLive(data) ? redirectRuleValues(entry.recordId, data) : null\n const step = planTransferUndo(entry, current)\n if (step.action === 'conflict' && decisions?.[entry.recordId] !== 'revert') {\n if (decisions?.[entry.recordId] === 'keep') {\n done.push({ action: 'nothing', recordId: entry.recordId, why: 'alreadyReverted' })\n } else conflicts.push(step)\n continue\n }\n const before = current ? announcedPath({ kind: String(current['kind']), source: String(current['source']) }) : null\n if (step.action === 'delete' || (step.action === 'conflict' && entry.action === 'created')) {\n await ref.delete()\n if (before) paths.push(before)\n } else if ((step.action === 'restore' || step.action === 'conflict') && current) {\n const restored = { ...current, ...step.values }\n const patch: Record<string, unknown> = {}\n for (const [fieldId, value] of Object.entries(step.values)) {\n patch[fieldId] = value === null || value === undefined ? FieldValue.delete() : value\n }\n // A destination on the site never keeps an approval.\n if (!isExternalRedirectDestination(text(restored['destination']))) patch[approvalField] = FieldValue.delete()\n patch['updatedAt'] = Timestamp.fromMillis(now())\n await ref.set(patch, { merge: true })\n const after = announcedPath({ kind: String(restored['kind']), source: String(restored['source']) })\n for (const path of [before, after]) if (path) paths.push(path)\n }\n done.push(step)\n }\n if (paths.length) await announce(site.hostId, [...new Set(paths)]).catch(() => undefined)\n return { done, conflicts }\n },\n }\n return resource\n}\n"],"names":["createResourceUid","holdsSiteKey","siteKeyRefusal","TENANT_APEX","checkEntitlement","checkQuota","countLiveDocuments","ACKNOWLEDGED_WARNING_CLASSES","TRANSFER_ID_FIELD","TRANSFER_WARNING_CLASSES","buildTransferPlan","isBlankTransferValue","matchLookupKey","planTransferUndo","transferValuesEqual","withTransferResourceFindings","pluginHostResource","dropPluginSiteCache","FieldPath","FieldValue","Timestamp","REDIRECT_DEFAULT_PRIORITY","REDIRECT_STATUS_CODES","findDuplicateRedirect","isExternalRedirectDestination","isSelfRedirect","normalizeRedirectDestination","normalizeRedirectSource","redirectHitKey","redirectSourceIsLivePage","validateRedirectRule","walkRedirectChain","REDIRECTS_ALIAS_DICTIONARIES","REDIRECTS_DEFAULT_POLICY","REDIRECTS_MATCH_KEYS","REDIRECT_FIELD","REDIRECT_HOST_RESOURCE_KIND","REDIRECT_WRITABLE_FIELDS","canonicalRedirectKind","redirectValuesEqual","redirectsTransferCatalog","ROW_RESERVE_MS","PAGE_SIZE_DEFAULT","PAGE_SIZE_MAX","IN_QUERY_MAX","GET_ALL_MAX","HIT_DAYS","DAY_MS","text","value","isoOf","undefined","toMillis","Date","toISOString","Number","isFinite","validStatusCode","includes","redirectRuleValues","id","data","priority","source","kind","destination","statusCode","enabled","approvedBy","createdAt","updatedAt","createdBy","lastHitAt","ruleRow","$id","isLive","Boolean","storedSource","trim","ruleKey","stored","announcedPath","rule","startsWith","finalRule","before","diff","values","change","fieldId","after","withDefaults","String","ownDomains","host","ruleProblem","problem","test","detail","storedFields","allowed","normalized","Object","fromEntries","entries","filter","key","hostnameOf","URL","hostname","requireHost","ctx","hostId","Error","redirectDeclaration","declared","pick","fieldIds","row","slices","items","size","out","at","length","push","slice","withCreateLimit","plan","remaining","limit","creates","held","rows","map","verdict","failed","reason","warnings","summary","create","update","unchanged","skip","fail","total","prior","find","entry","class","raised","count","samples","index","requiresAcknowledgement","flatMap","warning","acknowledgementsRequired","redirectMatchOutcomes","engine","existing","live","byKey","Map","set","get","found","byId","via","record","recordId","rowKind","candidates","recordIds","importedRedirectId","jobId","createRedirectsTransferResource","deps","now","firestore","announce","announcePaths","paths","hostIds","hostRef","collection","doc","rulesOf","readSite","snapshot","org","Promise","all","loadOrg","exists","roleKey","requiresPublishRole","roleOk","actorUid","billing","refusal","entitlement","quotaKey","POSITIVE_INFINITY","readAllRules","docs","readByIds","ids","Set","snapshots","getAll","readHits","today","refs","Array","from","_","totals","day","exportRow","hits","resource","fields","matchKeys","aliases","defaultPolicy","valuesEqual","match","input","records","outcomes","outcome","has","options","readPage","cursor","Math","min","max","floor","pageSize","start","next","query","orderBy","documentId","startAfter","last","lookup","requests","file","request","form","add","where","site","documents","matches","sourceKind","defaults","findings","writing","refuse","accepted","liveRows","touched","ruleset","kept","keptByKey","keptByRecord","candidate","repeated","loop","current","walk","path","hops","join","cycle","screens","apply","chunk","writer","actor","results","undo","message","result","approvalField","externalDestination","approvedByField","stampFor","stamp","ref","fromMillis","runTransaction","tx","existed","used","aggregate","quota","label","written","logCreated","catch","action","previous","keys","delete","merge","rulePath","alreadyApplied","timeLeftMs","markApplied","revert","decisions","done","conflicts","step","why","restored","patch"],"mappings":"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"}
|