@aglyn/tenant-data-admin 1.0.0-beta.218 → 1.0.0-beta.219

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.
Files changed (99) hide show
  1. package/package.json +7 -7
  2. package/src/lib/server/api-v1-description.d.ts +112 -0
  3. package/src/lib/server/api-v1-description.js +126 -0
  4. package/src/lib/server/api-v1-description.js.map +1 -0
  5. package/src/lib/server/api-v1-kit.d.ts +156 -0
  6. package/src/lib/server/api-v1-kit.js +191 -0
  7. package/src/lib/server/api-v1-kit.js.map +1 -0
  8. package/src/lib/server/api-v1-resources.d.ts +144 -0
  9. package/src/lib/server/api-v1-resources.js +126 -0
  10. package/src/lib/server/api-v1-resources.js.map +1 -0
  11. package/src/lib/server/consent-group-change.d.ts +2 -1
  12. package/src/lib/server/consent-group-change.js +41 -16
  13. package/src/lib/server/consent-group-change.js.map +1 -1
  14. package/src/lib/server/contact-email-index.d.ts +22 -0
  15. package/src/lib/server/contact-email-index.js +108 -0
  16. package/src/lib/server/contact-email-index.js.map +1 -1
  17. package/src/lib/server/crm-email-activity.d.ts +1 -1
  18. package/src/lib/server/crm-email-activity.js +1 -1
  19. package/src/lib/server/crm-email-activity.js.map +1 -1
  20. package/src/lib/server/crm-inbound-email.d.ts +23 -0
  21. package/src/lib/server/crm-inbound-email.js +1 -16
  22. package/src/lib/server/crm-inbound-email.js.map +1 -1
  23. package/src/lib/server/crm-records.d.ts +9 -1
  24. package/src/lib/server/crm-records.js +19 -2
  25. package/src/lib/server/crm-records.js.map +1 -1
  26. package/src/lib/server/cron-auth.d.ts +44 -0
  27. package/src/lib/server/cron-auth.js +76 -0
  28. package/src/lib/server/cron-auth.js.map +1 -0
  29. package/src/lib/server/dataset-live-pages.js +6 -6
  30. package/src/lib/server/dataset-live-pages.js.map +1 -1
  31. package/src/lib/server/duplicate-resource.d.ts +26 -0
  32. package/src/lib/server/duplicate-resource.js +47 -25
  33. package/src/lib/server/duplicate-resource.js.map +1 -1
  34. package/src/lib/server/dynamic-list-materialize.js +4 -1
  35. package/src/lib/server/dynamic-list-materialize.js.map +1 -1
  36. package/src/lib/server/email-suppression.d.ts +11 -0
  37. package/src/lib/server/email-suppression.js +36 -1
  38. package/src/lib/server/email-suppression.js.map +1 -1
  39. package/src/lib/server/email-unsubscribe-link.d.ts +8 -5
  40. package/src/lib/server/email-unsubscribe-link.js +7 -7
  41. package/src/lib/server/email-unsubscribe-link.js.map +1 -1
  42. package/src/lib/server/erase.d.ts +19 -33
  43. package/src/lib/server/erase.js +37 -162
  44. package/src/lib/server/erase.js.map +1 -1
  45. package/src/lib/server/host-email-tokens.js +1 -1
  46. package/src/lib/server/host-email-tokens.js.map +1 -1
  47. package/src/lib/server/host-memberships.js +3 -3
  48. package/src/lib/server/host-memberships.js.map +1 -1
  49. package/src/lib/server/member-email-aliases.d.ts +1 -1
  50. package/src/lib/server/member-email-aliases.js +1 -1
  51. package/src/lib/server/member-email-aliases.js.map +1 -1
  52. package/src/lib/server/new-account-announcement.d.ts +62 -0
  53. package/src/lib/server/new-account-announcement.js +77 -0
  54. package/src/lib/server/new-account-announcement.js.map +1 -0
  55. package/src/lib/server/notifications.d.ts +8 -2
  56. package/src/lib/server/notifications.js +11 -3
  57. package/src/lib/server/notifications.js.map +1 -1
  58. package/src/lib/server/{connect-payout-failure.d.ts → payment-provider-stripe-connect-payouts.d.ts} +3 -0
  59. package/src/lib/server/{connect-payout-failure.js → payment-provider-stripe-connect-payouts.js} +1 -1
  60. package/src/lib/server/payment-provider-stripe-connect-payouts.js.map +1 -0
  61. package/src/lib/server/{connect-account-status.d.ts → payment-provider-stripe-connect-status.d.ts} +3 -0
  62. package/src/lib/server/{connect-account-status.js → payment-provider-stripe-connect-status.js} +1 -1
  63. package/src/lib/server/payment-provider-stripe-connect-status.js.map +1 -0
  64. package/src/lib/server/{stripe-account-mode.d.ts → payment-provider-stripe-connect.d.ts} +45 -70
  65. package/src/lib/server/{stripe-account-mode.js → payment-provider-stripe-connect.js} +78 -52
  66. package/src/lib/server/payment-provider-stripe-connect.js.map +1 -0
  67. package/src/lib/server/payment-provider.d.ts +173 -0
  68. package/src/lib/server/payment-provider.js +70 -0
  69. package/src/lib/server/payment-provider.js.map +1 -0
  70. package/src/lib/server/personal-data-export.js +1 -1
  71. package/src/lib/server/personal-data-export.js.map +1 -1
  72. package/src/lib/server/rate-limit-store.js +13 -3
  73. package/src/lib/server/rate-limit-store.js.map +1 -1
  74. package/src/lib/server/realm-plugins.js +8 -3
  75. package/src/lib/server/realm-plugins.js.map +1 -1
  76. package/src/lib/server/retained-refusals.d.ts +142 -0
  77. package/src/lib/server/retained-refusals.js +325 -0
  78. package/src/lib/server/retained-refusals.js.map +1 -0
  79. package/src/lib/server/test-firestore-queries.d.ts +7 -0
  80. package/src/lib/server/test-firestore-queries.js +70 -27
  81. package/src/lib/server/test-firestore-queries.js.map +1 -1
  82. package/src/lib/server/upsert-contact.d.ts +46 -1
  83. package/src/lib/server/upsert-contact.js +85 -18
  84. package/src/lib/server/upsert-contact.js.map +1 -1
  85. package/src/tenancy.d.ts +4 -5
  86. package/src/tenancy.js +11 -6
  87. package/src/tenancy.js.map +1 -1
  88. package/src/lib/server/connect-account-status.js.map +0 -1
  89. package/src/lib/server/connect-payout-failure.js.map +0 -1
  90. package/src/lib/server/contact-lifecycle-floor.d.ts +0 -89
  91. package/src/lib/server/contact-lifecycle-floor.js +0 -91
  92. package/src/lib/server/contact-lifecycle-floor.js.map +0 -1
  93. package/src/lib/server/contact-merge.d.ts +0 -55
  94. package/src/lib/server/contact-merge.js +0 -288
  95. package/src/lib/server/contact-merge.js.map +0 -1
  96. package/src/lib/server/form-dataset-binding-token.d.ts +0 -35
  97. package/src/lib/server/form-dataset-binding-token.js +0 -107
  98. package/src/lib/server/form-dataset-binding-token.js.map +0 -1
  99. package/src/lib/server/stripe-account-mode.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/consent-group-change.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * RUNNING A CONSENT GROUP CHANGE (AGL-3320) — the executor.\n *\n * `consent-group-change.ts` in `app-utils` decides WHAT a change requires;\n * this carries it out, across as many invocations as it takes, without ever\n * leaving a refusal unhonored in between.\n *\n * ## The order is the safety argument\n *\n * 1. START — one transaction on the org document: the stored declaration is\n * the one the editor rendered, and no other change is in flight. It sets\n * the marker `consentGroupsChange` and creates the job document.\n * 2. CARRY — every carry of the plan through the three platform stores\n * (`consent-group-carry.ts`), then every participant's carry phase.\n * 3. CATCH UP — everything filed since the carry began, again.\n * 4. DECLARE — one transaction: the marker still names this change in its\n * carry phase and the stored declaration is still the one the job\n * started from. Only then is the new declaration written.\n * 5. CATCH UP — once more, for whatever raced the flip.\n * 6. RE-HOME — every participant's holder records.\n * 7. SWEEP — once the sweep delay has passed, the whole carry and every\n * participant's sweep, idempotently.\n * 8. DONE — the marker is cleared.\n *\n * The flip cannot happen before every carry and both catch-up passes are\n * done, because the steps run strictly in order and `declare` is only ever\n * reached from a finished `catch-up-pre`. Until the flip the declaration in\n * force is the old one, so a change that stalls or is canceled before it\n * has changed nothing anybody can observe — except refusals honored on more\n * sites, which is the safe direction. A rename moves nothing, and runs the\n * declare and finish alone.\n *\n * ## One invocation at a time, and any invocation may finish it\n *\n * The console's progress panel calls `continue` while it is open, and the\n * cron backstop calls every fifteen minutes. Each claims a LEASE on the job\n * by transaction and renews it on every unit it saves; a lease that lapses\n * (a process that died) is simply claimed by the next caller. A unit is one\n * page of one store, or one participant call, and every unit is idempotent,\n * so a unit re-run after a crash writes nothing it already wrote.\n *\n * ## A failure is retried, and five in a row are shown\n *\n * A unit that throws releases the lease and counts a failure against the job;\n * the next invocation retries it from its cursor. Five consecutive failures\n * mark the job STALLED — the activity log says so once, and the panel shows\n * the error with a retry — and the cron keeps trying. A participant's throw\n * is one of these: the change never flips past a participant that could not\n * carry its refusals (see `plugin-consent-group-change.ts`).\n */\n\nimport {\n CONSENT_GROUP_CHANGE_STEPS,\n CONSENT_GROUP_CHANGES_COLLECTION,\n CONSENT_GROUP_RENAME_STEPS,\n CONSENT_GROUP_SWEEP_DELAY_MS,\n CONSENT_GROUPS_CHANGE_FIELD,\n type ConsentGroupChangeCounts,\n type ConsentGroupChangeLine,\n type ConsentGroupChangePlan,\n type ConsentGroupChangePreview,\n type ConsentGroupChangeStatus,\n type ConsentGroupChangeStep,\n type ConsentGroupChangeProgress,\n type ConsentGroupDeclaration,\n type ConsentGroupsApiInFlight,\n type ConsentGroupsApiRefusal,\n type ConsentGroupsApiStale,\n type ConsentGroupsChangeMarker,\n type ConsentGroupsChangePhase,\n consentGroupChangePhaseOf,\n consentGroupDisclosureChanges,\n consentGroupsFingerprint,\n describeConsentGroupChangeLine,\n discardedConsentGroupIds,\n estimateConsentGroupChange,\n orgSiteIds,\n planConsentGroupChange,\n readConsentGroupsChange,\n validateConsentGroupDeclaration,\n} from '@aglyn/aglyn/app-utils/consent-group-change'\nimport {\n CONSENT_GROUPS_FIELD,\n consentGroupsAwaitConfirmation,\n readConsentGroups,\n} from '@aglyn/aglyn/app-utils/consent-groups'\nimport { createResourceUid } from '@aglyn/aglyn/app-utils/create-resource-uid'\nimport {\n DEFAULT_EMAIL_TOPICS,\n TOPIC_OPT_OUTS_SUBCOLLECTION,\n} from '@aglyn/aglyn/app-utils/email-topics'\nimport { resolveMarketingConsentPolicy } from '@aglyn/aglyn/app-utils/marketing-consent'\nimport { isOrgWideMember } from '@aglyn/aglyn/app-utils/organizations'\nimport {\n type ConsentGroupChangeParticipant,\n type ConsentGroupChangeParticipantPhase,\n listPluginConsentGroupParticipants,\n previewPluginConsentGroupChange,\n} from '@aglyn/aglyn/plugin-manager/plugin-consent-group-change'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport {\n CONSENT_GROUP_CARRY_STORES,\n CONSENT_GROUP_CARRY_SUBCOLLECTIONS,\n type ConsentGroupCarryStore,\n carryConsentGroupRefusals,\n} from './consent-group-carry'\nimport firebaseAdmin from './firebase-admin'\nimport { getLockdownVerdict } from './lockdown'\nimport { logOrgActivity } from './organizations'\n\n/** How long a claimed job stays claimed without a renewal. */\nexport const CONSENT_GROUP_CHANGE_LEASE_MS = 90_000\n\n/** Consecutive failed units before a job is shown as stalled. */\nexport const CONSENT_GROUP_CHANGE_STALL_AFTER = 5\n\n/**\n * How far before the carry began a catch-up reads. A refusal filed in the\n * seconds the carry's first page was being read, with a clock a little\n * behind, is inside it.\n */\nconst CATCH_UP_OVERLAP_MS = 5 * 60_000\n\n/** Left before a deadline for the last save. */\nconst DEADLINE_SAFETY_MS = 5_000\n\n/** The phases the cron looks for a change in. */\nconst RUNNING_PHASES: readonly ConsentGroupsChangePhase[] = ['carry', 'rehome', 'sweep']\n\n/** Who asked, as the activity log records it. */\nexport interface ConsentGroupChangeActor {\n uid: string | null\n email?: string | null\n}\n\n/** Everything the executor reaches outside itself, injectable for the specs. */\nexport interface ConsentGroupChangeContext {\n firestore?: FirebaseFirestore.Firestore\n log?: typeof logOrgActivity\n now?: () => number\n /** Whether a lockdown refuses the cron acting for this org and actor. */\n lockedOut?: (org: Record<string, unknown>, uid: string | null) => Promise<boolean>\n}\n\ninterface Context {\n firestore: FirebaseFirestore.Firestore\n log: typeof logOrgActivity\n now: () => number\n lockedOut: (org: Record<string, unknown>, uid: string | null) => Promise<boolean>\n}\n\nfunction contextOf(input: ConsentGroupChangeContext): Context {\n return {\n firestore: input.firestore ?? firebaseAdmin.app().firestore(),\n log: input.log ?? logOrgActivity,\n now: input.now ?? Date.now,\n lockedOut:\n input.lockedOut ??\n (async (org, uid) =>\n (await getLockdownVerdict({ org: org as never, uid, intent: 'write' })) !== null),\n }\n}\n\n/** Where a unit of the current step stands. */\ninterface UnitState {\n cursor: string | null\n done: boolean\n}\n\n/** The job document, `orgs/{orgId}/consentGroupChanges/{changeId}`. */\nexport interface ConsentGroupChangeJob {\n changeId: string\n orgId: string\n status: ConsentGroupChangeStatus\n step: ConsentGroupChangeStep\n /** The marker's phase while running; `done` or `canceled` after. */\n phase: ConsentGroupsChangePhase | 'done' | 'canceled'\n /** The steps this change runs, in order. */\n steps: ConsentGroupChangeStep[]\n /** The stored declaration it started from: what the flip re-checks. */\n beforeFingerprint: string\n before: ConsentGroupDeclaration\n after: ConsentGroupDeclaration\n plan: Omit<ConsentGroupChangePlan, 'before' | 'after'>\n /** The plan's lines as the activity log writes them. */\n lines: string[]\n siteNames: Record<string, string>\n /** The participants registered when it started — every one of them runs. */\n participants: string[]\n /** The current step's units, by key. */\n units: Record<string, UnitState>\n counts: ConsentGroupChangeCounts\n sitesReceiving: string[]\n plugins: Record<string, Record<string, number>>\n lease: { owner: string; untilMs: number } | null\n failures: number\n stalled: boolean\n lastError: string | null\n actor: { uid: string | null; email: string | null }\n startedAtMs: number\n carryStartedAtMs: number\n declaredAtMs: number | null\n finishedAtMs: number | null\n canceledAtMs: number | null\n}\n\n/** What the route answers for a job. */\nexport interface ConsentGroupChangeStatusBody {\n changeId: string\n phase: ConsentGroupsChangePhase | 'done' | 'canceled'\n done: boolean\n progress: ConsentGroupChangeProgress\n}\n\n/** A refusal the route turns into its status code and body. */\nexport type ConsentGroupChangeRefusal =\n | { ok: false; status: 400; body: ConsentGroupsApiRefusal }\n | { ok: false; status: 404; body: { error: string } }\n | { ok: false; status: 409; body: ConsentGroupsApiStale | ConsentGroupsApiInFlight }\n\nconst NOT_FOUND = { ok: false as const, status: 404 as const, body: { error: 'No such consent group change' } }\n\n/** A caller whose lease was taken, or whose job was canceled, under it. */\nclass LeaseLost extends Error {}\n\nconst orgDoc = (ctx: Context, orgId: string) => ctx.firestore.collection('orgs').doc(orgId)\nconst jobDoc = (ctx: Context, orgId: string, changeId: string) =>\n orgDoc(ctx, orgId).collection(CONSENT_GROUP_CHANGES_COLLECTION).doc(changeId)\n\n/** The job as the plan the participants and the carries read. */\nfunction planOf(job: ConsentGroupChangeJob): ConsentGroupChangePlan {\n return { ...job.plan, before: job.before, after: job.after }\n}\n\n/** An error as the job records it: bounded, and with no address in it. */\nfunction sanitizeError(error: unknown): string {\n const message = error instanceof Error ? error.message : String(error ?? 'Unknown error')\n return message.replace(/[^\\s@]+@[^\\s@]+/g, '[address]').slice(0, 300) || 'Unknown error'\n}\n\nexport function consentGroupChangeStatus(\n job: ConsentGroupChangeJob,\n nowMs: number = Date.now(),\n): ConsentGroupChangeStatusBody {\n const declaredAtMs = job.declaredAtMs ?? null\n const index = job.steps.indexOf(job.step)\n return {\n changeId: job.changeId,\n phase: job.phase,\n done: job.status !== 'running',\n progress: {\n status: job.status,\n step: job.step,\n stepIndex: index >= 0 ? index + 1 : job.steps.length,\n stepCount: job.steps.length,\n declaredAtMs,\n sweepAtMs:\n declaredAtMs !== null && !job.plan.renameOnly\n ? declaredAtMs + CONSENT_GROUP_SWEEP_DELAY_MS\n : null,\n stalled: job.stalled === true,\n failures: job.failures ?? 0,\n lastError: job.lastError ?? null,\n leaseUntilMs: job.lease && job.lease.untilMs > nowMs ? job.lease.untilMs : null,\n counts: { ...job.counts },\n sitesReceiving: job.sitesReceiving?.length ?? 0,\n plugins: job.plugins ?? {},\n },\n }\n}\n\n/*==========================================\n * VALIDATING AND PLANNING — shared by the preview and the start\n *=========================================*/\n\ninterface Prepared {\n org: Record<string, unknown>\n before: ConsentGroupDeclaration\n after: ConsentGroupDeclaration\n plan: ConsentGroupChangePlan\n siteIds: string[]\n}\n\nfunction prepare(\n org: Record<string, unknown>,\n expected: unknown,\n groups: unknown,\n): Prepared | ConsentGroupChangeRefusal {\n const marker = readConsentGroupsChange(org)\n if (marker) {\n return {\n ok: false,\n status: 409,\n body: {\n error: 'A consent group change is still finishing',\n changeId: marker.changeId,\n phase: marker.phase,\n },\n }\n }\n const raw = org[CONSENT_GROUPS_FIELD]\n if (consentGroupsFingerprint(raw) !== consentGroupsFingerprint(expected)) {\n return {\n ok: false,\n status: 409,\n body: {\n error: 'Someone else changed consent groups while you were editing',\n current:\n raw && typeof raw === 'object' && !Array.isArray(raw)\n ? (raw as Record<string, unknown>)\n : null,\n },\n }\n }\n const before = readConsentGroups(org)\n const siteIds = orgSiteIds(org)\n const validation = validateConsentGroupDeclaration({\n groups,\n before,\n siteIds,\n reserved:\n raw && typeof raw === 'object' && !Array.isArray(raw)\n ? Object.keys(raw as Record<string, unknown>)\n : [],\n })\n if (!validation.ok) {\n return {\n ok: false,\n status: 400,\n body: {\n error: 'Those consent groups cannot be saved',\n errors: (validation as { errors: ConsentGroupsApiRefusal['errors'] }).errors,\n },\n }\n }\n const after = (validation as { after: ConsentGroupDeclaration }).after\n return {\n org,\n before,\n after,\n plan: planConsentGroupChange(before, after, { siteIds }),\n siteIds,\n }\n}\n\nconst isRefusal = (value: unknown): value is ConsentGroupChangeRefusal =>\n Boolean(value && typeof value === 'object' && (value as { ok?: unknown }).ok === false)\n\n/** Every site a plan names, for the names its lines are written with. */\nfunction plannedSites(plan: ConsentGroupChangePlan): string[] {\n return [\n ...new Set([\n ...plan.hostIds,\n ...Object.values(plan.before).flatMap((group) => group.hostIds),\n ...Object.values(plan.after).flatMap((group) => group.hostIds),\n ]),\n ]\n}\n\n/** What the console calls each site: its name, its subdomain, or its id. */\nasync function siteNamesFor(ctx: Context, hostIds: readonly string[]): Promise<Record<string, string>> {\n if (!hostIds.length) return {}\n const hosts = ctx.firestore.collection('hosts')\n const snapshots = await ctx.firestore.getAll(...hostIds.map((id) => hosts.doc(id)))\n const names: Record<string, string> = {}\n snapshots.forEach((snapshot, index) => {\n const id = hostIds[index]\n names[id] = String(snapshot.get('name') || snapshot.get('subdomain') || id)\n })\n return names\n}\n\nconst describeLines = (lines: readonly ConsentGroupChangeLine[], names: Record<string, string>) =>\n lines.map((line) => describeConsentGroupChangeLine(line, (hostId) => names[hostId] ?? hostId))\n\n/*==========================================\n * THE PREVIEW\n *=========================================*/\n\nasync function countOf(query: FirebaseFirestore.Query): Promise<number> {\n const snapshot = await query.count().get()\n return Number(snapshot.data().count) || 0\n}\n\n/**\n * What a change would do, counted and worded, writing nothing.\n *\n * Refused exactly as `apply` would be — a change in flight, a stale\n * `expected`, an invalid declaration — so the review step never shows a\n * change the next click cannot make.\n */\nexport async function previewConsentGroupChange(\n input: {\n orgId: string\n expected: unknown\n groups: unknown\n /** The org document, when the caller already read it. */\n org?: Record<string, unknown> | null\n } & ConsentGroupChangeContext,\n): Promise<{ ok: true; preview: ConsentGroupChangePreview } | ConsentGroupChangeRefusal> {\n const ctx = contextOf(input)\n const org =\n input.org ?? ((await orgDoc(ctx, input.orgId).get()).data() as Record<string, unknown> | undefined)\n if (!org) return { ok: false, status: 404, body: { error: 'No such workspace' } }\n const prepared = prepare(org, input.expected, input.groups)\n if (isRefusal(prepared)) return prepared\n const { plan, before, after } = prepared as Prepared\n\n const db = ctx.firestore\n const cache = new Map<string, Promise<number>>()\n const cached = (key: string, query: () => FirebaseFirestore.Query) => {\n let pending = cache.get(key)\n if (!pending) {\n pending = countOf(query())\n cache.set(key, pending)\n }\n return pending\n }\n const store = (hostId: string, name: ConsentGroupCarryStore) =>\n db.collection('hosts').doc(hostId).collection(CONSENT_GROUP_CARRY_SUBCOLLECTIONS[name])\n const suppressions = (hostId: string) => cached(`s:${hostId}`, () => store(hostId, 'siteSuppressions'))\n const optOuts = (hostId: string) => cached(`t:${hostId}`, () => store(hostId, 'topicOptOuts'))\n const paces = (hostId: string) =>\n cached(`p:${hostId}`, () => store(hostId, 'paces').where('cadence', '!=', null))\n\n const carries = await Promise.all(\n plan.carries.map(async (carry) => ({\n toHostId: carry.toHostId,\n fromHostId: carry.fromHostId,\n siteSuppressions: await suppressions(carry.fromHostId),\n topicOptOuts: await optOuts(carry.fromHostId),\n paces: await paces(carry.fromHostId),\n })),\n )\n\n const membersIn = (declaration: ConsentGroupDeclaration, hostId: string) =>\n Object.values(declaration).find((group) => group.hostIds.includes(hostId))?.hostIds ?? [hostId]\n const joining: Array<{ hostId: string; siblings: string[] }> = []\n for (const hostId of [...new Set(Object.values(after).flatMap((group) => group.hostIds))].sort()) {\n const had = new Set(membersIn(before, hostId))\n const siblings = membersIn(after, hostId).filter((id) => id !== hostId && !had.has(id))\n if (siblings.length) joining.push({ hostId, siblings })\n }\n const inherited = await Promise.all(\n joining.map(async ({ hostId, siblings }) => {\n let refusals = 0\n for (const sibling of siblings) {\n refusals += (await suppressions(sibling)) + (await optOuts(sibling))\n }\n return { hostId, refusals }\n }),\n )\n\n /*\n * A pending confirmation holds a sibling's mail only with the switch on,\n * so only then does a separation release anything. Counted over the\n * built-in streams: the catalog of an organization's own streams belongs to\n * the Email plugin, and a stream it added is not counted here.\n */\n const pendingHolds: ConsentGroupChangePreview['pendingHolds'] = []\n if (consentGroupsAwaitConfirmation(org)) {\n const released = new Map<string, string[]>()\n for (const carry of plan.carries) {\n released.set(carry.toHostId, [...(released.get(carry.toHostId) ?? []), carry.fromHostId])\n }\n for (const [hostId, releasedHostIds] of [...released.entries()].sort()) {\n for (const topic of DEFAULT_EMAIL_TOPICS) {\n const count = await countOf(\n db\n .collection('hosts')\n .doc(hostId)\n .collection(TOPIC_OPT_OUTS_SUBCOLLECTION)\n .where(`topics.${topic.id}.confirmedAt`, '==', null),\n )\n if (count > 0) pendingHolds.push({ hostId, topicId: topic.id, count, releasedHostIds })\n }\n }\n }\n\n const changedGroups = Object.entries(after)\n .filter(([id, group]) => {\n const was = before[id]\n return !was || was.hostIds.join('\\n') !== group.hostIds.join('\\n')\n })\n .map(([, group]) => group.hostIds)\n let partialAccessMembers = 0\n if (changedGroups.length) {\n const members = await orgDoc(ctx, input.orgId).collection('members').get()\n for (const member of members.docs) {\n const data = member.data() as Record<string, unknown>\n if (isOrgWideMember(data)) continue\n const reach = new Set(Object.keys((data['hostAccess'] ?? {}) as Record<string, unknown>))\n if (\n changedGroups.some(\n (hostIds) =>\n hostIds.some((id) => reach.has(id)) && !hostIds.every((id) => reach.has(id)),\n )\n ) {\n partialAccessMembers += 1\n }\n }\n }\n\n const policy = resolveMarketingConsentPolicy(org['marketingConsentPolicy'])\n const forwardPolicyWarning =\n policy.mode === 'forward' && joining.length\n ? { hostIds: joining.map((entry) => entry.hostId), enforceFromMs: policy.enforceFromMs }\n : null\n\n const participants = await previewPluginConsentGroupChange({ orgId: input.orgId, plan })\n const names = await siteNamesFor(ctx, plannedSites(plan))\n const texts = describeLines(plan.lines, names)\n const documents =\n carries.reduce((total, carry) => total + carry.siteSuppressions + carry.topicOptOuts + carry.paces, 0) +\n participants.reduce(\n (total, entry) =>\n total + (entry.lines ?? []).reduce((sum, line) => sum + (line.count ?? 0), 0),\n 0,\n )\n\n return {\n ok: true,\n preview: {\n before,\n after,\n discarded: discardedConsentGroupIds(org),\n lines: plan.lines.map((line, index) => ({ ...line, text: texts[index] })),\n disclosures: consentGroupDisclosureChanges(before, after),\n carries,\n inherited,\n pendingHolds,\n partialAccessMembers,\n forwardPolicyWarning,\n participants,\n capturesDisclosing: ['form'],\n estimate: estimateConsentGroupChange(plan, documents),\n },\n }\n}\n\n/*==========================================\n * STARTING\n *=========================================*/\n\n/**\n * Starts a change: the org marker and the job, in one transaction that\n * refuses a change in flight, a stale `expected` and an invalid declaration.\n * Writes nothing else — the caller advances it.\n */\nexport async function startConsentGroupChange(\n input: {\n orgId: string\n actor: ConsentGroupChangeActor\n expected: unknown\n groups: unknown\n } & ConsentGroupChangeContext,\n): Promise<{ ok: true; changeId: string; job: ConsentGroupChangeJob } | ConsentGroupChangeRefusal> {\n const ctx = contextOf(input)\n const orgRef = orgDoc(ctx, input.orgId)\n const snapshot = await orgRef.get()\n if (!snapshot.exists) return { ok: false, status: 404, body: { error: 'No such workspace' } }\n const outside = prepare(snapshot.data() as Record<string, unknown>, input.expected, input.groups)\n if (isRefusal(outside)) return outside\n // Names are words for the log, read once and outside the transaction.\n const names = await siteNamesFor(ctx, plannedSites((outside as Prepared).plan))\n const participants = listPluginConsentGroupParticipants().map((entry) => entry.pluginId)\n const changeId = createResourceUid()\n const jobRef = jobDoc(ctx, input.orgId, changeId)\n const nowMs = ctx.now()\n\n const started = await ctx.firestore.runTransaction(async (transaction) => {\n const current = await transaction.get(orgRef)\n if (!current.exists) {\n return { ok: false as const, status: 404 as const, body: { error: 'No such workspace' } }\n }\n const prepared = prepare(current.data() as Record<string, unknown>, input.expected, input.groups)\n if (isRefusal(prepared)) return prepared\n const { plan, before, after, org } = prepared as Prepared\n const steps = plan.renameOnly ? [...CONSENT_GROUP_RENAME_STEPS] : [...CONSENT_GROUP_CHANGE_STEPS]\n const job: ConsentGroupChangeJob = {\n changeId,\n orgId: input.orgId,\n status: 'running',\n step: steps[0],\n phase: 'carry',\n steps,\n beforeFingerprint: consentGroupsFingerprint(org[CONSENT_GROUPS_FIELD]),\n before,\n after,\n plan: {\n carries: plan.carries,\n flows: plan.flows,\n lines: plan.lines,\n hostIds: plan.hostIds,\n renameOnly: plan.renameOnly,\n },\n lines: describeLines(plan.lines, names),\n siteNames: names,\n participants: plan.renameOnly ? [] : participants,\n units: {},\n counts: { siteSuppressions: 0, topicOptOuts: 0, paces: 0 },\n sitesReceiving: [],\n plugins: {},\n lease: null,\n failures: 0,\n stalled: false,\n lastError: null,\n actor: { uid: input.actor.uid ?? null, email: input.actor.email ?? null },\n startedAtMs: nowMs,\n carryStartedAtMs: nowMs,\n declaredAtMs: null,\n finishedAtMs: null,\n canceledAtMs: null,\n }\n const marker: ConsentGroupsChangeMarker = {\n changeId,\n phase: 'carry',\n hostIds: plan.hostIds,\n startedAtMs: nowMs,\n }\n transaction.update(orgRef, {\n [CONSENT_GROUPS_CHANGE_FIELD]: marker,\n updatedAt: FieldValue.serverTimestamp(),\n })\n transaction.create(jobRef, {\n ...job,\n createdAt: FieldValue.serverTimestamp(),\n updatedAt: FieldValue.serverTimestamp(),\n })\n return { ok: true as const, job }\n })\n if (isRefusal(started)) return started\n const { job } = started as { job: ConsentGroupChangeJob }\n await ctx.log(\n input.orgId,\n { uid: job.actor.uid, email: job.actor.email },\n `Started a consent group change: ${job.lines.join('; ')}`,\n { type: 'org', id: input.orgId },\n )\n return { ok: true, changeId, job }\n}\n\n/*==========================================\n * ADVANCING\n *=========================================*/\n\ntype Unit =\n | { kind: 'carry'; key: string; carryIndex: number; store: ConsentGroupCarryStore; sinceMs: number | null }\n | { kind: 'participant'; key: string; pluginId: string; phase: ConsentGroupChangeParticipantPhase }\n\n/** The current step's units, in the order they run. */\nfunction unitsOf(job: ConsentGroupChangeJob): Unit[] {\n const carries = (sinceMs: number | null): Unit[] =>\n job.plan.carries.flatMap((_carry, carryIndex) =>\n CONSENT_GROUP_CARRY_STORES.map((store) => ({\n kind: 'carry' as const,\n key: `c${carryIndex}:${store}`,\n carryIndex,\n store,\n sinceMs,\n })),\n )\n const participants = (phase: ConsentGroupChangeParticipantPhase): Unit[] =>\n job.participants.map((pluginId) => ({\n kind: 'participant' as const,\n key: `p:${pluginId}`,\n pluginId,\n phase,\n }))\n const since = job.carryStartedAtMs - CATCH_UP_OVERLAP_MS\n switch (job.step) {\n case 'carry':\n return [...carries(null), ...participants('carry')]\n case 'catch-up-pre':\n case 'catch-up-post':\n return [...carries(since), ...participants('carry')]\n case 'rehome':\n return participants('rehome')\n case 'sweep':\n return [...carries(null), ...participants('sweep')]\n default:\n return []\n }\n}\n\nfunction jobFrom(snapshot: FirebaseFirestore.DocumentSnapshot): ConsentGroupChangeJob | null {\n return snapshot.exists ? (snapshot.data() as ConsentGroupChangeJob) : null\n}\n\n/**\n * Saves a unit's result, renewing the lease — only while the job is still\n * running and still this caller's.\n */\nasync function save(\n ctx: Context,\n jobRef: FirebaseFirestore.DocumentReference,\n owner: string,\n fields: Partial<ConsentGroupChangeJob>,\n extra?: (transaction: FirebaseFirestore.Transaction, job: ConsentGroupChangeJob) => Promise<void>,\n): Promise<ConsentGroupChangeJob> {\n return ctx.firestore.runTransaction(async (transaction) => {\n const job = jobFrom(await transaction.get(jobRef))\n if (!job || job.status !== 'running' || job.lease?.owner !== owner) throw new LeaseLost()\n if (extra) await extra(transaction, job)\n const next: Partial<ConsentGroupChangeJob> = {\n ...fields,\n lease: { owner, untilMs: ctx.now() + CONSENT_GROUP_CHANGE_LEASE_MS },\n failures: 0,\n stalled: false,\n lastError: null,\n }\n transaction.update(jobRef, { ...next, updatedAt: FieldValue.serverTimestamp() })\n return { ...job, ...next }\n })\n}\n\nfunction addCounts(\n base: Record<string, number> | undefined,\n more: Record<string, number>,\n): Record<string, number> {\n const out: Record<string, number> = { ...(base ?? {}) }\n for (const [key, value] of Object.entries(more)) {\n if (typeof value === 'number' && Number.isFinite(value)) out[key] = (out[key] ?? 0) + value\n }\n return out\n}\n\nasync function runUnit(\n ctx: Context,\n jobRef: FirebaseFirestore.DocumentReference,\n job: ConsentGroupChangeJob,\n owner: string,\n unit: Unit,\n deadlineMs: number,\n): Promise<ConsentGroupChangeJob> {\n const state = job.units[unit.key] ?? { cursor: null, done: false }\n if (unit.kind === 'carry') {\n const carry = job.plan.carries[unit.carryIndex]\n const page = await carryConsentGroupRefusals({\n firestore: ctx.firestore,\n store: unit.store,\n carry,\n changeId: job.changeId,\n cursor: state.cursor,\n sinceMs: unit.sinceMs,\n })\n const counts = { ...job.counts, [unit.store]: (job.counts[unit.store] ?? 0) + page.written }\n const sitesReceiving =\n page.written > 0 && !job.sitesReceiving.includes(carry.toHostId)\n ? [...job.sitesReceiving, carry.toHostId]\n : job.sitesReceiving\n return save(ctx, jobRef, owner, {\n units: { ...job.units, [unit.key]: { cursor: page.cursor, done: page.done } },\n counts,\n sitesReceiving,\n })\n }\n const participant = participantFor(unit.pluginId)\n const result = await participant.run({\n orgId: job.orgId,\n changeId: job.changeId,\n plan: planOf(job),\n phase: unit.phase,\n cursor: state.cursor,\n deadlineMs: deadlineMs - DEADLINE_SAFETY_MS,\n dryRun: false,\n })\n return save(ctx, jobRef, owner, {\n units: { ...job.units, [unit.key]: { cursor: result.cursor ?? null, done: result.done === true } },\n plugins: { ...job.plugins, [unit.pluginId]: addCounts(job.plugins[unit.pluginId], result.counts ?? {}) },\n })\n}\n\nfunction participantFor(pluginId: string): ConsentGroupChangeParticipant {\n const entry = listPluginConsentGroupParticipants().find((candidate) => candidate.pluginId === pluginId)\n if (!entry) {\n // The change started with this participant; running on without it would\n // skip its share. Another process — one whose declarations registered it —\n // picks the job up.\n throw new Error(`The ${pluginId} plugin's part of this change is not available here`)\n }\n return entry.participant\n}\n\n/** Moves the job to the step after its current one. */\nasync function nextStep(\n ctx: Context,\n orgId: string,\n jobRef: FirebaseFirestore.DocumentReference,\n job: ConsentGroupChangeJob,\n owner: string,\n): Promise<ConsentGroupChangeJob> {\n const next = job.steps[job.steps.indexOf(job.step) + 1] ?? 'finish'\n const phase = consentGroupChangePhaseOf(next)\n if (phase === job.phase) return save(ctx, jobRef, owner, { step: next, units: {} })\n // Entering the sweep: the marker says so, for the readers that hold off\n // while holder records may still be moving.\n const orgRef = orgDoc(ctx, orgId)\n return save(ctx, jobRef, owner, { step: next, units: {}, phase }, async (transaction) => {\n const marker = readConsentGroupsChange((await transaction.get(orgRef)).data())\n if (marker?.changeId === job.changeId) {\n transaction.update(orgRef, { [`${CONSENT_GROUPS_CHANGE_FIELD}.phase`]: phase })\n }\n })\n}\n\n/**\n * THE FLIP — the one write of `consentGroups` anywhere in the platform.\n *\n * The marker must still name this change in its carry phase and the stored\n * declaration must be the one the job started from; otherwise nothing is\n * written and the unit fails. `update`, not a merge, so a removed group's id\n * vanishes with it; an empty declaration deletes the field.\n */\nasync function declare(\n ctx: Context,\n jobRef: FirebaseFirestore.DocumentReference,\n job: ConsentGroupChangeJob,\n owner: string,\n): Promise<ConsentGroupChangeJob> {\n const orgRef = orgDoc(ctx, job.orgId)\n const nowMs = ctx.now()\n const renameOnly = job.plan.renameOnly\n const declared = await save(\n ctx,\n jobRef,\n owner,\n renameOnly\n ? {\n step: 'finish',\n status: 'done',\n phase: 'done',\n declaredAtMs: nowMs,\n finishedAtMs: nowMs,\n units: {},\n }\n : { step: 'catch-up-post', phase: 'rehome', declaredAtMs: nowMs, units: {} },\n async (transaction) => {\n const org = (await transaction.get(orgRef)).data() as Record<string, unknown> | undefined\n const marker = readConsentGroupsChange(org)\n if (!org || marker?.changeId !== job.changeId || marker.phase !== 'carry') {\n throw new Error('The change no longer holds the organization, so the new groups were not saved')\n }\n if (consentGroupsFingerprint(org[CONSENT_GROUPS_FIELD]) !== job.beforeFingerprint) {\n throw new Error('The consent groups changed outside this change, so the new groups were not saved')\n }\n transaction.update(orgRef, {\n [CONSENT_GROUPS_FIELD]: Object.keys(job.after).length ? job.after : FieldValue.delete(),\n [CONSENT_GROUPS_CHANGE_FIELD]: renameOnly\n ? FieldValue.delete()\n : { ...marker, phase: 'rehome', declaredAtMs: nowMs },\n updatedAt: FieldValue.serverTimestamp(),\n })\n },\n )\n const finished = renameOnly ? { ...declared, lease: null } : declared\n if (renameOnly) await jobRef.update({ lease: null })\n for (const line of job.lines) {\n await ctx.log(job.orgId, job.actor, line, { type: 'org', id: job.orgId })\n }\n if (renameOnly) {\n await ctx.log(job.orgId, job.actor, 'Finished a consent group change', {\n type: 'org',\n id: job.orgId,\n })\n }\n return finished\n}\n\n/** The \"Finished\" line: the carries' totals, then each participant's clause. */\nfunction finishedLine(job: ConsentGroupChangeJob): string {\n const copied = job.counts.siteSuppressions + job.counts.topicOptOuts + job.counts.paces\n const sites = job.sitesReceiving.length\n const clauses = [\n `${copied} ${copied === 1 ? 'opt-out' : 'opt-outs'} copied to ${sites} ${sites === 1 ? 'site' : 'sites'}`,\n ]\n for (const pluginId of job.participants) {\n const entry = listPluginConsentGroupParticipants().find((candidate) => candidate.pluginId === pluginId)\n const clause = entry?.participant.summarize?.(job.plugins[pluginId] ?? {})\n if (clause) clauses.push(clause)\n }\n return `Finished a consent group change: ${clauses.join('; ')}`\n}\n\nasync function finish(\n ctx: Context,\n jobRef: FirebaseFirestore.DocumentReference,\n job: ConsentGroupChangeJob,\n owner: string,\n): Promise<ConsentGroupChangeJob> {\n const orgRef = orgDoc(ctx, job.orgId)\n const nowMs = ctx.now()\n const done = await save(\n ctx,\n jobRef,\n owner,\n { status: 'done', phase: 'done', step: 'finish', finishedAtMs: nowMs, units: {} },\n async (transaction) => {\n const marker = readConsentGroupsChange((await transaction.get(orgRef)).data())\n if (marker?.changeId === job.changeId) {\n transaction.update(orgRef, {\n [CONSENT_GROUPS_CHANGE_FIELD]: FieldValue.delete(),\n updatedAt: FieldValue.serverTimestamp(),\n })\n }\n },\n )\n await jobRef.update({ lease: null })\n await ctx.log(job.orgId, job.actor, finishedLine(done), { type: 'org', id: job.orgId })\n return { ...done, lease: null }\n}\n\n/**\n * One unit of work, or nothing when the job is waiting for its sweep.\n */\nasync function advanceOnce(\n ctx: Context,\n jobRef: FirebaseFirestore.DocumentReference,\n job: ConsentGroupChangeJob,\n owner: string,\n deadlineMs: number,\n): Promise<{ job: ConsentGroupChangeJob; idle: boolean }> {\n if (job.step === 'declare') return { job: await declare(ctx, jobRef, job, owner), idle: false }\n if (job.step === 'finish') return { job: await finish(ctx, jobRef, job, owner), idle: false }\n if (job.step === 'sweep-wait') {\n if (ctx.now() < (job.declaredAtMs ?? 0) + CONSENT_GROUP_SWEEP_DELAY_MS) return { job, idle: true }\n return { job: await nextStep(ctx, job.orgId, jobRef, job, owner), idle: false }\n }\n const unit = unitsOf(job).find((candidate) => !job.units[candidate.key]?.done)\n if (!unit) return { job: await nextStep(ctx, job.orgId, jobRef, job, owner), idle: false }\n const before = job.units[unit.key]?.cursor ?? null\n const after = await runUnit(ctx, jobRef, job, owner, unit, deadlineMs)\n const state = after.units[unit.key]\n // A participant that stopped where it started — out of time before its\n // first page — has nothing more to give this invocation.\n return { job: after, idle: unit.kind === 'participant' && !state?.done && (state?.cursor ?? null) === before }\n}\n\n/** Claims the lease, or answers why not. `null` for a job that does not exist. */\nasync function claim(\n ctx: Context,\n jobRef: FirebaseFirestore.DocumentReference,\n owner: string,\n): Promise<{ job: ConsentGroupChangeJob; mine: boolean } | null> {\n return ctx.firestore.runTransaction(async (transaction) => {\n const job = jobFrom(await transaction.get(jobRef))\n if (!job) return null\n const nowMs = ctx.now()\n if (job.status !== 'running') return { job, mine: false }\n if (job.lease && job.lease.untilMs > nowMs && job.lease.owner !== owner) return { job, mine: false }\n const lease = { owner, untilMs: nowMs + CONSENT_GROUP_CHANGE_LEASE_MS }\n transaction.update(jobRef, { lease })\n return { job: { ...job, lease }, mine: true }\n })\n}\n\nasync function release(\n ctx: Context,\n jobRef: FirebaseFirestore.DocumentReference,\n owner: string,\n): Promise<ConsentGroupChangeJob | null> {\n return ctx.firestore.runTransaction(async (transaction) => {\n const job = jobFrom(await transaction.get(jobRef))\n if (!job) return null\n if (job.lease?.owner !== owner) return job\n transaction.update(jobRef, { lease: null })\n return { ...job, lease: null }\n })\n}\n\n/** Counts a failed unit against the job, and says so once when it stalls. */\nasync function recordFailure(\n ctx: Context,\n jobRef: FirebaseFirestore.DocumentReference,\n owner: string,\n error: unknown,\n): Promise<ConsentGroupChangeJob | null> {\n const lastError = sanitizeError(error)\n const outcome = await ctx.firestore.runTransaction(async (transaction) => {\n const job = jobFrom(await transaction.get(jobRef))\n if (!job) return null\n if (job.lease?.owner !== owner) return { job, newlyStalled: false }\n const failures = (job.failures ?? 0) + 1\n const stalled = failures >= CONSENT_GROUP_CHANGE_STALL_AFTER\n const fields = { failures, stalled, lastError, lease: null }\n transaction.update(jobRef, { ...fields, updatedAt: FieldValue.serverTimestamp() })\n return { job: { ...job, ...fields }, newlyStalled: stalled && !job.stalled }\n })\n if (!outcome) return null\n if (outcome.newlyStalled) {\n await ctx.log(\n outcome.job.orgId,\n outcome.job.actor,\n 'A consent group change stopped with an error; it will try again',\n { type: 'org', id: outcome.job.orgId },\n )\n }\n console.error(`[consent-group-change] a unit of ${outcome.job.changeId} failed: ${lastError}`)\n return outcome.job\n}\n\n/**\n * Advances a change until it finishes, waits for its sweep, or reaches\n * `deadlineMs`. Answers the job's status either way — including when another\n * caller holds it, which is not a failure.\n */\nexport async function advanceConsentGroupChange(\n input: {\n orgId: string\n changeId: string\n deadlineMs: number\n /** Who holds the lease; one per invocation. */\n owner?: string\n } & ConsentGroupChangeContext,\n): Promise<{ ok: true; status: ConsentGroupChangeStatusBody } | ConsentGroupChangeRefusal> {\n const ctx = contextOf(input)\n const jobRef = jobDoc(ctx, input.orgId, input.changeId)\n const owner = input.owner ?? `advance:${createResourceUid()}`\n const claimed = await claim(ctx, jobRef, owner)\n if (!claimed) return NOT_FOUND\n let job = claimed.job\n if (!claimed.mine) return { ok: true, status: consentGroupChangeStatus(job, ctx.now()) }\n try {\n while (job.status === 'running' && ctx.now() < input.deadlineMs - DEADLINE_SAFETY_MS) {\n const outcome = await advanceOnce(ctx, jobRef, job, owner, input.deadlineMs)\n job = outcome.job\n if (outcome.idle) break\n }\n } catch (error) {\n if (!(error instanceof LeaseLost)) {\n job = (await recordFailure(ctx, jobRef, owner, error)) ?? job\n return { ok: true, status: consentGroupChangeStatus(job, ctx.now()) }\n }\n job = jobFrom(await jobRef.get()) ?? job\n return { ok: true, status: consentGroupChangeStatus(job, ctx.now()) }\n }\n if (job.status === 'running') job = (await release(ctx, jobRef, owner)) ?? job\n return { ok: true, status: consentGroupChangeStatus(job, ctx.now()) }\n}\n\n/*==========================================\n * CANCELING AND READING\n *=========================================*/\n\n/**\n * Stops a change before it takes effect: clears the marker and leaves every\n * refusal already carried, which only ever honors an opt-out on more sites.\n * Refused once the declaration has flipped — there is nothing left to stop\n * before, and the only way back is another change.\n */\nexport async function cancelConsentGroupChange(\n input: { orgId: string; changeId: string; actor: ConsentGroupChangeActor } & ConsentGroupChangeContext,\n): Promise<{ ok: true; status: ConsentGroupChangeStatusBody } | ConsentGroupChangeRefusal> {\n const ctx = contextOf(input)\n const orgRef = orgDoc(ctx, input.orgId)\n const jobRef = jobDoc(ctx, input.orgId, input.changeId)\n const outcome = await ctx.firestore.runTransaction(async (transaction) => {\n const [orgSnapshot, jobSnapshot] = await Promise.all([transaction.get(orgRef), transaction.get(jobRef)])\n const job = jobFrom(jobSnapshot)\n if (!job) return NOT_FOUND\n if (job.status !== 'running' || job.declaredAtMs) {\n return {\n ok: false as const,\n status: 409 as const,\n body: {\n error:\n job.status === 'running' || job.status === 'done'\n ? 'This change has already taken effect'\n : 'This change is no longer running',\n changeId: job.changeId,\n phase: job.phase,\n },\n }\n }\n const marker = readConsentGroupsChange(orgSnapshot.data())\n if (marker?.changeId === job.changeId) {\n transaction.update(orgRef, {\n [CONSENT_GROUPS_CHANGE_FIELD]: FieldValue.delete(),\n updatedAt: FieldValue.serverTimestamp(),\n })\n }\n const fields = {\n status: 'canceled' as const,\n phase: 'canceled' as const,\n canceledAtMs: ctx.now(),\n lease: null,\n }\n transaction.update(jobRef, { ...fields, updatedAt: FieldValue.serverTimestamp() })\n return { ok: true as const, job: { ...job, ...fields } }\n })\n if (isRefusal(outcome)) return outcome\n const { job } = outcome as { job: ConsentGroupChangeJob }\n await ctx.log(\n input.orgId,\n { uid: input.actor.uid ?? null, email: input.actor.email ?? null },\n 'Canceled a consent group change before it took effect',\n { type: 'org', id: input.orgId },\n )\n return { ok: true, status: consentGroupChangeStatus(job, ctx.now()) }\n}\n\n/** A job's status, or `null` for a change the org never had. */\nexport async function readConsentGroupChangeStatus(\n input: { orgId: string; changeId: string } & ConsentGroupChangeContext,\n): Promise<ConsentGroupChangeStatusBody | null> {\n const ctx = contextOf(input)\n const job = jobFrom(await jobDoc(ctx, input.orgId, input.changeId).get())\n return job ? consentGroupChangeStatus(job, ctx.now()) : null\n}\n\n/*==========================================\n * THE CRON BACKSTOP\n *=========================================*/\n\nexport interface DueConsentGroupChangeReport {\n orgId: string\n changeId: string\n phase: ConsentGroupsChangePhase\n /** What happened on this run. */\n outcome: 'advanced' | 'listed' | 'locked' | 'orphaned' | 'skipped'\n status?: ConsentGroupChangeStatusBody\n error?: string\n}\n\n/**\n * Advances every change in flight, oldest org id first, until `deadlineMs`.\n *\n * A change nobody is watching still finishes: the progress panel is a\n * convenience, and this is the guarantee. A lockdown on the org — or on the\n * member who started the change — pauses it, like every other write the\n * workspace makes. A marker whose job document is gone is a change nothing\n * can finish, and is cleared rather than blocking every change after it.\n */\nexport async function advanceDueConsentGroupChanges(\n input: { deadlineMs: number; dryRun?: boolean; limit?: number } & ConsentGroupChangeContext,\n): Promise<DueConsentGroupChangeReport[]> {\n const ctx = contextOf(input)\n const snapshot = await ctx.firestore\n .collection('orgs')\n .where(`${CONSENT_GROUPS_CHANGE_FIELD}.phase`, 'in', [...RUNNING_PHASES])\n .limit(input.limit ?? 50)\n .get()\n const reports: DueConsentGroupChangeReport[] = []\n for (const org of snapshot.docs) {\n const data = org.data() as Record<string, unknown>\n const marker = readConsentGroupsChange(data)\n if (!marker) continue\n const base = { orgId: org.id, changeId: marker.changeId, phase: marker.phase }\n if (input.dryRun) {\n reports.push({ ...base, outcome: 'listed' })\n continue\n }\n if (ctx.now() >= input.deadlineMs - DEADLINE_SAFETY_MS) {\n reports.push({ ...base, outcome: 'skipped' })\n continue\n }\n try {\n const job = jobFrom(await jobDoc(ctx, org.id, marker.changeId).get())\n if (!job) {\n await orgDoc(ctx, org.id).update({ [CONSENT_GROUPS_CHANGE_FIELD]: FieldValue.delete() })\n console.error(`[consent-group-change] cleared a marker with no job in org ${org.id}`)\n reports.push({ ...base, outcome: 'orphaned' })\n continue\n }\n if (await ctx.lockedOut(data, job.actor?.uid ?? null)) {\n reports.push({ ...base, outcome: 'locked' })\n continue\n }\n const advanced = await advanceConsentGroupChange({\n orgId: org.id,\n changeId: marker.changeId,\n deadlineMs: input.deadlineMs,\n owner: `cron:${createResourceUid()}`,\n firestore: ctx.firestore,\n log: ctx.log,\n now: ctx.now,\n })\n reports.push({\n ...base,\n outcome: 'advanced',\n ...(advanced.ok ? { status: (advanced as { status: ConsentGroupChangeStatusBody }).status } : {}),\n })\n } catch (error) {\n reports.push({ ...base, outcome: 'advanced', error: sanitizeError(error) })\n }\n }\n return reports\n}\n"],"names":["CONSENT_GROUP_CHANGE_STEPS","CONSENT_GROUP_CHANGES_COLLECTION","CONSENT_GROUP_RENAME_STEPS","CONSENT_GROUP_SWEEP_DELAY_MS","CONSENT_GROUPS_CHANGE_FIELD","consentGroupChangePhaseOf","consentGroupDisclosureChanges","consentGroupsFingerprint","describeConsentGroupChangeLine","discardedConsentGroupIds","estimateConsentGroupChange","orgSiteIds","planConsentGroupChange","readConsentGroupsChange","validateConsentGroupDeclaration","CONSENT_GROUPS_FIELD","consentGroupsAwaitConfirmation","readConsentGroups","createResourceUid","DEFAULT_EMAIL_TOPICS","TOPIC_OPT_OUTS_SUBCOLLECTION","resolveMarketingConsentPolicy","isOrgWideMember","listPluginConsentGroupParticipants","previewPluginConsentGroupChange","FieldValue","CONSENT_GROUP_CARRY_STORES","CONSENT_GROUP_CARRY_SUBCOLLECTIONS","carryConsentGroupRefusals","firebaseAdmin","getLockdownVerdict","logOrgActivity","CONSENT_GROUP_CHANGE_LEASE_MS","CONSENT_GROUP_CHANGE_STALL_AFTER","CATCH_UP_OVERLAP_MS","DEADLINE_SAFETY_MS","RUNNING_PHASES","contextOf","input","firestore","app","log","now","Date","lockedOut","org","uid","intent","NOT_FOUND","ok","status","body","error","LeaseLost","Error","orgDoc","ctx","orgId","collection","doc","jobDoc","changeId","planOf","job","plan","before","after","sanitizeError","message","String","replace","slice","consentGroupChangeStatus","nowMs","declaredAtMs","index","steps","indexOf","step","phase","done","progress","stepIndex","length","stepCount","sweepAtMs","renameOnly","stalled","failures","lastError","leaseUntilMs","lease","untilMs","counts","sitesReceiving","plugins","prepare","expected","groups","marker","raw","current","Array","isArray","siteIds","validation","reserved","Object","keys","errors","isRefusal","value","Boolean","plannedSites","Set","hostIds","values","flatMap","group","siteNamesFor","hosts","snapshots","getAll","map","id","names","forEach","snapshot","get","describeLines","lines","line","hostId","countOf","query","count","Number","data","previewConsentGroupChange","prepared","db","cache","Map","cached","key","pending","set","store","name","suppressions","optOuts","paces","where","carries","Promise","all","carry","toHostId","fromHostId","siteSuppressions","topicOptOuts","membersIn","declaration","find","includes","joining","sort","had","siblings","filter","has","push","inherited","refusals","sibling","pendingHolds","released","releasedHostIds","entries","topic","topicId","changedGroups","was","join","partialAccessMembers","members","member","docs","reach","some","every","policy","forwardPolicyWarning","mode","entry","enforceFromMs","participants","texts","documents","reduce","total","sum","preview","discarded","text","disclosures","capturesDisclosing","estimate","startConsentGroupChange","orgRef","exists","outside","pluginId","jobRef","started","runTransaction","transaction","beforeFingerprint","flows","siteNames","units","actor","email","startedAtMs","carryStartedAtMs","finishedAtMs","canceledAtMs","update","updatedAt","serverTimestamp","create","createdAt","type","unitsOf","sinceMs","_carry","carryIndex","kind","since","jobFrom","save","owner","fields","extra","next","addCounts","base","more","out","isFinite","runUnit","unit","deadlineMs","result","state","cursor","page","written","participant","participantFor","run","dryRun","candidate","nextStep","declare","declared","delete","finished","finishedLine","copied","sites","clauses","clause","summarize","finish","advanceOnce","idle","claim","mine","release","recordFailure","outcome","newlyStalled","console","advanceConsentGroupChange","claimed","cancelConsentGroupChange","orgSnapshot","jobSnapshot","readConsentGroupChangeStatus","advanceDueConsentGroupChanges","limit","reports","advanced"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/consent-group-change.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * RUNNING A CONSENT GROUP CHANGE (AGL-3320) — the executor.\n *\n * `consent-group-change.ts` in `app-utils` decides WHAT a change requires;\n * this carries it out, across as many invocations as it takes, without ever\n * leaving a refusal unhonored in between.\n *\n * ## The order is the safety argument\n *\n * 1. START — one transaction on the org document: the stored declaration is\n * the one the editor rendered, and no other change is in flight. It sets\n * the marker `consentGroupsChange` and creates the job document.\n * 2. CARRY — every carry of the plan through the three platform stores\n * (`consent-group-carry.ts`) and the organization's retained refusals\n * (`retained-refusals.ts`), then every participant's carry phase.\n * 3. CATCH UP — everything filed since the carry began, again.\n * 4. DECLARE — one transaction: the marker still names this change in its\n * carry phase and the stored declaration is still the one the job\n * started from. Only then is the new declaration written.\n * 5. CATCH UP — once more, for whatever raced the flip.\n * 6. RE-HOME — every participant's holder records.\n * 7. SWEEP — once the sweep delay has passed, the whole carry and every\n * participant's sweep, idempotently.\n * 8. DONE — the marker is cleared.\n *\n * The flip cannot happen before every carry and both catch-up passes are\n * done, because the steps run strictly in order and `declare` is only ever\n * reached from a finished `catch-up-pre`. Until the flip the declaration in\n * force is the old one, so a change that stalls or is canceled before it\n * has changed nothing anybody can observe — except refusals honored on more\n * sites, which is the safe direction. A rename moves nothing, and runs the\n * declare and finish alone.\n *\n * ## One invocation at a time, and any invocation may finish it\n *\n * The console's progress panel calls `continue` while it is open, and the\n * cron backstop calls every fifteen minutes. Each claims a LEASE on the job\n * by transaction and renews it on every unit it saves; a lease that lapses\n * (a process that died) is simply claimed by the next caller. A unit is one\n * page of one store, or one participant call, and every unit is idempotent,\n * so a unit re-run after a crash writes nothing it already wrote.\n *\n * ## A failure is retried, and five in a row are shown\n *\n * A unit that throws releases the lease and counts a failure against the job;\n * the next invocation retries it from its cursor. Five consecutive failures\n * mark the job STALLED — the activity log says so once, and the panel shows\n * the error with a retry — and the cron keeps trying. A participant's throw\n * is one of these: the change never flips past a participant that could not\n * carry its refusals (see `plugin-consent-group-change.ts`).\n */\n\nimport {\n CONSENT_GROUP_CHANGE_STEPS,\n CONSENT_GROUP_CHANGES_COLLECTION,\n CONSENT_GROUP_RENAME_STEPS,\n CONSENT_GROUP_SWEEP_DELAY_MS,\n CONSENT_GROUPS_CHANGE_FIELD,\n type ConsentGroupChangeCounts,\n type ConsentGroupChangeLine,\n type ConsentGroupChangePlan,\n type ConsentGroupChangePreview,\n type ConsentGroupChangeStatus,\n type ConsentGroupChangeStep,\n type ConsentGroupChangeProgress,\n type ConsentGroupDeclaration,\n type ConsentGroupsApiInFlight,\n type ConsentGroupsApiRefusal,\n type ConsentGroupsApiStale,\n type ConsentGroupsChangeMarker,\n type ConsentGroupsChangePhase,\n consentGroupChangePhaseOf,\n consentGroupDisclosureChanges,\n consentGroupsFingerprint,\n describeConsentGroupChangeLine,\n discardedConsentGroupIds,\n estimateConsentGroupChange,\n orgSiteIds,\n planConsentGroupChange,\n readConsentGroupsChange,\n validateConsentGroupDeclaration,\n} from '@aglyn/aglyn/app-utils/consent-group-change'\nimport {\n CONSENT_GROUPS_FIELD,\n consentGroupsAwaitConfirmation,\n readConsentGroups,\n} from '@aglyn/aglyn/app-utils/consent-groups'\nimport { createResourceUid } from '@aglyn/aglyn/app-utils/create-resource-uid'\nimport {\n DEFAULT_EMAIL_TOPICS,\n TOPIC_OPT_OUTS_SUBCOLLECTION,\n} from '@aglyn/aglyn/app-utils/email-topics'\nimport {\n MARKETING_CONSENT_BY_HOST_FIELD,\n MARKETING_CONSENT_FIELD,\n resolveMarketingConsentPolicy,\n} from '@aglyn/aglyn/app-utils/marketing-consent'\nimport { isOrgWideMember } from '@aglyn/aglyn/app-utils/organizations'\nimport {\n type ConsentGroupChangeParticipant,\n type ConsentGroupChangeParticipantPhase,\n listPluginConsentGroupParticipants,\n previewPluginConsentGroupChange,\n} from '@aglyn/aglyn/plugin-manager/plugin-consent-group-change'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport {\n CONSENT_GROUP_CARRY_STORES,\n CONSENT_GROUP_CARRY_SUBCOLLECTIONS,\n type ConsentGroupCarryStore,\n carryConsentGroupRefusals,\n} from './consent-group-carry'\nimport firebaseAdmin from './firebase-admin'\nimport { getLockdownVerdict } from './lockdown'\nimport { logOrgActivity } from './organizations'\nimport { carryRetainedRefusals, retainedRefusalsStore } from './retained-refusals'\n\n/** How long a claimed job stays claimed without a renewal. */\nexport const CONSENT_GROUP_CHANGE_LEASE_MS = 90_000\n\n/** Consecutive failed units before a job is shown as stalled. */\nexport const CONSENT_GROUP_CHANGE_STALL_AFTER = 5\n\n/**\n * How far before the carry began a catch-up reads. A refusal filed in the\n * seconds the carry's first page was being read, with a clock a little\n * behind, is inside it.\n */\nconst CATCH_UP_OVERLAP_MS = 5 * 60_000\n\n/** Left before a deadline for the last save. */\nconst DEADLINE_SAFETY_MS = 5_000\n\n/** The phases the cron looks for a change in. */\nconst RUNNING_PHASES: readonly ConsentGroupsChangePhase[] = ['carry', 'rehome', 'sweep']\n\n/** Who asked, as the activity log records it. */\nexport interface ConsentGroupChangeActor {\n uid: string | null\n email?: string | null\n}\n\n/** Everything the executor reaches outside itself, injectable for the specs. */\nexport interface ConsentGroupChangeContext {\n firestore?: FirebaseFirestore.Firestore\n log?: typeof logOrgActivity\n now?: () => number\n /** Whether a lockdown refuses the cron acting for this org and actor. */\n lockedOut?: (org: Record<string, unknown>, uid: string | null) => Promise<boolean>\n}\n\ninterface Context {\n firestore: FirebaseFirestore.Firestore\n log: typeof logOrgActivity\n now: () => number\n lockedOut: (org: Record<string, unknown>, uid: string | null) => Promise<boolean>\n}\n\nfunction contextOf(input: ConsentGroupChangeContext): Context {\n return {\n firestore: input.firestore ?? firebaseAdmin.app().firestore(),\n log: input.log ?? logOrgActivity,\n now: input.now ?? Date.now,\n lockedOut:\n input.lockedOut ??\n (async (org, uid) =>\n (await getLockdownVerdict({ org: org as never, uid, intent: 'write' })) !== null),\n }\n}\n\n/** Where a unit of the current step stands. */\ninterface UnitState {\n cursor: string | null\n done: boolean\n}\n\n/** The job document, `orgs/{orgId}/consentGroupChanges/{changeId}`. */\nexport interface ConsentGroupChangeJob {\n changeId: string\n orgId: string\n status: ConsentGroupChangeStatus\n step: ConsentGroupChangeStep\n /** The marker's phase while running; `done` or `canceled` after. */\n phase: ConsentGroupsChangePhase | 'done' | 'canceled'\n /** The steps this change runs, in order. */\n steps: ConsentGroupChangeStep[]\n /** The stored declaration it started from: what the flip re-checks. */\n beforeFingerprint: string\n before: ConsentGroupDeclaration\n after: ConsentGroupDeclaration\n plan: Omit<ConsentGroupChangePlan, 'before' | 'after'>\n /** The plan's lines as the activity log writes them. */\n lines: string[]\n siteNames: Record<string, string>\n /** The participants registered when it started — every one of them runs. */\n participants: string[]\n /** The current step's units, by key. */\n units: Record<string, UnitState>\n counts: ConsentGroupChangeCounts\n sitesReceiving: string[]\n plugins: Record<string, Record<string, number>>\n lease: { owner: string; untilMs: number } | null\n failures: number\n stalled: boolean\n lastError: string | null\n actor: { uid: string | null; email: string | null }\n startedAtMs: number\n carryStartedAtMs: number\n declaredAtMs: number | null\n finishedAtMs: number | null\n canceledAtMs: number | null\n}\n\n/** What the route answers for a job. */\nexport interface ConsentGroupChangeStatusBody {\n changeId: string\n phase: ConsentGroupsChangePhase | 'done' | 'canceled'\n done: boolean\n progress: ConsentGroupChangeProgress\n}\n\n/** A refusal the route turns into its status code and body. */\nexport type ConsentGroupChangeRefusal =\n | { ok: false; status: 400; body: ConsentGroupsApiRefusal }\n | { ok: false; status: 404; body: { error: string } }\n | { ok: false; status: 409; body: ConsentGroupsApiStale | ConsentGroupsApiInFlight }\n\nconst NOT_FOUND = { ok: false as const, status: 404 as const, body: { error: 'No such consent group change' } }\n\n/** A caller whose lease was taken, or whose job was canceled, under it. */\nclass LeaseLost extends Error {}\n\nconst orgDoc = (ctx: Context, orgId: string) => ctx.firestore.collection('orgs').doc(orgId)\nconst jobDoc = (ctx: Context, orgId: string, changeId: string) =>\n orgDoc(ctx, orgId).collection(CONSENT_GROUP_CHANGES_COLLECTION).doc(changeId)\n\n/** The job as the plan the participants and the carries read. */\nfunction planOf(job: ConsentGroupChangeJob): ConsentGroupChangePlan {\n return { ...job.plan, before: job.before, after: job.after }\n}\n\n/** An error as the job records it: bounded, and with no address in it. */\nfunction sanitizeError(error: unknown): string {\n const message = error instanceof Error ? error.message : String(error ?? 'Unknown error')\n return message.replace(/[^\\s@]+@[^\\s@]+/g, '[address]').slice(0, 300) || 'Unknown error'\n}\n\nexport function consentGroupChangeStatus(\n job: ConsentGroupChangeJob,\n nowMs: number = Date.now(),\n): ConsentGroupChangeStatusBody {\n const declaredAtMs = job.declaredAtMs ?? null\n const index = job.steps.indexOf(job.step)\n return {\n changeId: job.changeId,\n phase: job.phase,\n done: job.status !== 'running',\n progress: {\n status: job.status,\n step: job.step,\n stepIndex: index >= 0 ? index + 1 : job.steps.length,\n stepCount: job.steps.length,\n declaredAtMs,\n sweepAtMs:\n declaredAtMs !== null && !job.plan.renameOnly\n ? declaredAtMs + CONSENT_GROUP_SWEEP_DELAY_MS\n : null,\n stalled: job.stalled === true,\n failures: job.failures ?? 0,\n lastError: job.lastError ?? null,\n leaseUntilMs: job.lease && job.lease.untilMs > nowMs ? job.lease.untilMs : null,\n // A job started before the retained store was carried has no count for it.\n counts: { retainedRefusals: 0, ...job.counts },\n sitesReceiving: job.sitesReceiving?.length ?? 0,\n plugins: job.plugins ?? {},\n },\n }\n}\n\n/*==========================================\n * VALIDATING AND PLANNING — shared by the preview and the start\n *=========================================*/\n\ninterface Prepared {\n org: Record<string, unknown>\n before: ConsentGroupDeclaration\n after: ConsentGroupDeclaration\n plan: ConsentGroupChangePlan\n siteIds: string[]\n}\n\nfunction prepare(\n org: Record<string, unknown>,\n expected: unknown,\n groups: unknown,\n): Prepared | ConsentGroupChangeRefusal {\n const marker = readConsentGroupsChange(org)\n if (marker) {\n return {\n ok: false,\n status: 409,\n body: {\n error: 'A consent group change is still finishing',\n changeId: marker.changeId,\n phase: marker.phase,\n },\n }\n }\n const raw = org[CONSENT_GROUPS_FIELD]\n if (consentGroupsFingerprint(raw) !== consentGroupsFingerprint(expected)) {\n return {\n ok: false,\n status: 409,\n body: {\n error: 'Someone else changed consent groups while you were editing',\n current:\n raw && typeof raw === 'object' && !Array.isArray(raw)\n ? (raw as Record<string, unknown>)\n : null,\n },\n }\n }\n const before = readConsentGroups(org)\n const siteIds = orgSiteIds(org)\n const validation = validateConsentGroupDeclaration({\n groups,\n before,\n siteIds,\n reserved:\n raw && typeof raw === 'object' && !Array.isArray(raw)\n ? Object.keys(raw as Record<string, unknown>)\n : [],\n })\n if (!validation.ok) {\n return {\n ok: false,\n status: 400,\n body: {\n error: 'Those consent groups cannot be saved',\n errors: (validation as { errors: ConsentGroupsApiRefusal['errors'] }).errors,\n },\n }\n }\n const after = (validation as { after: ConsentGroupDeclaration }).after\n return {\n org,\n before,\n after,\n plan: planConsentGroupChange(before, after, { siteIds }),\n siteIds,\n }\n}\n\nconst isRefusal = (value: unknown): value is ConsentGroupChangeRefusal =>\n Boolean(value && typeof value === 'object' && (value as { ok?: unknown }).ok === false)\n\n/** Every site a plan names, for the names its lines are written with. */\nfunction plannedSites(plan: ConsentGroupChangePlan): string[] {\n return [\n ...new Set([\n ...plan.hostIds,\n ...Object.values(plan.before).flatMap((group) => group.hostIds),\n ...Object.values(plan.after).flatMap((group) => group.hostIds),\n ]),\n ]\n}\n\n/** What the console calls each site: its name, its subdomain, or its id. */\nasync function siteNamesFor(ctx: Context, hostIds: readonly string[]): Promise<Record<string, string>> {\n if (!hostIds.length) return {}\n const hosts = ctx.firestore.collection('hosts')\n const snapshots = await ctx.firestore.getAll(...hostIds.map((id) => hosts.doc(id)))\n const names: Record<string, string> = {}\n snapshots.forEach((snapshot, index) => {\n const id = hostIds[index]\n names[id] = String(snapshot.get('name') || snapshot.get('subdomain') || id)\n })\n return names\n}\n\nconst describeLines = (lines: readonly ConsentGroupChangeLine[], names: Record<string, string>) =>\n lines.map((line) => describeConsentGroupChangeLine(line, (hostId) => names[hostId] ?? hostId))\n\n/*==========================================\n * THE PREVIEW\n *=========================================*/\n\nasync function countOf(query: FirebaseFirestore.Query): Promise<number> {\n const snapshot = await query.count().get()\n return Number(snapshot.data().count) || 0\n}\n\n/**\n * What a change would do, counted and worded, writing nothing.\n *\n * Refused exactly as `apply` would be — a change in flight, a stale\n * `expected`, an invalid declaration — so the review step never shows a\n * change the next click cannot make.\n */\nexport async function previewConsentGroupChange(\n input: {\n orgId: string\n expected: unknown\n groups: unknown\n /** The org document, when the caller already read it. */\n org?: Record<string, unknown> | null\n } & ConsentGroupChangeContext,\n): Promise<{ ok: true; preview: ConsentGroupChangePreview } | ConsentGroupChangeRefusal> {\n const ctx = contextOf(input)\n const org =\n input.org ?? ((await orgDoc(ctx, input.orgId).get()).data() as Record<string, unknown> | undefined)\n if (!org) return { ok: false, status: 404, body: { error: 'No such workspace' } }\n const prepared = prepare(org, input.expected, input.groups)\n if (isRefusal(prepared)) return prepared\n const { plan, before, after } = prepared as Prepared\n\n const db = ctx.firestore\n const cache = new Map<string, Promise<number>>()\n const cached = (key: string, query: () => FirebaseFirestore.Query) => {\n let pending = cache.get(key)\n if (!pending) {\n pending = countOf(query())\n cache.set(key, pending)\n }\n return pending\n }\n const store = (hostId: string, name: ConsentGroupCarryStore) =>\n db.collection('hosts').doc(hostId).collection(CONSENT_GROUP_CARRY_SUBCOLLECTIONS[name])\n const suppressions = (hostId: string) => cached(`s:${hostId}`, () => store(hostId, 'siteSuppressions'))\n const optOuts = (hostId: string) => cached(`t:${hostId}`, () => store(hostId, 'topicOptOuts'))\n const paces = (hostId: string) =>\n cached(`p:${hostId}`, () => store(hostId, 'paces').where('cadence', '!=', null))\n const retained = (hostId: string) =>\n cached(`r:${hostId}`, () =>\n retainedRefusalsStore(orgDoc(ctx, input.orgId)).where(\n `${MARKETING_CONSENT_BY_HOST_FIELD}.${hostId}.${MARKETING_CONSENT_FIELD}`,\n '==',\n false,\n ),\n )\n\n const carries = await Promise.all(\n plan.carries.map(async (carry) => ({\n toHostId: carry.toHostId,\n fromHostId: carry.fromHostId,\n siteSuppressions: await suppressions(carry.fromHostId),\n topicOptOuts: await optOuts(carry.fromHostId),\n paces: await paces(carry.fromHostId),\n retainedRefusals: await retained(carry.fromHostId),\n })),\n )\n\n const membersIn = (declaration: ConsentGroupDeclaration, hostId: string) =>\n Object.values(declaration).find((group) => group.hostIds.includes(hostId))?.hostIds ?? [hostId]\n const joining: Array<{ hostId: string; siblings: string[] }> = []\n for (const hostId of [...new Set(Object.values(after).flatMap((group) => group.hostIds))].sort()) {\n const had = new Set(membersIn(before, hostId))\n const siblings = membersIn(after, hostId).filter((id) => id !== hostId && !had.has(id))\n if (siblings.length) joining.push({ hostId, siblings })\n }\n const inherited = await Promise.all(\n joining.map(async ({ hostId, siblings }) => {\n let refusals = 0\n for (const sibling of siblings) {\n refusals += (await suppressions(sibling)) + (await optOuts(sibling))\n }\n return { hostId, refusals }\n }),\n )\n\n /*\n * A pending confirmation holds a sibling's mail only with the switch on,\n * so only then does a separation release anything. Counted over the\n * built-in streams: the catalog of an organization's own streams belongs to\n * the Email plugin, and a stream it added is not counted here.\n */\n const pendingHolds: ConsentGroupChangePreview['pendingHolds'] = []\n if (consentGroupsAwaitConfirmation(org)) {\n const released = new Map<string, string[]>()\n for (const carry of plan.carries) {\n released.set(carry.toHostId, [...(released.get(carry.toHostId) ?? []), carry.fromHostId])\n }\n for (const [hostId, releasedHostIds] of [...released.entries()].sort()) {\n for (const topic of DEFAULT_EMAIL_TOPICS) {\n const count = await countOf(\n db\n .collection('hosts')\n .doc(hostId)\n .collection(TOPIC_OPT_OUTS_SUBCOLLECTION)\n .where(`topics.${topic.id}.confirmedAt`, '==', null),\n )\n if (count > 0) pendingHolds.push({ hostId, topicId: topic.id, count, releasedHostIds })\n }\n }\n }\n\n const changedGroups = Object.entries(after)\n .filter(([id, group]) => {\n const was = before[id]\n return !was || was.hostIds.join('\\n') !== group.hostIds.join('\\n')\n })\n .map(([, group]) => group.hostIds)\n let partialAccessMembers = 0\n if (changedGroups.length) {\n const members = await orgDoc(ctx, input.orgId).collection('members').get()\n for (const member of members.docs) {\n const data = member.data() as Record<string, unknown>\n if (isOrgWideMember(data)) continue\n const reach = new Set(Object.keys((data['hostAccess'] ?? {}) as Record<string, unknown>))\n if (\n changedGroups.some(\n (hostIds) =>\n hostIds.some((id) => reach.has(id)) && !hostIds.every((id) => reach.has(id)),\n )\n ) {\n partialAccessMembers += 1\n }\n }\n }\n\n const policy = resolveMarketingConsentPolicy(org['marketingConsentPolicy'])\n const forwardPolicyWarning =\n policy.mode === 'forward' && joining.length\n ? { hostIds: joining.map((entry) => entry.hostId), enforceFromMs: policy.enforceFromMs }\n : null\n\n const participants = await previewPluginConsentGroupChange({ orgId: input.orgId, plan })\n const names = await siteNamesFor(ctx, plannedSites(plan))\n const texts = describeLines(plan.lines, names)\n const documents =\n carries.reduce(\n (total, carry) =>\n total + carry.siteSuppressions + carry.topicOptOuts + carry.paces + carry.retainedRefusals,\n 0,\n ) +\n participants.reduce(\n (total, entry) =>\n total + (entry.lines ?? []).reduce((sum, line) => sum + (line.count ?? 0), 0),\n 0,\n )\n\n return {\n ok: true,\n preview: {\n before,\n after,\n discarded: discardedConsentGroupIds(org),\n lines: plan.lines.map((line, index) => ({ ...line, text: texts[index] })),\n disclosures: consentGroupDisclosureChanges(before, after),\n carries,\n inherited,\n pendingHolds,\n partialAccessMembers,\n forwardPolicyWarning,\n participants,\n capturesDisclosing: ['form'],\n estimate: estimateConsentGroupChange(plan, documents),\n },\n }\n}\n\n/*==========================================\n * STARTING\n *=========================================*/\n\n/**\n * Starts a change: the org marker and the job, in one transaction that\n * refuses a change in flight, a stale `expected` and an invalid declaration.\n * Writes nothing else — the caller advances it.\n */\nexport async function startConsentGroupChange(\n input: {\n orgId: string\n actor: ConsentGroupChangeActor\n expected: unknown\n groups: unknown\n } & ConsentGroupChangeContext,\n): Promise<{ ok: true; changeId: string; job: ConsentGroupChangeJob } | ConsentGroupChangeRefusal> {\n const ctx = contextOf(input)\n const orgRef = orgDoc(ctx, input.orgId)\n const snapshot = await orgRef.get()\n if (!snapshot.exists) return { ok: false, status: 404, body: { error: 'No such workspace' } }\n const outside = prepare(snapshot.data() as Record<string, unknown>, input.expected, input.groups)\n if (isRefusal(outside)) return outside\n // Names are words for the log, read once and outside the transaction.\n const names = await siteNamesFor(ctx, plannedSites((outside as Prepared).plan))\n const participants = listPluginConsentGroupParticipants().map((entry) => entry.pluginId)\n const changeId = createResourceUid()\n const jobRef = jobDoc(ctx, input.orgId, changeId)\n const nowMs = ctx.now()\n\n const started = await ctx.firestore.runTransaction(async (transaction) => {\n const current = await transaction.get(orgRef)\n if (!current.exists) {\n return { ok: false as const, status: 404 as const, body: { error: 'No such workspace' } }\n }\n const prepared = prepare(current.data() as Record<string, unknown>, input.expected, input.groups)\n if (isRefusal(prepared)) return prepared\n const { plan, before, after, org } = prepared as Prepared\n const steps = plan.renameOnly ? [...CONSENT_GROUP_RENAME_STEPS] : [...CONSENT_GROUP_CHANGE_STEPS]\n const job: ConsentGroupChangeJob = {\n changeId,\n orgId: input.orgId,\n status: 'running',\n step: steps[0],\n phase: 'carry',\n steps,\n beforeFingerprint: consentGroupsFingerprint(org[CONSENT_GROUPS_FIELD]),\n before,\n after,\n plan: {\n carries: plan.carries,\n flows: plan.flows,\n lines: plan.lines,\n hostIds: plan.hostIds,\n renameOnly: plan.renameOnly,\n },\n lines: describeLines(plan.lines, names),\n siteNames: names,\n participants: plan.renameOnly ? [] : participants,\n units: {},\n counts: { siteSuppressions: 0, topicOptOuts: 0, paces: 0, retainedRefusals: 0 },\n sitesReceiving: [],\n plugins: {},\n lease: null,\n failures: 0,\n stalled: false,\n lastError: null,\n actor: { uid: input.actor.uid ?? null, email: input.actor.email ?? null },\n startedAtMs: nowMs,\n carryStartedAtMs: nowMs,\n declaredAtMs: null,\n finishedAtMs: null,\n canceledAtMs: null,\n }\n const marker: ConsentGroupsChangeMarker = {\n changeId,\n phase: 'carry',\n hostIds: plan.hostIds,\n startedAtMs: nowMs,\n }\n transaction.update(orgRef, {\n [CONSENT_GROUPS_CHANGE_FIELD]: marker,\n updatedAt: FieldValue.serverTimestamp(),\n })\n transaction.create(jobRef, {\n ...job,\n createdAt: FieldValue.serverTimestamp(),\n updatedAt: FieldValue.serverTimestamp(),\n })\n return { ok: true as const, job }\n })\n if (isRefusal(started)) return started\n const { job } = started as { job: ConsentGroupChangeJob }\n await ctx.log(\n input.orgId,\n { uid: job.actor.uid, email: job.actor.email },\n `Started a consent group change: ${job.lines.join('; ')}`,\n { type: 'org', id: input.orgId },\n )\n return { ok: true, changeId, job }\n}\n\n/*==========================================\n * ADVANCING\n *=========================================*/\n\ntype Unit =\n | { kind: 'carry'; key: string; carryIndex: number; store: ConsentGroupCarryStore; sinceMs: number | null }\n | { kind: 'retained'; key: string; carryIndex: number; sinceMs: number | null }\n | { kind: 'participant'; key: string; pluginId: string; phase: ConsentGroupChangeParticipantPhase }\n\n/** The current step's units, in the order they run. */\nfunction unitsOf(job: ConsentGroupChangeJob): Unit[] {\n const carries = (sinceMs: number | null): Unit[] =>\n job.plan.carries.flatMap((_carry, carryIndex): Unit[] => [\n ...CONSENT_GROUP_CARRY_STORES.map((store) => ({\n kind: 'carry' as const,\n key: `c${carryIndex}:${store}`,\n carryIndex,\n store,\n sinceMs,\n })),\n { kind: 'retained' as const, key: `c${carryIndex}:retainedRefusals`, carryIndex, sinceMs },\n ])\n const participants = (phase: ConsentGroupChangeParticipantPhase): Unit[] =>\n job.participants.map((pluginId) => ({\n kind: 'participant' as const,\n key: `p:${pluginId}`,\n pluginId,\n phase,\n }))\n const since = job.carryStartedAtMs - CATCH_UP_OVERLAP_MS\n switch (job.step) {\n case 'carry':\n return [...carries(null), ...participants('carry')]\n case 'catch-up-pre':\n case 'catch-up-post':\n return [...carries(since), ...participants('carry')]\n case 'rehome':\n return participants('rehome')\n case 'sweep':\n return [...carries(null), ...participants('sweep')]\n default:\n return []\n }\n}\n\nfunction jobFrom(snapshot: FirebaseFirestore.DocumentSnapshot): ConsentGroupChangeJob | null {\n return snapshot.exists ? (snapshot.data() as ConsentGroupChangeJob) : null\n}\n\n/**\n * Saves a unit's result, renewing the lease — only while the job is still\n * running and still this caller's.\n */\nasync function save(\n ctx: Context,\n jobRef: FirebaseFirestore.DocumentReference,\n owner: string,\n fields: Partial<ConsentGroupChangeJob>,\n extra?: (transaction: FirebaseFirestore.Transaction, job: ConsentGroupChangeJob) => Promise<void>,\n): Promise<ConsentGroupChangeJob> {\n return ctx.firestore.runTransaction(async (transaction) => {\n const job = jobFrom(await transaction.get(jobRef))\n if (!job || job.status !== 'running' || job.lease?.owner !== owner) throw new LeaseLost()\n if (extra) await extra(transaction, job)\n const next: Partial<ConsentGroupChangeJob> = {\n ...fields,\n lease: { owner, untilMs: ctx.now() + CONSENT_GROUP_CHANGE_LEASE_MS },\n failures: 0,\n stalled: false,\n lastError: null,\n }\n transaction.update(jobRef, { ...next, updatedAt: FieldValue.serverTimestamp() })\n return { ...job, ...next }\n })\n}\n\nfunction addCounts(\n base: Record<string, number> | undefined,\n more: Record<string, number>,\n): Record<string, number> {\n const out: Record<string, number> = { ...(base ?? {}) }\n for (const [key, value] of Object.entries(more)) {\n if (typeof value === 'number' && Number.isFinite(value)) out[key] = (out[key] ?? 0) + value\n }\n return out\n}\n\nasync function runUnit(\n ctx: Context,\n jobRef: FirebaseFirestore.DocumentReference,\n job: ConsentGroupChangeJob,\n owner: string,\n unit: Unit,\n deadlineMs: number,\n): Promise<ConsentGroupChangeJob> {\n const state = job.units[unit.key] ?? { cursor: null, done: false }\n if (unit.kind === 'carry' || unit.kind === 'retained') {\n const carry = job.plan.carries[unit.carryIndex]\n const page =\n unit.kind === 'carry'\n ? await carryConsentGroupRefusals({\n firestore: ctx.firestore,\n store: unit.store,\n carry,\n changeId: job.changeId,\n cursor: state.cursor,\n sinceMs: unit.sinceMs,\n })\n : await carryRetainedRefusals({\n firestore: ctx.firestore,\n orgId: job.orgId,\n carry,\n changeId: job.changeId,\n cursor: state.cursor,\n sinceMs: unit.sinceMs,\n })\n const counted = unit.kind === 'carry' ? unit.store : 'retainedRefusals'\n const counts = { ...job.counts, [counted]: (job.counts[counted] ?? 0) + page.written }\n const sitesReceiving =\n page.written > 0 && !job.sitesReceiving.includes(carry.toHostId)\n ? [...job.sitesReceiving, carry.toHostId]\n : job.sitesReceiving\n return save(ctx, jobRef, owner, {\n units: { ...job.units, [unit.key]: { cursor: page.cursor, done: page.done } },\n counts,\n sitesReceiving,\n })\n }\n const participant = participantFor(unit.pluginId)\n const result = await participant.run({\n orgId: job.orgId,\n changeId: job.changeId,\n plan: planOf(job),\n phase: unit.phase,\n cursor: state.cursor,\n deadlineMs: deadlineMs - DEADLINE_SAFETY_MS,\n dryRun: false,\n })\n return save(ctx, jobRef, owner, {\n units: { ...job.units, [unit.key]: { cursor: result.cursor ?? null, done: result.done === true } },\n plugins: { ...job.plugins, [unit.pluginId]: addCounts(job.plugins[unit.pluginId], result.counts ?? {}) },\n })\n}\n\nfunction participantFor(pluginId: string): ConsentGroupChangeParticipant {\n const entry = listPluginConsentGroupParticipants().find((candidate) => candidate.pluginId === pluginId)\n if (!entry) {\n // The change started with this participant; running on without it would\n // skip its share. Another process — one whose declarations registered it —\n // picks the job up.\n throw new Error(`The ${pluginId} plugin's part of this change is not available here`)\n }\n return entry.participant\n}\n\n/** Moves the job to the step after its current one. */\nasync function nextStep(\n ctx: Context,\n orgId: string,\n jobRef: FirebaseFirestore.DocumentReference,\n job: ConsentGroupChangeJob,\n owner: string,\n): Promise<ConsentGroupChangeJob> {\n const next = job.steps[job.steps.indexOf(job.step) + 1] ?? 'finish'\n const phase = consentGroupChangePhaseOf(next)\n if (phase === job.phase) return save(ctx, jobRef, owner, { step: next, units: {} })\n // Entering the sweep: the marker says so, for the readers that hold off\n // while holder records may still be moving.\n const orgRef = orgDoc(ctx, orgId)\n return save(ctx, jobRef, owner, { step: next, units: {}, phase }, async (transaction) => {\n const marker = readConsentGroupsChange((await transaction.get(orgRef)).data())\n if (marker?.changeId === job.changeId) {\n transaction.update(orgRef, { [`${CONSENT_GROUPS_CHANGE_FIELD}.phase`]: phase })\n }\n })\n}\n\n/**\n * THE FLIP — the one write of `consentGroups` anywhere in the platform.\n *\n * The marker must still name this change in its carry phase and the stored\n * declaration must be the one the job started from; otherwise nothing is\n * written and the unit fails. `update`, not a merge, so a removed group's id\n * vanishes with it; an empty declaration deletes the field.\n */\nasync function declare(\n ctx: Context,\n jobRef: FirebaseFirestore.DocumentReference,\n job: ConsentGroupChangeJob,\n owner: string,\n): Promise<ConsentGroupChangeJob> {\n const orgRef = orgDoc(ctx, job.orgId)\n const nowMs = ctx.now()\n const renameOnly = job.plan.renameOnly\n const declared = await save(\n ctx,\n jobRef,\n owner,\n renameOnly\n ? {\n step: 'finish',\n status: 'done',\n phase: 'done',\n declaredAtMs: nowMs,\n finishedAtMs: nowMs,\n units: {},\n }\n : { step: 'catch-up-post', phase: 'rehome', declaredAtMs: nowMs, units: {} },\n async (transaction) => {\n const org = (await transaction.get(orgRef)).data() as Record<string, unknown> | undefined\n const marker = readConsentGroupsChange(org)\n if (!org || marker?.changeId !== job.changeId || marker.phase !== 'carry') {\n throw new Error('The change no longer holds the organization, so the new groups were not saved')\n }\n if (consentGroupsFingerprint(org[CONSENT_GROUPS_FIELD]) !== job.beforeFingerprint) {\n throw new Error('The consent groups changed outside this change, so the new groups were not saved')\n }\n transaction.update(orgRef, {\n [CONSENT_GROUPS_FIELD]: Object.keys(job.after).length ? job.after : FieldValue.delete(),\n [CONSENT_GROUPS_CHANGE_FIELD]: renameOnly\n ? FieldValue.delete()\n : { ...marker, phase: 'rehome', declaredAtMs: nowMs },\n updatedAt: FieldValue.serverTimestamp(),\n })\n },\n )\n const finished = renameOnly ? { ...declared, lease: null } : declared\n if (renameOnly) await jobRef.update({ lease: null })\n for (const line of job.lines) {\n await ctx.log(job.orgId, job.actor, line, { type: 'org', id: job.orgId })\n }\n if (renameOnly) {\n await ctx.log(job.orgId, job.actor, 'Finished a consent group change', {\n type: 'org',\n id: job.orgId,\n })\n }\n return finished\n}\n\n/** The \"Finished\" line: the carries' totals, then each participant's clause. */\nfunction finishedLine(job: ConsentGroupChangeJob): string {\n const copied =\n job.counts.siteSuppressions +\n job.counts.topicOptOuts +\n job.counts.paces +\n (job.counts.retainedRefusals ?? 0)\n const sites = job.sitesReceiving.length\n const clauses = [\n `${copied} ${copied === 1 ? 'opt-out' : 'opt-outs'} copied to ${sites} ${sites === 1 ? 'site' : 'sites'}`,\n ]\n for (const pluginId of job.participants) {\n const entry = listPluginConsentGroupParticipants().find((candidate) => candidate.pluginId === pluginId)\n const clause = entry?.participant.summarize?.(job.plugins[pluginId] ?? {})\n if (clause) clauses.push(clause)\n }\n return `Finished a consent group change: ${clauses.join('; ')}`\n}\n\nasync function finish(\n ctx: Context,\n jobRef: FirebaseFirestore.DocumentReference,\n job: ConsentGroupChangeJob,\n owner: string,\n): Promise<ConsentGroupChangeJob> {\n const orgRef = orgDoc(ctx, job.orgId)\n const nowMs = ctx.now()\n const done = await save(\n ctx,\n jobRef,\n owner,\n { status: 'done', phase: 'done', step: 'finish', finishedAtMs: nowMs, units: {} },\n async (transaction) => {\n const marker = readConsentGroupsChange((await transaction.get(orgRef)).data())\n if (marker?.changeId === job.changeId) {\n transaction.update(orgRef, {\n [CONSENT_GROUPS_CHANGE_FIELD]: FieldValue.delete(),\n updatedAt: FieldValue.serverTimestamp(),\n })\n }\n },\n )\n await jobRef.update({ lease: null })\n await ctx.log(job.orgId, job.actor, finishedLine(done), { type: 'org', id: job.orgId })\n return { ...done, lease: null }\n}\n\n/**\n * One unit of work, or nothing when the job is waiting for its sweep.\n */\nasync function advanceOnce(\n ctx: Context,\n jobRef: FirebaseFirestore.DocumentReference,\n job: ConsentGroupChangeJob,\n owner: string,\n deadlineMs: number,\n): Promise<{ job: ConsentGroupChangeJob; idle: boolean }> {\n if (job.step === 'declare') return { job: await declare(ctx, jobRef, job, owner), idle: false }\n if (job.step === 'finish') return { job: await finish(ctx, jobRef, job, owner), idle: false }\n if (job.step === 'sweep-wait') {\n if (ctx.now() < (job.declaredAtMs ?? 0) + CONSENT_GROUP_SWEEP_DELAY_MS) return { job, idle: true }\n return { job: await nextStep(ctx, job.orgId, jobRef, job, owner), idle: false }\n }\n const unit = unitsOf(job).find((candidate) => !job.units[candidate.key]?.done)\n if (!unit) return { job: await nextStep(ctx, job.orgId, jobRef, job, owner), idle: false }\n const before = job.units[unit.key]?.cursor ?? null\n const after = await runUnit(ctx, jobRef, job, owner, unit, deadlineMs)\n const state = after.units[unit.key]\n // A participant that stopped where it started — out of time before its\n // first page — has nothing more to give this invocation.\n return { job: after, idle: unit.kind === 'participant' && !state?.done && (state?.cursor ?? null) === before }\n}\n\n/** Claims the lease, or answers why not. `null` for a job that does not exist. */\nasync function claim(\n ctx: Context,\n jobRef: FirebaseFirestore.DocumentReference,\n owner: string,\n): Promise<{ job: ConsentGroupChangeJob; mine: boolean } | null> {\n return ctx.firestore.runTransaction(async (transaction) => {\n const job = jobFrom(await transaction.get(jobRef))\n if (!job) return null\n const nowMs = ctx.now()\n if (job.status !== 'running') return { job, mine: false }\n if (job.lease && job.lease.untilMs > nowMs && job.lease.owner !== owner) return { job, mine: false }\n const lease = { owner, untilMs: nowMs + CONSENT_GROUP_CHANGE_LEASE_MS }\n transaction.update(jobRef, { lease })\n return { job: { ...job, lease }, mine: true }\n })\n}\n\nasync function release(\n ctx: Context,\n jobRef: FirebaseFirestore.DocumentReference,\n owner: string,\n): Promise<ConsentGroupChangeJob | null> {\n return ctx.firestore.runTransaction(async (transaction) => {\n const job = jobFrom(await transaction.get(jobRef))\n if (!job) return null\n if (job.lease?.owner !== owner) return job\n transaction.update(jobRef, { lease: null })\n return { ...job, lease: null }\n })\n}\n\n/** Counts a failed unit against the job, and says so once when it stalls. */\nasync function recordFailure(\n ctx: Context,\n jobRef: FirebaseFirestore.DocumentReference,\n owner: string,\n error: unknown,\n): Promise<ConsentGroupChangeJob | null> {\n const lastError = sanitizeError(error)\n const outcome = await ctx.firestore.runTransaction(async (transaction) => {\n const job = jobFrom(await transaction.get(jobRef))\n if (!job) return null\n if (job.lease?.owner !== owner) return { job, newlyStalled: false }\n const failures = (job.failures ?? 0) + 1\n const stalled = failures >= CONSENT_GROUP_CHANGE_STALL_AFTER\n const fields = { failures, stalled, lastError, lease: null }\n transaction.update(jobRef, { ...fields, updatedAt: FieldValue.serverTimestamp() })\n return { job: { ...job, ...fields }, newlyStalled: stalled && !job.stalled }\n })\n if (!outcome) return null\n if (outcome.newlyStalled) {\n await ctx.log(\n outcome.job.orgId,\n outcome.job.actor,\n 'A consent group change stopped with an error; it will try again',\n { type: 'org', id: outcome.job.orgId },\n )\n }\n console.error(`[consent-group-change] a unit of ${outcome.job.changeId} failed: ${lastError}`)\n return outcome.job\n}\n\n/**\n * Advances a change until it finishes, waits for its sweep, or reaches\n * `deadlineMs`. Answers the job's status either way — including when another\n * caller holds it, which is not a failure.\n */\nexport async function advanceConsentGroupChange(\n input: {\n orgId: string\n changeId: string\n deadlineMs: number\n /** Who holds the lease; one per invocation. */\n owner?: string\n } & ConsentGroupChangeContext,\n): Promise<{ ok: true; status: ConsentGroupChangeStatusBody } | ConsentGroupChangeRefusal> {\n const ctx = contextOf(input)\n const jobRef = jobDoc(ctx, input.orgId, input.changeId)\n const owner = input.owner ?? `advance:${createResourceUid()}`\n const claimed = await claim(ctx, jobRef, owner)\n if (!claimed) return NOT_FOUND\n let job = claimed.job\n if (!claimed.mine) return { ok: true, status: consentGroupChangeStatus(job, ctx.now()) }\n try {\n while (job.status === 'running' && ctx.now() < input.deadlineMs - DEADLINE_SAFETY_MS) {\n const outcome = await advanceOnce(ctx, jobRef, job, owner, input.deadlineMs)\n job = outcome.job\n if (outcome.idle) break\n }\n } catch (error) {\n if (!(error instanceof LeaseLost)) {\n job = (await recordFailure(ctx, jobRef, owner, error)) ?? job\n return { ok: true, status: consentGroupChangeStatus(job, ctx.now()) }\n }\n job = jobFrom(await jobRef.get()) ?? job\n return { ok: true, status: consentGroupChangeStatus(job, ctx.now()) }\n }\n if (job.status === 'running') job = (await release(ctx, jobRef, owner)) ?? job\n return { ok: true, status: consentGroupChangeStatus(job, ctx.now()) }\n}\n\n/*==========================================\n * CANCELING AND READING\n *=========================================*/\n\n/**\n * Stops a change before it takes effect: clears the marker and leaves every\n * refusal already carried, which only ever honors an opt-out on more sites.\n * Refused once the declaration has flipped — there is nothing left to stop\n * before, and the only way back is another change.\n */\nexport async function cancelConsentGroupChange(\n input: { orgId: string; changeId: string; actor: ConsentGroupChangeActor } & ConsentGroupChangeContext,\n): Promise<{ ok: true; status: ConsentGroupChangeStatusBody } | ConsentGroupChangeRefusal> {\n const ctx = contextOf(input)\n const orgRef = orgDoc(ctx, input.orgId)\n const jobRef = jobDoc(ctx, input.orgId, input.changeId)\n const outcome = await ctx.firestore.runTransaction(async (transaction) => {\n const [orgSnapshot, jobSnapshot] = await Promise.all([transaction.get(orgRef), transaction.get(jobRef)])\n const job = jobFrom(jobSnapshot)\n if (!job) return NOT_FOUND\n if (job.status !== 'running' || job.declaredAtMs) {\n return {\n ok: false as const,\n status: 409 as const,\n body: {\n error:\n job.status === 'running' || job.status === 'done'\n ? 'This change has already taken effect'\n : 'This change is no longer running',\n changeId: job.changeId,\n phase: job.phase,\n },\n }\n }\n const marker = readConsentGroupsChange(orgSnapshot.data())\n if (marker?.changeId === job.changeId) {\n transaction.update(orgRef, {\n [CONSENT_GROUPS_CHANGE_FIELD]: FieldValue.delete(),\n updatedAt: FieldValue.serverTimestamp(),\n })\n }\n const fields = {\n status: 'canceled' as const,\n phase: 'canceled' as const,\n canceledAtMs: ctx.now(),\n lease: null,\n }\n transaction.update(jobRef, { ...fields, updatedAt: FieldValue.serverTimestamp() })\n return { ok: true as const, job: { ...job, ...fields } }\n })\n if (isRefusal(outcome)) return outcome\n const { job } = outcome as { job: ConsentGroupChangeJob }\n await ctx.log(\n input.orgId,\n { uid: input.actor.uid ?? null, email: input.actor.email ?? null },\n 'Canceled a consent group change before it took effect',\n { type: 'org', id: input.orgId },\n )\n return { ok: true, status: consentGroupChangeStatus(job, ctx.now()) }\n}\n\n/** A job's status, or `null` for a change the org never had. */\nexport async function readConsentGroupChangeStatus(\n input: { orgId: string; changeId: string } & ConsentGroupChangeContext,\n): Promise<ConsentGroupChangeStatusBody | null> {\n const ctx = contextOf(input)\n const job = jobFrom(await jobDoc(ctx, input.orgId, input.changeId).get())\n return job ? consentGroupChangeStatus(job, ctx.now()) : null\n}\n\n/*==========================================\n * THE CRON BACKSTOP\n *=========================================*/\n\nexport interface DueConsentGroupChangeReport {\n orgId: string\n changeId: string\n phase: ConsentGroupsChangePhase\n /** What happened on this run. */\n outcome: 'advanced' | 'listed' | 'locked' | 'orphaned' | 'skipped'\n status?: ConsentGroupChangeStatusBody\n error?: string\n}\n\n/**\n * Advances every change in flight, oldest org id first, until `deadlineMs`.\n *\n * A change nobody is watching still finishes: the progress panel is a\n * convenience, and this is the guarantee. A lockdown on the org — or on the\n * member who started the change — pauses it, like every other write the\n * workspace makes. A marker whose job document is gone is a change nothing\n * can finish, and is cleared rather than blocking every change after it.\n */\nexport async function advanceDueConsentGroupChanges(\n input: { deadlineMs: number; dryRun?: boolean; limit?: number } & ConsentGroupChangeContext,\n): Promise<DueConsentGroupChangeReport[]> {\n const ctx = contextOf(input)\n const snapshot = await ctx.firestore\n .collection('orgs')\n .where(`${CONSENT_GROUPS_CHANGE_FIELD}.phase`, 'in', [...RUNNING_PHASES])\n .limit(input.limit ?? 50)\n .get()\n const reports: DueConsentGroupChangeReport[] = []\n for (const org of snapshot.docs) {\n const data = org.data() as Record<string, unknown>\n const marker = readConsentGroupsChange(data)\n if (!marker) continue\n const base = { orgId: org.id, changeId: marker.changeId, phase: marker.phase }\n if (input.dryRun) {\n reports.push({ ...base, outcome: 'listed' })\n continue\n }\n if (ctx.now() >= input.deadlineMs - DEADLINE_SAFETY_MS) {\n reports.push({ ...base, outcome: 'skipped' })\n continue\n }\n try {\n const job = jobFrom(await jobDoc(ctx, org.id, marker.changeId).get())\n if (!job) {\n await orgDoc(ctx, org.id).update({ [CONSENT_GROUPS_CHANGE_FIELD]: FieldValue.delete() })\n console.error(`[consent-group-change] cleared a marker with no job in org ${org.id}`)\n reports.push({ ...base, outcome: 'orphaned' })\n continue\n }\n if (await ctx.lockedOut(data, job.actor?.uid ?? null)) {\n reports.push({ ...base, outcome: 'locked' })\n continue\n }\n const advanced = await advanceConsentGroupChange({\n orgId: org.id,\n changeId: marker.changeId,\n deadlineMs: input.deadlineMs,\n owner: `cron:${createResourceUid()}`,\n firestore: ctx.firestore,\n log: ctx.log,\n now: ctx.now,\n })\n reports.push({\n ...base,\n outcome: 'advanced',\n ...(advanced.ok ? { status: (advanced as { status: ConsentGroupChangeStatusBody }).status } : {}),\n })\n } catch (error) {\n reports.push({ ...base, outcome: 'advanced', error: sanitizeError(error) })\n }\n }\n return reports\n}\n"],"names":["CONSENT_GROUP_CHANGE_STEPS","CONSENT_GROUP_CHANGES_COLLECTION","CONSENT_GROUP_RENAME_STEPS","CONSENT_GROUP_SWEEP_DELAY_MS","CONSENT_GROUPS_CHANGE_FIELD","consentGroupChangePhaseOf","consentGroupDisclosureChanges","consentGroupsFingerprint","describeConsentGroupChangeLine","discardedConsentGroupIds","estimateConsentGroupChange","orgSiteIds","planConsentGroupChange","readConsentGroupsChange","validateConsentGroupDeclaration","CONSENT_GROUPS_FIELD","consentGroupsAwaitConfirmation","readConsentGroups","createResourceUid","DEFAULT_EMAIL_TOPICS","TOPIC_OPT_OUTS_SUBCOLLECTION","MARKETING_CONSENT_BY_HOST_FIELD","MARKETING_CONSENT_FIELD","resolveMarketingConsentPolicy","isOrgWideMember","listPluginConsentGroupParticipants","previewPluginConsentGroupChange","FieldValue","CONSENT_GROUP_CARRY_STORES","CONSENT_GROUP_CARRY_SUBCOLLECTIONS","carryConsentGroupRefusals","firebaseAdmin","getLockdownVerdict","logOrgActivity","carryRetainedRefusals","retainedRefusalsStore","CONSENT_GROUP_CHANGE_LEASE_MS","CONSENT_GROUP_CHANGE_STALL_AFTER","CATCH_UP_OVERLAP_MS","DEADLINE_SAFETY_MS","RUNNING_PHASES","contextOf","input","firestore","app","log","now","Date","lockedOut","org","uid","intent","NOT_FOUND","ok","status","body","error","LeaseLost","Error","orgDoc","ctx","orgId","collection","doc","jobDoc","changeId","planOf","job","plan","before","after","sanitizeError","message","String","replace","slice","consentGroupChangeStatus","nowMs","declaredAtMs","index","steps","indexOf","step","phase","done","progress","stepIndex","length","stepCount","sweepAtMs","renameOnly","stalled","failures","lastError","leaseUntilMs","lease","untilMs","counts","retainedRefusals","sitesReceiving","plugins","prepare","expected","groups","marker","raw","current","Array","isArray","siteIds","validation","reserved","Object","keys","errors","isRefusal","value","Boolean","plannedSites","Set","hostIds","values","flatMap","group","siteNamesFor","hosts","snapshots","getAll","map","id","names","forEach","snapshot","get","describeLines","lines","line","hostId","countOf","query","count","Number","data","previewConsentGroupChange","prepared","db","cache","Map","cached","key","pending","set","store","name","suppressions","optOuts","paces","where","retained","carries","Promise","all","carry","toHostId","fromHostId","siteSuppressions","topicOptOuts","membersIn","declaration","find","includes","joining","sort","had","siblings","filter","has","push","inherited","refusals","sibling","pendingHolds","released","releasedHostIds","entries","topic","topicId","changedGroups","was","join","partialAccessMembers","members","member","docs","reach","some","every","policy","forwardPolicyWarning","mode","entry","enforceFromMs","participants","texts","documents","reduce","total","sum","preview","discarded","text","disclosures","capturesDisclosing","estimate","startConsentGroupChange","orgRef","exists","outside","pluginId","jobRef","started","runTransaction","transaction","beforeFingerprint","flows","siteNames","units","actor","email","startedAtMs","carryStartedAtMs","finishedAtMs","canceledAtMs","update","updatedAt","serverTimestamp","create","createdAt","type","unitsOf","sinceMs","_carry","carryIndex","kind","since","jobFrom","save","owner","fields","extra","next","addCounts","base","more","out","isFinite","runUnit","unit","deadlineMs","result","state","cursor","page","counted","written","participant","participantFor","run","dryRun","candidate","nextStep","declare","declared","delete","finished","finishedLine","copied","sites","clauses","clause","summarize","finish","advanceOnce","idle","claim","mine","release","recordFailure","outcome","newlyStalled","console","advanceConsentGroupChange","claimed","cancelConsentGroupChange","orgSnapshot","jobSnapshot","readConsentGroupChangeStatus","advanceDueConsentGroupChanges","limit","reports","advanced"],"mappings":"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"}
