@aglyn/tenant-data-admin 1.0.0-beta.165 → 1.0.0-beta.167
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +6 -6
- package/src/lib/server/contact-merge.js +24 -19
- package/src/lib/server/contact-merge.js.map +1 -1
- package/src/lib/server/crm-booking-activity.js +8 -3
- package/src/lib/server/crm-booking-activity.js.map +1 -1
- package/src/lib/server/crm-inbound-email.js +12 -2
- package/src/lib/server/crm-inbound-email.js.map +1 -1
- package/src/lib/server/email-flow-gate.js +8 -1
- package/src/lib/server/email-flow-gate.js.map +1 -1
- package/src/lib/server/erase-person.js +20 -1
- package/src/lib/server/erase-person.js.map +1 -1
- package/src/lib/server/host-visitor-records.d.ts +78 -0
- package/src/lib/server/host-visitor-records.js +238 -17
- package/src/lib/server/host-visitor-records.js.map +1 -1
- package/src/lib/server/organizations.d.ts +1 -1
- package/src/lib/server/organizations.js.map +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/organizations.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 * Server-side organization operations (AGL-233/234). Everything here is\n * Admin-SDK-only by design: org creation, slug reservation, membership\n * and the projections the security rules authorize against are never\n * client-writable (docs/MULTI_TENANT_FIRESTORE.md §8).\n */\n\nimport {\n consentGroupForHost,\n type ConsentGroup,\n checkHostCollaboratorQuota,\n checkSeatQuota,\n countCollaboratorSeats,\n countManagerSeatsExcluding,\n createResourceUid,\n generateOrgSlug,\n projectMemberResolvedPermissions,\n resolveOrgPermissions,\n isOrgWideMember,\n isValidOrgSlug,\n hostPermissionKeys,\n orgPermissionLabel,\n pluginOrgPermissionKeys,\n projectHostMemberPermissions,\n projectHostMemberRoles,\n projectMemberScopeTokens,\n resolveCollaboratorHostPermissions,\n scopeTokensForHost,\n type AglynOrganization,\n type AglynOrgBilling,\n type AglynOrgCustomRole,\n type AglynOrgMember,\n type CollaboratorSeatEntry,\n type HostAccessRole,\n type OrgPermission,\n type OrgRole,\n} from '@aglyn/aglyn/server'\nimport type { PluginActivityTargetType } from '@aglyn/aglyn/plugin-manager/plugin-activity-actions'\nimport type { HostActivityActor } from '@aglyn/aglyn/app-utils/activity-presenter'\nimport {\n nameSearchKey,\n nameSearchReversed,\n nameSearchTokens,\n} from '@aglyn/aglyn/app-utils/name-search'\n// LEAF MODULE, NOT THE BARREL (AGL-1289). This file is itself reachable\n// through `@aglyn/aglyn/server`, and the verdict route proved this week that a\n// constant pulled from that barrel inside the cycle typechecks and then\n// resolves `undefined` at runtime.\nimport {\n ORG_BILLING_DOC_ID,\n ORG_BILLING_SUBCOLLECTION,\n} from '@aglyn/aglyn/app-utils/org-billing-doc'\nimport { MEMBER_EMAIL_ALIASES_COLLECTION } from '@aglyn/aglyn/app-utils/member-email-aliases'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { cache } from 'react'\nimport { findUserByUidAcrossPools } from './auth-pools'\nimport firebaseAdmin from './firebase-admin'\nimport {\n enforceFreeWorkspaceCapInTransaction,\n readFreeWorkspaceCapConfig,\n type FreeWorkspaceCapConfig,\n} from './free-workspace-cap'\nimport {\n deleteMemberHostProjections,\n syncHostProjectionForMembers,\n syncMemberHostProjections,\n} from './host-memberships'\nimport { updateExisting } from './update-existing'\nimport { attachWorkspaceDomain } from './workspace-domains'\n\nconst firestore = () => firebaseAdmin.app().firestore()\n\n/** Firestore's hard cap on writes in one batched commit. */\nconst FIRESTORE_BATCH_LIMIT = 500\n\nexport class OrgSlugTakenError extends Error {\n constructor(slug: string) {\n super(`Org slug already reserved: ${slug}`)\n this.name = 'OrgSlugTakenError'\n }\n}\n\n/**\n * ⛔ NOTHING WRITES A NEW `reservedUntil` ANY MORE — AND THE RULES THAT READ\n * ONE STAY (AGL-2590).\n *\n * AGL-2585 gave a workspace created by an UNVERIFIED owner a held address\n * rather than a granted one, because a signup could create a workspace before\n * anything proved the email belonged to the person typing it. That is no\n * longer possible: `/api/orgs/create` refuses an unverified caller outright,\n * the sign-up form holds its typed name against the account instead, and the\n * workspace is created on the first verified session. There is no path left\n * that can produce an unproven address, so `createOrganization` writes the\n * plain grant this collection has always held and the twenty-one-day\n * reservation window is gone with the code that set it.\n *\n * The READ side below is deliberately kept, and it is not dead weight:\n * production holds `orgSlugs` documents that WERE written with an expiry,\n * before this. Deleting the lapse rules would silently promote every one of\n * those squats to a permanent grant — the exact outcome AGL-2585 existed to\n * end. They stay until `reap-unverified-orgs` has erased or promoted the last\n * of them.\n *\n * Has a PENDING address reservation run out?\n *\n * A reservation with no `reservedUntil` is a GRANT and never lapses, which is\n * what keeps every workspace made by a verified owner — and every one that\n * predates the field — untouchable by this rule.\n *\n * A `reservedUntil` that is not a finite number never lapses either: a\n * corrupt or half-written expiry is a reason to leave an address alone, not a\n * reason to hand it to the next caller.\n */\nexport function isSlugReservationLapsed(\n // The whole `orgSlugs/{slug}` document, not just the field being read: every\n // caller has one in hand, and a parameter narrowed to `reservedUntil` alone\n // makes the ordinary case — a grant, which carries `orgId` and no expiry —\n // an excess-property error at the call site.\n reservation:\n | { orgId?: unknown; movedTo?: unknown; reservedUntil?: unknown }\n | undefined,\n now: number = Date.now(),\n): boolean {\n const until = reservation?.reservedUntil\n if (typeof until !== 'number' || !Number.isFinite(until)) return false\n return until <= now\n}\n\n/**\n * Whether an `orgSlugs/{slug}` reservation may be (re)claimed (AGL-585):\n * free when the doc is missing, when the claimant already owns it, or when\n * it is a tombstone (`movedTo` set) — a renamed-away slug keeps redirecting\n * old URLs only until someone wants it, it is never reserved forever.\n * Claiming writes a full-replace `{ orgId }`, which ends the redirect —\n * links to a reclaimed slug resolve to the new owner from then on.\n *\n * A LAPSED PENDING RESERVATION is claimable too (AGL-2585). The reservation\n * an unverified signup takes is a hold, not a grant, and a hold that never\n * expires is the squat this rule exists to end.\n *\n * ⚠️ Claimable here is NOT the whole answer for a lapsed reservation — see\n * {@link lapsedReservationIsStillHeld}, which both call sites consult before\n * they act on a `true` that came from the lapse branch. This function is pure\n * and cannot ask whether the owner has verified since; treating its answer as\n * final would let a customer who verified on day one lose their address on\n * day twenty-one because a sweep was down.\n */\nexport function isSlugReservationClaimable(\n reservation:\n | { orgId?: unknown; movedTo?: unknown; reservedUntil?: unknown }\n | undefined,\n claimingOrgId: string | null,\n now: number = Date.now(),\n): boolean {\n if (!reservation) return true\n if (claimingOrgId !== null && reservation.orgId === claimingOrgId) return true\n if (reservation.movedTo) return true\n return isSlugReservationLapsed(reservation, now)\n}\n\n/**\n * Is a LAPSED reservation nonetheless still its holder's? (AGL-2585)\n *\n * The lapse rule above is pure, and the fact it cannot see is the only one\n * that matters here: whether the owner verified their address after the\n * workspace was made. `reap-unverified-orgs` clears `reservedUntil` on its\n * next pass when they have, but \"on its next pass\" is a promise about a\n * scheduled job, and a scheduled job can stop. Between a verification and the\n * promotion that records it, the pure rule would say this address is free.\n *\n * So the two paths that take a slug ask this before they take a lapsed one,\n * and it answers from the auth record — the only source of truth for whether\n * an address was ever confirmed.\n *\n * FAILS CLOSED, in every direction. A missing org, a missing owner, an auth\n * lookup that throws: all of them return `true`, meaning the reservation\n * stands and the claim is refused. Refusing to hand over an address costs the\n * claimant one attempt at a name; granting one wrongly costs its holder the\n * URL their customers use.\n */\nasync function lapsedReservationIsStillHeld(\n reservation: { orgId?: unknown } | undefined,\n): Promise<boolean> {\n const holderOrgId =\n typeof reservation?.orgId === 'string' ? reservation.orgId : null\n if (!holderOrgId) return true\n try {\n const holder = await firestore().collection('orgs').doc(holderOrgId).get()\n if (!holder.exists) {\n // The workspace is gone and only the reservation outlived it. Nothing\n // is being taken from anyone.\n return false\n }\n const ownerUid = holder.get('ownerUid')\n if (typeof ownerUid !== 'string' || !ownerUid) return true\n const found = await findUserByUidAcrossPools(ownerUid)\n if (!found) return true\n return found.record.emailVerified === true\n } catch (error) {\n console.error('[orgs] lapsed reservation check failed', error)\n return true\n }\n}\n\nexport interface CreateOrganizationOptions {\n name: string\n slug: string\n ownerUid: string\n ownerEmail?: string | null\n ownerDisplayName?: string | null\n /**\n * Skip the AGL-2265 free-workspace ceiling.\n *\n * For staff provisioning on a customer's behalf and for the migration and\n * backfill scripts — a ceiling that stops support from fixing a workspace\n * is a ceiling that produces the ticket it was meant to prevent. Never set\n * from a self-serve path; `/api/orgs/create` passes the staff claim and\n * nothing else.\n */\n bypassFreeWorkspaceCap?: boolean\n}\n\n/**\n * Creates an org in one transaction: slug reservation (uniqueness), org\n * doc, owner membership, and the owner's reverse-index entry. Throws\n * `OrgSlugTakenError` when the slug is reserved; slug validity is the\n * caller's job (API routes return 400 with policy copy).\n */\nexport async function createOrganization(\n options: CreateOrganizationOptions,\n): Promise<string> {\n const { name, slug, ownerUid, ownerEmail, ownerDisplayName } = options\n const db = firestore()\n const orgId = createResourceUid()\n // The free-workspace ceiling (AGL-2265). Read OUTSIDE the transaction —\n // it is a platform setting on a 15s cache, not a document this creation\n // races with, and putting it in the read set would make every workspace\n // creation on the platform contend on one document. `ready` rides along so\n // the verdict knows the difference between \"staff set no limit\" and \"we\n // could not read it\", and never treats the second as the first.\n const capConfig: FreeWorkspaceCapConfig | null = options.bypassFreeWorkspaceCap\n ? null\n : await readFreeWorkspaceCapConfig()\n await db.runTransaction(async (tx) => {\n const reservation = await tx.get(db.collection('orgSlugs').doc(slug))\n const held = reservation.exists\n ? (reservation.data() as {\n orgId?: unknown\n movedTo?: unknown\n reservedUntil?: unknown\n })\n : undefined\n // Tombstones (renamed-away slugs) are claimable by new orgs (AGL-585),\n // and so is a reservation left by an unverified signup made before\n // AGL-2590 that has since run out — but only once the auth record agrees\n // it was never confirmed.\n if (\n !isSlugReservationClaimable(held, null) ||\n (isSlugReservationLapsed(held) && (await lapsedReservationIsStillHeld(held)))\n ) {\n throw new OrgSlugTakenError(slug)\n }\n // Last read, first write: the ceiling counts inside this transaction, so\n // a retry recounts, and it writes the per-owner marker that makes two\n // concurrent creates by one account contend. Throws\n // `FreeWorkspaceCapError`, which the API routes turn into a 403 with the\n // numbers in it.\n if (capConfig) {\n await enforceFreeWorkspaceCapInTransaction({\n tx,\n firestore: db,\n uid: ownerUid,\n config: capConfig,\n })\n }\n // The plain grant, always (AGL-2590): no caller can reach this with an\n // unproven address any more. `orgSlugs` is world-readable — the console\n // resolves a workspace subdomain client-side from it — so the id is all\n // that goes in.\n tx.set(db.collection('orgSlugs').doc(slug), { orgId })\n /*\n * THE BILLING DOCUMENT EXISTS FROM BIRTH (AGL-1152).\n *\n * `readOrgBilling` reads `orgs/{id}/billing/stripe` and falls back to the\n * org doc when it is absent — and Firestore BILLS a read for a document\n * that does not exist. An org created without one therefore pays a\n * NOT_FOUND plus the fallback lookup on every read, forever, on the\n * tenant's hot path behind a deliberately short TTL.\n *\n * Measured before this: 14,498 NOT_FOUND reads/day on production, 15% of\n * all Firestore reads, from four orgs that had never had a document. The\n * `--seed-empty` pass in `backfill-org-billing.mjs` repaired those; this is\n * what stops the next org recreating the problem.\n *\n * EMPTY IS THE HONEST VALUE, not a placeholder: a new org has no Stripe\n * relationship, and `readOrgBilling`'s fallback returned `{}` for exactly\n * this case anyway. `writeOrgBilling` merge-sets, so the first real\n * subscription composes with this rather than racing it.\n */\n tx.set(\n db\n .collection('orgs')\n .doc(orgId)\n .collection(ORG_BILLING_SUBCOLLECTION)\n .doc(ORG_BILLING_DOC_ID),\n {},\n )\n tx.set(db.collection('orgs').doc(orgId), {\n name,\n /*\n * The searchable form of `name`, written beside it (AGL-2501).\n *\n * Firestore cannot search a string it has not been given in search\n * form: a prefix range needs the normalized key to ORDER by, and\n * `name` carries case and stray whitespace. Without this the staff\n * organization list can only filter the rows already on screen — ten\n * of them — which stops being a search the moment there are more\n * organizations than a page.\n *\n * Denormalized rather than computed at query time because there is no\n * query-time in Firestore. Every writer of `name` owes this field; the\n * rename in `/api/orgs/settings` is the other one.\n */\n nameLower: nameSearchKey(name),\n // Word-prefix tokens, so the staff search can answer \"contains a word\n // starting with X\" rather than only \"starts with X\" (AGL-2501).\n nameTokens: nameSearchTokens(name),\n // Reversed, so the list's \"ends with\" filter is a prefix range like\n // every other string operator Firestore can answer.\n nameReversed: nameSearchReversed(name),\n slug,\n ownerUid,\n // Stamped once and never mutated — `transferOrgOwnership` moves\n // `ownerUid` and deliberately leaves this alone (AGL-2265). It is what\n // stops \"hand the workspace to an alt account, create another, take it\n // back\" from being a way past the free-workspace ceiling.\n createdByUid: ownerUid,\n hosts: {},\n createdAt: FieldValue.serverTimestamp(),\n updatedAt: FieldValue.serverTimestamp(),\n })\n tx.set(\n db.collection('orgs').doc(orgId).collection('members').doc(ownerUid),\n {\n role: 'owner',\n allHosts: true,\n email: ownerEmail ?? null,\n displayName: ownerDisplayName ?? null,\n joinedAt: FieldValue.serverTimestamp(),\n /*\n * The rules projection, stamped AT CREATION (AGL-1038).\n *\n * Every other membership write reaches `syncOrgAuthProjections`,\n * which recomputes this for the whole roster. This one does not —\n * it is inside the creating transaction, and nothing runs after it\n * — so a brand-new org's owner had no `scopeTokens` at all and the\n * weekly scope-drift detector reported the org from the day it was\n * made until some later membership change happened to heal it.\n *\n * Computed rather than written as a literal, so it cannot disagree\n * with the projection every other path uses.\n */\n scopeTokens: projectMemberScopeTokens({ role: 'owner', allHosts: true }),\n /*\n * The permission projection, stamped here for the same reason and\n * with the same consequence if it is missed.\n *\n * No custom role can exist in an org being created, so the resolver\n * is handed an explicit null and returns the owner's role defaults —\n * which is also what the rules fall back to for a member carrying no\n * map, so a failure to stamp this is invisible rather than a lockout.\n * It is written anyway: an unstamped owner is a row the drift check\n * has to keep explaining.\n */\n resolvedPermissions: projectMemberResolvedPermissions(\n { role: 'owner', allHosts: true },\n null,\n ),\n },\n )\n tx.set(\n db.collection('users').doc(ownerUid).collection('orgs').doc(orgId),\n // The owner reaches every site by definition (AGL-1032).\n { role: 'owner', orgName: name, slug, orgWide: true },\n )\n })\n // Make `{slug}.aglyn.com` resolve (AGL-1136). AGL-1135 removed the\n // `*.aglyn.com` wildcard — it served a real sign-in page on every hostname\n // under the domain — so a workspace subdomain now only works if the domain\n // is attached to the project.\n //\n // AFTER the transaction, and AWAITED. It was `void`, on the reasoning that\n // no workspace should fail to be created because a DNS API was slow — right\n // requirement, wrong mechanism (AGL-1136). On a serverless runtime `void`\n // does not mean \"in the background\", it means \"may never run\": the instance\n // can be frozen the moment the response is flushed. Confirmed twice on this\n // codebase already, on the Stripe org sync and the profile seed.\n //\n // Awaiting cannot fail org creation, and that property comes from the\n // helper, not from the `void` — `attachWorkspaceDomain` swallows every\n // error and returns an outcome rather than throwing. The cost is one HTTP\n // round trip on an operation that already runs a Firestore transaction; the\n // alternative was advertising a workspace URL that 404s.\n //\n // `erase.ts` already awaits the matching detach, which is what made the\n // asymmetry worth looking at.\n await attachWorkspaceDomain(slug)\n // The first entry in the workspace's log (AGL-118). Creation is the one\n // category the activity log never covered — it was assembled by adding\n // calls at mutation points in the console UI, and the acts that bring a\n // top-level object into existence happen out here, in provisioning code no\n // UI mutation point ever reaches. The visible symptom was a customer whose\n // page read as though they had never used the product, because their whole\n // session had been creation.\n await logOrgActivity(\n orgId,\n { uid: ownerUid, email: ownerEmail ?? null },\n 'Created the workspace',\n { type: 'org', id: orgId, name },\n )\n return orgId\n}\n\nexport interface OrgMembershipResolution {\n orgId: string\n member: AglynOrgMember\n /**\n * True only when THIS call provisioned the org, so a caller can report the\n * activation (AGL-2587). `ensureOrgForUser` is the third org-creation door\n * and the only server-side one, and it looked identical from outside to a\n * resolution of an org that already existed — which is why `org_created`\n * counted none of the workspaces it makes. Absent on `resolveOrgMembership`,\n * which never creates anything.\n */\n created?: boolean\n}\n\n/**\n * The signed-in user's membership in one org, or null. When `orgId` is\n * omitted, resolves the user's first org from the reverse index (the\n * single-org case every pre-org account lands in after backfill).\n */\nexport async function resolveOrgMembership(\n uid: string,\n orgId?: string | null,\n): Promise<OrgMembershipResolution | null> {\n const db = firestore()\n let resolved = orgId ?? null\n if (!resolved) {\n const mine = await db\n .collection('users')\n .doc(uid)\n .collection('orgs')\n .limit(1)\n .get()\n resolved = mine.empty ? null : mine.docs[0].id\n }\n if (!resolved) return null\n const memberSnapshot = await db\n .collection('orgs')\n .doc(resolved)\n .collection('members')\n .doc(uid)\n .get()\n if (!memberSnapshot.exists) return null\n return {\n orgId: resolved,\n member: { $id: uid, ...memberSnapshot.data() } as AglynOrgMember,\n }\n}\n\n/**\n * The user's org, creating a personal one on first need (signup flows and\n * pre-backfill accounts): name from the display name or email local part,\n * slug generated with numeric-suffix retries on collision.\n */\nexport async function ensureOrgForUser(\n uid: string,\n profile: { email?: string | null; displayName?: string | null } = {},\n): Promise<OrgMembershipResolution> {\n const existing = await resolveOrgMembership(uid)\n if (existing) return existing\n\n const base =\n profile.displayName?.trim() ||\n profile.email?.split('@')[0]?.trim() ||\n 'workspace'\n const name = base.slice(0, 80)\n let slug = generateOrgSlug(name) || `org-${createResourceUid().slice(0, 8)}`\n for (let attempt = 0; ; attempt += 1) {\n try {\n const orgId = await createOrganization({\n name,\n slug,\n ownerUid: uid,\n ownerEmail: profile.email ?? null,\n ownerDisplayName: profile.displayName ?? null,\n })\n const created = await resolveOrgMembership(uid, orgId)\n if (!created) throw new Error('Org membership missing after create')\n // Marked so the caller can count the activation (AGL-2587).\n return { ...created, created: true }\n } catch (error) {\n if (!(error instanceof OrgSlugTakenError) || attempt >= 4) throw error\n slug = `${slug.slice(0, 26)}-${attempt + 2}`\n if (!isValidOrgSlug(slug)) {\n slug = `org-${createResourceUid().slice(0, 8)}`\n }\n }\n }\n}\n\n/**\n * Changes an org's workspace slug (AGL-236): reserves the new slug and\n * updates the org doc in one transaction, leaving the old reservation as\n * a tombstone (`movedTo`) so existing workspace URLs keep resolving —\n * the middleware redirects them. Reverse-index slugs fan out after.\n * Throws `OrgSlugTakenError` only when another org ACTIVELY holds the new\n * slug — tombstones are claimable (AGL-585). Slug validity/authorization\n * are the API route's job.\n */\nexport async function changeOrgSlug(\n orgId: string,\n newSlug: string,\n): Promise<{ previousSlug: string | null }> {\n const db = firestore()\n let previousSlug: string | null = null\n await db.runTransaction(async (tx) => {\n const orgRef = db.collection('orgs').doc(orgId)\n const orgSnapshot = await tx.get(orgRef)\n if (!orgSnapshot.exists) throw new Error(`Unknown org: ${orgId}`)\n previousSlug = (orgSnapshot.get('slug') as string | undefined) ?? null\n if (previousSlug === newSlug) return\n const reservation = await tx.get(db.collection('orgSlugs').doc(newSlug))\n const held = reservation.exists\n ? (reservation.data() as {\n orgId?: unknown\n movedTo?: unknown\n reservedUntil?: unknown\n })\n : undefined\n // Claimable when free, own (moving back), a tombstone another org renamed\n // away from (AGL-585), or an unverified signup's reservation that ran out\n // (AGL-2585) — abandoned slugs are never reserved forever. Only another\n // org's ACTIVE slug blocks the change, and a lapsed reservation whose\n // holder has since verified is still active, which the auth record decides.\n if (\n !isSlugReservationClaimable(held, orgId) ||\n (held?.orgId !== orgId &&\n isSlugReservationLapsed(held) &&\n (await lapsedReservationIsStillHeld(held)))\n ) {\n throw new OrgSlugTakenError(newSlug)\n }\n tx.set(db.collection('orgSlugs').doc(newSlug), { orgId })\n tx.set(\n orgRef,\n { slug: newSlug, updatedAt: FieldValue.serverTimestamp() },\n { merge: true },\n )\n if (previousSlug) {\n tx.set(db.collection('orgSlugs').doc(previousSlug), {\n orgId,\n movedTo: newSlug,\n renamedAt: FieldValue.serverTimestamp(),\n })\n }\n })\n // Attach the new subdomain, and deliberately KEEP the old one (AGL-1136).\n // The previous slug's tombstone 308s to the new one, and a redirect can\n // only run on a hostname that still resolves — detaching it here would\n // break the very redirect the tombstone exists to serve.\n // Awaited for the same reason as the create path above (AGL-1136): a\n // `void` here is not a background task, it is a coin flip.\n await attachWorkspaceDomain(newSlug)\n // Reverse index carries the slug for the switcher display.\n const members = await listOrgMembers(orgId)\n const batch = db.batch()\n for (const member of members) {\n batch.set(\n db.collection('users').doc(member.$id).collection('orgs').doc(orgId),\n { slug: newSlug },\n { merge: true },\n )\n }\n await batch.commit()\n return { previousSlug }\n}\n\n/**\n * Host → org resolution via the server-written `hostIndex` mirror.\n *\n * `React.cache`-deduped PER REQUEST (AGL-1302): one tenant render resolved\n * this hop up to five times — org billing, datasets, plugin installs, realm\n * installs and the publish-schedule executor each re-read the same\n * `hostIndex/{hostId}` doc. Per-request memoization is zero-staleness by\n * construction; outside a React render (route handlers, jest) `cache` is a\n * pass-through, so nothing changes for the console's authz paths.\n */\nexport const resolveOrgIdForHost = cache(\n async (hostId: string): Promise<string | null> => {\n const snapshot = await firestore().collection('hostIndex').doc(hostId).get()\n const orgId = snapshot.data()?.['orgId']\n return typeof orgId === 'string' ? orgId : null\n },\n)\n\n/**\n * The org doc itself — billing, plan, entitlements and suspension (the\n * shape the legacy tenants/{uid} doc carried; orgs are the only billing\n * source since AGL-238). Null when the doc is missing.\n */\n/**\n * `React.cache`-deduped per request like {@link resolveOrgIdForHost}\n * (AGL-1302). NOTE: within one render every caller receives the SAME object\n * — treat it as read-only, as every current caller already does.\n */\nexport const getOrgDoc = cache(\n async (orgId: string): Promise<Partial<AglynOrganization> | null> => {\n const snapshot = await firestore().collection('orgs').doc(orgId).get()\n return snapshot.exists\n ? ({ $id: snapshot.id, ...snapshot.data() } as Partial<AglynOrganization>)\n : null\n },\n)\n\n/**\n * Billing/entitlement source for a host (AGL-238): the owning org's doc\n * via the hostIndex mirror. Null for unindexed hosts — callers treat that\n * as the pre-billing fail-open (every feature on), the same contract the\n * legacy tenants/{uid} read had.\n */\nexport async function getOrgForHost(hostId: string): Promise<{\n orgId: string\n org: Partial<AglynOrganization>\n} | null> {\n const orgId = await resolveOrgIdForHost(hostId)\n if (!orgId) return null\n const org = await getOrgDoc(orgId)\n return org ? { orgId, org } : null\n}\n\n/**\n * The raw host doc, `React.cache`-deduped per request like\n * {@link resolveOrgIdForHost}. Null when missing. Added for AGL-1506 so a\n * dispatcher that already pays this read for the plugin deny-list can also\n * feed the host's `suspendedAt` family to the lockdown verdict without a\n * second get. Same read-only contract as {@link getOrgDoc}.\n */\nexport const getHostDocAdmin = cache(\n async (hostId: string): Promise<Record<string, unknown> | null> => {\n const snapshot = await firestore().collection('hosts').doc(hostId).get()\n return snapshot.exists ? (snapshot.data() as Record<string, unknown>) : null\n },\n)\n\n/**\n * The host's per-site plugin deny-list (AGL-1014), for API dispatch and any\n * other server consumer of `resolveHostEnabledPlugins`. Rides\n * {@link getHostDocAdmin}'s request-cached read. Fail-open to [] — an absent\n * host doc or field means \"nothing disabled here\", never a lockout.\n */\nexport const getHostDisabledPlugins = cache(\n async (hostId: string): Promise<string[]> => {\n const disabled = (await getHostDocAdmin(hostId))?.['disabledPlugins']\n return Array.isArray(disabled) ? disabled.map(String) : []\n },\n)\n\n/**\n * Billing/entitlement source for a user without host context (account-\n * level APIs): the explicit workspace org when given, else the first org\n * from the reverse index. Null for accounts with no org yet.\n */\nexport async function getOrgForUser(\n uid: string,\n orgId?: string | null,\n): Promise<{\n orgId: string\n org: Partial<AglynOrganization>\n member: AglynOrgMember\n} | null> {\n const membership = await resolveOrgMembership(uid, orgId)\n if (!membership) return null\n const org = await getOrgDoc(membership.orgId)\n return org\n ? { orgId: membership.orgId, org, member: membership.member }\n : null\n}\n\n/**\n * Org-scoped data collection for a host (AGL-237): datasets, contacts and\n * contactSegments live on the org so every host shares them.\n *\n * The pre-migration fallback to `hosts/{hostId}/{name}` is GONE (AGL-1050).\n * The AGL-1040 backfill counted the docs still on it in production and\n * found zero, so it was dead code rather than a migration — and a second\n * storage path that can still be WRITTEN is a second boundary to enforce\n * forever, which undoes the premise of scoped sharing: one home per\n * resource plus an explicit scope.\n *\n * A host with no org is now an error rather than a silent write into a\n * collection nothing reads. Every host has an org; `hostIndex` is written\n * by `registerOrgHost` at creation.\n */\n/**\n * The org-owned collections a host reads in its own context. Every one of\n * these carries `visibleTo` (AGL-1037) and so must go through\n * `scopedToHost` on any Admin-SDK path — `media` and `mediaFolders` were\n * added for the export route (AGL-1046), which had been reading the\n * legacy host path and exporting nothing at all.\n */\nexport type OrgDataCollection =\n | 'datasets'\n | 'contacts'\n | 'contactSegments'\n // A saved Contacts view, resolved by the dynamic-list sweep the way a\n // segment is (AGL-2617).\n | 'crmViews'\n | 'lists'\n | 'media'\n | 'mediaFolders'\n\nexport async function orgDataCollectionForHost(\n hostId: string,\n name: OrgDataCollection,\n): Promise<FirebaseFirestore.CollectionReference> {\n const orgId = await resolveOrgIdForHost(hostId)\n if (!orgId) {\n throw new Error(`Host ${hostId} has no org — cannot resolve ${name}`)\n }\n return firestore().collection('orgs').doc(orgId).collection(name)\n}\n\n/**\n * Narrows an org-scoped collection to what ONE host may see (AGL-1039).\n *\n * The Admin SDK does not evaluate Firestore rules, so AGL-1041's\n * `visibleTo.hasAny(...)` protects the console and nothing else — every\n * server read has to filter for itself or a client site can render another\n * client's data. Use this instead of the bare collection ref anywhere a\n * request is being served in the context of a single host.\n *\n * Only the ORG path is filtered — but no longer because of the legacy\n * `hosts/{hostId}/…` fallback, which AGL-1050 removed on both the server\n * (above) and the client. What survives it is the reason stated at the\n * check itself: callers may hand this helper a ref they built themselves,\n * and a host-library ref must never be filtered, since its docs carry no\n * `visibleTo` and the filter would match nothing and blank the site.\n */\nexport function scopedToHost(\n ref: FirebaseFirestore.CollectionReference,\n hostId: string,\n): FirebaseFirestore.Query {\n // The org-path check is retained even though AGL-1050 removed the host\n // fallback: this helper is also handed refs by callers that build their\n // own paths, and a host-library ref must never be filtered — its docs\n // carry no `visibleTo`, so the filter would match nothing.\n const orgScoped = ref.parent?.parent?.id === 'orgs'\n if (!orgScoped) return ref\n return ref.where(\n 'visibleTo',\n 'array-contains-any',\n scopeTokensForHost(hostId),\n )\n}\n\n/**\n * `orgDataCollectionForHost` + `scopedToHost` in one call — the form every\n * host-context read should use. Returns the collection ref too, for the\n * writes and `doc()` lookups a Query cannot express.\n */\nexport async function orgDataQueryForHost(\n hostId: string,\n name: OrgDataCollection,\n): Promise<{\n ref: FirebaseFirestore.CollectionReference\n query: FirebaseFirestore.Query\n}> {\n const ref = await orgDataCollectionForHost(hostId, name)\n return { ref, query: scopedToHost(ref, hostId) }\n}\n\n/**\n * Server-side permission check (AGL-243): the member's org-role defaults\n * refined by their custom role doc (one read, only when assigned). API\n * routes call this before privileged mutations.\n */\n/**\n * The member's FULL granular permission set, custom role and per-member\n * overrides applied (AGL-2350).\n *\n * `memberHasOrgPermission` below is the single-permission form and now\n * delegates here, so the two cannot answer differently. Split out because\n * `resolveOrgPermissions` in `libs/tenant/runtime` needs the whole set to\n * project onto the legacy flag map that the marketplace install and publish\n * gates read — it previously derived those flags from the built-in role tier\n * alone, which silently ignored both refinements.\n *\n * One conditional read, only when a custom role is actually assigned. A\n * dangling `roleId` resolves to `null` and falls back to the role defaults\n * rather than denying, matching what the console hook does with the same\n * dangling id — a deleted role must not lock a member out of surfaces their\n * base role allows.\n */\nexport async function resolveMemberOrgPermissions(\n orgId: string,\n member: Partial<AglynOrgMember> | null | undefined,\n): Promise<Record<OrgPermission, boolean>> {\n let customRole: AglynOrgCustomRole | null = null\n if (member?.roleId) {\n const snapshot = await firestore()\n .collection('orgs')\n .doc(orgId)\n .collection('roles')\n .doc(member.roleId)\n .get()\n customRole = snapshot.exists\n ? (snapshot.data() as AglynOrgCustomRole)\n : null\n }\n return resolveOrgPermissions(member, customRole)\n}\n\nexport async function memberHasOrgPermission(\n orgId: string,\n member: Partial<AglynOrgMember> | null | undefined,\n permission: OrgPermission,\n): Promise<boolean> {\n if (!member) return false\n return (await resolveMemberOrgPermissions(orgId, member))[permission]\n}\n\n/**\n * May this member hold a catalog permission on this site (AGL-2927,\n * AGL-2984)?\n *\n * The one resolver a door calls for a key a plugin declared with host-role\n * defaults, so the two membership axes cannot be answered differently by two\n * routes. An org-wide member is decided by the org catalog through\n * `resolveMemberOrgPermissions` — custom role and overrides applied, one\n * conditional read. A site collaborator is decided by the host role they hold\n * on the site the request NAMES, refined by the per-site toggle on their\n * member document; a collaborator whose request names no site is refused,\n * because there is no host role to read a default from and omitting the site\n * must not be a way around the toggle.\n *\n * `hostId` is whatever the body carried, trimmed by the caller or not — an\n * empty string and `undefined` both mean \"no site named\".\n */\nexport async function memberHasPermissionOnHost(\n orgId: string,\n hostId: string | null | undefined,\n member: Partial<AglynOrgMember> | null | undefined,\n permission: OrgPermission,\n): Promise<boolean> {\n if (!member) return false\n if (isOrgWideMember(member)) {\n return (await resolveMemberOrgPermissions(orgId, member))[permission] === true\n }\n const site = typeof hostId === 'string' ? hostId.trim() : ''\n return resolveCollaboratorHostPermissions(member, site)?.[permission] ?? false\n}\n\n/**\n * The 403 a door sends when `memberHasPermissionOnHost` says no: the same\n * customer-safe shape the doors' other refusals use — one sentence, a\n * `reason` a client can branch on — naming the permission by its catalog\n * label so the reader can find it on the Team page, and who to ask.\n */\nexport function permissionRefusal(permission: OrgPermission): Response {\n return Response.json(\n {\n error: `Your role does not include \"${orgPermissionLabel(permission)}\" — ask an organization admin`,\n reason: 'permission',\n permission,\n },\n { status: 403 },\n )\n}\n\n/**\n * An org-wide member's verdict for every key PLUGINS declared into the\n * catalog, read fresh (AGL-2929, AGL-2984): what `memberHasPermissionOnHost`\n * answers for them with no site named, as a whole map. `null` for a uid with\n * no member document and for a site collaborator, whose per-site keys are\n * decided per site by `setHostPermissions` and have no org-level verdict to\n * compare. Read on either side of a membership write, it is how the members\n * route tells which declared keys the write moved.\n */\nexport async function resolveMemberPluginPermissionsOnOrg(\n orgId: string,\n uid: string,\n): Promise<Record<string, boolean> | null> {\n const snapshot = await firestore()\n .collection('orgs')\n .doc(orgId)\n .collection('members')\n .doc(uid)\n .get()\n if (!snapshot.exists) return null\n const member = { $id: uid, ...snapshot.data() } as AglynOrgMember\n if (!isOrgWideMember(member)) return null\n const granted = await resolveMemberOrgPermissions(orgId, member)\n return Object.fromEntries(\n pluginOrgPermissionKeys().map((key) => [key, granted[key] === true]),\n )\n}\n\n/** A collaborator's per-site verdict on either side of a toggle write. */\nexport interface HostPermissionsWrite {\n /** The verdict before the write; `null` when the member had no access to the site. */\n before: Record<string, boolean> | null\n after: Record<string, boolean>\n}\n\n/**\n * Set a collaborator's per-site toggles (AGL-2927, AGL-2984) and re-project.\n *\n * A merge on the nested map, so the member's other sites and every other\n * field stay untouched; keys no plugin declared as per-site are dropped\n * rather than stored, and a non-boolean is ignored rather than coerced.\n * Re-projection is scoped to the one host whose `memberPermissions` changed.\n *\n * The verdict before the write comes back beside the one after it, because\n * the caller records one activity row per key that moved (AGL-2929) and a\n * toggle set to the value it already had is not a change.\n */\nexport async function setHostPermissions(options: {\n orgId: string\n uid: string\n hostId: string\n permissions: Partial<Record<string, unknown>>\n}): Promise<HostPermissionsWrite> {\n const { orgId, uid, hostId } = options\n const keys = hostPermissionKeys()\n const accepted: Record<string, boolean> = {}\n for (const key of keys) {\n const value = options.permissions[key]\n if (typeof value === 'boolean') accepted[key] = value\n }\n const ref = firestore().collection('orgs').doc(orgId).collection('members').doc(uid)\n const stored = { $id: uid, ...(await ref.get()).data() } as AglynOrgMember\n const before = resolveCollaboratorHostPermissions(stored, hostId)\n await ref.set({ hostPermissions: { [hostId]: accepted } }, { merge: true })\n await syncOrgAuthProjections(orgId, hostId)\n const member = { $id: uid, ...(await ref.get()).data() } as AglynOrgMember\n return {\n before,\n after:\n resolveCollaboratorHostPermissions(member, hostId) ??\n Object.fromEntries(keys.map((key) => [key, false])),\n }\n}\n\nexport async function listOrgMembers(\n orgId: string,\n): Promise<AglynOrgMember[]> {\n const snapshot = await firestore()\n .collection('orgs')\n .doc(orgId)\n .collection('members')\n .get()\n return snapshot.docs.map(\n (doc) => ({ $id: doc.id, ...doc.data() }) as AglynOrgMember,\n )\n}\n\n/**\n * The custom role documents this roster actually references, read once each.\n *\n * A roster of hundreds shares a handful of roles, so this is bounded by the\n * number of DISTINCT `roleId`s and not by the member count.\n *\n * A role id that resolves to nothing is left ABSENT rather than recorded as\n * an empty role. The two happen to reach the same verdict today —\n * `resolveOrgPermissions` skips a key whose value is not a boolean, so an\n * empty map changes nothing — but they are different claims, and only one of\n * them is true: a dangling id means the lookup MISSED, not that a role\n * granting nothing was found. Recording the miss honestly is what keeps the\n * fallback correct if that resolver ever treats an empty map as a revocation,\n * which is what its own type comment already says it does.\n */\nasync function loadOrgCustomRoles(\n orgId: string,\n members: readonly AglynOrgMember[],\n): Promise<Map<string, AglynOrgCustomRole>> {\n const roleIds = [\n ...new Set(\n members\n .map((member) => member.roleId)\n .filter((roleId): roleId is string => typeof roleId === 'string' && !!roleId),\n ),\n ]\n const rolesRef = firestore()\n .collection('orgs')\n .doc(orgId)\n .collection('roles')\n const found = new Map<string, AglynOrgCustomRole>()\n await Promise.all(\n roleIds.map(async (roleId) => {\n const snapshot = await rolesRef.doc(roleId).get()\n if (snapshot.exists) {\n found.set(roleId, snapshot.data() as AglynOrgCustomRole)\n }\n }),\n )\n return found\n}\n\n/**\n * How the projections are written: REPLACED, not merged (AGL-2985).\n *\n * `{ merge: true }` merges a map key by key and keeps every key the new map\n * does not name, so a projection written that way could add a member and\n * never take one away. A collaborator whose site access was revoked, or a\n * member removed from the organization, drops out of the recomputed\n * `memberRoles` — and kept their old key on the host document, which is the\n * one thing the Firestore rules read to let a person edit and publish a\n * site. `memberPermissions` kept their AI verdict the same way, and the\n * plugin half of a member's `resolvedPermissions` would keep a withdrawn\n * grant `true` (AGL-2974).\n *\n * `mergeFields` overwrites exactly the listed fields whole and leaves the\n * rest of the document untouched, which is all the merge was for. Every\n * listed map is recomputed from the complete roster on every write, so\n * replacing it loses nothing a merge would have kept correctly.\n */\nconst HOST_PROJECTION_WRITE: FirebaseFirestore.SetOptions = {\n mergeFields: ['orgId', 'memberRoles', 'memberPermissions', 'updatedAt'],\n}\nconst MEMBER_PROJECTION_WRITE: FirebaseFirestore.SetOptions = {\n mergeFields: ['scopeTokens', 'resolvedPermissions'],\n}\n\n/**\n * Recomputes the denormalized authorization projections after a membership\n * change: `memberRoles` and `memberPermissions` on every host the org owns\n * (or one host when given), and `scopeTokens` + `resolvedPermissions` on\n * every member doc.\n *\n * The rules resolve a request from these reads — the host doc for host\n * content (docs/MULTI_TENANT_FIRESTORE.md §5), the member doc for scoped\n * org resources (AGL-1038) — so this is what makes a membership effective.\n * They live here, in one writer called by every mutation below, because a\n * grant path that updates one projection and forgets another silently over-\n * or under-grants.\n *\n * Everything is recomputed for the whole roster rather than the changed\n * member: the roster is already loaded for `memberRoles`, and a full pass\n * self-heals rows that an earlier partial failure left stale.\n *\n * ## `resolvedPermissions`, and why the rules need it denormalized\n *\n * Security rules cannot resolve a custom role. `member.roleId` points at\n * `orgs/{orgId}/roles/{roleId}`, and reproducing the three-layer precedence\n * (per-member beats custom role beats role default) in CEL takes a second\n * cross-document get() plus a correct handling of a dangling id — where a\n * naive version over-denies and locks out paying customers. So the rules read\n * the ANSWER instead of the inputs, which is the same trade `scopeTokens`\n * already makes for a reason the rules language shares: it has no `.map()`\n * either.\n *\n * The map is `projectMemberResolvedPermissions`: `resolveOrgPermissions`'\n * own catalog verdict, plus the plugin-declared keys a custom role or an\n * override set explicitly (AGL-2974), so the rules and every\n * server route are reading one resolver's verdict rather than two\n * implementations of it.\n *\n * ONE READ PER DISTINCT ROLE, not per member: an org assigns a handful of\n * custom roles across a roster that can run to hundreds, and resolving each\n * member independently would re-read the same few documents once each.\n */\nexport async function syncOrgAuthProjections(\n orgId: string,\n hostId?: string,\n): Promise<void> {\n const db = firestore()\n const orgRef = db.collection('orgs').doc(orgId)\n const members = await listOrgMembers(orgId)\n const customRoles = await loadOrgCustomRoles(orgId, members)\n const hostIds = hostId\n ? [hostId]\n : Object.keys(\n ((await orgRef.get()).data() as AglynOrganization | undefined)\n ?.hosts ?? {},\n )\n const writes: Array<\n [FirebaseFirestore.DocumentReference, object, FirebaseFirestore.SetOptions]\n > = [\n ...hostIds.map(\n (id) =>\n [\n db.collection('hosts').doc(id),\n {\n orgId,\n memberRoles: projectHostMemberRoles(members, id),\n // Each member's per-site permission verdicts ON this site\n // (AGL-2927), beside the role they derive from, so a reader of\n // the host document has both answers from the one get it\n // already does.\n memberPermissions: projectHostMemberPermissions(\n members,\n id,\n customRoles,\n ),\n updatedAt: FieldValue.serverTimestamp(),\n },\n HOST_PROJECTION_WRITE,\n ] as [\n FirebaseFirestore.DocumentReference,\n object,\n FirebaseFirestore.SetOptions,\n ],\n ),\n ...members.map(\n (member) =>\n [\n orgRef.collection('members').doc(member.$id),\n {\n scopeTokens: projectMemberScopeTokens(member),\n resolvedPermissions: projectMemberResolvedPermissions(\n member,\n // `?? null`, never `?? undefined`: a member whose `roleId`\n // points at a DELETED role must resolve to their role\n // defaults, which is what the resolver does with an explicit\n // null and what every server route already does with the same\n // dangling id. Leaving it undefined would be the same value,\n // but the null says the lookup happened and missed.\n member.roleId ? (customRoles.get(member.roleId) ?? null) : null,\n ),\n },\n MEMBER_PROJECTION_WRITE,\n ] as [\n FirebaseFirestore.DocumentReference,\n object,\n FirebaseFirestore.SetOptions,\n ],\n ),\n ]\n // Hosts alone rarely approached the 500-write batch cap; hosts plus the\n // whole roster can, so commit in chunks rather than throwing on big orgs.\n for (let i = 0; i < writes.length; i += FIRESTORE_BATCH_LIMIT) {\n const batch = db.batch()\n for (const [ref, data, options] of writes.slice(\n i,\n i + FIRESTORE_BATCH_LIMIT,\n )) {\n batch.set(ref, data, options)\n }\n await batch.commit()\n }\n}\n\n/**\n * @deprecated Renamed to `syncOrgAuthProjections` (AGL-1038) now that it\n * also writes member `scopeTokens`. Kept as an alias for out-of-tree\n * callers; delete once none remain.\n */\nexport const syncHostMemberRoles = syncOrgAuthProjections\n\n/** What an org activity entry points at; `id` lets detail views filter. */\nexport interface OrgActivityTarget {\n /**\n * `host` and `subscription` are the two facts about a workspace that no\n * host feed can hold (AGL-118).\n *\n * A site's own log lives at `hosts/{hostId}/activity` and is destroyed with\n * the site — `eraseHost` recursive-deletes the whole tree — so \"this site\n * was deleted\" written there is an entry with no reader by construction.\n * A subscription belongs to no single site at all. Both are org-level\n * events, and this is the only feed that outlives them.\n */\n type:\n | 'org' | 'member' | 'invite' | 'host' | 'subscription'\n // The CRM's records (AGL-2634), written by the plugin's server routes\n // for an act performed at the ORGANIZATION level — a deal moved from the\n // org board, two contacts merged over every site, a bulk bar's action —\n // where there is no one site's feed to hold it. The org feed's presenter\n // links them into the org-level hub.\n | 'contact' | 'company' | 'deal' | 'lead' | 'task'\n // AI rows (AGL-2929): a generation job, the resources it produced, and\n // a custom role whose AI permission moved. The job's outputs are org\n // events with a resource target because the job is org-scoped and the\n // org feed is the one place every output of one job is listed together;\n // the host feed gets its own copy of the host-scoped ones.\n | 'aiJob' | 'role'\n | 'screen' | 'layout' | 'component' | 'template' | 'workflow' | 'content'\n // A theme proposal a job produced (AGL-2938), filed under the site's\n // theme the way the host feed files a saved theme.\n | 'theme'\n // A plugin's own resource, `pluginId:noun` (AGL-2978). Plugins file\n // their rows under their own namespace, so this list names none of them.\n | PluginActivityTargetType\n id?: string\n name?: string\n /** Present on a generated screen output so the deep link can hit the exact version. */\n versionId?: string\n}\n\n/**\n * Org-level counterpart to the host activity log (AGL-118): fire-and-\n * forget append to `orgs/{orgId}/activity` from the org API routes. Never\n * throws — an audit miss must not break the mutation that triggered it.\n * Admin-SDK-only, like the rest of this file; the rules deny client writes.\n */\nexport async function logOrgActivity(\n orgId: string,\n /**\n * `uid` is nullable because some org events HAVE no actor (AGL-118). Stripe\n * cancels a subscription after a month of failed retries with nobody\n * present, and the honest record of that says so. Naming the last person\n * who touched billing instead would put a real name on an act nobody\n * performed — and `actorId` is a filterable field, so the invented\n * attribution would then show up under that person when somebody asks what\n * they have done.\n */\n actor: { uid: string | null; email?: string | null },\n action: string,\n target: OrgActivityTarget,\n): Promise<void> {\n await firestore()\n .collection('orgs')\n .doc(orgId)\n .collection('activity')\n .add({\n actorId: actor.uid ?? null,\n actorEmail: actor.email ?? null,\n action,\n target: {\n type: target.type,\n ...(target.id ? { id: target.id } : {}),\n ...(target.name ? { name: target.name } : {}),\n ...(target.versionId ? { versionId: target.versionId } : {}),\n },\n createdAt: FieldValue.serverTimestamp(),\n })\n .catch(() => undefined)\n}\n\n/** What a host activity entry points at. Mirrors `HostActivityTarget`. */\nexport interface HostActivityTarget {\n type:\n | 'host' | 'screen' | 'layout' | 'theme' | 'media' | 'content' | 'variable'\n | 'function' | 'workflow' | 'member' | 'component' | 'template'\n // The CRM's records (AGL-2622), written by the plugin's server routes —\n // a contact added by hand, a lead converted — and read back by the\n // feed's presenter as links into the hub.\n | 'contact' | 'company' | 'deal' | 'lead'\n id?: string\n name?: string\n versionId?: string\n}\n\n/**\n * Append to `hosts/{hostId}/activity` with the ADMIN SDK (AGL-118).\n *\n * The host log's twin of {@link logOrgActivity}, and the beginning of the\n * migration off the browser. Every entry in this collection has been written\n * by the client since the log existed, which makes it an audit trail its\n * subject can decline to write: three template surfaces created screens,\n * layouts and components while calling no logger at all, and nothing noticed\n * for months because a log that is missing an entry looks exactly like a\n * person who did nothing.\n *\n * A route that already authenticated the caller has the two things the client\n * cannot be trusted for — a VERIFIED uid, and the certainty that the write it\n * is recording actually happened, because it performed it. So an entry from\n * here is worth more than the one it replaces, not merely more reliable.\n *\n * Never throws, for the reason the client logger never throws: an audit miss\n * must not turn a successful create into a failed request. It is `await`ed\n * rather than floated because a serverless response ending cancels in-flight\n * work, which would make the drop the common case rather than the rare one.\n */\nexport async function logHostActivity(\n hostId: string,\n actor: HostActivityActor,\n action: string,\n target: HostActivityTarget,\n): Promise<void> {\n await firestore()\n .collection('hosts')\n .doc(hostId)\n .collection('activity')\n .add({\n actorId: actor.uid,\n actorEmail: actor.email ?? null,\n // A key's entry names the key (AGL-2632); a person's carries no such\n // field, so the two are told apart by its presence.\n ...(actor.apiKeyName ? { apiKeyName: actor.apiKeyName } : {}),\n action,\n target: {\n type: target.type,\n ...(target.id ? { id: target.id } : {}),\n ...(target.name ? { name: target.name } : {}),\n ...(target.versionId ? { versionId: target.versionId } : {}),\n },\n createdAt: FieldValue.serverTimestamp(),\n })\n .catch(() => undefined)\n}\n\n/**\n * A collaborator seat refusal, raised from INSIDE the grant transaction\n * (AGL-2068).\n *\n * An exception rather than a return value because it has to travel out of\n * `upsertOrgMember` / `grantHostAccess`, whose contract is \"make it so\" and\n * which four routes already call as a bare `await`. Returning a verdict would\n * have let every existing call site ignore it silently, which is the shape of\n * the bug being fixed.\n */\nexport class CollaboratorSeatLimitError extends Error {\n readonly hostId: string\n readonly limit: number\n readonly upgradeRequired: boolean\n readonly addonPriceUsd: number | null\n /**\n * Seats this site holds ABOVE `limit` (AGL-2439). Non-zero means the site\n * is GRANDFATHERED: those collaborators keep their access, and the refusal\n * is only of the NEXT one. Carried on the error so the refusal copy can say\n * that rather than letting the admin read a 403 as \"somebody was removed\".\n */\n readonly retainedOverCap: number\n constructor(\n hostId: string,\n quota: {\n limit: number\n upgradeRequired: boolean\n addonPriceUsd: number | null\n retainedOverCap?: number\n },\n ) {\n super(collaboratorSeatMessage(quota))\n this.name = 'CollaboratorSeatLimitError'\n this.hostId = hostId\n this.limit = quota.limit\n this.upgradeRequired = quota.upgradeRequired\n this.addonPriceUsd = quota.addonPriceUsd\n this.retainedOverCap = Math.max(0, quota.retainedOverCap ?? 0)\n }\n}\n\n/**\n * The two refusal strings, verbatim from `/api/hosts/members` where they have\n * always lived. Kept byte-identical on purpose: this is now the ONE place\n * they are produced, and any client or spec matching \"Collaborator limit\n * reached\" must keep matching.\n */\nfunction collaboratorSeatMessage(quota: {\n limit: number\n upgradeRequired: boolean\n addonPriceUsd: number | null\n}): string {\n return quota.upgradeRequired\n ? `Collaborator limit reached (${quota.limit}) — upgrade ` +\n 'your plan to add more collaborators'\n : `Collaborator seats full (${quota.limit}) — add seats for ` +\n `$${quota.addonPriceUsd}/mo each from Billing`\n}\n\n/**\n * Everyone who could be holding a collaborator seat in this org: the whole\n * roster plus every un-accepted invite (AGL-2068).\n *\n * Both collections in full, rather than a `where('hostAccess.X','!=',null)`:\n * the predicate that decides a seat is `isOrgWideMember`, which reads three\n * fields and treats an ABSENT `allHosts` as org-wide. Firestore cannot\n * express \"field absent\" in a filter, so a query-side count gets the legacy\n * rows wrong in the direction that over-charges. These collections are\n * bounded by the very caps being enforced, so reading them whole is cheap and\n * — inside a transaction — is exactly the lock that serialises concurrent\n * grants.\n */\nasync function readSeatEntries(\n orgRef: FirebaseFirestore.DocumentReference,\n read: (query: FirebaseFirestore.Query) => Promise<FirebaseFirestore.QuerySnapshot>,\n): Promise<CollaboratorSeatEntry[]> {\n const [members, invites] = await Promise.all([\n read(orgRef.collection('members')),\n read(orgRef.collection('invites').where('acceptedAt', '==', null)),\n ])\n return [\n // The uid is the DOCUMENT ID on the roster and is not a field, so it has\n // to be put back or every legacy row without a mirrored email identifies\n // nobody and silently stops consuming its seat.\n ...members.docs.map(\n (doc) => ({ uid: doc.id, ...doc.data() }) as CollaboratorSeatEntry,\n ),\n ...invites.docs.map((doc) => doc.data() as CollaboratorSeatEntry),\n ]\n}\n\n/**\n * The hard cap itself, evaluated against the POST-state and inside the same\n * transaction that performs the grant (AGL-2068).\n *\n * A create-time quota that reads, decides, and then writes is not a cap —\n * this repo has now relearned that three times in one day (AGL-1390 laundering\n * a count, AGL-2057 the assist cap, AGL-2063 the site limit): N concurrent\n * requests all read the same pre-count, all pass, and all land. Doing the\n * read through the transaction is what fixes it. Firestore tracks the read\n * SET, so a second grant that read the same roster cannot commit — it retries,\n * re-reads a roster that now holds the first grant, and refuses.\n *\n * Only NEWLY granted hosts are charged. Changing an existing collaborator's\n * role on a site they already reach re-writes the same seat, and refusing that\n * would strand an over-limit org unable to even demote its way back.\n *\n * THE CAP IS PER SITE AND SO IS THE QUESTION (AGL-2439). This calls\n * `checkHostCollaboratorQuota(org, hostId, used)` and not\n * `checkSeatQuota(org, 'members', used)`: since AGL-2439 the purchased\n * quantity is an org-level POOL and the latter deliberately answers the\n * PLAN's cap with no pool in it. Passing the plan cap here would refuse a\n * site the seats the org bought and assigned to it.\n *\n * THE GRANDFATHER LIVES HERE, in what this function does NOT do. It runs on\n * the GRANT path only — `newlyScopedHosts` is empty for an existing seat — so\n * a site already above its corrected cap keeps every collaborator it has and\n * is merely refused the next one. There is no sweep, no reconciliation and no\n * revocation anywhere in this file, and none may be added: the cap binds\n * ALLOCATION, never ACCESS. `quota.retainedOverCap` is how many seats a site\n * is over by, carried on the refusal so the console can say it out loud\n * rather than leaving the customer to infer it from a rejected click.\n */\nasync function assertCollaboratorSeats(options: {\n orgRef: FirebaseFirestore.DocumentReference\n org: Partial<AglynOrgBilling>\n hostIds: string[]\n self: {\n uid?: string | null\n email?: string | null\n emails?: readonly (string | null | undefined)[] | null\n }\n read: (query: FirebaseFirestore.Query) => Promise<FirebaseFirestore.QuerySnapshot>\n}): Promise<void> {\n const { orgRef, org, hostIds, self, read } = options\n if (!hostIds.length) return\n const entries = await readSeatEntries(orgRef, read)\n for (const hostId of hostIds) {\n const used = countCollaboratorSeats(entries, hostId, self)\n const quota = checkHostCollaboratorQuota(org, hostId, used)\n if (!quota.allowed) throw new CollaboratorSeatLimitError(hostId, quota)\n }\n}\n\n/**\n * Which hosts a membership is about to reach for the FIRST time as a scoped\n * collaborator — the set the seat cap is charged for.\n *\n * Empty when the resulting membership is org-wide: a manager already reaches\n * every host and pays for it with a manager seat.\n */\nfunction newlyScopedHosts(options: {\n role: OrgRole | undefined\n allHosts: boolean\n hostAccess: Record<string, unknown>\n existing: Partial<AglynOrgMember> | undefined\n}): string[] {\n const { role, allHosts, hostAccess, existing } = options\n if (isOrgWideMember({ role, allHosts, hostAccess } as Partial<AglynOrgMember>)) {\n return []\n }\n const prior = (existing?.hostAccess ?? {}) as Record<string, unknown>\n return Object.keys(hostAccess).filter((hostId) => !prior[hostId])\n}\n\n/**\n * Turn a seat refusal into the 403 the four admitting routes return, or null\n * when the error is something else and must keep propagating to the 500.\n *\n * Lives here beside `emailUnverifiedResponse` and `lockdownRefusal` so a\n * route's catch block is one line and cannot accidentally mask a real fault.\n */\nexport function collaboratorSeatRefusalResponse(\n error: unknown,\n): Response | null {\n if (!(error instanceof CollaboratorSeatLimitError)) return null\n return Response.json(\n {\n error: error.message,\n code: 'collaborator_seat_limit',\n limit: error.limit,\n upgradeRequired: error.upgradeRequired,\n // AGL-2439: how many seats this site is over by. NOBODY was removed —\n // the client renders this as retention, not as a loss.\n retainedOverCap: error.retainedOverCap,\n },\n { status: 403 },\n )\n}\n\n/**\n * The same cap, asked BEFORE anything is written (AGL-2068).\n *\n * Not the enforcement — the transaction inside the grant is. This exists so\n * the two doors that only ever create an INVITE (`/api/hosts/members` for an\n * address with no account yet, and `/api/orgs/invites` create) refuse at the\n * point the admin is looking at, rather than mailing someone a link that will\n * be refused when they click it. A race here over-reserves invites; it cannot\n * over-grant access, because access is only ever granted through the\n * transactional path.\n */\nexport async function collaboratorSeatRefusal(options: {\n orgId: string\n org: Partial<AglynOrgBilling>\n hostIds: string[]\n self?: { uid?: string | null; email?: string | null }\n}): Promise<Response | null> {\n const { orgId, org, hostIds, self } = options\n if (!hostIds.length) return null\n try {\n await assertCollaboratorSeats({\n orgRef: firestore().collection('orgs').doc(orgId),\n org,\n hostIds,\n self: self ?? {},\n read: (query) => query.get(),\n })\n } catch (error) {\n const refusal = collaboratorSeatRefusalResponse(error)\n if (refusal) return refusal\n throw error\n }\n return null\n}\n\n/**\n * The refusal string, taken from `/api/orgs/members`.\n *\n * The four doors each phrased this differently — \"upgrade your plan to invite\n * more members\", \"to add more members\", \"This organization is out of team\n * seats\", \"This workspace has used all N of its team seats\" — which is what a\n * gate copied four times produces. One wording now, from the one place the\n * refusal is built. Nothing matches these strings but a human, so the\n * consolidation costs no caller.\n */\nfunction managerSeatMessage(quota: {\n limit: number\n upgradeRequired: boolean\n addonPriceUsd: number | null\n}): string {\n return quota.upgradeRequired\n ? `Team seat limit reached (${quota.limit}) — upgrade your ` +\n 'plan to add more members'\n : `Team seats full (${quota.limit}) — add seats for ` +\n `$${quota.addonPriceUsd}/mo each from Billing`\n}\n\n/**\n * A manager seat refused, thrown rather than returned, for the reason\n * {@link CollaboratorSeatLimitError} is thrown: it has to travel out of\n * `upsertOrgMember`, whose contract is \"make it so\" and which three routes\n * already call as a bare `await`. A verdict would be silently discarded by\n * every one of them, which is the shape of the bug being fixed.\n */\nexport class ManagerSeatLimitError extends Error {\n readonly limit: number\n readonly upgradeRequired: boolean\n readonly addonPriceUsd: number | null\n /**\n * Seats the org holds ABOVE `limit`. Non-zero means it is GRANDFATHERED:\n * those managers keep their access and only the NEXT one is refused, so the\n * console can say that instead of letting an admin read a 403 as \"somebody\n * was removed\".\n */\n readonly retainedOverCap: number\n constructor(quota: {\n limit: number\n upgradeRequired: boolean\n addonPriceUsd: number | null\n retainedOverCap?: number\n }) {\n super(managerSeatMessage(quota))\n this.name = 'ManagerSeatLimitError'\n this.limit = quota.limit\n this.upgradeRequired = quota.upgradeRequired\n this.addonPriceUsd = quota.addonPriceUsd\n this.retainedOverCap = Math.max(0, quota.retainedOverCap ?? 0)\n }\n}\n\n/**\n * The manager cap, evaluated against the POST-state and inside the same\n * transaction that performs the grant (AGL-2068, on the manager key).\n *\n * The collaborator cap above learned this the hard way and this is the same\n * defect one key over: all four doors that admit a manager — invite create,\n * invite accept, direct member add and SSO-JIT — read the roster, decided,\n * and then wrote, with nothing between the read and the write. N concurrent\n * accepts all measured against the same roster, all passed, and all landed.\n * Reading THROUGH the transaction is the fix: Firestore tracks the read set,\n * so a second grant that measured the same roster cannot commit — it retries,\n * re-reads a roster that now holds the first, and refuses.\n *\n * PENDING INVITES COUNT, AT EVERY DOOR. Only invite-create counted them\n * before, so the cap was enforced against a different population depending on\n * which door was used — and the doors that ignored them are the ones that\n * actually grant access. An invite reserves the seat it will become, and a\n * cap that only bites on acceptance is walked past by mailing N invitations\n * first. `readSeatEntries` is shared with the collaborator gate precisely so\n * the two populations cannot drift apart again.\n *\n * `checkSeatQuota(org, 'managers', used)` and NOT the per-host collaborator\n * quota: `managersPerOrg` really is org-level, so purchased add-ons raise it\n * (AGL-2439 removed that only for the per-site `members` key).\n *\n * THE GRANDFATHER LIVES HERE, in what this does NOT do. It charges only the\n * TRANSITION into an org-wide seat — `becomesManager` is false when the\n * membership already held one — so an org already above its cap keeps every\n * manager it has, can still have their role or profile rewritten, and is\n * merely refused the next one. There is no sweep and no revocation, and none\n * may be added: the cap binds ADMISSION, never ACCESS.\n */\nasync function assertManagerSeats(options: {\n orgRef: FirebaseFirestore.DocumentReference\n org: Partial<AglynOrgBilling>\n /** Is this write ADMITTING a manager who was not one already? */\n becomesManager: boolean\n self: {\n uid?: string | null\n email?: string | null\n emails?: readonly (string | null | undefined)[] | null\n }\n read: (query: FirebaseFirestore.Query) => Promise<FirebaseFirestore.QuerySnapshot>\n}): Promise<void> {\n const { orgRef, org, becomesManager, self, read } = options\n if (!becomesManager) return\n const entries = await readSeatEntries(orgRef, read)\n const used = countManagerSeatsExcluding(entries, self)\n const quota = checkSeatQuota(org, 'managers', used)\n if (!quota.allowed) {\n throw new ManagerSeatLimitError({\n ...quota,\n retainedOverCap: Math.max(0, used - quota.limit),\n })\n }\n}\n\n/**\n * Is this write admitting a manager who was not one already?\n *\n * The manager analogue of `newlyScopedHosts`, and it exists for the same\n * reason: a seat is charged when it is TAKEN, not every time the row holding\n * it is rewritten. Re-saving an existing manager's title, or moving them from\n * `editor` to `admin`, re-writes a seat they already hold — charging that\n * would strand an over-cap org unable to even demote its way back down.\n *\n * A scoped collaborator being promoted to org-wide DOES take a manager seat,\n * and gives one up on the collaborator side; that is a real transition and is\n * charged.\n */\nfunction becomesOrgManager(options: {\n role: OrgRole\n allHosts: boolean\n hostAccess: Record<string, HostAccessRole>\n existing: Partial<AglynOrgMember> | undefined\n}): boolean {\n const next = isOrgWideMember({\n role: options.role,\n allHosts: options.allHosts,\n hostAccess: options.hostAccess,\n } as Partial<AglynOrgMember>)\n if (!next) return false\n // An ABSENT row is not a manager, and `isOrgWideMember(undefined)` is\n // already false — but saying so explicitly keeps the \"was it one before?\"\n // question readable next to the legacy shape that predates `allHosts`.\n return !options.existing || !isOrgWideMember(options.existing)\n}\n\n/**\n * Turn a manager-seat refusal into the 403 the admitting routes return, or\n * null when the error is something else and must keep propagating to the 500.\n *\n * Sits beside `collaboratorSeatRefusalResponse` and stacks with it in a\n * route's catch block, each returning null for a non-match.\n */\nexport function managerSeatRefusalResponse(error: unknown): Response | null {\n if (!(error instanceof ManagerSeatLimitError)) return null\n return Response.json(\n {\n error: error.message,\n code: 'manager_seat_limit',\n limit: error.limit,\n upgradeRequired: error.upgradeRequired,\n // How many seats the org is over by. NOBODY was removed — the client\n // renders this as retention, not as a loss.\n retainedOverCap: error.retainedOverCap,\n },\n { status: 403 },\n )\n}\n\n/**\n * The same cap, asked BEFORE anything is written.\n *\n * Not the enforcement — the transaction inside `upsertOrgMember` is. This\n * exists for the one door that never calls it: `/api/orgs/invites` create\n * writes an invite document directly, so it refuses at the point the admin is\n * looking at rather than mailing someone a link that will be refused when\n * they click it. A race here over-reserves invites; it cannot over-grant\n * access, because access is only ever granted through the transactional path.\n */\nexport async function managerSeatRefusal(options: {\n orgId: string\n org: Partial<AglynOrgBilling>\n becomesManager: boolean\n self?: { uid?: string | null; email?: string | null }\n}): Promise<Response | null> {\n const { orgId, org, becomesManager, self } = options\n if (!becomesManager) return null\n try {\n await assertManagerSeats({\n orgRef: firestore().collection('orgs').doc(orgId),\n org,\n becomesManager,\n self: self ?? {},\n read: (query) => query.get(),\n })\n } catch (error) {\n const refusal = managerSeatRefusalResponse(error)\n if (refusal) return refusal\n throw error\n }\n return null\n}\n\nexport interface UpsertOrgMemberOptions {\n orgId: string\n uid: string\n role: OrgRole\n allHosts?: boolean\n /** Per-site grants. `author` (AGL-2334) rides the shared union. */\n hostAccess?: Record<string, HostAccessRole>\n /**\n * Further CONFIRMED addresses on the joining account (AGL-2486), so a\n * pending invite addressed to a secondary is recognised as this same\n * person and does not bill them a second collaborator seat. Must contain\n * only addresses proven to belong to `uid`.\n */\n seatAliasEmails?: readonly (string | null | undefined)[] | null\n /** Custom role reference (AGL-243); null clears it. */\n roleId?: string | null\n email?: string | null\n displayName?: string | null\n /**\n * The member's provider photo, mirrored onto the roster (AGL-1126).\n *\n * Every member surface reads the roster; none of them can read Firebase\n * Auth for an SSO member, whose record lives in a per-org tenant pool\n * (AGL-1122). Without this the console falls back to drawn initials for\n * everyone — fine, but it means a member who HAS a picture still never\n * shows it. This is the ONLY source of a real face now that the Gravatar\n * fallback is gone (AGL-1683), so keeping it populated matters more than\n * it did. Display data only: never an identity or authorization source.\n */\n photoURL?: string | null\n /** Job title shown on the roster/member page (AGL-364). */\n title?: string | null\n invitedBy?: string | null\n}\n\n/**\n * The owner seat is not writable through the membership door (AGL-1888).\n *\n * An exception, and modelled on {@link CollaboratorSeatLimitError}, for the\n * same reason: it has to travel out of a function whose contract is \"make it\n * so\" and which three routes call as a bare `await`. A returned verdict would\n * be ignorable at every one of them, which is the shape of the bug.\n */\nexport class OrgOwnerSeatError extends Error {\n /** Which invariant refused, for the log and the tests. */\n readonly reason: 'grant' | 'demote'\n constructor(reason: 'grant' | 'demote') {\n super(\n reason === 'grant'\n ? 'The owner role cannot be granted through org membership — ' +\n 'ownership moves only by transfer.'\n : 'This person owns the organization. Ownership moves only by ' +\n 'transfer, from Settings — an invitation cannot change it.',\n )\n this.name = 'OrgOwnerSeatError'\n this.reason = reason\n }\n}\n\n/**\n * Turn an owner-seat refusal into a 409, or null when the error is something\n * else and must keep propagating to the 500.\n *\n * Beside {@link collaboratorSeatRefusalResponse} so a route's catch block\n * stays one line and cannot accidentally mask a real fault.\n */\nexport function orgOwnerSeatRefusalResponse(error: unknown): Response | null {\n if (!(error instanceof OrgOwnerSeatError)) return null\n return Response.json(\n { error: error.message, code: 'org_owner_seat' },\n { status: 409 },\n )\n}\n\n/**\n * Creates or updates a member transactionally with its reverse-index\n * entry, then re-syncs host projections.\n *\n * ## The owner seat is refused here, not only in the routes (AGL-1888)\n *\n * It used to say \"owner-role guards live in the API routes — this is the\n * mechanism\", and that was the defect. Both halves of the org-owner invariant\n * were enforced only at the doors an admin clicks, and invite ACCEPTANCE is a\n * door that re-validates neither:\n *\n * - **Granting.** `/api/orgs/members` and `/api/orgs/invites` create both\n * refuse `role === 'owner'` outright, but acceptance passes the invite\n * doc's STORED role straight through (`/api/orgs/invites` accept, and\n * `/api/auth/sso-jit`). That is safe today only because every writer of an\n * invite doc refuses `owner` and the collection is `allow write: if false`\n * — a latent escalation the moment a fourth invite-writer forgets, and the\n * invariant that an org has exactly ONE owner is what the whole SSO\n * break-glass guarantee rests on ({@link transferOrgOwnership} MOVES the\n * seat; nothing else may create one).\n * - **Demoting**, which was reachable, self-serve, and irreversible. Invite\n * creation never checked that the address is already a member, and\n * acceptance accommodates an existing member re-accepting. So any admin\n * could invite the OWNER'S own verified address as `viewer`; the owner\n * clicks a normal-looking invitation to their own organization; this\n * function merge-writes `role: 'viewer'`, `allHosts: false` onto the owner's\n * member doc. `orgs/{orgId}.ownerUid` still names them, but every\n * authorization read goes through the member doc — so `canManageOrg` is\n * now false, `transfer-ownership` checks `membership.member.role ===\n * 'owner'` and refuses them, `/api/orgs/members` refuses to edit the owner's\n * membership at all, and `findBreakGlassOrgOwners` (`where role == owner`)\n * finds nobody. The org loses its owner permanently, recoverable only by\n * staff. It is the AGL-1375 one-way door rebuilt out of the invite path,\n * and it needs no SSO to reach.\n *\n * Both checks live HERE because this is the single transaction every door\n * funnels through, and the org doc and the existing member doc are already in\n * its read set — so it costs nothing and cannot be forgotten by a fifth\n * caller. The route-level refusals stay: they are better error messages at\n * the point the admin is looking, not the control.\n *\n * The demotion guard asks BOTH `org.ownerUid` and the stored role, rather\n * than trusting either to stand for the other. They are supposed to agree;\n * an org where they have already diverged is exactly the one that most needs\n * the write refused.\n *\n * {@link createOrganization} and {@link transferOrgOwnership} are unaffected —\n * both write `role: 'owner'` with their own `tx.set`, and remain the only two\n * producers of an owner in the product.\n */\nexport async function upsertOrgMember(\n options: UpsertOrgMemberOptions,\n): Promise<void> {\n const {\n orgId,\n uid,\n role,\n allHosts,\n hostAccess,\n roleId,\n email,\n seatAliasEmails,\n displayName,\n photoURL,\n title,\n invitedBy,\n } = options\n // Before the transaction is even opened: this one needs no reads, and\n // refusing here is what lets the spec assert that NOTHING was written\n // rather than that a throw happened somewhere.\n if (role === 'owner') throw new OrgOwnerSeatError('grant')\n const db = firestore()\n await db.runTransaction(async (tx) => {\n const orgSnapshot = await tx.get(db.collection('orgs').doc(orgId))\n if (!orgSnapshot.exists) throw new Error(`Unknown org: ${orgId}`)\n const org = orgSnapshot.data() as AglynOrganization\n const memberRef = db\n .collection('orgs')\n .doc(orgId)\n .collection('members')\n .doc(uid)\n const existing = await tx.get(memberRef)\n // The owner's own row is not writable here (AGL-1888). Both facts, not\n // one standing in for the other — see the note on this function.\n if (\n org.ownerUid === uid ||\n (existing.data() as Partial<AglynOrgMember> | undefined)?.role === 'owner'\n ) {\n throw new OrgOwnerSeatError('demote')\n }\n // Collaborator seat cap (AGL-2068), inside this transaction and before\n // any write. This is the door `/api/orgs/members` and invite ACCEPTANCE\n // come through, and neither metered `membersPerHost` at all — both gate\n // on `isOrgWideMember`, which is false for exactly the site-scoped\n // collaborator this charges for. The roster read below joins this\n // transaction's read set, so concurrent accepts serialise instead of all\n // passing the same pre-count.\n await assertCollaboratorSeats({\n orgRef: db.collection('orgs').doc(orgId),\n org: orgSnapshot.data() as Partial<AglynOrgBilling>,\n hostIds: newlyScopedHosts({\n role,\n allHosts: allHosts ?? false,\n hostAccess: hostAccess ?? {},\n existing: existing.data() as Partial<AglynOrgMember> | undefined,\n }),\n self: { uid, email, emails: seatAliasEmails },\n read: (query) => tx.get(query),\n })\n // Manager seat cap, in the same read slot and for the same reason. This\n // is the door invite ACCEPTANCE, `/api/orgs/members` and SSO-JIT all come\n // through, and all three read the roster outside any transaction before\n // this — so concurrent accepts measured one roster and every one of them\n // passed. The read below joins this transaction's read set, which is what\n // serialises them.\n await assertManagerSeats({\n orgRef: db.collection('orgs').doc(orgId),\n org: orgSnapshot.data() as Partial<AglynOrgBilling>,\n becomesManager: becomesOrgManager({\n role,\n allHosts: allHosts ?? false,\n hostAccess: hostAccess ?? {},\n existing: existing.data() as Partial<AglynOrgMember> | undefined,\n }),\n self: { uid, email, emails: seatAliasEmails },\n read: (query) => tx.get(query),\n })\n tx.set(\n memberRef,\n {\n role,\n allHosts: allHosts ?? false,\n hostAccess: hostAccess ?? {},\n ...(roleId !== undefined ? { roleId } : {}),\n ...(email !== undefined ? { email } : {}),\n ...(displayName !== undefined ? { displayName } : {}),\n // Absent leaves the stored photo alone; an explicit null clears it.\n // A provider that stops sending a picture must not silently wipe one\n // the member is still using.\n ...(photoURL !== undefined ? { photoURL } : {}),\n ...(title !== undefined ? { title } : {}),\n ...(invitedBy ? { invitedBy } : {}),\n ...(existing.exists\n ? {}\n : { joinedAt: FieldValue.serverTimestamp() }),\n },\n { merge: true },\n )\n tx.set(\n db.collection('users').doc(uid).collection('orgs').doc(orgId),\n {\n role,\n orgName: org.name ?? null,\n slug: org.slug ?? null,\n // Mirrored from the member doc written just above (AGL-1032) — this\n // `set` has no merge, so the flag has to be part of it or the\n // console loses the collaborator/viewer distinction until the\n // projection pass below rewrites it.\n orgWide: isOrgWideMember({\n role,\n allHosts: allHosts ?? false,\n hostAccess: hostAccess ?? {},\n }),\n },\n )\n })\n await syncOrgAuthProjections(orgId)\n // Reverse-index this member's now-current host access (AGL-844).\n await syncMemberHostProjections(orgId, uid)\n}\n\n/**\n * Fill in a roster row's display identity from an identity provider, writing\n * ONLY the fields that are currently blank (AGL-1131).\n *\n * Separate from `upsertOrgMember` because the caller is the SSO sign-in path\n * on its already-a-member branch, where the member's role, host access and\n * invite state are settled and must not be touched. `upsertOrgMember`\n * requires a `role` and re-asserts it, so reusing it here would let an SSO\n * sign-in quietly reset an admin to the org's `sso.defaultRole`.\n *\n * Absent-only, so it is safe on every sign-in: it backfills the rows that\n * predate the IdP mapping and then never writes again, and it can never\n * overwrite a name or photo a person chose.\n *\n * @returns the field names it wrote, for logging and tests.\n */\nexport async function backfillMemberIdentity(\n orgId: string,\n uid: string,\n identity: { displayName?: string | null; photoURL?: string | null },\n db = firestore(),\n): Promise<string[]> {\n const ref = db.collection('orgs').doc(orgId).collection('members').doc(uid)\n const snapshot = await ref.get()\n // A missing row is NOT this function's job to create — creating one here\n // would mint a membership with no role, which every permission check reads\n // as a member of some kind.\n if (!snapshot.exists) return []\n\n const blank = (value: unknown) => typeof value !== 'string' || !value.trim()\n const patch: Record<string, string> = {}\n const displayName = identity.displayName?.trim()\n const photoURL = identity.photoURL?.trim()\n if (displayName && blank(snapshot.get('displayName'))) {\n patch['displayName'] = displayName\n }\n if (photoURL && blank(snapshot.get('photoURL'))) {\n patch['photoURL'] = photoURL\n }\n if (!Object.keys(patch).length) return []\n\n await ref.set(patch, { merge: true })\n return Object.keys(patch)\n}\n\n/**\n * The same absent-only backfill, across every roster row that names `uid`.\n *\n * ## The hole this closes\n *\n * `orgs/{orgId}/members/{uid}.photoURL` is the ONLY avatar a member surface\n * can read — a colleague's auth record is unreadable from another member's\n * session, and an SSO member's lives in a pool the project cannot see at all\n * (AGL-1122). Three writers filled it, and between them they missed the\n * commonest account there is:\n *\n * - `upsertOrgMember` — someone ADDED you, so the adder's lookup had a record\n * to copy from.\n * - `backfillMemberIdentity` via the SSO sign-in (AGL-1131) — enterprise only.\n * - `propagateMemberPhoto` via Manage Account → Profile image (AGL-1976) — a\n * photo the person typed or browsed to.\n *\n * Nobody adds the person who CREATES a workspace, `createOrganization` writes\n * their row with a name and an email and no photo, and a Google sign-in never\n * visits the other two. So the owner of a workspace saw their own face in the\n * app bar, which reads the live auth record, and a grey initial in their own\n * Team list — measured on both rows of `test-org`, each with `photoURL` absent\n * while the auth record and `users/{uid}.photoUrl` carried the picture.\n *\n * ## Absent-only, like the function it fans out\n *\n * It runs on EVERY sign-in, so the reasoning in `backfillMemberIdentity`\n * applies unchanged and is the reason this is a fan-out of that function\n * rather than a second writer: an overwriting version would replace a photo\n * the person chose in Manage Account with their provider thumbnail on their\n * next sign-in, silently, forever. `propagateMemberPhoto` is the overwriting\n * direction and stays the only one, because its input is a choice the person\n * made rather than an assertion a directory made about them.\n *\n * Memberships come from `users/{uid}/orgs`, the reverse index — never a\n * collection-group query over `members`, which would read every workspace's\n * roster in the estate to find one person's rows.\n *\n * @returns the org ids whose row was written, for logging and tests.\n */\nexport async function backfillMemberIdentityEverywhere(\n uid: string,\n identity: { displayName?: string | null; photoURL?: string | null },\n db = firestore(),\n): Promise<string[]> {\n if (!uid) return []\n // Nothing to write beats a fan-out that reads every membership to discover\n // it has nothing to write — this runs on every sign-in.\n if (!identity.displayName?.trim() && !identity.photoURL?.trim()) return []\n\n const memberships = await db.collection('users').doc(uid).collection('orgs').get()\n const written: string[] = []\n for (const row of memberships.docs) {\n const fields = await backfillMemberIdentity(row.id, uid, identity, db)\n if (fields.length) written.push(row.id)\n }\n return written\n}\n\n/**\n * Transfers org ownership (AGL-232): the target must already be on the\n * roster; the previous owner steps down to admin. One transaction across\n * the org doc, both member docs and both reverse-index entries, then the\n * host projections re-sync.\n *\n * **It moves `ownerUid` and must never touch `createdByUid`** (AGL-2265).\n * That field is the creator attribution the free-workspace ceiling counts\n * against, and it is what stops a transfer from being a way to launder the\n * count: hand a workspace to an alt account, create a fourth, take it back.\n * Nothing here writes it, and `free-workspace-cap.spec.ts` runs exactly that\n * sequence to keep it that way.\n */\nexport async function transferOrgOwnership(\n orgId: string,\n fromUid: string,\n toUid: string,\n): Promise<void> {\n if (fromUid === toUid) throw new Error('Target already owns this org')\n const db = firestore()\n await db.runTransaction(async (tx) => {\n const orgRef = db.collection('orgs').doc(orgId)\n const orgSnapshot = await tx.get(orgRef)\n if (!orgSnapshot.exists) throw new Error(`Unknown org: ${orgId}`)\n const org = orgSnapshot.data() as AglynOrganization\n if (org.ownerUid !== fromUid) {\n throw new Error('Only the current owner can transfer ownership')\n }\n const targetRef = orgRef.collection('members').doc(toUid)\n const target = await tx.get(targetRef)\n if (!target.exists) {\n throw new Error('The new owner must already be an org member')\n }\n tx.set(\n orgRef,\n { ownerUid: toUid, updatedAt: FieldValue.serverTimestamp() },\n { merge: true },\n )\n tx.set(targetRef, { role: 'owner', allHosts: true }, { merge: true })\n tx.set(\n orgRef.collection('members').doc(fromUid),\n { role: 'admin' },\n { merge: true },\n )\n tx.set(\n db.collection('users').doc(toUid).collection('orgs').doc(orgId),\n // Both principals end up owner/admin, which is org-wide reach whatever\n // they were before — a promoted site collaborator must lose the scoped\n // console along with the scoped membership (AGL-1032).\n { role: 'owner', orgWide: true },\n { merge: true },\n )\n tx.set(\n db.collection('users').doc(fromUid).collection('orgs').doc(orgId),\n { role: 'admin', orgWide: true },\n { merge: true },\n )\n })\n await syncOrgAuthProjections(orgId)\n // Both principals' host access changed (owner spans every host) — AGL-844.\n await Promise.all([\n syncMemberHostProjections(orgId, toUid),\n syncMemberHostProjections(orgId, fromUid),\n ])\n /*\n * A workspace changing hands is the highest-consequence thing that can\n * happen to an account, and until AGL-118 it left no trace anywhere: the\n * transaction above rewrites five documents and wrote nothing that says it\n * happened, so the only evidence was the new state itself.\n *\n * BOTH principals are on the row. The actor is the outgoing owner, who is\n * the only party allowed to perform this, and the target names the\n * incoming one — a transfer identified by one party is half a record, and\n * the half it keeps is the one already implied by `ownerUid`.\n *\n * Emails are read after the fact and best-effort. The uids are the\n * identity; the addresses only save a reader a lookup, so a failure to\n * resolve them must not cost the entry.\n */\n const [fromEmail, toEmail] = await Promise.all(\n [fromUid, toUid].map(async (uid) =>\n firestore()\n .collection('orgs')\n .doc(orgId)\n .collection('members')\n .doc(uid)\n .get()\n .then((snapshot) => {\n const email = snapshot.get('email')\n return typeof email === 'string' ? email : null\n })\n .catch(() => null),\n ),\n )\n await logOrgActivity(\n orgId,\n { uid: fromUid, email: fromEmail },\n 'Transferred workspace ownership',\n { type: 'member', id: toUid, ...(toEmail ? { name: toEmail } : {}) },\n )\n}\n\n/**\n * Grants (or updates) per-host access for a uid without disturbing an\n * existing membership's org role or allHosts flag (AGL-238: the host user\n * manager rides org membership). Creates a viewer membership scoped to\n * just this host when the uid is not on the roster yet.\n */\nexport async function grantHostAccess(options: {\n orgId: string\n uid: string\n hostId: string\n /** `author` (AGL-2334) edits content and cannot publish. */\n role: HostAccessRole\n email?: string | null\n displayName?: string | null\n invitedBy?: string\n}): Promise<void> {\n const { orgId, uid, hostId, role, email, displayName, invitedBy } = options\n const db = firestore()\n await db.runTransaction(async (tx) => {\n const orgRef = db.collection('orgs').doc(orgId)\n const orgSnapshot = await tx.get(orgRef)\n if (!orgSnapshot.exists) throw new Error(`Unknown org: ${orgId}`)\n const org = orgSnapshot.data() as AglynOrganization\n const memberRef = orgRef.collection('members').doc(uid)\n const existing = await tx.get(memberRef)\n // Collaborator seat cap (AGL-2068). This door DID meter, but against\n // `hosts/{hostId}/members` — a display roster only its own route writes,\n // so it could not see anyone admitted by invite or by `/api/orgs/members`\n // and under-counted even when it fired. The count now comes off the org\n // roster + pending invites, which is where every door lands.\n await assertCollaboratorSeats({\n orgRef,\n org: orgSnapshot.data() as Partial<AglynOrgBilling>,\n // Asked of the membership AS IT STANDS, not of the merged shape.\n // `grantHostAccess` never touches `role` or `allHosts`, so someone who\n // is already a manager stays one and keeps paying a manager seat — and\n // a legacy pre-`allHosts` row, which `isOrgWideMember` reads as org-wide\n // precisely so it is not locked out, must not be re-classified into a\n // collaborator seat by the act of writing a host key onto it.\n hostIds: (() => {\n const current = existing.data() as Partial<AglynOrgMember> | undefined\n if (existing.exists && isOrgWideMember(current)) return []\n if (current?.hostAccess?.[hostId]) return []\n return [hostId]\n })(),\n self: { uid, email },\n read: (query) => tx.get(query),\n })\n tx.set(\n memberRef,\n {\n ...(existing.exists\n ? {}\n : {\n role: 'viewer' as OrgRole,\n allHosts: false,\n joinedAt: FieldValue.serverTimestamp(),\n }),\n hostAccess: { [hostId]: role },\n ...(email !== undefined ? { email } : {}),\n ...(displayName !== undefined ? { displayName } : {}),\n ...(invitedBy ? { invitedBy } : {}),\n },\n // merge deep-merges the hostAccess map, so other host grants and\n // the existing role/allHosts stay untouched.\n { merge: true },\n )\n if (!existing.exists) {\n tx.set(db.collection('users').doc(uid).collection('orgs').doc(orgId), {\n role: 'viewer',\n orgName: org.name ?? null,\n slug: org.slug ?? null,\n // A brand-new site collaborator: on the org roster, but their console\n // is one site (AGL-1032). `role: 'viewer'` here is indistinguishable\n // from a genuine org-wide viewer's, which is the whole reason for\n // this flag. An EXISTING member keeps whatever reach they had — a\n // host grant never widens or narrows it.\n orgWide: false,\n })\n }\n })\n await syncOrgAuthProjections(orgId)\n await syncMemberHostProjections(orgId, uid)\n}\n\n/**\n * Drops one host from a member's hostAccess map, then re-projects.\n *\n * `updateExisting`, not a merge-set (AGL-1766). A merge-set whose entire\n * payload is a delete sentinel still CREATES the document when it is absent,\n * and the row it minted here is not merely untidy — it is a MEMBERSHIP, and\n * one that reads as org-wide. `isOrgWideMember` treats \"no `role`, no\n * `allHosts`, empty `hostAccess`\" as the pre-`allHosts` LEGACY shape and\n * answers true (deliberately: reading it as \"scoped, with access to nothing\"\n * would lock real members out). A genuine site collaborator never looks like\n * that — `grantHostAccess` always writes `allHosts: false` — but a document\n * conjured from this patch alone does, exactly.\n *\n * So the consequences land away from here, which is what made it hard to see:\n * `resolveOrgMembership` finds the doc and returns a membership for someone\n * who was removed from the org; `syncOrgAuthProjections` on the next line\n * stamps it `scopeTokens: ['org']`, the read set the rules and every\n * Admin-SDK `memberCanSee` resolve from; and `countManagerSeats` bills it as\n * a manager seat. (It does NOT reach `hosts/*.memberRoles`, as AGL-1763\n * supposed — `hostRoleFor` requires an `isOrgRole(role)` and the phantom has\n * none.)\n *\n * Reachable without any race: `removeOrgMember` deletes the org member doc\n * but leaves the `hosts/{hostId}/members` roster row, which is what this is\n * called from. Deleting that leftover row re-created the membership it was\n * meant to finish removing. (AGL-1766's \"stale double-submit\" is NOT a route:\n * the caller 404s on the missing roster row before reaching here.)\n *\n * DOTTED FIELD PATH, not the nested map: `update()` accepts a delete sentinel\n * only at the top level of its patch (`@google-cloud/firestore` serializer,\n * `allowDeletes: 'root'`), so the nested form would throw INVALID_ARGUMENT.\n * The dotted path is top-level and clears the one key while leaving the rest\n * of `hostAccess` alone — the same field-by-field semantics the merge had.\n * Safe as a string path because host ids are `createResourceUid()` nanoids\n * (`A-Za-z0-9_-`), so none can contain the `.` the SDK splits on.\n *\n * REFUSE, and ignore the answer: revoking a grant that is not there is a\n * no-op and discards nothing (AGL-1760). The projections still run — they are\n * recomputed from the roster, so a pass that finds no member doc is exactly\n * the self-heal a stale row needs.\n */\nexport async function revokeHostAccess(\n orgId: string,\n uid: string,\n hostId: string,\n): Promise<void> {\n await updateExisting(\n firestore().collection('orgs').doc(orgId).collection('members').doc(uid),\n { [`hostAccess.${hostId}`]: FieldValue.delete() },\n )\n await syncOrgAuthProjections(orgId)\n await syncMemberHostProjections(orgId, uid)\n}\n\n/**\n * Removes a member + reverse index entry, then re-syncs projections.\n *\n * The addresses the member added in this workspace (AGL-2975) go in the\n * same batch. They sit beside the roster row rather than under it, so no\n * delete of the row reaches them, and an erasure of the person runs through\n * here once per workspace.\n */\nexport async function removeOrgMember(\n orgId: string,\n uid: string,\n): Promise<void> {\n const db = firestore()\n const batch = db.batch()\n batch.delete(\n db.collection('orgs').doc(orgId).collection('members').doc(uid),\n )\n batch.delete(\n db\n .collection('orgs')\n .doc(orgId)\n .collection(MEMBER_EMAIL_ALIASES_COLLECTION)\n .doc(uid),\n )\n batch.delete(db.collection('users').doc(uid).collection('orgs').doc(orgId))\n await batch.commit()\n await syncOrgAuthProjections(orgId)\n // The member is off the roster, so the sync above can't reach their rows —\n // drop the reverse index explicitly (AGL-844), like the orgs entry above.\n await deleteMemberHostProjections(orgId, uid)\n}\n\n/**\n * Registers a host under its org: org directory entry, hostIndex mirror,\n * and the initial memberRoles projection on the host doc.\n */\nexport async function registerOrgHost(\n orgId: string,\n hostId: string,\n subdomain?: string,\n): Promise<void> {\n const db = firestore()\n await db\n .collection('orgs')\n .doc(orgId)\n .set(\n {\n hosts: { [hostId]: true },\n updatedAt: FieldValue.serverTimestamp(),\n },\n { merge: true },\n )\n await db\n .collection('hostIndex')\n .doc(hostId)\n .set({ orgId, ...(subdomain ? { subdomain } : {}) })\n await syncOrgAuthProjections(orgId, hostId)\n // Seed the per-user projection for everyone who can reach the new host.\n await syncHostProjectionForMembers(orgId, hostId)\n}\n\n/**\n * The consent group a site belongs to, read off its owning org.\n *\n * The ONE server-side door to pooling. Every capture surface and every send\n * path resolves a group through this rather than reading\n * `CONSENT_GROUPS_FIELD` itself, so there is one place that decides what a\n * site's consent covers and one place a mistake could live.\n *\n * FAILS TO THE GROUP OF ONE. An org that cannot be resolved, or a read that\n * throws, answers \"this site alone\" — which withholds mail from an org that\n * had legitimately pooled and never sends mail on a pooling nobody could\n * confirm. That is the only direction a failure here may fall.\n *\n * The org read is `React.cache`-deduped per request by {@link getOrgForHost},\n * so a send that already resolved the org for its policy pays nothing extra.\n */\nexport async function consentGroupForSite(\n hostId: string,\n org?: Record<string, unknown> | null,\n): Promise<ConsentGroup> {\n if (!hostId) throw new Error('[organizations] no site to resolve a group for')\n if (org) return consentGroupForHost(org, hostId)\n const resolved = await getOrgForHost(hostId).catch(() => null)\n return consentGroupForHost(\n (resolved?.org as Record<string, unknown> | undefined) ?? null,\n hostId,\n )\n}\n"],"names":["consentGroupForHost","checkHostCollaboratorQuota","checkSeatQuota","countCollaboratorSeats","countManagerSeatsExcluding","createResourceUid","generateOrgSlug","projectMemberResolvedPermissions","resolveOrgPermissions","isOrgWideMember","isValidOrgSlug","hostPermissionKeys","orgPermissionLabel","pluginOrgPermissionKeys","projectHostMemberPermissions","projectHostMemberRoles","projectMemberScopeTokens","resolveCollaboratorHostPermissions","scopeTokensForHost","nameSearchKey","nameSearchReversed","nameSearchTokens","ORG_BILLING_DOC_ID","ORG_BILLING_SUBCOLLECTION","MEMBER_EMAIL_ALIASES_COLLECTION","FieldValue","cache","findUserByUidAcrossPools","firebaseAdmin","enforceFreeWorkspaceCapInTransaction","readFreeWorkspaceCapConfig","deleteMemberHostProjections","syncHostProjectionForMembers","syncMemberHostProjections","updateExisting","attachWorkspaceDomain","firestore","app","FIRESTORE_BATCH_LIMIT","OrgSlugTakenError","Error","slug","name","isSlugReservationLapsed","reservation","now","Date","until","reservedUntil","Number","isFinite","isSlugReservationClaimable","claimingOrgId","orgId","movedTo","lapsedReservationIsStillHeld","holderOrgId","holder","collection","doc","get","exists","ownerUid","found","record","emailVerified","error","console","createOrganization","options","ownerEmail","ownerDisplayName","db","capConfig","bypassFreeWorkspaceCap","runTransaction","tx","held","data","undefined","uid","config","set","nameLower","nameTokens","nameReversed","createdByUid","hosts","createdAt","serverTimestamp","updatedAt","role","allHosts","email","displayName","joinedAt","scopeTokens","resolvedPermissions","orgName","orgWide","logOrgActivity","type","id","resolveOrgMembership","resolved","mine","limit","empty","docs","memberSnapshot","member","$id","ensureOrgForUser","profile","existing","base","trim","split","slice","attempt","created","changeOrgSlug","newSlug","previousSlug","orgSnapshot","orgRef","merge","renamedAt","members","listOrgMembers","batch","commit","resolveOrgIdForHost","hostId","snapshot","getOrgDoc","getOrgForHost","org","getHostDocAdmin","getHostDisabledPlugins","disabled","Array","isArray","map","String","getOrgForUser","membership","orgDataCollectionForHost","scopedToHost","ref","orgScoped","parent","where","orgDataQueryForHost","query","resolveMemberOrgPermissions","customRole","roleId","memberHasOrgPermission","permission","memberHasPermissionOnHost","site","permissionRefusal","Response","json","reason","status","resolveMemberPluginPermissionsOnOrg","granted","Object","fromEntries","key","setHostPermissions","keys","accepted","value","permissions","stored","before","hostPermissions","syncOrgAuthProjections","after","loadOrgCustomRoles","roleIds","Set","filter","rolesRef","Map","Promise","all","HOST_PROJECTION_WRITE","mergeFields","MEMBER_PROJECTION_WRITE","customRoles","hostIds","writes","memberRoles","memberPermissions","i","length","syncHostMemberRoles","actor","action","target","add","actorId","actorEmail","versionId","catch","logHostActivity","apiKeyName","CollaboratorSeatLimitError","quota","collaboratorSeatMessage","upgradeRequired","addonPriceUsd","retainedOverCap","Math","max","readSeatEntries","read","invites","assertCollaboratorSeats","self","entries","used","allowed","newlyScopedHosts","hostAccess","prior","collaboratorSeatRefusalResponse","message","code","collaboratorSeatRefusal","refusal","managerSeatMessage","ManagerSeatLimitError","assertManagerSeats","becomesManager","becomesOrgManager","next","managerSeatRefusalResponse","managerSeatRefusal","OrgOwnerSeatError","orgOwnerSeatRefusalResponse","upsertOrgMember","seatAliasEmails","photoURL","title","invitedBy","memberRef","emails","backfillMemberIdentity","identity","blank","patch","backfillMemberIdentityEverywhere","memberships","written","row","fields","push","transferOrgOwnership","fromUid","toUid","targetRef","fromEmail","toEmail","then","grantHostAccess","current","revokeHostAccess","delete","removeOrgMember","registerOrgHost","subdomain","consentGroupForSite"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/organizations.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 * Server-side organization operations (AGL-233/234). Everything here is\n * Admin-SDK-only by design: org creation, slug reservation, membership\n * and the projections the security rules authorize against are never\n * client-writable (docs/MULTI_TENANT_FIRESTORE.md §8).\n */\n\nimport {\n consentGroupForHost,\n type ConsentGroup,\n checkHostCollaboratorQuota,\n checkSeatQuota,\n countCollaboratorSeats,\n countManagerSeatsExcluding,\n createResourceUid,\n generateOrgSlug,\n projectMemberResolvedPermissions,\n resolveOrgPermissions,\n isOrgWideMember,\n isValidOrgSlug,\n hostPermissionKeys,\n orgPermissionLabel,\n pluginOrgPermissionKeys,\n projectHostMemberPermissions,\n projectHostMemberRoles,\n projectMemberScopeTokens,\n resolveCollaboratorHostPermissions,\n scopeTokensForHost,\n type AglynOrganization,\n type AglynOrgBilling,\n type AglynOrgCustomRole,\n type AglynOrgMember,\n type CollaboratorSeatEntry,\n type HostAccessRole,\n type OrgPermission,\n type OrgRole,\n} from '@aglyn/aglyn/server'\nimport type { PluginActivityTargetType } from '@aglyn/aglyn/plugin-manager/plugin-activity-actions'\nimport type { HostActivityActor } from '@aglyn/aglyn/app-utils/activity-presenter'\nimport {\n nameSearchKey,\n nameSearchReversed,\n nameSearchTokens,\n} from '@aglyn/aglyn/app-utils/name-search'\n// LEAF MODULE, NOT THE BARREL (AGL-1289). This file is itself reachable\n// through `@aglyn/aglyn/server`, and the verdict route proved this week that a\n// constant pulled from that barrel inside the cycle typechecks and then\n// resolves `undefined` at runtime.\nimport {\n ORG_BILLING_DOC_ID,\n ORG_BILLING_SUBCOLLECTION,\n} from '@aglyn/aglyn/app-utils/org-billing-doc'\nimport { MEMBER_EMAIL_ALIASES_COLLECTION } from '@aglyn/aglyn/app-utils/member-email-aliases'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { cache } from 'react'\nimport { findUserByUidAcrossPools } from './auth-pools'\nimport firebaseAdmin from './firebase-admin'\nimport {\n enforceFreeWorkspaceCapInTransaction,\n readFreeWorkspaceCapConfig,\n type FreeWorkspaceCapConfig,\n} from './free-workspace-cap'\nimport {\n deleteMemberHostProjections,\n syncHostProjectionForMembers,\n syncMemberHostProjections,\n} from './host-memberships'\nimport { updateExisting } from './update-existing'\nimport { attachWorkspaceDomain } from './workspace-domains'\n\nconst firestore = () => firebaseAdmin.app().firestore()\n\n/** Firestore's hard cap on writes in one batched commit. */\nconst FIRESTORE_BATCH_LIMIT = 500\n\nexport class OrgSlugTakenError extends Error {\n constructor(slug: string) {\n super(`Org slug already reserved: ${slug}`)\n this.name = 'OrgSlugTakenError'\n }\n}\n\n/**\n * ⛔ NOTHING WRITES A NEW `reservedUntil` ANY MORE — AND THE RULES THAT READ\n * ONE STAY (AGL-2590).\n *\n * AGL-2585 gave a workspace created by an UNVERIFIED owner a held address\n * rather than a granted one, because a signup could create a workspace before\n * anything proved the email belonged to the person typing it. That is no\n * longer possible: `/api/orgs/create` refuses an unverified caller outright,\n * the sign-up form holds its typed name against the account instead, and the\n * workspace is created on the first verified session. There is no path left\n * that can produce an unproven address, so `createOrganization` writes the\n * plain grant this collection has always held and the twenty-one-day\n * reservation window is gone with the code that set it.\n *\n * The READ side below is deliberately kept, and it is not dead weight:\n * production holds `orgSlugs` documents that WERE written with an expiry,\n * before this. Deleting the lapse rules would silently promote every one of\n * those squats to a permanent grant — the exact outcome AGL-2585 existed to\n * end. They stay until `reap-unverified-orgs` has erased or promoted the last\n * of them.\n *\n * Has a PENDING address reservation run out?\n *\n * A reservation with no `reservedUntil` is a GRANT and never lapses, which is\n * what keeps every workspace made by a verified owner — and every one that\n * predates the field — untouchable by this rule.\n *\n * A `reservedUntil` that is not a finite number never lapses either: a\n * corrupt or half-written expiry is a reason to leave an address alone, not a\n * reason to hand it to the next caller.\n */\nexport function isSlugReservationLapsed(\n // The whole `orgSlugs/{slug}` document, not just the field being read: every\n // caller has one in hand, and a parameter narrowed to `reservedUntil` alone\n // makes the ordinary case — a grant, which carries `orgId` and no expiry —\n // an excess-property error at the call site.\n reservation:\n | { orgId?: unknown; movedTo?: unknown; reservedUntil?: unknown }\n | undefined,\n now: number = Date.now(),\n): boolean {\n const until = reservation?.reservedUntil\n if (typeof until !== 'number' || !Number.isFinite(until)) return false\n return until <= now\n}\n\n/**\n * Whether an `orgSlugs/{slug}` reservation may be (re)claimed (AGL-585):\n * free when the doc is missing, when the claimant already owns it, or when\n * it is a tombstone (`movedTo` set) — a renamed-away slug keeps redirecting\n * old URLs only until someone wants it, it is never reserved forever.\n * Claiming writes a full-replace `{ orgId }`, which ends the redirect —\n * links to a reclaimed slug resolve to the new owner from then on.\n *\n * A LAPSED PENDING RESERVATION is claimable too (AGL-2585). The reservation\n * an unverified signup takes is a hold, not a grant, and a hold that never\n * expires is the squat this rule exists to end.\n *\n * ⚠️ Claimable here is NOT the whole answer for a lapsed reservation — see\n * {@link lapsedReservationIsStillHeld}, which both call sites consult before\n * they act on a `true` that came from the lapse branch. This function is pure\n * and cannot ask whether the owner has verified since; treating its answer as\n * final would let a customer who verified on day one lose their address on\n * day twenty-one because a sweep was down.\n */\nexport function isSlugReservationClaimable(\n reservation:\n | { orgId?: unknown; movedTo?: unknown; reservedUntil?: unknown }\n | undefined,\n claimingOrgId: string | null,\n now: number = Date.now(),\n): boolean {\n if (!reservation) return true\n if (claimingOrgId !== null && reservation.orgId === claimingOrgId) return true\n if (reservation.movedTo) return true\n return isSlugReservationLapsed(reservation, now)\n}\n\n/**\n * Is a LAPSED reservation nonetheless still its holder's? (AGL-2585)\n *\n * The lapse rule above is pure, and the fact it cannot see is the only one\n * that matters here: whether the owner verified their address after the\n * workspace was made. `reap-unverified-orgs` clears `reservedUntil` on its\n * next pass when they have, but \"on its next pass\" is a promise about a\n * scheduled job, and a scheduled job can stop. Between a verification and the\n * promotion that records it, the pure rule would say this address is free.\n *\n * So the two paths that take a slug ask this before they take a lapsed one,\n * and it answers from the auth record — the only source of truth for whether\n * an address was ever confirmed.\n *\n * FAILS CLOSED, in every direction. A missing org, a missing owner, an auth\n * lookup that throws: all of them return `true`, meaning the reservation\n * stands and the claim is refused. Refusing to hand over an address costs the\n * claimant one attempt at a name; granting one wrongly costs its holder the\n * URL their customers use.\n */\nasync function lapsedReservationIsStillHeld(\n reservation: { orgId?: unknown } | undefined,\n): Promise<boolean> {\n const holderOrgId =\n typeof reservation?.orgId === 'string' ? reservation.orgId : null\n if (!holderOrgId) return true\n try {\n const holder = await firestore().collection('orgs').doc(holderOrgId).get()\n if (!holder.exists) {\n // The workspace is gone and only the reservation outlived it. Nothing\n // is being taken from anyone.\n return false\n }\n const ownerUid = holder.get('ownerUid')\n if (typeof ownerUid !== 'string' || !ownerUid) return true\n const found = await findUserByUidAcrossPools(ownerUid)\n if (!found) return true\n return found.record.emailVerified === true\n } catch (error) {\n console.error('[orgs] lapsed reservation check failed', error)\n return true\n }\n}\n\nexport interface CreateOrganizationOptions {\n name: string\n slug: string\n ownerUid: string\n ownerEmail?: string | null\n ownerDisplayName?: string | null\n /**\n * Skip the AGL-2265 free-workspace ceiling.\n *\n * For staff provisioning on a customer's behalf and for the migration and\n * backfill scripts — a ceiling that stops support from fixing a workspace\n * is a ceiling that produces the ticket it was meant to prevent. Never set\n * from a self-serve path; `/api/orgs/create` passes the staff claim and\n * nothing else.\n */\n bypassFreeWorkspaceCap?: boolean\n}\n\n/**\n * Creates an org in one transaction: slug reservation (uniqueness), org\n * doc, owner membership, and the owner's reverse-index entry. Throws\n * `OrgSlugTakenError` when the slug is reserved; slug validity is the\n * caller's job (API routes return 400 with policy copy).\n */\nexport async function createOrganization(\n options: CreateOrganizationOptions,\n): Promise<string> {\n const { name, slug, ownerUid, ownerEmail, ownerDisplayName } = options\n const db = firestore()\n const orgId = createResourceUid()\n // The free-workspace ceiling (AGL-2265). Read OUTSIDE the transaction —\n // it is a platform setting on a 15s cache, not a document this creation\n // races with, and putting it in the read set would make every workspace\n // creation on the platform contend on one document. `ready` rides along so\n // the verdict knows the difference between \"staff set no limit\" and \"we\n // could not read it\", and never treats the second as the first.\n const capConfig: FreeWorkspaceCapConfig | null = options.bypassFreeWorkspaceCap\n ? null\n : await readFreeWorkspaceCapConfig()\n await db.runTransaction(async (tx) => {\n const reservation = await tx.get(db.collection('orgSlugs').doc(slug))\n const held = reservation.exists\n ? (reservation.data() as {\n orgId?: unknown\n movedTo?: unknown\n reservedUntil?: unknown\n })\n : undefined\n // Tombstones (renamed-away slugs) are claimable by new orgs (AGL-585),\n // and so is a reservation left by an unverified signup made before\n // AGL-2590 that has since run out — but only once the auth record agrees\n // it was never confirmed.\n if (\n !isSlugReservationClaimable(held, null) ||\n (isSlugReservationLapsed(held) && (await lapsedReservationIsStillHeld(held)))\n ) {\n throw new OrgSlugTakenError(slug)\n }\n // Last read, first write: the ceiling counts inside this transaction, so\n // a retry recounts, and it writes the per-owner marker that makes two\n // concurrent creates by one account contend. Throws\n // `FreeWorkspaceCapError`, which the API routes turn into a 403 with the\n // numbers in it.\n if (capConfig) {\n await enforceFreeWorkspaceCapInTransaction({\n tx,\n firestore: db,\n uid: ownerUid,\n config: capConfig,\n })\n }\n // The plain grant, always (AGL-2590): no caller can reach this with an\n // unproven address any more. `orgSlugs` is world-readable — the console\n // resolves a workspace subdomain client-side from it — so the id is all\n // that goes in.\n tx.set(db.collection('orgSlugs').doc(slug), { orgId })\n /*\n * THE BILLING DOCUMENT EXISTS FROM BIRTH (AGL-1152).\n *\n * `readOrgBilling` reads `orgs/{id}/billing/stripe` and falls back to the\n * org doc when it is absent — and Firestore BILLS a read for a document\n * that does not exist. An org created without one therefore pays a\n * NOT_FOUND plus the fallback lookup on every read, forever, on the\n * tenant's hot path behind a deliberately short TTL.\n *\n * Measured before this: 14,498 NOT_FOUND reads/day on production, 15% of\n * all Firestore reads, from four orgs that had never had a document. The\n * `--seed-empty` pass in `backfill-org-billing.mjs` repaired those; this is\n * what stops the next org recreating the problem.\n *\n * EMPTY IS THE HONEST VALUE, not a placeholder: a new org has no Stripe\n * relationship, and `readOrgBilling`'s fallback returned `{}` for exactly\n * this case anyway. `writeOrgBilling` merge-sets, so the first real\n * subscription composes with this rather than racing it.\n */\n tx.set(\n db\n .collection('orgs')\n .doc(orgId)\n .collection(ORG_BILLING_SUBCOLLECTION)\n .doc(ORG_BILLING_DOC_ID),\n {},\n )\n tx.set(db.collection('orgs').doc(orgId), {\n name,\n /*\n * The searchable form of `name`, written beside it (AGL-2501).\n *\n * Firestore cannot search a string it has not been given in search\n * form: a prefix range needs the normalized key to ORDER by, and\n * `name` carries case and stray whitespace. Without this the staff\n * organization list can only filter the rows already on screen — ten\n * of them — which stops being a search the moment there are more\n * organizations than a page.\n *\n * Denormalized rather than computed at query time because there is no\n * query-time in Firestore. Every writer of `name` owes this field; the\n * rename in `/api/orgs/settings` is the other one.\n */\n nameLower: nameSearchKey(name),\n // Word-prefix tokens, so the staff search can answer \"contains a word\n // starting with X\" rather than only \"starts with X\" (AGL-2501).\n nameTokens: nameSearchTokens(name),\n // Reversed, so the list's \"ends with\" filter is a prefix range like\n // every other string operator Firestore can answer.\n nameReversed: nameSearchReversed(name),\n slug,\n ownerUid,\n // Stamped once and never mutated — `transferOrgOwnership` moves\n // `ownerUid` and deliberately leaves this alone (AGL-2265). It is what\n // stops \"hand the workspace to an alt account, create another, take it\n // back\" from being a way past the free-workspace ceiling.\n createdByUid: ownerUid,\n hosts: {},\n createdAt: FieldValue.serverTimestamp(),\n updatedAt: FieldValue.serverTimestamp(),\n })\n tx.set(\n db.collection('orgs').doc(orgId).collection('members').doc(ownerUid),\n {\n role: 'owner',\n allHosts: true,\n email: ownerEmail ?? null,\n displayName: ownerDisplayName ?? null,\n joinedAt: FieldValue.serverTimestamp(),\n /*\n * The rules projection, stamped AT CREATION (AGL-1038).\n *\n * Every other membership write reaches `syncOrgAuthProjections`,\n * which recomputes this for the whole roster. This one does not —\n * it is inside the creating transaction, and nothing runs after it\n * — so a brand-new org's owner had no `scopeTokens` at all and the\n * weekly scope-drift detector reported the org from the day it was\n * made until some later membership change happened to heal it.\n *\n * Computed rather than written as a literal, so it cannot disagree\n * with the projection every other path uses.\n */\n scopeTokens: projectMemberScopeTokens({ role: 'owner', allHosts: true }),\n /*\n * The permission projection, stamped here for the same reason and\n * with the same consequence if it is missed.\n *\n * No custom role can exist in an org being created, so the resolver\n * is handed an explicit null and returns the owner's role defaults —\n * which is also what the rules fall back to for a member carrying no\n * map, so a failure to stamp this is invisible rather than a lockout.\n * It is written anyway: an unstamped owner is a row the drift check\n * has to keep explaining.\n */\n resolvedPermissions: projectMemberResolvedPermissions(\n { role: 'owner', allHosts: true },\n null,\n ),\n },\n )\n tx.set(\n db.collection('users').doc(ownerUid).collection('orgs').doc(orgId),\n // The owner reaches every site by definition (AGL-1032).\n { role: 'owner', orgName: name, slug, orgWide: true },\n )\n })\n // Make `{slug}.aglyn.com` resolve (AGL-1136). AGL-1135 removed the\n // `*.aglyn.com` wildcard — it served a real sign-in page on every hostname\n // under the domain — so a workspace subdomain now only works if the domain\n // is attached to the project.\n //\n // AFTER the transaction, and AWAITED. It was `void`, on the reasoning that\n // no workspace should fail to be created because a DNS API was slow — right\n // requirement, wrong mechanism (AGL-1136). On a serverless runtime `void`\n // does not mean \"in the background\", it means \"may never run\": the instance\n // can be frozen the moment the response is flushed. Confirmed twice on this\n // codebase already, on the Stripe org sync and the profile seed.\n //\n // Awaiting cannot fail org creation, and that property comes from the\n // helper, not from the `void` — `attachWorkspaceDomain` swallows every\n // error and returns an outcome rather than throwing. The cost is one HTTP\n // round trip on an operation that already runs a Firestore transaction; the\n // alternative was advertising a workspace URL that 404s.\n //\n // `erase.ts` already awaits the matching detach, which is what made the\n // asymmetry worth looking at.\n await attachWorkspaceDomain(slug)\n // The first entry in the workspace's log (AGL-118). Creation is the one\n // category the activity log never covered — it was assembled by adding\n // calls at mutation points in the console UI, and the acts that bring a\n // top-level object into existence happen out here, in provisioning code no\n // UI mutation point ever reaches. The visible symptom was a customer whose\n // page read as though they had never used the product, because their whole\n // session had been creation.\n await logOrgActivity(\n orgId,\n { uid: ownerUid, email: ownerEmail ?? null },\n 'Created the workspace',\n { type: 'org', id: orgId, name },\n )\n return orgId\n}\n\nexport interface OrgMembershipResolution {\n orgId: string\n member: AglynOrgMember\n /**\n * True only when THIS call provisioned the org, so a caller can report the\n * activation (AGL-2587). `ensureOrgForUser` is the third org-creation door\n * and the only server-side one, and it looked identical from outside to a\n * resolution of an org that already existed — which is why `org_created`\n * counted none of the workspaces it makes. Absent on `resolveOrgMembership`,\n * which never creates anything.\n */\n created?: boolean\n}\n\n/**\n * The signed-in user's membership in one org, or null. When `orgId` is\n * omitted, resolves the user's first org from the reverse index (the\n * single-org case every pre-org account lands in after backfill).\n */\nexport async function resolveOrgMembership(\n uid: string,\n orgId?: string | null,\n): Promise<OrgMembershipResolution | null> {\n const db = firestore()\n let resolved = orgId ?? null\n if (!resolved) {\n const mine = await db\n .collection('users')\n .doc(uid)\n .collection('orgs')\n .limit(1)\n .get()\n resolved = mine.empty ? null : mine.docs[0].id\n }\n if (!resolved) return null\n const memberSnapshot = await db\n .collection('orgs')\n .doc(resolved)\n .collection('members')\n .doc(uid)\n .get()\n if (!memberSnapshot.exists) return null\n return {\n orgId: resolved,\n member: { $id: uid, ...memberSnapshot.data() } as AglynOrgMember,\n }\n}\n\n/**\n * The user's org, creating a personal one on first need (signup flows and\n * pre-backfill accounts): name from the display name or email local part,\n * slug generated with numeric-suffix retries on collision.\n */\nexport async function ensureOrgForUser(\n uid: string,\n profile: { email?: string | null; displayName?: string | null } = {},\n): Promise<OrgMembershipResolution> {\n const existing = await resolveOrgMembership(uid)\n if (existing) return existing\n\n const base =\n profile.displayName?.trim() ||\n profile.email?.split('@')[0]?.trim() ||\n 'workspace'\n const name = base.slice(0, 80)\n let slug = generateOrgSlug(name) || `org-${createResourceUid().slice(0, 8)}`\n for (let attempt = 0; ; attempt += 1) {\n try {\n const orgId = await createOrganization({\n name,\n slug,\n ownerUid: uid,\n ownerEmail: profile.email ?? null,\n ownerDisplayName: profile.displayName ?? null,\n })\n const created = await resolveOrgMembership(uid, orgId)\n if (!created) throw new Error('Org membership missing after create')\n // Marked so the caller can count the activation (AGL-2587).\n return { ...created, created: true }\n } catch (error) {\n if (!(error instanceof OrgSlugTakenError) || attempt >= 4) throw error\n slug = `${slug.slice(0, 26)}-${attempt + 2}`\n if (!isValidOrgSlug(slug)) {\n slug = `org-${createResourceUid().slice(0, 8)}`\n }\n }\n }\n}\n\n/**\n * Changes an org's workspace slug (AGL-236): reserves the new slug and\n * updates the org doc in one transaction, leaving the old reservation as\n * a tombstone (`movedTo`) so existing workspace URLs keep resolving —\n * the middleware redirects them. Reverse-index slugs fan out after.\n * Throws `OrgSlugTakenError` only when another org ACTIVELY holds the new\n * slug — tombstones are claimable (AGL-585). Slug validity/authorization\n * are the API route's job.\n */\nexport async function changeOrgSlug(\n orgId: string,\n newSlug: string,\n): Promise<{ previousSlug: string | null }> {\n const db = firestore()\n let previousSlug: string | null = null\n await db.runTransaction(async (tx) => {\n const orgRef = db.collection('orgs').doc(orgId)\n const orgSnapshot = await tx.get(orgRef)\n if (!orgSnapshot.exists) throw new Error(`Unknown org: ${orgId}`)\n previousSlug = (orgSnapshot.get('slug') as string | undefined) ?? null\n if (previousSlug === newSlug) return\n const reservation = await tx.get(db.collection('orgSlugs').doc(newSlug))\n const held = reservation.exists\n ? (reservation.data() as {\n orgId?: unknown\n movedTo?: unknown\n reservedUntil?: unknown\n })\n : undefined\n // Claimable when free, own (moving back), a tombstone another org renamed\n // away from (AGL-585), or an unverified signup's reservation that ran out\n // (AGL-2585) — abandoned slugs are never reserved forever. Only another\n // org's ACTIVE slug blocks the change, and a lapsed reservation whose\n // holder has since verified is still active, which the auth record decides.\n if (\n !isSlugReservationClaimable(held, orgId) ||\n (held?.orgId !== orgId &&\n isSlugReservationLapsed(held) &&\n (await lapsedReservationIsStillHeld(held)))\n ) {\n throw new OrgSlugTakenError(newSlug)\n }\n tx.set(db.collection('orgSlugs').doc(newSlug), { orgId })\n tx.set(\n orgRef,\n { slug: newSlug, updatedAt: FieldValue.serverTimestamp() },\n { merge: true },\n )\n if (previousSlug) {\n tx.set(db.collection('orgSlugs').doc(previousSlug), {\n orgId,\n movedTo: newSlug,\n renamedAt: FieldValue.serverTimestamp(),\n })\n }\n })\n // Attach the new subdomain, and deliberately KEEP the old one (AGL-1136).\n // The previous slug's tombstone 308s to the new one, and a redirect can\n // only run on a hostname that still resolves — detaching it here would\n // break the very redirect the tombstone exists to serve.\n // Awaited for the same reason as the create path above (AGL-1136): a\n // `void` here is not a background task, it is a coin flip.\n await attachWorkspaceDomain(newSlug)\n // Reverse index carries the slug for the switcher display.\n const members = await listOrgMembers(orgId)\n const batch = db.batch()\n for (const member of members) {\n batch.set(\n db.collection('users').doc(member.$id).collection('orgs').doc(orgId),\n { slug: newSlug },\n { merge: true },\n )\n }\n await batch.commit()\n return { previousSlug }\n}\n\n/**\n * Host → org resolution via the server-written `hostIndex` mirror.\n *\n * `React.cache`-deduped PER REQUEST (AGL-1302): one tenant render resolved\n * this hop up to five times — org billing, datasets, plugin installs, realm\n * installs and the publish-schedule executor each re-read the same\n * `hostIndex/{hostId}` doc. Per-request memoization is zero-staleness by\n * construction; outside a React render (route handlers, jest) `cache` is a\n * pass-through, so nothing changes for the console's authz paths.\n */\nexport const resolveOrgIdForHost = cache(\n async (hostId: string): Promise<string | null> => {\n const snapshot = await firestore().collection('hostIndex').doc(hostId).get()\n const orgId = snapshot.data()?.['orgId']\n return typeof orgId === 'string' ? orgId : null\n },\n)\n\n/**\n * The org doc itself — billing, plan, entitlements and suspension (the\n * shape the legacy tenants/{uid} doc carried; orgs are the only billing\n * source since AGL-238). Null when the doc is missing.\n */\n/**\n * `React.cache`-deduped per request like {@link resolveOrgIdForHost}\n * (AGL-1302). NOTE: within one render every caller receives the SAME object\n * — treat it as read-only, as every current caller already does.\n */\nexport const getOrgDoc = cache(\n async (orgId: string): Promise<Partial<AglynOrganization> | null> => {\n const snapshot = await firestore().collection('orgs').doc(orgId).get()\n return snapshot.exists\n ? ({ $id: snapshot.id, ...snapshot.data() } as Partial<AglynOrganization>)\n : null\n },\n)\n\n/**\n * Billing/entitlement source for a host (AGL-238): the owning org's doc\n * via the hostIndex mirror. Null for unindexed hosts — callers treat that\n * as the pre-billing fail-open (every feature on), the same contract the\n * legacy tenants/{uid} read had.\n */\nexport async function getOrgForHost(hostId: string): Promise<{\n orgId: string\n org: Partial<AglynOrganization>\n} | null> {\n const orgId = await resolveOrgIdForHost(hostId)\n if (!orgId) return null\n const org = await getOrgDoc(orgId)\n return org ? { orgId, org } : null\n}\n\n/**\n * The raw host doc, `React.cache`-deduped per request like\n * {@link resolveOrgIdForHost}. Null when missing. Added for AGL-1506 so a\n * dispatcher that already pays this read for the plugin deny-list can also\n * feed the host's `suspendedAt` family to the lockdown verdict without a\n * second get. Same read-only contract as {@link getOrgDoc}.\n */\nexport const getHostDocAdmin = cache(\n async (hostId: string): Promise<Record<string, unknown> | null> => {\n const snapshot = await firestore().collection('hosts').doc(hostId).get()\n return snapshot.exists ? (snapshot.data() as Record<string, unknown>) : null\n },\n)\n\n/**\n * The host's per-site plugin deny-list (AGL-1014), for API dispatch and any\n * other server consumer of `resolveHostEnabledPlugins`. Rides\n * {@link getHostDocAdmin}'s request-cached read. Fail-open to [] — an absent\n * host doc or field means \"nothing disabled here\", never a lockout.\n */\nexport const getHostDisabledPlugins = cache(\n async (hostId: string): Promise<string[]> => {\n const disabled = (await getHostDocAdmin(hostId))?.['disabledPlugins']\n return Array.isArray(disabled) ? disabled.map(String) : []\n },\n)\n\n/**\n * Billing/entitlement source for a user without host context (account-\n * level APIs): the explicit workspace org when given, else the first org\n * from the reverse index. Null for accounts with no org yet.\n */\nexport async function getOrgForUser(\n uid: string,\n orgId?: string | null,\n): Promise<{\n orgId: string\n org: Partial<AglynOrganization>\n member: AglynOrgMember\n} | null> {\n const membership = await resolveOrgMembership(uid, orgId)\n if (!membership) return null\n const org = await getOrgDoc(membership.orgId)\n return org\n ? { orgId: membership.orgId, org, member: membership.member }\n : null\n}\n\n/**\n * Org-scoped data collection for a host (AGL-237): datasets, contacts and\n * contactSegments live on the org so every host shares them.\n *\n * The pre-migration fallback to `hosts/{hostId}/{name}` is GONE (AGL-1050).\n * The AGL-1040 backfill counted the docs still on it in production and\n * found zero, so it was dead code rather than a migration — and a second\n * storage path that can still be WRITTEN is a second boundary to enforce\n * forever, which undoes the premise of scoped sharing: one home per\n * resource plus an explicit scope.\n *\n * A host with no org is now an error rather than a silent write into a\n * collection nothing reads. Every host has an org; `hostIndex` is written\n * by `registerOrgHost` at creation.\n */\n/**\n * The org-owned collections a host reads in its own context. Every one of\n * these carries `visibleTo` (AGL-1037) and so must go through\n * `scopedToHost` on any Admin-SDK path — `media` and `mediaFolders` were\n * added for the export route (AGL-1046), which had been reading the\n * legacy host path and exporting nothing at all.\n */\nexport type OrgDataCollection =\n | 'datasets'\n | 'contacts'\n | 'contactSegments'\n // A saved Contacts view, resolved by the dynamic-list sweep the way a\n // segment is (AGL-2617).\n | 'crmViews'\n // A lead, keyed by `personKey` and scoped by consent group like a contact\n // (AGL-3275) — see `host-visitor-records.ts` for why the host path it\n // replaces could not serve a multi-brand org at all.\n | 'leads'\n | 'lists'\n | 'media'\n | 'mediaFolders'\n\nexport async function orgDataCollectionForHost(\n hostId: string,\n name: OrgDataCollection,\n): Promise<FirebaseFirestore.CollectionReference> {\n const orgId = await resolveOrgIdForHost(hostId)\n if (!orgId) {\n throw new Error(`Host ${hostId} has no org — cannot resolve ${name}`)\n }\n return firestore().collection('orgs').doc(orgId).collection(name)\n}\n\n/**\n * Narrows an org-scoped collection to what ONE host may see (AGL-1039).\n *\n * The Admin SDK does not evaluate Firestore rules, so AGL-1041's\n * `visibleTo.hasAny(...)` protects the console and nothing else — every\n * server read has to filter for itself or a client site can render another\n * client's data. Use this instead of the bare collection ref anywhere a\n * request is being served in the context of a single host.\n *\n * Only the ORG path is filtered — but no longer because of the legacy\n * `hosts/{hostId}/…` fallback, which AGL-1050 removed on both the server\n * (above) and the client. What survives it is the reason stated at the\n * check itself: callers may hand this helper a ref they built themselves,\n * and a host-library ref must never be filtered, since its docs carry no\n * `visibleTo` and the filter would match nothing and blank the site.\n */\nexport function scopedToHost(\n ref: FirebaseFirestore.CollectionReference,\n hostId: string,\n): FirebaseFirestore.Query {\n // The org-path check is retained even though AGL-1050 removed the host\n // fallback: this helper is also handed refs by callers that build their\n // own paths, and a host-library ref must never be filtered — its docs\n // carry no `visibleTo`, so the filter would match nothing.\n const orgScoped = ref.parent?.parent?.id === 'orgs'\n if (!orgScoped) return ref\n return ref.where(\n 'visibleTo',\n 'array-contains-any',\n scopeTokensForHost(hostId),\n )\n}\n\n/**\n * `orgDataCollectionForHost` + `scopedToHost` in one call — the form every\n * host-context read should use. Returns the collection ref too, for the\n * writes and `doc()` lookups a Query cannot express.\n */\nexport async function orgDataQueryForHost(\n hostId: string,\n name: OrgDataCollection,\n): Promise<{\n ref: FirebaseFirestore.CollectionReference\n query: FirebaseFirestore.Query\n}> {\n const ref = await orgDataCollectionForHost(hostId, name)\n return { ref, query: scopedToHost(ref, hostId) }\n}\n\n/**\n * Server-side permission check (AGL-243): the member's org-role defaults\n * refined by their custom role doc (one read, only when assigned). API\n * routes call this before privileged mutations.\n */\n/**\n * The member's FULL granular permission set, custom role and per-member\n * overrides applied (AGL-2350).\n *\n * `memberHasOrgPermission` below is the single-permission form and now\n * delegates here, so the two cannot answer differently. Split out because\n * `resolveOrgPermissions` in `libs/tenant/runtime` needs the whole set to\n * project onto the legacy flag map that the marketplace install and publish\n * gates read — it previously derived those flags from the built-in role tier\n * alone, which silently ignored both refinements.\n *\n * One conditional read, only when a custom role is actually assigned. A\n * dangling `roleId` resolves to `null` and falls back to the role defaults\n * rather than denying, matching what the console hook does with the same\n * dangling id — a deleted role must not lock a member out of surfaces their\n * base role allows.\n */\nexport async function resolveMemberOrgPermissions(\n orgId: string,\n member: Partial<AglynOrgMember> | null | undefined,\n): Promise<Record<OrgPermission, boolean>> {\n let customRole: AglynOrgCustomRole | null = null\n if (member?.roleId) {\n const snapshot = await firestore()\n .collection('orgs')\n .doc(orgId)\n .collection('roles')\n .doc(member.roleId)\n .get()\n customRole = snapshot.exists\n ? (snapshot.data() as AglynOrgCustomRole)\n : null\n }\n return resolveOrgPermissions(member, customRole)\n}\n\nexport async function memberHasOrgPermission(\n orgId: string,\n member: Partial<AglynOrgMember> | null | undefined,\n permission: OrgPermission,\n): Promise<boolean> {\n if (!member) return false\n return (await resolveMemberOrgPermissions(orgId, member))[permission]\n}\n\n/**\n * May this member hold a catalog permission on this site (AGL-2927,\n * AGL-2984)?\n *\n * The one resolver a door calls for a key a plugin declared with host-role\n * defaults, so the two membership axes cannot be answered differently by two\n * routes. An org-wide member is decided by the org catalog through\n * `resolveMemberOrgPermissions` — custom role and overrides applied, one\n * conditional read. A site collaborator is decided by the host role they hold\n * on the site the request NAMES, refined by the per-site toggle on their\n * member document; a collaborator whose request names no site is refused,\n * because there is no host role to read a default from and omitting the site\n * must not be a way around the toggle.\n *\n * `hostId` is whatever the body carried, trimmed by the caller or not — an\n * empty string and `undefined` both mean \"no site named\".\n */\nexport async function memberHasPermissionOnHost(\n orgId: string,\n hostId: string | null | undefined,\n member: Partial<AglynOrgMember> | null | undefined,\n permission: OrgPermission,\n): Promise<boolean> {\n if (!member) return false\n if (isOrgWideMember(member)) {\n return (await resolveMemberOrgPermissions(orgId, member))[permission] === true\n }\n const site = typeof hostId === 'string' ? hostId.trim() : ''\n return resolveCollaboratorHostPermissions(member, site)?.[permission] ?? false\n}\n\n/**\n * The 403 a door sends when `memberHasPermissionOnHost` says no: the same\n * customer-safe shape the doors' other refusals use — one sentence, a\n * `reason` a client can branch on — naming the permission by its catalog\n * label so the reader can find it on the Team page, and who to ask.\n */\nexport function permissionRefusal(permission: OrgPermission): Response {\n return Response.json(\n {\n error: `Your role does not include \"${orgPermissionLabel(permission)}\" — ask an organization admin`,\n reason: 'permission',\n permission,\n },\n { status: 403 },\n )\n}\n\n/**\n * An org-wide member's verdict for every key PLUGINS declared into the\n * catalog, read fresh (AGL-2929, AGL-2984): what `memberHasPermissionOnHost`\n * answers for them with no site named, as a whole map. `null` for a uid with\n * no member document and for a site collaborator, whose per-site keys are\n * decided per site by `setHostPermissions` and have no org-level verdict to\n * compare. Read on either side of a membership write, it is how the members\n * route tells which declared keys the write moved.\n */\nexport async function resolveMemberPluginPermissionsOnOrg(\n orgId: string,\n uid: string,\n): Promise<Record<string, boolean> | null> {\n const snapshot = await firestore()\n .collection('orgs')\n .doc(orgId)\n .collection('members')\n .doc(uid)\n .get()\n if (!snapshot.exists) return null\n const member = { $id: uid, ...snapshot.data() } as AglynOrgMember\n if (!isOrgWideMember(member)) return null\n const granted = await resolveMemberOrgPermissions(orgId, member)\n return Object.fromEntries(\n pluginOrgPermissionKeys().map((key) => [key, granted[key] === true]),\n )\n}\n\n/** A collaborator's per-site verdict on either side of a toggle write. */\nexport interface HostPermissionsWrite {\n /** The verdict before the write; `null` when the member had no access to the site. */\n before: Record<string, boolean> | null\n after: Record<string, boolean>\n}\n\n/**\n * Set a collaborator's per-site toggles (AGL-2927, AGL-2984) and re-project.\n *\n * A merge on the nested map, so the member's other sites and every other\n * field stay untouched; keys no plugin declared as per-site are dropped\n * rather than stored, and a non-boolean is ignored rather than coerced.\n * Re-projection is scoped to the one host whose `memberPermissions` changed.\n *\n * The verdict before the write comes back beside the one after it, because\n * the caller records one activity row per key that moved (AGL-2929) and a\n * toggle set to the value it already had is not a change.\n */\nexport async function setHostPermissions(options: {\n orgId: string\n uid: string\n hostId: string\n permissions: Partial<Record<string, unknown>>\n}): Promise<HostPermissionsWrite> {\n const { orgId, uid, hostId } = options\n const keys = hostPermissionKeys()\n const accepted: Record<string, boolean> = {}\n for (const key of keys) {\n const value = options.permissions[key]\n if (typeof value === 'boolean') accepted[key] = value\n }\n const ref = firestore().collection('orgs').doc(orgId).collection('members').doc(uid)\n const stored = { $id: uid, ...(await ref.get()).data() } as AglynOrgMember\n const before = resolveCollaboratorHostPermissions(stored, hostId)\n await ref.set({ hostPermissions: { [hostId]: accepted } }, { merge: true })\n await syncOrgAuthProjections(orgId, hostId)\n const member = { $id: uid, ...(await ref.get()).data() } as AglynOrgMember\n return {\n before,\n after:\n resolveCollaboratorHostPermissions(member, hostId) ??\n Object.fromEntries(keys.map((key) => [key, false])),\n }\n}\n\nexport async function listOrgMembers(\n orgId: string,\n): Promise<AglynOrgMember[]> {\n const snapshot = await firestore()\n .collection('orgs')\n .doc(orgId)\n .collection('members')\n .get()\n return snapshot.docs.map(\n (doc) => ({ $id: doc.id, ...doc.data() }) as AglynOrgMember,\n )\n}\n\n/**\n * The custom role documents this roster actually references, read once each.\n *\n * A roster of hundreds shares a handful of roles, so this is bounded by the\n * number of DISTINCT `roleId`s and not by the member count.\n *\n * A role id that resolves to nothing is left ABSENT rather than recorded as\n * an empty role. The two happen to reach the same verdict today —\n * `resolveOrgPermissions` skips a key whose value is not a boolean, so an\n * empty map changes nothing — but they are different claims, and only one of\n * them is true: a dangling id means the lookup MISSED, not that a role\n * granting nothing was found. Recording the miss honestly is what keeps the\n * fallback correct if that resolver ever treats an empty map as a revocation,\n * which is what its own type comment already says it does.\n */\nasync function loadOrgCustomRoles(\n orgId: string,\n members: readonly AglynOrgMember[],\n): Promise<Map<string, AglynOrgCustomRole>> {\n const roleIds = [\n ...new Set(\n members\n .map((member) => member.roleId)\n .filter((roleId): roleId is string => typeof roleId === 'string' && !!roleId),\n ),\n ]\n const rolesRef = firestore()\n .collection('orgs')\n .doc(orgId)\n .collection('roles')\n const found = new Map<string, AglynOrgCustomRole>()\n await Promise.all(\n roleIds.map(async (roleId) => {\n const snapshot = await rolesRef.doc(roleId).get()\n if (snapshot.exists) {\n found.set(roleId, snapshot.data() as AglynOrgCustomRole)\n }\n }),\n )\n return found\n}\n\n/**\n * How the projections are written: REPLACED, not merged (AGL-2985).\n *\n * `{ merge: true }` merges a map key by key and keeps every key the new map\n * does not name, so a projection written that way could add a member and\n * never take one away. A collaborator whose site access was revoked, or a\n * member removed from the organization, drops out of the recomputed\n * `memberRoles` — and kept their old key on the host document, which is the\n * one thing the Firestore rules read to let a person edit and publish a\n * site. `memberPermissions` kept their AI verdict the same way, and the\n * plugin half of a member's `resolvedPermissions` would keep a withdrawn\n * grant `true` (AGL-2974).\n *\n * `mergeFields` overwrites exactly the listed fields whole and leaves the\n * rest of the document untouched, which is all the merge was for. Every\n * listed map is recomputed from the complete roster on every write, so\n * replacing it loses nothing a merge would have kept correctly.\n */\nconst HOST_PROJECTION_WRITE: FirebaseFirestore.SetOptions = {\n mergeFields: ['orgId', 'memberRoles', 'memberPermissions', 'updatedAt'],\n}\nconst MEMBER_PROJECTION_WRITE: FirebaseFirestore.SetOptions = {\n mergeFields: ['scopeTokens', 'resolvedPermissions'],\n}\n\n/**\n * Recomputes the denormalized authorization projections after a membership\n * change: `memberRoles` and `memberPermissions` on every host the org owns\n * (or one host when given), and `scopeTokens` + `resolvedPermissions` on\n * every member doc.\n *\n * The rules resolve a request from these reads — the host doc for host\n * content (docs/MULTI_TENANT_FIRESTORE.md §5), the member doc for scoped\n * org resources (AGL-1038) — so this is what makes a membership effective.\n * They live here, in one writer called by every mutation below, because a\n * grant path that updates one projection and forgets another silently over-\n * or under-grants.\n *\n * Everything is recomputed for the whole roster rather than the changed\n * member: the roster is already loaded for `memberRoles`, and a full pass\n * self-heals rows that an earlier partial failure left stale.\n *\n * ## `resolvedPermissions`, and why the rules need it denormalized\n *\n * Security rules cannot resolve a custom role. `member.roleId` points at\n * `orgs/{orgId}/roles/{roleId}`, and reproducing the three-layer precedence\n * (per-member beats custom role beats role default) in CEL takes a second\n * cross-document get() plus a correct handling of a dangling id — where a\n * naive version over-denies and locks out paying customers. So the rules read\n * the ANSWER instead of the inputs, which is the same trade `scopeTokens`\n * already makes for a reason the rules language shares: it has no `.map()`\n * either.\n *\n * The map is `projectMemberResolvedPermissions`: `resolveOrgPermissions`'\n * own catalog verdict, plus the plugin-declared keys a custom role or an\n * override set explicitly (AGL-2974), so the rules and every\n * server route are reading one resolver's verdict rather than two\n * implementations of it.\n *\n * ONE READ PER DISTINCT ROLE, not per member: an org assigns a handful of\n * custom roles across a roster that can run to hundreds, and resolving each\n * member independently would re-read the same few documents once each.\n */\nexport async function syncOrgAuthProjections(\n orgId: string,\n hostId?: string,\n): Promise<void> {\n const db = firestore()\n const orgRef = db.collection('orgs').doc(orgId)\n const members = await listOrgMembers(orgId)\n const customRoles = await loadOrgCustomRoles(orgId, members)\n const hostIds = hostId\n ? [hostId]\n : Object.keys(\n ((await orgRef.get()).data() as AglynOrganization | undefined)\n ?.hosts ?? {},\n )\n const writes: Array<\n [FirebaseFirestore.DocumentReference, object, FirebaseFirestore.SetOptions]\n > = [\n ...hostIds.map(\n (id) =>\n [\n db.collection('hosts').doc(id),\n {\n orgId,\n memberRoles: projectHostMemberRoles(members, id),\n // Each member's per-site permission verdicts ON this site\n // (AGL-2927), beside the role they derive from, so a reader of\n // the host document has both answers from the one get it\n // already does.\n memberPermissions: projectHostMemberPermissions(\n members,\n id,\n customRoles,\n ),\n updatedAt: FieldValue.serverTimestamp(),\n },\n HOST_PROJECTION_WRITE,\n ] as [\n FirebaseFirestore.DocumentReference,\n object,\n FirebaseFirestore.SetOptions,\n ],\n ),\n ...members.map(\n (member) =>\n [\n orgRef.collection('members').doc(member.$id),\n {\n scopeTokens: projectMemberScopeTokens(member),\n resolvedPermissions: projectMemberResolvedPermissions(\n member,\n // `?? null`, never `?? undefined`: a member whose `roleId`\n // points at a DELETED role must resolve to their role\n // defaults, which is what the resolver does with an explicit\n // null and what every server route already does with the same\n // dangling id. Leaving it undefined would be the same value,\n // but the null says the lookup happened and missed.\n member.roleId ? (customRoles.get(member.roleId) ?? null) : null,\n ),\n },\n MEMBER_PROJECTION_WRITE,\n ] as [\n FirebaseFirestore.DocumentReference,\n object,\n FirebaseFirestore.SetOptions,\n ],\n ),\n ]\n // Hosts alone rarely approached the 500-write batch cap; hosts plus the\n // whole roster can, so commit in chunks rather than throwing on big orgs.\n for (let i = 0; i < writes.length; i += FIRESTORE_BATCH_LIMIT) {\n const batch = db.batch()\n for (const [ref, data, options] of writes.slice(\n i,\n i + FIRESTORE_BATCH_LIMIT,\n )) {\n batch.set(ref, data, options)\n }\n await batch.commit()\n }\n}\n\n/**\n * @deprecated Renamed to `syncOrgAuthProjections` (AGL-1038) now that it\n * also writes member `scopeTokens`. Kept as an alias for out-of-tree\n * callers; delete once none remain.\n */\nexport const syncHostMemberRoles = syncOrgAuthProjections\n\n/** What an org activity entry points at; `id` lets detail views filter. */\nexport interface OrgActivityTarget {\n /**\n * `host` and `subscription` are the two facts about a workspace that no\n * host feed can hold (AGL-118).\n *\n * A site's own log lives at `hosts/{hostId}/activity` and is destroyed with\n * the site — `eraseHost` recursive-deletes the whole tree — so \"this site\n * was deleted\" written there is an entry with no reader by construction.\n * A subscription belongs to no single site at all. Both are org-level\n * events, and this is the only feed that outlives them.\n */\n type:\n | 'org' | 'member' | 'invite' | 'host' | 'subscription'\n // The CRM's records (AGL-2634), written by the plugin's server routes\n // for an act performed at the ORGANIZATION level — a deal moved from the\n // org board, two contacts merged over every site, a bulk bar's action —\n // where there is no one site's feed to hold it. The org feed's presenter\n // links them into the org-level hub.\n | 'contact' | 'company' | 'deal' | 'lead' | 'task'\n // AI rows (AGL-2929): a generation job, the resources it produced, and\n // a custom role whose AI permission moved. The job's outputs are org\n // events with a resource target because the job is org-scoped and the\n // org feed is the one place every output of one job is listed together;\n // the host feed gets its own copy of the host-scoped ones.\n | 'aiJob' | 'role'\n | 'screen' | 'layout' | 'component' | 'template' | 'workflow' | 'content'\n // A theme proposal a job produced (AGL-2938), filed under the site's\n // theme the way the host feed files a saved theme.\n | 'theme'\n // A plugin's own resource, `pluginId:noun` (AGL-2978). Plugins file\n // their rows under their own namespace, so this list names none of them.\n | PluginActivityTargetType\n id?: string\n name?: string\n /** Present on a generated screen output so the deep link can hit the exact version. */\n versionId?: string\n}\n\n/**\n * Org-level counterpart to the host activity log (AGL-118): fire-and-\n * forget append to `orgs/{orgId}/activity` from the org API routes. Never\n * throws — an audit miss must not break the mutation that triggered it.\n * Admin-SDK-only, like the rest of this file; the rules deny client writes.\n */\nexport async function logOrgActivity(\n orgId: string,\n /**\n * `uid` is nullable because some org events HAVE no actor (AGL-118). Stripe\n * cancels a subscription after a month of failed retries with nobody\n * present, and the honest record of that says so. Naming the last person\n * who touched billing instead would put a real name on an act nobody\n * performed — and `actorId` is a filterable field, so the invented\n * attribution would then show up under that person when somebody asks what\n * they have done.\n */\n actor: { uid: string | null; email?: string | null },\n action: string,\n target: OrgActivityTarget,\n): Promise<void> {\n await firestore()\n .collection('orgs')\n .doc(orgId)\n .collection('activity')\n .add({\n actorId: actor.uid ?? null,\n actorEmail: actor.email ?? null,\n action,\n target: {\n type: target.type,\n ...(target.id ? { id: target.id } : {}),\n ...(target.name ? { name: target.name } : {}),\n ...(target.versionId ? { versionId: target.versionId } : {}),\n },\n createdAt: FieldValue.serverTimestamp(),\n })\n .catch(() => undefined)\n}\n\n/** What a host activity entry points at. Mirrors `HostActivityTarget`. */\nexport interface HostActivityTarget {\n type:\n | 'host' | 'screen' | 'layout' | 'theme' | 'media' | 'content' | 'variable'\n | 'function' | 'workflow' | 'member' | 'component' | 'template'\n // The CRM's records (AGL-2622), written by the plugin's server routes —\n // a contact added by hand, a lead converted — and read back by the\n // feed's presenter as links into the hub.\n | 'contact' | 'company' | 'deal' | 'lead'\n id?: string\n name?: string\n versionId?: string\n}\n\n/**\n * Append to `hosts/{hostId}/activity` with the ADMIN SDK (AGL-118).\n *\n * The host log's twin of {@link logOrgActivity}, and the beginning of the\n * migration off the browser. Every entry in this collection has been written\n * by the client since the log existed, which makes it an audit trail its\n * subject can decline to write: three template surfaces created screens,\n * layouts and components while calling no logger at all, and nothing noticed\n * for months because a log that is missing an entry looks exactly like a\n * person who did nothing.\n *\n * A route that already authenticated the caller has the two things the client\n * cannot be trusted for — a VERIFIED uid, and the certainty that the write it\n * is recording actually happened, because it performed it. So an entry from\n * here is worth more than the one it replaces, not merely more reliable.\n *\n * Never throws, for the reason the client logger never throws: an audit miss\n * must not turn a successful create into a failed request. It is `await`ed\n * rather than floated because a serverless response ending cancels in-flight\n * work, which would make the drop the common case rather than the rare one.\n */\nexport async function logHostActivity(\n hostId: string,\n actor: HostActivityActor,\n action: string,\n target: HostActivityTarget,\n): Promise<void> {\n await firestore()\n .collection('hosts')\n .doc(hostId)\n .collection('activity')\n .add({\n actorId: actor.uid,\n actorEmail: actor.email ?? null,\n // A key's entry names the key (AGL-2632); a person's carries no such\n // field, so the two are told apart by its presence.\n ...(actor.apiKeyName ? { apiKeyName: actor.apiKeyName } : {}),\n action,\n target: {\n type: target.type,\n ...(target.id ? { id: target.id } : {}),\n ...(target.name ? { name: target.name } : {}),\n ...(target.versionId ? { versionId: target.versionId } : {}),\n },\n createdAt: FieldValue.serverTimestamp(),\n })\n .catch(() => undefined)\n}\n\n/**\n * A collaborator seat refusal, raised from INSIDE the grant transaction\n * (AGL-2068).\n *\n * An exception rather than a return value because it has to travel out of\n * `upsertOrgMember` / `grantHostAccess`, whose contract is \"make it so\" and\n * which four routes already call as a bare `await`. Returning a verdict would\n * have let every existing call site ignore it silently, which is the shape of\n * the bug being fixed.\n */\nexport class CollaboratorSeatLimitError extends Error {\n readonly hostId: string\n readonly limit: number\n readonly upgradeRequired: boolean\n readonly addonPriceUsd: number | null\n /**\n * Seats this site holds ABOVE `limit` (AGL-2439). Non-zero means the site\n * is GRANDFATHERED: those collaborators keep their access, and the refusal\n * is only of the NEXT one. Carried on the error so the refusal copy can say\n * that rather than letting the admin read a 403 as \"somebody was removed\".\n */\n readonly retainedOverCap: number\n constructor(\n hostId: string,\n quota: {\n limit: number\n upgradeRequired: boolean\n addonPriceUsd: number | null\n retainedOverCap?: number\n },\n ) {\n super(collaboratorSeatMessage(quota))\n this.name = 'CollaboratorSeatLimitError'\n this.hostId = hostId\n this.limit = quota.limit\n this.upgradeRequired = quota.upgradeRequired\n this.addonPriceUsd = quota.addonPriceUsd\n this.retainedOverCap = Math.max(0, quota.retainedOverCap ?? 0)\n }\n}\n\n/**\n * The two refusal strings, verbatim from `/api/hosts/members` where they have\n * always lived. Kept byte-identical on purpose: this is now the ONE place\n * they are produced, and any client or spec matching \"Collaborator limit\n * reached\" must keep matching.\n */\nfunction collaboratorSeatMessage(quota: {\n limit: number\n upgradeRequired: boolean\n addonPriceUsd: number | null\n}): string {\n return quota.upgradeRequired\n ? `Collaborator limit reached (${quota.limit}) — upgrade ` +\n 'your plan to add more collaborators'\n : `Collaborator seats full (${quota.limit}) — add seats for ` +\n `$${quota.addonPriceUsd}/mo each from Billing`\n}\n\n/**\n * Everyone who could be holding a collaborator seat in this org: the whole\n * roster plus every un-accepted invite (AGL-2068).\n *\n * Both collections in full, rather than a `where('hostAccess.X','!=',null)`:\n * the predicate that decides a seat is `isOrgWideMember`, which reads three\n * fields and treats an ABSENT `allHosts` as org-wide. Firestore cannot\n * express \"field absent\" in a filter, so a query-side count gets the legacy\n * rows wrong in the direction that over-charges. These collections are\n * bounded by the very caps being enforced, so reading them whole is cheap and\n * — inside a transaction — is exactly the lock that serialises concurrent\n * grants.\n */\nasync function readSeatEntries(\n orgRef: FirebaseFirestore.DocumentReference,\n read: (query: FirebaseFirestore.Query) => Promise<FirebaseFirestore.QuerySnapshot>,\n): Promise<CollaboratorSeatEntry[]> {\n const [members, invites] = await Promise.all([\n read(orgRef.collection('members')),\n read(orgRef.collection('invites').where('acceptedAt', '==', null)),\n ])\n return [\n // The uid is the DOCUMENT ID on the roster and is not a field, so it has\n // to be put back or every legacy row without a mirrored email identifies\n // nobody and silently stops consuming its seat.\n ...members.docs.map(\n (doc) => ({ uid: doc.id, ...doc.data() }) as CollaboratorSeatEntry,\n ),\n ...invites.docs.map((doc) => doc.data() as CollaboratorSeatEntry),\n ]\n}\n\n/**\n * The hard cap itself, evaluated against the POST-state and inside the same\n * transaction that performs the grant (AGL-2068).\n *\n * A create-time quota that reads, decides, and then writes is not a cap —\n * this repo has now relearned that three times in one day (AGL-1390 laundering\n * a count, AGL-2057 the assist cap, AGL-2063 the site limit): N concurrent\n * requests all read the same pre-count, all pass, and all land. Doing the\n * read through the transaction is what fixes it. Firestore tracks the read\n * SET, so a second grant that read the same roster cannot commit — it retries,\n * re-reads a roster that now holds the first grant, and refuses.\n *\n * Only NEWLY granted hosts are charged. Changing an existing collaborator's\n * role on a site they already reach re-writes the same seat, and refusing that\n * would strand an over-limit org unable to even demote its way back.\n *\n * THE CAP IS PER SITE AND SO IS THE QUESTION (AGL-2439). This calls\n * `checkHostCollaboratorQuota(org, hostId, used)` and not\n * `checkSeatQuota(org, 'members', used)`: since AGL-2439 the purchased\n * quantity is an org-level POOL and the latter deliberately answers the\n * PLAN's cap with no pool in it. Passing the plan cap here would refuse a\n * site the seats the org bought and assigned to it.\n *\n * THE GRANDFATHER LIVES HERE, in what this function does NOT do. It runs on\n * the GRANT path only — `newlyScopedHosts` is empty for an existing seat — so\n * a site already above its corrected cap keeps every collaborator it has and\n * is merely refused the next one. There is no sweep, no reconciliation and no\n * revocation anywhere in this file, and none may be added: the cap binds\n * ALLOCATION, never ACCESS. `quota.retainedOverCap` is how many seats a site\n * is over by, carried on the refusal so the console can say it out loud\n * rather than leaving the customer to infer it from a rejected click.\n */\nasync function assertCollaboratorSeats(options: {\n orgRef: FirebaseFirestore.DocumentReference\n org: Partial<AglynOrgBilling>\n hostIds: string[]\n self: {\n uid?: string | null\n email?: string | null\n emails?: readonly (string | null | undefined)[] | null\n }\n read: (query: FirebaseFirestore.Query) => Promise<FirebaseFirestore.QuerySnapshot>\n}): Promise<void> {\n const { orgRef, org, hostIds, self, read } = options\n if (!hostIds.length) return\n const entries = await readSeatEntries(orgRef, read)\n for (const hostId of hostIds) {\n const used = countCollaboratorSeats(entries, hostId, self)\n const quota = checkHostCollaboratorQuota(org, hostId, used)\n if (!quota.allowed) throw new CollaboratorSeatLimitError(hostId, quota)\n }\n}\n\n/**\n * Which hosts a membership is about to reach for the FIRST time as a scoped\n * collaborator — the set the seat cap is charged for.\n *\n * Empty when the resulting membership is org-wide: a manager already reaches\n * every host and pays for it with a manager seat.\n */\nfunction newlyScopedHosts(options: {\n role: OrgRole | undefined\n allHosts: boolean\n hostAccess: Record<string, unknown>\n existing: Partial<AglynOrgMember> | undefined\n}): string[] {\n const { role, allHosts, hostAccess, existing } = options\n if (isOrgWideMember({ role, allHosts, hostAccess } as Partial<AglynOrgMember>)) {\n return []\n }\n const prior = (existing?.hostAccess ?? {}) as Record<string, unknown>\n return Object.keys(hostAccess).filter((hostId) => !prior[hostId])\n}\n\n/**\n * Turn a seat refusal into the 403 the four admitting routes return, or null\n * when the error is something else and must keep propagating to the 500.\n *\n * Lives here beside `emailUnverifiedResponse` and `lockdownRefusal` so a\n * route's catch block is one line and cannot accidentally mask a real fault.\n */\nexport function collaboratorSeatRefusalResponse(\n error: unknown,\n): Response | null {\n if (!(error instanceof CollaboratorSeatLimitError)) return null\n return Response.json(\n {\n error: error.message,\n code: 'collaborator_seat_limit',\n limit: error.limit,\n upgradeRequired: error.upgradeRequired,\n // AGL-2439: how many seats this site is over by. NOBODY was removed —\n // the client renders this as retention, not as a loss.\n retainedOverCap: error.retainedOverCap,\n },\n { status: 403 },\n )\n}\n\n/**\n * The same cap, asked BEFORE anything is written (AGL-2068).\n *\n * Not the enforcement — the transaction inside the grant is. This exists so\n * the two doors that only ever create an INVITE (`/api/hosts/members` for an\n * address with no account yet, and `/api/orgs/invites` create) refuse at the\n * point the admin is looking at, rather than mailing someone a link that will\n * be refused when they click it. A race here over-reserves invites; it cannot\n * over-grant access, because access is only ever granted through the\n * transactional path.\n */\nexport async function collaboratorSeatRefusal(options: {\n orgId: string\n org: Partial<AglynOrgBilling>\n hostIds: string[]\n self?: { uid?: string | null; email?: string | null }\n}): Promise<Response | null> {\n const { orgId, org, hostIds, self } = options\n if (!hostIds.length) return null\n try {\n await assertCollaboratorSeats({\n orgRef: firestore().collection('orgs').doc(orgId),\n org,\n hostIds,\n self: self ?? {},\n read: (query) => query.get(),\n })\n } catch (error) {\n const refusal = collaboratorSeatRefusalResponse(error)\n if (refusal) return refusal\n throw error\n }\n return null\n}\n\n/**\n * The refusal string, taken from `/api/orgs/members`.\n *\n * The four doors each phrased this differently — \"upgrade your plan to invite\n * more members\", \"to add more members\", \"This organization is out of team\n * seats\", \"This workspace has used all N of its team seats\" — which is what a\n * gate copied four times produces. One wording now, from the one place the\n * refusal is built. Nothing matches these strings but a human, so the\n * consolidation costs no caller.\n */\nfunction managerSeatMessage(quota: {\n limit: number\n upgradeRequired: boolean\n addonPriceUsd: number | null\n}): string {\n return quota.upgradeRequired\n ? `Team seat limit reached (${quota.limit}) — upgrade your ` +\n 'plan to add more members'\n : `Team seats full (${quota.limit}) — add seats for ` +\n `$${quota.addonPriceUsd}/mo each from Billing`\n}\n\n/**\n * A manager seat refused, thrown rather than returned, for the reason\n * {@link CollaboratorSeatLimitError} is thrown: it has to travel out of\n * `upsertOrgMember`, whose contract is \"make it so\" and which three routes\n * already call as a bare `await`. A verdict would be silently discarded by\n * every one of them, which is the shape of the bug being fixed.\n */\nexport class ManagerSeatLimitError extends Error {\n readonly limit: number\n readonly upgradeRequired: boolean\n readonly addonPriceUsd: number | null\n /**\n * Seats the org holds ABOVE `limit`. Non-zero means it is GRANDFATHERED:\n * those managers keep their access and only the NEXT one is refused, so the\n * console can say that instead of letting an admin read a 403 as \"somebody\n * was removed\".\n */\n readonly retainedOverCap: number\n constructor(quota: {\n limit: number\n upgradeRequired: boolean\n addonPriceUsd: number | null\n retainedOverCap?: number\n }) {\n super(managerSeatMessage(quota))\n this.name = 'ManagerSeatLimitError'\n this.limit = quota.limit\n this.upgradeRequired = quota.upgradeRequired\n this.addonPriceUsd = quota.addonPriceUsd\n this.retainedOverCap = Math.max(0, quota.retainedOverCap ?? 0)\n }\n}\n\n/**\n * The manager cap, evaluated against the POST-state and inside the same\n * transaction that performs the grant (AGL-2068, on the manager key).\n *\n * The collaborator cap above learned this the hard way and this is the same\n * defect one key over: all four doors that admit a manager — invite create,\n * invite accept, direct member add and SSO-JIT — read the roster, decided,\n * and then wrote, with nothing between the read and the write. N concurrent\n * accepts all measured against the same roster, all passed, and all landed.\n * Reading THROUGH the transaction is the fix: Firestore tracks the read set,\n * so a second grant that measured the same roster cannot commit — it retries,\n * re-reads a roster that now holds the first, and refuses.\n *\n * PENDING INVITES COUNT, AT EVERY DOOR. Only invite-create counted them\n * before, so the cap was enforced against a different population depending on\n * which door was used — and the doors that ignored them are the ones that\n * actually grant access. An invite reserves the seat it will become, and a\n * cap that only bites on acceptance is walked past by mailing N invitations\n * first. `readSeatEntries` is shared with the collaborator gate precisely so\n * the two populations cannot drift apart again.\n *\n * `checkSeatQuota(org, 'managers', used)` and NOT the per-host collaborator\n * quota: `managersPerOrg` really is org-level, so purchased add-ons raise it\n * (AGL-2439 removed that only for the per-site `members` key).\n *\n * THE GRANDFATHER LIVES HERE, in what this does NOT do. It charges only the\n * TRANSITION into an org-wide seat — `becomesManager` is false when the\n * membership already held one — so an org already above its cap keeps every\n * manager it has, can still have their role or profile rewritten, and is\n * merely refused the next one. There is no sweep and no revocation, and none\n * may be added: the cap binds ADMISSION, never ACCESS.\n */\nasync function assertManagerSeats(options: {\n orgRef: FirebaseFirestore.DocumentReference\n org: Partial<AglynOrgBilling>\n /** Is this write ADMITTING a manager who was not one already? */\n becomesManager: boolean\n self: {\n uid?: string | null\n email?: string | null\n emails?: readonly (string | null | undefined)[] | null\n }\n read: (query: FirebaseFirestore.Query) => Promise<FirebaseFirestore.QuerySnapshot>\n}): Promise<void> {\n const { orgRef, org, becomesManager, self, read } = options\n if (!becomesManager) return\n const entries = await readSeatEntries(orgRef, read)\n const used = countManagerSeatsExcluding(entries, self)\n const quota = checkSeatQuota(org, 'managers', used)\n if (!quota.allowed) {\n throw new ManagerSeatLimitError({\n ...quota,\n retainedOverCap: Math.max(0, used - quota.limit),\n })\n }\n}\n\n/**\n * Is this write admitting a manager who was not one already?\n *\n * The manager analogue of `newlyScopedHosts`, and it exists for the same\n * reason: a seat is charged when it is TAKEN, not every time the row holding\n * it is rewritten. Re-saving an existing manager's title, or moving them from\n * `editor` to `admin`, re-writes a seat they already hold — charging that\n * would strand an over-cap org unable to even demote its way back down.\n *\n * A scoped collaborator being promoted to org-wide DOES take a manager seat,\n * and gives one up on the collaborator side; that is a real transition and is\n * charged.\n */\nfunction becomesOrgManager(options: {\n role: OrgRole\n allHosts: boolean\n hostAccess: Record<string, HostAccessRole>\n existing: Partial<AglynOrgMember> | undefined\n}): boolean {\n const next = isOrgWideMember({\n role: options.role,\n allHosts: options.allHosts,\n hostAccess: options.hostAccess,\n } as Partial<AglynOrgMember>)\n if (!next) return false\n // An ABSENT row is not a manager, and `isOrgWideMember(undefined)` is\n // already false — but saying so explicitly keeps the \"was it one before?\"\n // question readable next to the legacy shape that predates `allHosts`.\n return !options.existing || !isOrgWideMember(options.existing)\n}\n\n/**\n * Turn a manager-seat refusal into the 403 the admitting routes return, or\n * null when the error is something else and must keep propagating to the 500.\n *\n * Sits beside `collaboratorSeatRefusalResponse` and stacks with it in a\n * route's catch block, each returning null for a non-match.\n */\nexport function managerSeatRefusalResponse(error: unknown): Response | null {\n if (!(error instanceof ManagerSeatLimitError)) return null\n return Response.json(\n {\n error: error.message,\n code: 'manager_seat_limit',\n limit: error.limit,\n upgradeRequired: error.upgradeRequired,\n // How many seats the org is over by. NOBODY was removed — the client\n // renders this as retention, not as a loss.\n retainedOverCap: error.retainedOverCap,\n },\n { status: 403 },\n )\n}\n\n/**\n * The same cap, asked BEFORE anything is written.\n *\n * Not the enforcement — the transaction inside `upsertOrgMember` is. This\n * exists for the one door that never calls it: `/api/orgs/invites` create\n * writes an invite document directly, so it refuses at the point the admin is\n * looking at rather than mailing someone a link that will be refused when\n * they click it. A race here over-reserves invites; it cannot over-grant\n * access, because access is only ever granted through the transactional path.\n */\nexport async function managerSeatRefusal(options: {\n orgId: string\n org: Partial<AglynOrgBilling>\n becomesManager: boolean\n self?: { uid?: string | null; email?: string | null }\n}): Promise<Response | null> {\n const { orgId, org, becomesManager, self } = options\n if (!becomesManager) return null\n try {\n await assertManagerSeats({\n orgRef: firestore().collection('orgs').doc(orgId),\n org,\n becomesManager,\n self: self ?? {},\n read: (query) => query.get(),\n })\n } catch (error) {\n const refusal = managerSeatRefusalResponse(error)\n if (refusal) return refusal\n throw error\n }\n return null\n}\n\nexport interface UpsertOrgMemberOptions {\n orgId: string\n uid: string\n role: OrgRole\n allHosts?: boolean\n /** Per-site grants. `author` (AGL-2334) rides the shared union. */\n hostAccess?: Record<string, HostAccessRole>\n /**\n * Further CONFIRMED addresses on the joining account (AGL-2486), so a\n * pending invite addressed to a secondary is recognised as this same\n * person and does not bill them a second collaborator seat. Must contain\n * only addresses proven to belong to `uid`.\n */\n seatAliasEmails?: readonly (string | null | undefined)[] | null\n /** Custom role reference (AGL-243); null clears it. */\n roleId?: string | null\n email?: string | null\n displayName?: string | null\n /**\n * The member's provider photo, mirrored onto the roster (AGL-1126).\n *\n * Every member surface reads the roster; none of them can read Firebase\n * Auth for an SSO member, whose record lives in a per-org tenant pool\n * (AGL-1122). Without this the console falls back to drawn initials for\n * everyone — fine, but it means a member who HAS a picture still never\n * shows it. This is the ONLY source of a real face now that the Gravatar\n * fallback is gone (AGL-1683), so keeping it populated matters more than\n * it did. Display data only: never an identity or authorization source.\n */\n photoURL?: string | null\n /** Job title shown on the roster/member page (AGL-364). */\n title?: string | null\n invitedBy?: string | null\n}\n\n/**\n * The owner seat is not writable through the membership door (AGL-1888).\n *\n * An exception, and modelled on {@link CollaboratorSeatLimitError}, for the\n * same reason: it has to travel out of a function whose contract is \"make it\n * so\" and which three routes call as a bare `await`. A returned verdict would\n * be ignorable at every one of them, which is the shape of the bug.\n */\nexport class OrgOwnerSeatError extends Error {\n /** Which invariant refused, for the log and the tests. */\n readonly reason: 'grant' | 'demote'\n constructor(reason: 'grant' | 'demote') {\n super(\n reason === 'grant'\n ? 'The owner role cannot be granted through org membership — ' +\n 'ownership moves only by transfer.'\n : 'This person owns the organization. Ownership moves only by ' +\n 'transfer, from Settings — an invitation cannot change it.',\n )\n this.name = 'OrgOwnerSeatError'\n this.reason = reason\n }\n}\n\n/**\n * Turn an owner-seat refusal into a 409, or null when the error is something\n * else and must keep propagating to the 500.\n *\n * Beside {@link collaboratorSeatRefusalResponse} so a route's catch block\n * stays one line and cannot accidentally mask a real fault.\n */\nexport function orgOwnerSeatRefusalResponse(error: unknown): Response | null {\n if (!(error instanceof OrgOwnerSeatError)) return null\n return Response.json(\n { error: error.message, code: 'org_owner_seat' },\n { status: 409 },\n )\n}\n\n/**\n * Creates or updates a member transactionally with its reverse-index\n * entry, then re-syncs host projections.\n *\n * ## The owner seat is refused here, not only in the routes (AGL-1888)\n *\n * It used to say \"owner-role guards live in the API routes — this is the\n * mechanism\", and that was the defect. Both halves of the org-owner invariant\n * were enforced only at the doors an admin clicks, and invite ACCEPTANCE is a\n * door that re-validates neither:\n *\n * - **Granting.** `/api/orgs/members` and `/api/orgs/invites` create both\n * refuse `role === 'owner'` outright, but acceptance passes the invite\n * doc's STORED role straight through (`/api/orgs/invites` accept, and\n * `/api/auth/sso-jit`). That is safe today only because every writer of an\n * invite doc refuses `owner` and the collection is `allow write: if false`\n * — a latent escalation the moment a fourth invite-writer forgets, and the\n * invariant that an org has exactly ONE owner is what the whole SSO\n * break-glass guarantee rests on ({@link transferOrgOwnership} MOVES the\n * seat; nothing else may create one).\n * - **Demoting**, which was reachable, self-serve, and irreversible. Invite\n * creation never checked that the address is already a member, and\n * acceptance accommodates an existing member re-accepting. So any admin\n * could invite the OWNER'S own verified address as `viewer`; the owner\n * clicks a normal-looking invitation to their own organization; this\n * function merge-writes `role: 'viewer'`, `allHosts: false` onto the owner's\n * member doc. `orgs/{orgId}.ownerUid` still names them, but every\n * authorization read goes through the member doc — so `canManageOrg` is\n * now false, `transfer-ownership` checks `membership.member.role ===\n * 'owner'` and refuses them, `/api/orgs/members` refuses to edit the owner's\n * membership at all, and `findBreakGlassOrgOwners` (`where role == owner`)\n * finds nobody. The org loses its owner permanently, recoverable only by\n * staff. It is the AGL-1375 one-way door rebuilt out of the invite path,\n * and it needs no SSO to reach.\n *\n * Both checks live HERE because this is the single transaction every door\n * funnels through, and the org doc and the existing member doc are already in\n * its read set — so it costs nothing and cannot be forgotten by a fifth\n * caller. The route-level refusals stay: they are better error messages at\n * the point the admin is looking, not the control.\n *\n * The demotion guard asks BOTH `org.ownerUid` and the stored role, rather\n * than trusting either to stand for the other. They are supposed to agree;\n * an org where they have already diverged is exactly the one that most needs\n * the write refused.\n *\n * {@link createOrganization} and {@link transferOrgOwnership} are unaffected —\n * both write `role: 'owner'` with their own `tx.set`, and remain the only two\n * producers of an owner in the product.\n */\nexport async function upsertOrgMember(\n options: UpsertOrgMemberOptions,\n): Promise<void> {\n const {\n orgId,\n uid,\n role,\n allHosts,\n hostAccess,\n roleId,\n email,\n seatAliasEmails,\n displayName,\n photoURL,\n title,\n invitedBy,\n } = options\n // Before the transaction is even opened: this one needs no reads, and\n // refusing here is what lets the spec assert that NOTHING was written\n // rather than that a throw happened somewhere.\n if (role === 'owner') throw new OrgOwnerSeatError('grant')\n const db = firestore()\n await db.runTransaction(async (tx) => {\n const orgSnapshot = await tx.get(db.collection('orgs').doc(orgId))\n if (!orgSnapshot.exists) throw new Error(`Unknown org: ${orgId}`)\n const org = orgSnapshot.data() as AglynOrganization\n const memberRef = db\n .collection('orgs')\n .doc(orgId)\n .collection('members')\n .doc(uid)\n const existing = await tx.get(memberRef)\n // The owner's own row is not writable here (AGL-1888). Both facts, not\n // one standing in for the other — see the note on this function.\n if (\n org.ownerUid === uid ||\n (existing.data() as Partial<AglynOrgMember> | undefined)?.role === 'owner'\n ) {\n throw new OrgOwnerSeatError('demote')\n }\n // Collaborator seat cap (AGL-2068), inside this transaction and before\n // any write. This is the door `/api/orgs/members` and invite ACCEPTANCE\n // come through, and neither metered `membersPerHost` at all — both gate\n // on `isOrgWideMember`, which is false for exactly the site-scoped\n // collaborator this charges for. The roster read below joins this\n // transaction's read set, so concurrent accepts serialise instead of all\n // passing the same pre-count.\n await assertCollaboratorSeats({\n orgRef: db.collection('orgs').doc(orgId),\n org: orgSnapshot.data() as Partial<AglynOrgBilling>,\n hostIds: newlyScopedHosts({\n role,\n allHosts: allHosts ?? false,\n hostAccess: hostAccess ?? {},\n existing: existing.data() as Partial<AglynOrgMember> | undefined,\n }),\n self: { uid, email, emails: seatAliasEmails },\n read: (query) => tx.get(query),\n })\n // Manager seat cap, in the same read slot and for the same reason. This\n // is the door invite ACCEPTANCE, `/api/orgs/members` and SSO-JIT all come\n // through, and all three read the roster outside any transaction before\n // this — so concurrent accepts measured one roster and every one of them\n // passed. The read below joins this transaction's read set, which is what\n // serialises them.\n await assertManagerSeats({\n orgRef: db.collection('orgs').doc(orgId),\n org: orgSnapshot.data() as Partial<AglynOrgBilling>,\n becomesManager: becomesOrgManager({\n role,\n allHosts: allHosts ?? false,\n hostAccess: hostAccess ?? {},\n existing: existing.data() as Partial<AglynOrgMember> | undefined,\n }),\n self: { uid, email, emails: seatAliasEmails },\n read: (query) => tx.get(query),\n })\n tx.set(\n memberRef,\n {\n role,\n allHosts: allHosts ?? false,\n hostAccess: hostAccess ?? {},\n ...(roleId !== undefined ? { roleId } : {}),\n ...(email !== undefined ? { email } : {}),\n ...(displayName !== undefined ? { displayName } : {}),\n // Absent leaves the stored photo alone; an explicit null clears it.\n // A provider that stops sending a picture must not silently wipe one\n // the member is still using.\n ...(photoURL !== undefined ? { photoURL } : {}),\n ...(title !== undefined ? { title } : {}),\n ...(invitedBy ? { invitedBy } : {}),\n ...(existing.exists\n ? {}\n : { joinedAt: FieldValue.serverTimestamp() }),\n },\n { merge: true },\n )\n tx.set(\n db.collection('users').doc(uid).collection('orgs').doc(orgId),\n {\n role,\n orgName: org.name ?? null,\n slug: org.slug ?? null,\n // Mirrored from the member doc written just above (AGL-1032) — this\n // `set` has no merge, so the flag has to be part of it or the\n // console loses the collaborator/viewer distinction until the\n // projection pass below rewrites it.\n orgWide: isOrgWideMember({\n role,\n allHosts: allHosts ?? false,\n hostAccess: hostAccess ?? {},\n }),\n },\n )\n })\n await syncOrgAuthProjections(orgId)\n // Reverse-index this member's now-current host access (AGL-844).\n await syncMemberHostProjections(orgId, uid)\n}\n\n/**\n * Fill in a roster row's display identity from an identity provider, writing\n * ONLY the fields that are currently blank (AGL-1131).\n *\n * Separate from `upsertOrgMember` because the caller is the SSO sign-in path\n * on its already-a-member branch, where the member's role, host access and\n * invite state are settled and must not be touched. `upsertOrgMember`\n * requires a `role` and re-asserts it, so reusing it here would let an SSO\n * sign-in quietly reset an admin to the org's `sso.defaultRole`.\n *\n * Absent-only, so it is safe on every sign-in: it backfills the rows that\n * predate the IdP mapping and then never writes again, and it can never\n * overwrite a name or photo a person chose.\n *\n * @returns the field names it wrote, for logging and tests.\n */\nexport async function backfillMemberIdentity(\n orgId: string,\n uid: string,\n identity: { displayName?: string | null; photoURL?: string | null },\n db = firestore(),\n): Promise<string[]> {\n const ref = db.collection('orgs').doc(orgId).collection('members').doc(uid)\n const snapshot = await ref.get()\n // A missing row is NOT this function's job to create — creating one here\n // would mint a membership with no role, which every permission check reads\n // as a member of some kind.\n if (!snapshot.exists) return []\n\n const blank = (value: unknown) => typeof value !== 'string' || !value.trim()\n const patch: Record<string, string> = {}\n const displayName = identity.displayName?.trim()\n const photoURL = identity.photoURL?.trim()\n if (displayName && blank(snapshot.get('displayName'))) {\n patch['displayName'] = displayName\n }\n if (photoURL && blank(snapshot.get('photoURL'))) {\n patch['photoURL'] = photoURL\n }\n if (!Object.keys(patch).length) return []\n\n await ref.set(patch, { merge: true })\n return Object.keys(patch)\n}\n\n/**\n * The same absent-only backfill, across every roster row that names `uid`.\n *\n * ## The hole this closes\n *\n * `orgs/{orgId}/members/{uid}.photoURL` is the ONLY avatar a member surface\n * can read — a colleague's auth record is unreadable from another member's\n * session, and an SSO member's lives in a pool the project cannot see at all\n * (AGL-1122). Three writers filled it, and between them they missed the\n * commonest account there is:\n *\n * - `upsertOrgMember` — someone ADDED you, so the adder's lookup had a record\n * to copy from.\n * - `backfillMemberIdentity` via the SSO sign-in (AGL-1131) — enterprise only.\n * - `propagateMemberPhoto` via Manage Account → Profile image (AGL-1976) — a\n * photo the person typed or browsed to.\n *\n * Nobody adds the person who CREATES a workspace, `createOrganization` writes\n * their row with a name and an email and no photo, and a Google sign-in never\n * visits the other two. So the owner of a workspace saw their own face in the\n * app bar, which reads the live auth record, and a grey initial in their own\n * Team list — measured on both rows of `test-org`, each with `photoURL` absent\n * while the auth record and `users/{uid}.photoUrl` carried the picture.\n *\n * ## Absent-only, like the function it fans out\n *\n * It runs on EVERY sign-in, so the reasoning in `backfillMemberIdentity`\n * applies unchanged and is the reason this is a fan-out of that function\n * rather than a second writer: an overwriting version would replace a photo\n * the person chose in Manage Account with their provider thumbnail on their\n * next sign-in, silently, forever. `propagateMemberPhoto` is the overwriting\n * direction and stays the only one, because its input is a choice the person\n * made rather than an assertion a directory made about them.\n *\n * Memberships come from `users/{uid}/orgs`, the reverse index — never a\n * collection-group query over `members`, which would read every workspace's\n * roster in the estate to find one person's rows.\n *\n * @returns the org ids whose row was written, for logging and tests.\n */\nexport async function backfillMemberIdentityEverywhere(\n uid: string,\n identity: { displayName?: string | null; photoURL?: string | null },\n db = firestore(),\n): Promise<string[]> {\n if (!uid) return []\n // Nothing to write beats a fan-out that reads every membership to discover\n // it has nothing to write — this runs on every sign-in.\n if (!identity.displayName?.trim() && !identity.photoURL?.trim()) return []\n\n const memberships = await db.collection('users').doc(uid).collection('orgs').get()\n const written: string[] = []\n for (const row of memberships.docs) {\n const fields = await backfillMemberIdentity(row.id, uid, identity, db)\n if (fields.length) written.push(row.id)\n }\n return written\n}\n\n/**\n * Transfers org ownership (AGL-232): the target must already be on the\n * roster; the previous owner steps down to admin. One transaction across\n * the org doc, both member docs and both reverse-index entries, then the\n * host projections re-sync.\n *\n * **It moves `ownerUid` and must never touch `createdByUid`** (AGL-2265).\n * That field is the creator attribution the free-workspace ceiling counts\n * against, and it is what stops a transfer from being a way to launder the\n * count: hand a workspace to an alt account, create a fourth, take it back.\n * Nothing here writes it, and `free-workspace-cap.spec.ts` runs exactly that\n * sequence to keep it that way.\n */\nexport async function transferOrgOwnership(\n orgId: string,\n fromUid: string,\n toUid: string,\n): Promise<void> {\n if (fromUid === toUid) throw new Error('Target already owns this org')\n const db = firestore()\n await db.runTransaction(async (tx) => {\n const orgRef = db.collection('orgs').doc(orgId)\n const orgSnapshot = await tx.get(orgRef)\n if (!orgSnapshot.exists) throw new Error(`Unknown org: ${orgId}`)\n const org = orgSnapshot.data() as AglynOrganization\n if (org.ownerUid !== fromUid) {\n throw new Error('Only the current owner can transfer ownership')\n }\n const targetRef = orgRef.collection('members').doc(toUid)\n const target = await tx.get(targetRef)\n if (!target.exists) {\n throw new Error('The new owner must already be an org member')\n }\n tx.set(\n orgRef,\n { ownerUid: toUid, updatedAt: FieldValue.serverTimestamp() },\n { merge: true },\n )\n tx.set(targetRef, { role: 'owner', allHosts: true }, { merge: true })\n tx.set(\n orgRef.collection('members').doc(fromUid),\n { role: 'admin' },\n { merge: true },\n )\n tx.set(\n db.collection('users').doc(toUid).collection('orgs').doc(orgId),\n // Both principals end up owner/admin, which is org-wide reach whatever\n // they were before — a promoted site collaborator must lose the scoped\n // console along with the scoped membership (AGL-1032).\n { role: 'owner', orgWide: true },\n { merge: true },\n )\n tx.set(\n db.collection('users').doc(fromUid).collection('orgs').doc(orgId),\n { role: 'admin', orgWide: true },\n { merge: true },\n )\n })\n await syncOrgAuthProjections(orgId)\n // Both principals' host access changed (owner spans every host) — AGL-844.\n await Promise.all([\n syncMemberHostProjections(orgId, toUid),\n syncMemberHostProjections(orgId, fromUid),\n ])\n /*\n * A workspace changing hands is the highest-consequence thing that can\n * happen to an account, and until AGL-118 it left no trace anywhere: the\n * transaction above rewrites five documents and wrote nothing that says it\n * happened, so the only evidence was the new state itself.\n *\n * BOTH principals are on the row. The actor is the outgoing owner, who is\n * the only party allowed to perform this, and the target names the\n * incoming one — a transfer identified by one party is half a record, and\n * the half it keeps is the one already implied by `ownerUid`.\n *\n * Emails are read after the fact and best-effort. The uids are the\n * identity; the addresses only save a reader a lookup, so a failure to\n * resolve them must not cost the entry.\n */\n const [fromEmail, toEmail] = await Promise.all(\n [fromUid, toUid].map(async (uid) =>\n firestore()\n .collection('orgs')\n .doc(orgId)\n .collection('members')\n .doc(uid)\n .get()\n .then((snapshot) => {\n const email = snapshot.get('email')\n return typeof email === 'string' ? email : null\n })\n .catch(() => null),\n ),\n )\n await logOrgActivity(\n orgId,\n { uid: fromUid, email: fromEmail },\n 'Transferred workspace ownership',\n { type: 'member', id: toUid, ...(toEmail ? { name: toEmail } : {}) },\n )\n}\n\n/**\n * Grants (or updates) per-host access for a uid without disturbing an\n * existing membership's org role or allHosts flag (AGL-238: the host user\n * manager rides org membership). Creates a viewer membership scoped to\n * just this host when the uid is not on the roster yet.\n */\nexport async function grantHostAccess(options: {\n orgId: string\n uid: string\n hostId: string\n /** `author` (AGL-2334) edits content and cannot publish. */\n role: HostAccessRole\n email?: string | null\n displayName?: string | null\n invitedBy?: string\n}): Promise<void> {\n const { orgId, uid, hostId, role, email, displayName, invitedBy } = options\n const db = firestore()\n await db.runTransaction(async (tx) => {\n const orgRef = db.collection('orgs').doc(orgId)\n const orgSnapshot = await tx.get(orgRef)\n if (!orgSnapshot.exists) throw new Error(`Unknown org: ${orgId}`)\n const org = orgSnapshot.data() as AglynOrganization\n const memberRef = orgRef.collection('members').doc(uid)\n const existing = await tx.get(memberRef)\n // Collaborator seat cap (AGL-2068). This door DID meter, but against\n // `hosts/{hostId}/members` — a display roster only its own route writes,\n // so it could not see anyone admitted by invite or by `/api/orgs/members`\n // and under-counted even when it fired. The count now comes off the org\n // roster + pending invites, which is where every door lands.\n await assertCollaboratorSeats({\n orgRef,\n org: orgSnapshot.data() as Partial<AglynOrgBilling>,\n // Asked of the membership AS IT STANDS, not of the merged shape.\n // `grantHostAccess` never touches `role` or `allHosts`, so someone who\n // is already a manager stays one and keeps paying a manager seat — and\n // a legacy pre-`allHosts` row, which `isOrgWideMember` reads as org-wide\n // precisely so it is not locked out, must not be re-classified into a\n // collaborator seat by the act of writing a host key onto it.\n hostIds: (() => {\n const current = existing.data() as Partial<AglynOrgMember> | undefined\n if (existing.exists && isOrgWideMember(current)) return []\n if (current?.hostAccess?.[hostId]) return []\n return [hostId]\n })(),\n self: { uid, email },\n read: (query) => tx.get(query),\n })\n tx.set(\n memberRef,\n {\n ...(existing.exists\n ? {}\n : {\n role: 'viewer' as OrgRole,\n allHosts: false,\n joinedAt: FieldValue.serverTimestamp(),\n }),\n hostAccess: { [hostId]: role },\n ...(email !== undefined ? { email } : {}),\n ...(displayName !== undefined ? { displayName } : {}),\n ...(invitedBy ? { invitedBy } : {}),\n },\n // merge deep-merges the hostAccess map, so other host grants and\n // the existing role/allHosts stay untouched.\n { merge: true },\n )\n if (!existing.exists) {\n tx.set(db.collection('users').doc(uid).collection('orgs').doc(orgId), {\n role: 'viewer',\n orgName: org.name ?? null,\n slug: org.slug ?? null,\n // A brand-new site collaborator: on the org roster, but their console\n // is one site (AGL-1032). `role: 'viewer'` here is indistinguishable\n // from a genuine org-wide viewer's, which is the whole reason for\n // this flag. An EXISTING member keeps whatever reach they had — a\n // host grant never widens or narrows it.\n orgWide: false,\n })\n }\n })\n await syncOrgAuthProjections(orgId)\n await syncMemberHostProjections(orgId, uid)\n}\n\n/**\n * Drops one host from a member's hostAccess map, then re-projects.\n *\n * `updateExisting`, not a merge-set (AGL-1766). A merge-set whose entire\n * payload is a delete sentinel still CREATES the document when it is absent,\n * and the row it minted here is not merely untidy — it is a MEMBERSHIP, and\n * one that reads as org-wide. `isOrgWideMember` treats \"no `role`, no\n * `allHosts`, empty `hostAccess`\" as the pre-`allHosts` LEGACY shape and\n * answers true (deliberately: reading it as \"scoped, with access to nothing\"\n * would lock real members out). A genuine site collaborator never looks like\n * that — `grantHostAccess` always writes `allHosts: false` — but a document\n * conjured from this patch alone does, exactly.\n *\n * So the consequences land away from here, which is what made it hard to see:\n * `resolveOrgMembership` finds the doc and returns a membership for someone\n * who was removed from the org; `syncOrgAuthProjections` on the next line\n * stamps it `scopeTokens: ['org']`, the read set the rules and every\n * Admin-SDK `memberCanSee` resolve from; and `countManagerSeats` bills it as\n * a manager seat. (It does NOT reach `hosts/*.memberRoles`, as AGL-1763\n * supposed — `hostRoleFor` requires an `isOrgRole(role)` and the phantom has\n * none.)\n *\n * Reachable without any race: `removeOrgMember` deletes the org member doc\n * but leaves the `hosts/{hostId}/members` roster row, which is what this is\n * called from. Deleting that leftover row re-created the membership it was\n * meant to finish removing. (AGL-1766's \"stale double-submit\" is NOT a route:\n * the caller 404s on the missing roster row before reaching here.)\n *\n * DOTTED FIELD PATH, not the nested map: `update()` accepts a delete sentinel\n * only at the top level of its patch (`@google-cloud/firestore` serializer,\n * `allowDeletes: 'root'`), so the nested form would throw INVALID_ARGUMENT.\n * The dotted path is top-level and clears the one key while leaving the rest\n * of `hostAccess` alone — the same field-by-field semantics the merge had.\n * Safe as a string path because host ids are `createResourceUid()` nanoids\n * (`A-Za-z0-9_-`), so none can contain the `.` the SDK splits on.\n *\n * REFUSE, and ignore the answer: revoking a grant that is not there is a\n * no-op and discards nothing (AGL-1760). The projections still run — they are\n * recomputed from the roster, so a pass that finds no member doc is exactly\n * the self-heal a stale row needs.\n */\nexport async function revokeHostAccess(\n orgId: string,\n uid: string,\n hostId: string,\n): Promise<void> {\n await updateExisting(\n firestore().collection('orgs').doc(orgId).collection('members').doc(uid),\n { [`hostAccess.${hostId}`]: FieldValue.delete() },\n )\n await syncOrgAuthProjections(orgId)\n await syncMemberHostProjections(orgId, uid)\n}\n\n/**\n * Removes a member + reverse index entry, then re-syncs projections.\n *\n * The addresses the member added in this workspace (AGL-2975) go in the\n * same batch. They sit beside the roster row rather than under it, so no\n * delete of the row reaches them, and an erasure of the person runs through\n * here once per workspace.\n */\nexport async function removeOrgMember(\n orgId: string,\n uid: string,\n): Promise<void> {\n const db = firestore()\n const batch = db.batch()\n batch.delete(\n db.collection('orgs').doc(orgId).collection('members').doc(uid),\n )\n batch.delete(\n db\n .collection('orgs')\n .doc(orgId)\n .collection(MEMBER_EMAIL_ALIASES_COLLECTION)\n .doc(uid),\n )\n batch.delete(db.collection('users').doc(uid).collection('orgs').doc(orgId))\n await batch.commit()\n await syncOrgAuthProjections(orgId)\n // The member is off the roster, so the sync above can't reach their rows —\n // drop the reverse index explicitly (AGL-844), like the orgs entry above.\n await deleteMemberHostProjections(orgId, uid)\n}\n\n/**\n * Registers a host under its org: org directory entry, hostIndex mirror,\n * and the initial memberRoles projection on the host doc.\n */\nexport async function registerOrgHost(\n orgId: string,\n hostId: string,\n subdomain?: string,\n): Promise<void> {\n const db = firestore()\n await db\n .collection('orgs')\n .doc(orgId)\n .set(\n {\n hosts: { [hostId]: true },\n updatedAt: FieldValue.serverTimestamp(),\n },\n { merge: true },\n )\n await db\n .collection('hostIndex')\n .doc(hostId)\n .set({ orgId, ...(subdomain ? { subdomain } : {}) })\n await syncOrgAuthProjections(orgId, hostId)\n // Seed the per-user projection for everyone who can reach the new host.\n await syncHostProjectionForMembers(orgId, hostId)\n}\n\n/**\n * The consent group a site belongs to, read off its owning org.\n *\n * The ONE server-side door to pooling. Every capture surface and every send\n * path resolves a group through this rather than reading\n * `CONSENT_GROUPS_FIELD` itself, so there is one place that decides what a\n * site's consent covers and one place a mistake could live.\n *\n * FAILS TO THE GROUP OF ONE. An org that cannot be resolved, or a read that\n * throws, answers \"this site alone\" — which withholds mail from an org that\n * had legitimately pooled and never sends mail on a pooling nobody could\n * confirm. That is the only direction a failure here may fall.\n *\n * The org read is `React.cache`-deduped per request by {@link getOrgForHost},\n * so a send that already resolved the org for its policy pays nothing extra.\n */\nexport async function consentGroupForSite(\n hostId: string,\n org?: Record<string, unknown> | null,\n): Promise<ConsentGroup> {\n if (!hostId) throw new Error('[organizations] no site to resolve a group for')\n if (org) return consentGroupForHost(org, hostId)\n const resolved = await getOrgForHost(hostId).catch(() => null)\n return consentGroupForHost(\n (resolved?.org as Record<string, unknown> | undefined) ?? null,\n hostId,\n )\n}\n"],"names":["consentGroupForHost","checkHostCollaboratorQuota","checkSeatQuota","countCollaboratorSeats","countManagerSeatsExcluding","createResourceUid","generateOrgSlug","projectMemberResolvedPermissions","resolveOrgPermissions","isOrgWideMember","isValidOrgSlug","hostPermissionKeys","orgPermissionLabel","pluginOrgPermissionKeys","projectHostMemberPermissions","projectHostMemberRoles","projectMemberScopeTokens","resolveCollaboratorHostPermissions","scopeTokensForHost","nameSearchKey","nameSearchReversed","nameSearchTokens","ORG_BILLING_DOC_ID","ORG_BILLING_SUBCOLLECTION","MEMBER_EMAIL_ALIASES_COLLECTION","FieldValue","cache","findUserByUidAcrossPools","firebaseAdmin","enforceFreeWorkspaceCapInTransaction","readFreeWorkspaceCapConfig","deleteMemberHostProjections","syncHostProjectionForMembers","syncMemberHostProjections","updateExisting","attachWorkspaceDomain","firestore","app","FIRESTORE_BATCH_LIMIT","OrgSlugTakenError","Error","slug","name","isSlugReservationLapsed","reservation","now","Date","until","reservedUntil","Number","isFinite","isSlugReservationClaimable","claimingOrgId","orgId","movedTo","lapsedReservationIsStillHeld","holderOrgId","holder","collection","doc","get","exists","ownerUid","found","record","emailVerified","error","console","createOrganization","options","ownerEmail","ownerDisplayName","db","capConfig","bypassFreeWorkspaceCap","runTransaction","tx","held","data","undefined","uid","config","set","nameLower","nameTokens","nameReversed","createdByUid","hosts","createdAt","serverTimestamp","updatedAt","role","allHosts","email","displayName","joinedAt","scopeTokens","resolvedPermissions","orgName","orgWide","logOrgActivity","type","id","resolveOrgMembership","resolved","mine","limit","empty","docs","memberSnapshot","member","$id","ensureOrgForUser","profile","existing","base","trim","split","slice","attempt","created","changeOrgSlug","newSlug","previousSlug","orgSnapshot","orgRef","merge","renamedAt","members","listOrgMembers","batch","commit","resolveOrgIdForHost","hostId","snapshot","getOrgDoc","getOrgForHost","org","getHostDocAdmin","getHostDisabledPlugins","disabled","Array","isArray","map","String","getOrgForUser","membership","orgDataCollectionForHost","scopedToHost","ref","orgScoped","parent","where","orgDataQueryForHost","query","resolveMemberOrgPermissions","customRole","roleId","memberHasOrgPermission","permission","memberHasPermissionOnHost","site","permissionRefusal","Response","json","reason","status","resolveMemberPluginPermissionsOnOrg","granted","Object","fromEntries","key","setHostPermissions","keys","accepted","value","permissions","stored","before","hostPermissions","syncOrgAuthProjections","after","loadOrgCustomRoles","roleIds","Set","filter","rolesRef","Map","Promise","all","HOST_PROJECTION_WRITE","mergeFields","MEMBER_PROJECTION_WRITE","customRoles","hostIds","writes","memberRoles","memberPermissions","i","length","syncHostMemberRoles","actor","action","target","add","actorId","actorEmail","versionId","catch","logHostActivity","apiKeyName","CollaboratorSeatLimitError","quota","collaboratorSeatMessage","upgradeRequired","addonPriceUsd","retainedOverCap","Math","max","readSeatEntries","read","invites","assertCollaboratorSeats","self","entries","used","allowed","newlyScopedHosts","hostAccess","prior","collaboratorSeatRefusalResponse","message","code","collaboratorSeatRefusal","refusal","managerSeatMessage","ManagerSeatLimitError","assertManagerSeats","becomesManager","becomesOrgManager","next","managerSeatRefusalResponse","managerSeatRefusal","OrgOwnerSeatError","orgOwnerSeatRefusalResponse","upsertOrgMember","seatAliasEmails","photoURL","title","invitedBy","memberRef","emails","backfillMemberIdentity","identity","blank","patch","backfillMemberIdentityEverywhere","memberships","written","row","fields","push","transferOrgOwnership","fromUid","toUid","targetRef","fromEmail","toEmail","then","grantHostAccess","current","revokeHostAccess","delete","removeOrgMember","registerOrgHost","subdomain","consentGroupForSite"],"mappings":"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"}
|