@@ -54,3 +54,25 @@ export interface FindContactByEmailOptions {
54
54
  * the last one that has to.
55
55
  */
56
56
  export declare function findContactByEmail(contactsRef: FirebaseFirestore.CollectionReference, email: unknown, options?: FindContactByEmailOptions): Promise<FirebaseFirestore.DocumentSnapshot | null>;
57
+ /**
58
+ * {@link findContactByEmail} for a whole file of addresses (AGL-3423).
59
+ *
60
+ * The same answer per address — the index first, the contact it names only
61
+ * if that still exists, the `email` query when the index has nothing — paid
62
+ * in a number of round trips that does not grow with the file: one `getAll`
63
+ * of the index entries, one `getAll` of the contacts they name, and one
64
+ * `in` query per thirty addresses the index missed, every page at once. A
65
+ * row found by the query gets its index entry the way a single lookup
66
+ * writes one, in one batch for the whole file.
67
+ *
68
+ * Unscoped, like the capture door's own lookup: one human who touched two
69
+ * sites is one person, and the door decides visibility, not the lookup.
70
+ *
71
+ * The map is keyed by normalized address. An address ABSENT from it is one
72
+ * this call could not answer — its page of the query failed — and a caller
73
+ * asks {@link findContactByEmail} for it rather than taking it as new;
74
+ * `null` is an address that resolves to nobody. When the query returns two
75
+ * documents for one address, the first in document order is the answer, as
76
+ * `limit(1)` gives the single lookup.
77
+ */
78
+ export declare function findContactsByEmail(contactsRef: FirebaseFirestore.CollectionReference, emails: readonly unknown[]): Promise<Map<string, FirebaseFirestore.DocumentSnapshot | null>>;
@@ -154,5 +154,113 @@ import { FieldValue } from "firebase-admin/firestore";
154
154
  }
155
155
  return hit;
156
156
  }
157
+ /** The most values one Firestore `in` filter may carry. */ const IN_FILTER_LIMIT = 30;
158
+ /** The most references handed to one `getAll`, and the most writes in one batch. */ const PAGE_LIMIT = 500;
159
+ function pagesOf(items, size) {
160
+ const out = [];
161
+ for(let at = 0; at < items.length; at += size)out.push(items.slice(at, at + size));
162
+ return out;
163
+ }
164
+ async function getAllPaged(firestore, refs) {
165
+ if (!refs.length) return [];
166
+ const pages = await Promise.all(pagesOf(refs, PAGE_LIMIT).map((page)=>firestore.getAll(...page)));
167
+ return pages.flat();
168
+ }
169
+ /**
170
+ * {@link findContactByEmail} for a whole file of addresses (AGL-3423).
171
+ *
172
+ * The same answer per address — the index first, the contact it names only
173
+ * if that still exists, the `email` query when the index has nothing — paid
174
+ * in a number of round trips that does not grow with the file: one `getAll`
175
+ * of the index entries, one `getAll` of the contacts they name, and one
176
+ * `in` query per thirty addresses the index missed, every page at once. A
177
+ * row found by the query gets its index entry the way a single lookup
178
+ * writes one, in one batch for the whole file.
179
+ *
180
+ * Unscoped, like the capture door's own lookup: one human who touched two
181
+ * sites is one person, and the door decides visibility, not the lookup.
182
+ *
183
+ * The map is keyed by normalized address. An address ABSENT from it is one
184
+ * this call could not answer — its page of the query failed — and a caller
185
+ * asks {@link findContactByEmail} for it rather than taking it as new;
186
+ * `null` is an address that resolves to nobody. When the query returns two
187
+ * documents for one address, the first in document order is the answer, as
188
+ * `limit(1)` gives the single lookup.
189
+ */ export async function findContactsByEmail(contactsRef, emails) {
190
+ const found = new Map();
191
+ const wanted = [
192
+ ...new Set(emails.map((email)=>normalizeContactEmail(email)).filter((email)=>!!email))
193
+ ];
194
+ if (!wanted.length) return found;
195
+ const firestore = contactsRef.firestore;
196
+ const index = emailIndexBeside(contactsRef);
197
+ if (index) {
198
+ try {
199
+ const keyed = wanted.map((email)=>({
200
+ email,
201
+ key: personKey(email)
202
+ })).filter((entry)=>!!entry.key);
203
+ const entries = await getAllPaged(firestore, keyed.map((entry)=>index.doc(entry.key)));
204
+ const pointers = keyed.map((entry, at)=>{
205
+ var _entries_at_get;
206
+ var _entries_at;
207
+ return {
208
+ email: entry.email,
209
+ contactId: ((_entries_at = entries[at]) == null ? void 0 : _entries_at.exists) ? String((_entries_at_get = entries[at].get('contactId')) != null ? _entries_at_get : '') : ''
210
+ };
211
+ }).filter((pointer)=>pointer.contactId);
212
+ const contacts = await getAllPaged(firestore, pointers.map((pointer)=>contactsRef.doc(pointer.contactId)));
213
+ pointers.forEach((pointer, at)=>{
214
+ var _contacts_at;
215
+ if ((_contacts_at = contacts[at]) == null ? void 0 : _contacts_at.exists) found.set(pointer.email, contacts[at]);
216
+ });
217
+ } catch (error) {
218
+ console.error('[contact-email-index] bulk lookup failed', error);
219
+ }
220
+ }
221
+ const missed = wanted.filter((email)=>!found.has(email));
222
+ const repaired = [];
223
+ await Promise.all(pagesOf(missed, IN_FILTER_LIMIT).map(async (page)=>{
224
+ try {
225
+ const snapshot = await contactsRef.where('email', 'in', page).get();
226
+ for (const doc of snapshot.docs){
227
+ var _doc_get;
228
+ const email = String((_doc_get = doc.get('email')) != null ? _doc_get : '');
229
+ if (!page.includes(email) || found.get(email)) continue;
230
+ found.set(email, doc);
231
+ repaired.push({
232
+ email,
233
+ contactId: doc.id
234
+ });
235
+ }
236
+ for (const email of page)if (!found.has(email)) found.set(email, null);
237
+ } catch (error) {
238
+ console.error('[contact-email-index] bulk query failed', error);
239
+ }
240
+ }));
241
+ if (index && repaired.length) {
242
+ try {
243
+ for (const page of pagesOf(repaired, PAGE_LIMIT)){
244
+ const batch = firestore.batch();
245
+ for (const { email, contactId } of page){
246
+ const key = personKey(email);
247
+ if (!key) continue;
248
+ const entry = {
249
+ email,
250
+ contactId,
251
+ updatedAt: FieldValue.serverTimestamp()
252
+ };
253
+ batch.set(index.doc(key), entry, {
254
+ merge: true
255
+ });
256
+ }
257
+ await batch.commit();
258
+ }
259
+ } catch (error) {
260
+ console.error('[contact-email-index] bulk write failed', error);
261
+ }
262
+ }
263
+ return found;
264
+ }
157
265
 
158
266
  //# sourceMappingURL=contact-email-index.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/contact-email-index.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * THE ADDRESS INDEX (AGL-2625): `orgs/{orgId}/emailIndex/{personKey}`.\n *\n * `upsertHostContact` finds a person by `where('email', '==', …)` on the\n * contacts collection, and that query answers one document per address by\n * construction — which is exactly what stops a merged record's address from\n * ever finding the survivor. The survivor's `email` is the identity that\n * stayed; the merged address is in its `alternateEmails`, and Firestore\n * cannot answer \"the document whose primary OR alternate address is X\" as\n * one indexed query. So the address is indexed on its own: one document per\n * address, naming the contact it belongs to.\n *\n * ## Consulted first, never trusted alone\n *\n * The lookup reads the index entry and then the contact it names, and\n * takes the contact only if it still exists. A stale entry — a contact\n * detached and deleted after its entry was written — falls through to the\n * query, which is the truth the index only summarizes.\n *\n * ## Written lazily, never backfilled\n *\n * An entry is written when a capture CREATES a contact, and when a lookup\n * finds one through the query rather than the index — so every existing\n * contact gains an entry on its next write, and no job has to walk the\n * collection. A merge writes one entry per address the survivor answers to.\n *\n * ## Never in the way of a capture\n *\n * Every read and write here is wrapped: a form submission or an order must\n * succeed whatever the index did, exactly as the capture itself swallows\n * its own failures. An index that cannot be reached is a miss, and a miss\n * is the query.\n *\n * ## One lookup for every door (AGL-2633)\n *\n * The capture door was the first reader; every other place that finds a\n * person by address — an automation step, the flow-email consent gate, the\n * delivery webhook's engagement stamp, the Inbox add-to-list check, a\n * refund, a campaign's proof and consent reads, the REST create and\n * `?email=` list — resolves through this same function, so a merged\n * record's alternate address answers the same person everywhere. Two of\n * those doors read differently from the capture, and the options carry it:\n *\n * - **`hostId`** narrows the answer to what ONE site may see. The index is\n * org-wide, so the lookup is too; the scope check is applied to the\n * document it names, in memory, which answers exactly what the scoped\n * `email ==` query used to — one document per address, so a contact the\n * site cannot see is `null` rather than a reason to look further.\n * - **`transaction`** routes every read through the caller's transaction,\n * for a door that updates the contact it finds inside one. The lazy index\n * write stays OUTSIDE the transaction, deliberately: a queued write would\n * forbid the caller any later read, and the entry is a cache fill that\n * never has to be atomic with anything. It is idempotent, so a retried\n * transaction body writing it twice is harmless.\n *\n * Imported by module path rather than through the `@aglyn/aglyn/server`\n * barrel, for the reason `upsert-contact.ts` gives: the door's specs\n * substitute a fixture barrel, and a helper that reached the normalizer only\n * through it would find `undefined` there. Callers outside this library\n * import THIS module by its leaf path for the mirror-image reason: most of\n * their specs substitute the `@aglyn/tenant-data-admin` barrel wholesale.\n */\n\nimport {\n CONTACT_EMAIL_INDEX_COLLECTION,\n normalizeContactEmail,\n} from '@aglyn/aglyn/app-utils/contacts'\nimport { personKey } from '@aglyn/aglyn/app-utils/person-key'\nimport { visibleToHost } from '@aglyn/aglyn/app-utils/scope-tokens'\nimport { FieldValue } from 'firebase-admin/firestore'\n\n/** One entry: the address as stored, and the contact it resolves to. */\nexport interface ContactEmailIndexEntry {\n email: string\n contactId: string\n}\n\n/**\n * The org's index collection, beside its contacts one — or `null` when the\n * handle has no parent, which is a fixture's contacts reference and never\n * a real org subcollection. `null` means \"no index\": the caller falls back\n * to the query and writes nothing.\n */\nexport function emailIndexBeside(\n contactsRef: FirebaseFirestore.CollectionReference,\n): FirebaseFirestore.CollectionReference | null {\n try {\n const parent = contactsRef.parent\n return parent ? parent.collection(CONTACT_EMAIL_INDEX_COLLECTION) : null\n } catch {\n return null\n }\n}\n\n/**\n * Point every address in `emails` at `contactId`. Never rejects: an entry\n * that could not be written is one the next lookup writes again.\n */\nexport async function writeContactEmailIndex(\n index: FirebaseFirestore.CollectionReference | null,\n contactId: string,\n emails: readonly string[],\n): Promise<void> {\n if (!index || !contactId) return\n await Promise.all(\n emails.map(async (raw) => {\n const email = normalizeContactEmail(raw)\n const key = email ? personKey(email) : null\n if (!email || !key) return\n const entry: ContactEmailIndexEntry & { updatedAt: unknown } = {\n email,\n contactId,\n updatedAt: FieldValue.serverTimestamp(),\n }\n await index.doc(key).set(entry, { merge: true })\n }),\n ).catch((error: unknown) => {\n console.error('[contact-email-index] write failed', contactId, error)\n })\n}\n\n/** How one door reads differently from the capture; see the module notes. */\nexport interface FindContactByEmailOptions {\n /**\n * Answer only a contact THIS site may see. A document the address names\n * but the site cannot read is `null` — the same answer the scoped\n * `email ==` query gave, since one address names one document.\n */\n hostId?: string\n /**\n * Read through this transaction. Every read — the index entry, the\n * contact it names, the fallback query — goes through it; the lazy index\n * write does not, so the caller may still read after this returns.\n */\n transaction?: FirebaseFirestore.Transaction\n}\n\n/**\n * The contact an address belongs to, as a document snapshot — or `null`.\n *\n * The index first, then the per-document query it summarizes. A query hit\n * writes the entry the index lacked, so the read that paid for the miss is\n * the last one that has to.\n */\nexport async function findContactByEmail(\n contactsRef: FirebaseFirestore.CollectionReference,\n email: unknown,\n options: FindContactByEmailOptions = {},\n): Promise<FirebaseFirestore.DocumentSnapshot | null> {\n const normalized = normalizeContactEmail(email)\n if (!normalized) return null\n const { hostId, transaction } = options\n const readDoc = (ref: FirebaseFirestore.DocumentReference) =>\n transaction ? transaction.get(ref) : ref.get()\n const readQuery = (query: FirebaseFirestore.Query) =>\n transaction ? transaction.get(query) : query.get()\n\n const index = emailIndexBeside(contactsRef)\n const key = index ? personKey(normalized) : null\n let hit: FirebaseFirestore.DocumentSnapshot | null = null\n if (index && key) {\n try {\n const entry = await readDoc(index.doc(key))\n const contactId = entry.exists ? String(entry.get('contactId') ?? '') : ''\n if (contactId) {\n const contact = await readDoc(contactsRef.doc(contactId))\n if (contact.exists) hit = contact\n }\n } catch (error) {\n console.error('[contact-email-index] lookup failed', error)\n }\n }\n if (!hit) {\n const found = await readQuery(\n contactsRef.where('email', '==', normalized).limit(1),\n )\n hit = found.docs[0] ?? null\n if (!hit) return null\n await writeContactEmailIndex(index, hit.id, [normalized])\n }\n if (hostId && !visibleToHost(hit.get('visibleTo') as string[] | undefined, hostId)) {\n return null\n }\n return hit\n}\n"],"names":["CONTACT_EMAIL_INDEX_COLLECTION","normalizeContactEmail","personKey","visibleToHost","FieldValue","emailIndexBeside","contactsRef","parent","collection","writeContactEmailIndex","index","contactId","emails","Promise","all","map","raw","email","key","entry","updatedAt","serverTimestamp","doc","set","merge","catch","error","console","findContactByEmail","options","normalized","hostId","transaction","readDoc","ref","get","readQuery","query","hit","exists","String","contact","found","where","limit","docs","id"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6DC,GAED,SACEA,8BAA8B,EAC9BC,qBAAqB,QAChB,kCAAiC;AACxC,SAASC,SAAS,QAAQ,oCAAmC;AAC7D,SAASC,aAAa,QAAQ,sCAAqC;AACnE,SAASC,UAAU,QAAQ,2BAA0B;AAQrD;;;;;CAKC,GACD,OAAO,SAASC,iBACdC,WAAkD;IAElD,IAAI;QACF,MAAMC,SAASD,YAAYC,MAAM;QACjC,OAAOA,SAASA,OAAOC,UAAU,CAACR,kCAAkC;IACtE,EAAE,eAAM;QACN,OAAO;IACT;AACF;AAEA;;;CAGC,GACD,OAAO,eAAeS,uBACpBC,KAAmD,EACnDC,SAAiB,EACjBC,MAAyB;IAEzB,IAAI,CAACF,SAAS,CAACC,WAAW;IAC1B,MAAME,QAAQC,GAAG,CACfF,OAAOG,GAAG,CAAC,OAAOC;QAChB,MAAMC,QAAQhB,sBAAsBe;QACpC,MAAME,MAAMD,QAAQf,UAAUe,SAAS;QACvC,IAAI,CAACA,SAAS,CAACC,KAAK;QACpB,MAAMC,QAAyD;YAC7DF;YACAN;YACAS,WAAWhB,WAAWiB,eAAe;QACvC;QACA,MAAMX,MAAMY,GAAG,CAACJ,KAAKK,GAAG,CAACJ,OAAO;YAAEK,OAAO;QAAK;IAChD,IACAC,KAAK,CAAC,CAACC;QACPC,QAAQD,KAAK,CAAC,sCAAsCf,WAAWe;IACjE;AACF;AAkBA;;;;;;CAMC,GACD,OAAO,eAAeE,mBACpBtB,WAAkD,EAClDW,KAAc,EACdY,UAAqC,CAAC,CAAC;IAEvC,MAAMC,aAAa7B,sBAAsBgB;IACzC,IAAI,CAACa,YAAY,OAAO;IACxB,MAAM,EAAEC,MAAM,EAAEC,WAAW,EAAE,GAAGH;IAChC,MAAMI,UAAU,CAACC,MACfF,cAAcA,YAAYG,GAAG,CAACD,OAAOA,IAAIC,GAAG;IAC9C,MAAMC,YAAY,CAACC,QACjBL,cAAcA,YAAYG,GAAG,CAACE,SAASA,MAAMF,GAAG;IAElD,MAAMzB,QAAQL,iBAAiBC;IAC/B,MAAMY,MAAMR,QAAQR,UAAU4B,cAAc;IAC5C,IAAIQ,MAAiD;IACrD,IAAI5B,SAASQ,KAAK;QAChB,IAAI;gBAEsCC;YADxC,MAAMA,QAAQ,MAAMc,QAAQvB,MAAMY,GAAG,CAACJ;YACtC,MAAMP,YAAYQ,MAAMoB,MAAM,GAAGC,QAAOrB,aAAAA,MAAMgB,GAAG,CAAC,wBAAVhB,aAA0B,MAAM;YACxE,IAAIR,WAAW;gBACb,MAAM8B,UAAU,MAAMR,QAAQ3B,YAAYgB,GAAG,CAACX;gBAC9C,IAAI8B,QAAQF,MAAM,EAAED,MAAMG;YAC5B;QACF,EAAE,OAAOf,OAAO;YACdC,QAAQD,KAAK,CAAC,uCAAuCA;QACvD;IACF;IACA,IAAI,CAACY,KAAK;YAIFI;QAHN,MAAMA,QAAQ,MAAMN,UAClB9B,YAAYqC,KAAK,CAAC,SAAS,MAAMb,YAAYc,KAAK,CAAC;QAErDN,OAAMI,eAAAA,MAAMG,IAAI,CAAC,EAAE,YAAbH,eAAiB;QACvB,IAAI,CAACJ,KAAK,OAAO;QACjB,MAAM7B,uBAAuBC,OAAO4B,IAAIQ,EAAE,EAAE;YAAChB;SAAW;IAC1D;IACA,IAAIC,UAAU,CAAC5B,cAAcmC,IAAIH,GAAG,CAAC,cAAsCJ,SAAS;QAClF,OAAO;IACT;IACA,OAAOO;AACT"}
1
+ {"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/contact-email-index.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * THE ADDRESS INDEX (AGL-2625): `orgs/{orgId}/emailIndex/{personKey}`.\n *\n * `upsertHostContact` finds a person by `where('email', '==', …)` on the\n * contacts collection, and that query answers one document per address by\n * construction — which is exactly what stops a merged record's address from\n * ever finding the survivor. The survivor's `email` is the identity that\n * stayed; the merged address is in its `alternateEmails`, and Firestore\n * cannot answer \"the document whose primary OR alternate address is X\" as\n * one indexed query. So the address is indexed on its own: one document per\n * address, naming the contact it belongs to.\n *\n * ## Consulted first, never trusted alone\n *\n * The lookup reads the index entry and then the contact it names, and\n * takes the contact only if it still exists. A stale entry — a contact\n * detached and deleted after its entry was written — falls through to the\n * query, which is the truth the index only summarizes.\n *\n * ## Written lazily, never backfilled\n *\n * An entry is written when a capture CREATES a contact, and when a lookup\n * finds one through the query rather than the index — so every existing\n * contact gains an entry on its next write, and no job has to walk the\n * collection. A merge writes one entry per address the survivor answers to.\n *\n * ## Never in the way of a capture\n *\n * Every read and write here is wrapped: a form submission or an order must\n * succeed whatever the index did, exactly as the capture itself swallows\n * its own failures. An index that cannot be reached is a miss, and a miss\n * is the query.\n *\n * ## One lookup for every door (AGL-2633)\n *\n * The capture door was the first reader; every other place that finds a\n * person by address — an automation step, the flow-email consent gate, the\n * delivery webhook's engagement stamp, the Inbox add-to-list check, a\n * refund, a campaign's proof and consent reads, the REST create and\n * `?email=` list — resolves through this same function, so a merged\n * record's alternate address answers the same person everywhere. Two of\n * those doors read differently from the capture, and the options carry it:\n *\n * - **`hostId`** narrows the answer to what ONE site may see. The index is\n * org-wide, so the lookup is too; the scope check is applied to the\n * document it names, in memory, which answers exactly what the scoped\n * `email ==` query used to — one document per address, so a contact the\n * site cannot see is `null` rather than a reason to look further.\n * - **`transaction`** routes every read through the caller's transaction,\n * for a door that updates the contact it finds inside one. The lazy index\n * write stays OUTSIDE the transaction, deliberately: a queued write would\n * forbid the caller any later read, and the entry is a cache fill that\n * never has to be atomic with anything. It is idempotent, so a retried\n * transaction body writing it twice is harmless.\n *\n * Imported by module path rather than through the `@aglyn/aglyn/server`\n * barrel, for the reason `upsert-contact.ts` gives: the door's specs\n * substitute a fixture barrel, and a helper that reached the normalizer only\n * through it would find `undefined` there. Callers outside this library\n * import THIS module by its leaf path for the mirror-image reason: most of\n * their specs substitute the `@aglyn/tenant-data-admin` barrel wholesale.\n */\n\nimport {\n CONTACT_EMAIL_INDEX_COLLECTION,\n normalizeContactEmail,\n} from '@aglyn/aglyn/app-utils/contacts'\nimport { personKey } from '@aglyn/aglyn/app-utils/person-key'\nimport { visibleToHost } from '@aglyn/aglyn/app-utils/scope-tokens'\nimport { FieldValue } from 'firebase-admin/firestore'\n\n/** One entry: the address as stored, and the contact it resolves to. */\nexport interface ContactEmailIndexEntry {\n email: string\n contactId: string\n}\n\n/**\n * The org's index collection, beside its contacts one — or `null` when the\n * handle has no parent, which is a fixture's contacts reference and never\n * a real org subcollection. `null` means \"no index\": the caller falls back\n * to the query and writes nothing.\n */\nexport function emailIndexBeside(\n contactsRef: FirebaseFirestore.CollectionReference,\n): FirebaseFirestore.CollectionReference | null {\n try {\n const parent = contactsRef.parent\n return parent ? parent.collection(CONTACT_EMAIL_INDEX_COLLECTION) : null\n } catch {\n return null\n }\n}\n\n/**\n * Point every address in `emails` at `contactId`. Never rejects: an entry\n * that could not be written is one the next lookup writes again.\n */\nexport async function writeContactEmailIndex(\n index: FirebaseFirestore.CollectionReference | null,\n contactId: string,\n emails: readonly string[],\n): Promise<void> {\n if (!index || !contactId) return\n await Promise.all(\n emails.map(async (raw) => {\n const email = normalizeContactEmail(raw)\n const key = email ? personKey(email) : null\n if (!email || !key) return\n const entry: ContactEmailIndexEntry & { updatedAt: unknown } = {\n email,\n contactId,\n updatedAt: FieldValue.serverTimestamp(),\n }\n await index.doc(key).set(entry, { merge: true })\n }),\n ).catch((error: unknown) => {\n console.error('[contact-email-index] write failed', contactId, error)\n })\n}\n\n/** How one door reads differently from the capture; see the module notes. */\nexport interface FindContactByEmailOptions {\n /**\n * Answer only a contact THIS site may see. A document the address names\n * but the site cannot read is `null` — the same answer the scoped\n * `email ==` query gave, since one address names one document.\n */\n hostId?: string\n /**\n * Read through this transaction. Every read — the index entry, the\n * contact it names, the fallback query — goes through it; the lazy index\n * write does not, so the caller may still read after this returns.\n */\n transaction?: FirebaseFirestore.Transaction\n}\n\n/**\n * The contact an address belongs to, as a document snapshot — or `null`.\n *\n * The index first, then the per-document query it summarizes. A query hit\n * writes the entry the index lacked, so the read that paid for the miss is\n * the last one that has to.\n */\nexport async function findContactByEmail(\n contactsRef: FirebaseFirestore.CollectionReference,\n email: unknown,\n options: FindContactByEmailOptions = {},\n): Promise<FirebaseFirestore.DocumentSnapshot | null> {\n const normalized = normalizeContactEmail(email)\n if (!normalized) return null\n const { hostId, transaction } = options\n const readDoc = (ref: FirebaseFirestore.DocumentReference) =>\n transaction ? transaction.get(ref) : ref.get()\n const readQuery = (query: FirebaseFirestore.Query) =>\n transaction ? transaction.get(query) : query.get()\n\n const index = emailIndexBeside(contactsRef)\n const key = index ? personKey(normalized) : null\n let hit: FirebaseFirestore.DocumentSnapshot | null = null\n if (index && key) {\n try {\n const entry = await readDoc(index.doc(key))\n const contactId = entry.exists ? String(entry.get('contactId') ?? '') : ''\n if (contactId) {\n const contact = await readDoc(contactsRef.doc(contactId))\n if (contact.exists) hit = contact\n }\n } catch (error) {\n console.error('[contact-email-index] lookup failed', error)\n }\n }\n if (!hit) {\n const found = await readQuery(\n contactsRef.where('email', '==', normalized).limit(1),\n )\n hit = found.docs[0] ?? null\n if (!hit) return null\n await writeContactEmailIndex(index, hit.id, [normalized])\n }\n if (hostId && !visibleToHost(hit.get('visibleTo') as string[] | undefined, hostId)) {\n return null\n }\n return hit\n}\n\n/** The most values one Firestore `in` filter may carry. */\nconst IN_FILTER_LIMIT = 30\n\n/** The most references handed to one `getAll`, and the most writes in one batch. */\nconst PAGE_LIMIT = 500\n\nfunction pagesOf<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\nasync function getAllPaged(\n firestore: FirebaseFirestore.Firestore,\n refs: readonly FirebaseFirestore.DocumentReference[],\n): Promise<FirebaseFirestore.DocumentSnapshot[]> {\n if (!refs.length) return []\n const pages = await Promise.all(\n pagesOf(refs, PAGE_LIMIT).map((page) => firestore.getAll(...page)),\n )\n return pages.flat()\n}\n\n/**\n * {@link findContactByEmail} for a whole file of addresses (AGL-3423).\n *\n * The same answer per address — the index first, the contact it names only\n * if that still exists, the `email` query when the index has nothing — paid\n * in a number of round trips that does not grow with the file: one `getAll`\n * of the index entries, one `getAll` of the contacts they name, and one\n * `in` query per thirty addresses the index missed, every page at once. A\n * row found by the query gets its index entry the way a single lookup\n * writes one, in one batch for the whole file.\n *\n * Unscoped, like the capture door's own lookup: one human who touched two\n * sites is one person, and the door decides visibility, not the lookup.\n *\n * The map is keyed by normalized address. An address ABSENT from it is one\n * this call could not answer — its page of the query failed — and a caller\n * asks {@link findContactByEmail} for it rather than taking it as new;\n * `null` is an address that resolves to nobody. When the query returns two\n * documents for one address, the first in document order is the answer, as\n * `limit(1)` gives the single lookup.\n */\nexport async function findContactsByEmail(\n contactsRef: FirebaseFirestore.CollectionReference,\n emails: readonly unknown[],\n): Promise<Map<string, FirebaseFirestore.DocumentSnapshot | null>> {\n const found = new Map<string, FirebaseFirestore.DocumentSnapshot | null>()\n const wanted = [\n ...new Set(\n emails\n .map((email) => normalizeContactEmail(email))\n .filter((email): email is string => !!email),\n ),\n ]\n if (!wanted.length) return found\n const firestore = contactsRef.firestore\n const index = emailIndexBeside(contactsRef)\n\n if (index) {\n try {\n const keyed = wanted\n .map((email) => ({ email, key: personKey(email) }))\n .filter((entry): entry is { email: string; key: string } => !!entry.key)\n const entries = await getAllPaged(\n firestore,\n keyed.map((entry) => index.doc(entry.key)),\n )\n const pointers = keyed\n .map((entry, at) => ({\n email: entry.email,\n contactId: entries[at]?.exists ? String(entries[at].get('contactId') ?? '') : '',\n }))\n .filter((pointer) => pointer.contactId)\n const contacts = await getAllPaged(\n firestore,\n pointers.map((pointer) => contactsRef.doc(pointer.contactId)),\n )\n pointers.forEach((pointer, at) => {\n if (contacts[at]?.exists) found.set(pointer.email, contacts[at])\n })\n } catch (error) {\n console.error('[contact-email-index] bulk lookup failed', error)\n }\n }\n\n const missed = wanted.filter((email) => !found.has(email))\n const repaired: { email: string; contactId: string }[] = []\n await Promise.all(\n pagesOf(missed, IN_FILTER_LIMIT).map(async (page) => {\n try {\n const snapshot = await contactsRef.where('email', 'in', page).get()\n for (const doc of snapshot.docs) {\n const email = String(doc.get('email') ?? '')\n if (!page.includes(email) || found.get(email)) continue\n found.set(email, doc)\n repaired.push({ email, contactId: doc.id })\n }\n for (const email of page) if (!found.has(email)) found.set(email, null)\n } catch (error) {\n console.error('[contact-email-index] bulk query failed', error)\n }\n }),\n )\n\n if (index && repaired.length) {\n try {\n for (const page of pagesOf(repaired, PAGE_LIMIT)) {\n const batch = firestore.batch()\n for (const { email, contactId } of page) {\n const key = personKey(email)\n if (!key) continue\n const entry: ContactEmailIndexEntry & { updatedAt: unknown } = {\n email,\n contactId,\n updatedAt: FieldValue.serverTimestamp(),\n }\n batch.set(index.doc(key), entry, { merge: true })\n }\n await batch.commit()\n }\n } catch (error) {\n console.error('[contact-email-index] bulk write failed', error)\n }\n }\n return found\n}\n"],"names":["CONTACT_EMAIL_INDEX_COLLECTION","normalizeContactEmail","personKey","visibleToHost","FieldValue","emailIndexBeside","contactsRef","parent","collection","writeContactEmailIndex","index","contactId","emails","Promise","all","map","raw","email","key","entry","updatedAt","serverTimestamp","doc","set","merge","catch","error","console","findContactByEmail","options","normalized","hostId","transaction","readDoc","ref","get","readQuery","query","hit","exists","String","contact","found","where","limit","docs","id","IN_FILTER_LIMIT","PAGE_LIMIT","pagesOf","items","size","out","at","length","push","slice","getAllPaged","firestore","refs","pages","page","getAll","flat","findContactsByEmail","Map","wanted","Set","filter","keyed","entries","pointers","pointer","contacts","forEach","missed","has","repaired","snapshot","includes","batch","commit"],"mappings":"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"}