@aglyn/aglyn 1.0.0-beta.229 → 1.0.0-beta.230
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +11 -11
- package/src/lib/app-utils/analytics-events.d.ts +18 -0
- package/src/lib/app-utils/analytics-events.js +2 -0
- package/src/lib/app-utils/analytics-events.js.map +1 -1
- package/src/lib/app-utils/docs-help.generated.d.ts +31 -7
- package/src/lib/app-utils/docs-help.generated.js +63 -2
- package/src/lib/app-utils/docs-help.generated.js.map +1 -1
- package/src/lib/app-utils/docs-index.generated.js +295 -43
- package/src/lib/app-utils/docs-index.generated.js.map +1 -1
- package/src/lib/app-utils/plan-entitlements.js +20 -0
- package/src/lib/app-utils/plan-entitlements.js.map +1 -1
- package/src/lib/app-utils/plugin-host-events.generated.d.ts +1 -1
- package/src/lib/app-utils/plugin-host-events.generated.js +15 -0
- package/src/lib/app-utils/plugin-host-events.generated.js.map +1 -1
- package/src/lib/app-utils/site-journey.d.ts +143 -0
- package/src/lib/app-utils/site-journey.js +282 -0
- package/src/lib/app-utils/site-journey.js.map +1 -0
- package/src/lib/foundation/definitions/org-billing.types.d.ts +16 -0
- package/src/lib/foundation/definitions/org-billing.types.js.map +1 -1
- package/src/lib/plugin-manager/feature-plugins.d.ts +85 -0
- package/src/lib/plugin-manager/feature-plugins.js +18 -1
- package/src/lib/plugin-manager/feature-plugins.js.map +1 -1
- package/src/lib/plugin-manager/first-party-plugins.generated.js +29 -0
- package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
- package/src/lib/plugin-manager/plugin-ai-capabilities.d.ts +192 -0
- package/src/lib/plugin-manager/plugin-ai-capabilities.js +157 -0
- package/src/lib/plugin-manager/plugin-ai-capabilities.js.map +1 -0
- package/src/lib/plugin-manager/plugin-events.d.ts +21 -0
- package/src/lib/plugin-manager/plugin-events.js.map +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/aglyn/src/lib/foundation/definitions/org-billing.types.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * The org's billing/entitlement vocabulary (AGL-443 naming cleanup).\n *\n * HISTORY: these types were spelled `Tenant*` until AGL-444 — they\n * predate the organizations migration (AGL-232..238); the retired\n * `tenants/{uid}` collection's billing shape was mirrored ONTO the org\n * doc and the names came along. The alias is gone: everything here is\n * `Org*` and describes fields of `orgs/{orgId}`. The last persisted\n * tenant spellings (the `users.{uid}.tenants` map, Stripe\n * `metadata[tenantId]`, `host.tenantId`) were retired pre-launch in\n * AGL-445 — billing keys off `metadata[orgId]` now.\n *\n * Convention (see the docs-site glossary): \"organization/org\" is the\n * entity; \"workspace\" is the user-facing word for it; \"tenant\" is\n * reserved for the published-site runtime (`apps/tenant`,\n * `@aglyn/tenant-*` libs).\n */\n\nimport type { ITimestamp } from '@aglyn/shared-util-timestamp'\nimport type {\n AglynDocument,\n HostUid,\n OrgUid,\n UserUid,\n} from './platform.types'\n/*\n * The plugin half of the two composed shapes below (AGL-3124).\n *\n * Each is an EMPTY interface a plugin augments with `declare module`, so the\n * keys a plugin owns are part of `OrgEntitlements` and `OrgFeatureFlags`\n * without the core listing them. The AI credits, the CRM's one-to-one email\n * pace, the form-submission and workflow-run bands, the marketplace's take\n * rate and selling gate and commerce's three gated features are declared that\n * way, each in its plugin's `plan-entitlements` module (AGL-3080).\n *\n * `import type` and nothing else: TypeScript erases it, so the plugin-manager\n * module is not on this module's runtime graph and a published page that\n * reaches the billing types (through the foundation barrel it must not reach\n * anyway) carries nothing new.\n */\nimport type {\n PluginEntitlementFeatures,\n PluginEntitlementQuotas,\n} from '../../plugin-manager/plugin-entitlement-keys'\n\nexport type { OrgUid } from './platform.types'\n\n\n/** Hosted in master catalog */\n/**\n * SaaS subscription tiers (Tenant Billing & SaaS Plans, AGL-38..41).\n * Pricing v3 (2026-07) inserted `scale` between business and advanced to\n * fill the $139→$399 gap, and added `agency` above advanced for\n * high-volume multi-site orgs — see the Pricing Decision Log.\n *\n * `enterprise` (AGL-1118) is a REAL plan, not a display label: it tops the\n * ladder with unlimited capacity plus white-label and SSO, and it is the ONE\n * tier with no list price — it is staff-provisioned per deal (AGL-1110), never\n * self-serve. Surfaces that offer plans for sale iterate `SELF_SERVE_PLANS`,\n * which excludes it; surfaces that merely NAME the org's plan read\n * `PLAN_LABELS` and get \"Enterprise\" for free.\n */\nexport type OrgPlan =\n | 'free'\n | 'starter'\n | 'pro'\n | 'business'\n | 'scale'\n | 'advanced'\n | 'agency'\n | 'enterprise'\n\n/**\n * The CORE boolean feature gates per plan; quotas live beside them as\n * numbers. Read {@link OrgFeatureFlags} instead — this half exists so the\n * plan tables can stay exhaustive over the platform's own gates while a\n * plugin declares its own (AGL-3124).\n */\nexport interface CoreOrgFeatureFlags {\n /** A/B experiments (AGL-252); Business tier. */\n abTesting?: boolean\n versioning?: boolean\n reusableComponents?: boolean\n customDomain?: boolean\n /**\n * Send mail as a domain the CUSTOMER owns — `hello@acme.com` — by publishing\n * our SPF, DKIM and return-path records in their own zone.\n *\n * Distinct from `customDomain`, which is the site's public web address and\n * authorizes nothing about mail; and distinct from `whiteLabel`, which\n * replaces the Aglyn brand — product name, logo, colors, support URL,\n * console chrome, favicon — across every branded surface. Sending as your\n * own name is one narrow consequence of white-labeling rather than the\n * whole of it, so the two cannot share a flag without the wider capability\n * following the narrower one wherever it goes.\n *\n * It is also the CHEAP half of the sending model, which is why it sits low\n * on the ladder. A customer-owned domain costs the platform one provider\n * domain object and NOTHING in our own DNS zone, because the customer\n * publishes the records; the platform subdomain a site is issued costs a\n * provider slot, three records in our zone and a permanent place in the\n * re-verification sweep. Holding the free-to-us option behind the highest\n * paywall makes the expensive one the default at every tier that can send.\n *\n * Read through `checkEntitlement` at the two routes that write a sending\n * identity: the org's domain list, and the per-site selection.\n */\n customSendingDomain?: boolean\n /**\n * Hold a sending subdomain of the PLATFORM's own —\n * `hello@{label}.mail.aglyn.app` — provisioned for one site inside Aglyn's\n * mail apex.\n *\n * The EXPENSIVE half of the sending model, and the reason it is a separate\n * flag from `customSendingDomain`. A domain the customer owns costs one\n * provider domain object and nothing in our zone, because they publish the\n * records. One of these costs a provider slot, THREE records in our own\n * zone, and a permanent place in the re-verification sweep — per site,\n * forever. The two capabilities are decided against different resources, so\n * a shared flag would move a bounded one whenever the unbounded one is\n * re-cut.\n *\n * The flag is the SECOND of two conditions and never the only one. Nothing\n * provisions on entitlement alone: a merchant asks from the sending-identity\n * route, this says whether the org carries what they asked for, and the\n * provider ceiling still refuses beyond the account's allowance. A site\n * refused at any of the three sends on the shared pool rather than not at\n * all.\n *\n * Read through `checkEntitlement` at\n * `libs/aglyn/src/lib/app-utils/dedicated-sending-domain.ts`, which is the\n * only reader — the claim path in `host-sending-domain.ts` goes through it.\n */\n dedicatedSendingDomain?: boolean\n removeBranding?: boolean\n /** Schedule a version to publish at a date/time (tier above versioning). */\n scheduledPublishing?: boolean\n /** AI copy assist in the besigner (AGL-89). */\n aiAssist?: boolean\n /**\n * Generative building and automation (AGL-2896): pages, components,\n * layouts, templates, SEO, email, campaigns, A/B variants, analytics\n * insights, products, CRM records, onboarding and workflows produced by\n * the assistant rather than assembled by hand.\n *\n * Distinct from `aiAssist`, which is the chat guide, copy assist and\n * generate-section rung every tier from Pro up carries. This is the\n * expensive rung: a generated surface carries the node tree, the catalog\n * and the theme in and iterates, so it draws credits by the hundreds where\n * a question draws tens. No self-serve tier includes it; the Aglyn AI\n * add-on (`OrgSeatAddons.aiAddon`) switches it on and adds the credit band\n * that funds it, and `resolveOrgEntitlements` flips this flag together\n * with `aiAssist` when the add-on is present. Enterprise carries it in the\n * agreement.\n */\n aiGenerative?: boolean\n /** No-code workflow builder (AGL-101). */\n workflows?: boolean\n /** Datasets + repeatable components (AGL-102/103). */\n dataStore?: boolean\n /** Video/file uploads in the media manager (AGL-162). */\n videoMedia?: boolean\n /** Appointment bookings (AGL-159). */\n bookings?: boolean\n /**\n * THE CRM (AGL-2611): contacts, leads, companies, the deals pipeline,\n * tasks, reports and custom fields, the CRM automation steps, and the\n * `crm:*` REST resources.\n *\n * The Contacts section included (AGL-2851). Capture still writes a site's\n * audience into the contacts collection on every plan including Free,\n * banded by `contactsPerHost`, and exporting or erasing those people stays\n * on every plan in Settings → Privacy. What this flag gates is the CRM a\n * team works them in, and it is the upgrade motive from Free to the first\n * paid tier rather than a line on top of one: for the small-business buyer\n * the CRM is the reason to pick a platform over a page builder.\n *\n * Read by the console shell through the CRM extension's and its widgets'\n * `featureFlag` (so the shell refuses every CRM section and page before\n * one mounts), by the `crm/*` routes, by the automation executor before a\n * CRM step runs, by the REST dispatcher in front of the CRM resources,\n * contacts included, and, restated, by the Firestore rules on a client's\n * CRM writes. A per-org override on `entitlements.features.crm` works the\n * way every other flag's does.\n */\n crm?: boolean\n /**\n * SEQUENCES (AGL-2974): one-to-one, multi-step email sequences a rep sends\n * from their own connected mailbox, logged on the CRM's records. The key\n * below keeps the name the feature shipped under, because it is stored on\n * the org (AGL-3199).\n *\n * False on EVERY plan, Enterprise included. Which tiers carry sequences\n * and connected mailboxes, and at what caps, is a packaging decision that\n * has not been made, so no plan may claim it. An organization reaches it\n * only through the per-org override on `entitlements.features.outreach`.\n *\n * Read by the console shell through the Sequences extension's\n * `featureFlag`, by the plugin's routes, and, restated, by the Firestore\n * rules in front of the `outreach*` collections. The rules carry no plan\n * list for it because no plan grants it; when packaging lands, the plan\n * table and those rules change together.\n */\n outreach?: boolean\n /**\n * Basic presentational interactions (AGL-577): menu/drawer open-close,\n * element show/hide, class toggles, sticky nav, navigation, site\n * alerts. Included on ALL plans — pure client-side DOM with no server\n * cost. The `actions` flag below gates the powerful automation steps\n * (server dispatch, runJs, analytics, overlays, raw HTML).\n *\n * `true` on all eight tiers, and NO code gates on it — which makes it look\n * dead, and it has now been filed as dead once (AGL-2082). It is not.\n * `tools/marketing/build-pricing-tables.mts` reads its value to emit the\n * \"Interactions\" row on the public /pricing compare table, ticked on every\n * plan, and the Free plan card bullets it. That row is the claim that a\n * hover-to-open menu is not a paid feature — a real competitive statement,\n * and the reason `true` everywhere is a DECISION rather than a default.\n *\n * So: do not delete it as dead weight. Deleting it silently removes a\n * public pricing row while the Figma frames still carry it in all four\n * responsive variants, which is a pricing call and not a cleanup.\n */\n interactions?: boolean\n /** Event → action automation builder (AGL-148). */\n actions?: boolean\n /** Outbound/inbound webhooks (AGL-149). */\n webhooks?: boolean\n /** Customer REST API v1 + API keys (AGL-615); Business tier. */\n apiAccess?: boolean\n /** Whole-site export/backup + restore (AGL-163). */\n siteExport?: boolean\n /** Multilingual sites (AGL-164): locale variants + switcher. */\n multilingual?: boolean\n /** Event Calendar add-on (AGL-145); paid, not part of any base tier. */\n eventCalendar?: boolean\n /** URL redirects manager (AGL-154). */\n redirects?: boolean\n /** Per-screen traffic analytics (AGL-150). */\n screenAnalytics?: boolean\n /** CDN delivery + responsive image variants for media (AGL-175). */\n mediaCdn?: boolean\n /** Announcement bar + promotional popups (AGL-195/196). */\n marketingOverlays?: boolean\n /** Full storefront commerce: catalog, cart, checkout (AGL-278). */\n commerce?: boolean\n /** Console point-of-sale mode (AGL-312). */\n pos?: boolean\n /** Entitlement-gated screens/sections/video paywalls (AGL-309). */\n contentGating?: boolean\n /** Verified-buyer product reviews (AGL-324). */\n productReviews?: boolean\n /** Abandoned checkout recovery emails (AGL-323). */\n abandonedCart?: boolean\n /** Dropship supplier routing on paid orders (AGL-289). */\n dropshipRouting?: boolean\n /**\n * White-label the platform (White-Label Phase 1): replace the Aglyn brand\n * — product name, logo, colors, support URL, transactional email from-name\n * — with the org's own `brandingProfile` across every branded surface.\n * Agency ($799) and Enterprise carry it on the plan (AGL-1118); any other\n * tier needs a per-org `entitlements` override. Strictly broader than\n * `removeBranding`, which only drops the\n * \"Made with Aglyn\" badge on published sites; white-label REPLACES the\n * brand rather than merely hiding it. Every branded surface resolves the\n * effective brand through `resolveBrandingProfile` so it can never drift.\n */\n whiteLabel?: boolean\n /**\n * Enterprise SSO (AGL-1101): the org's console users sign in through the\n * org's own SAML/OIDC IdP, wired as a per-org GCIP tenant (`org.sso`).\n * Distinct from `whiteLabel` — an Agency org can have one without the other.\n * Carried by the `enterprise` plan (AGL-1118) and false on every other base\n * plan; a lower tier needs a per-org `entitlements` override, which is how\n * enterprise orgs provisioned before that plan existed still get it. Gates\n * the staff SSO-config card and the SSO sign-in path; a non-entitled org can\n * neither configure nor use SSO.\n */\n ssoEnabled?: boolean\n}\n\n/**\n * Boolean feature gates per plan, the platform's and its plugins' together.\n *\n * {@link CoreOrgFeatureFlags} is the platform's own set — what\n * `PLAN_ENTITLEMENTS` declares for every plan, and what\n * `Required<CoreOrgFeatureFlags>` keeps exhaustive.\n * {@link PluginEntitlementFeatures} is the plugin half (AGL-3124): a plugin\n * declares a gate of its own into it with `declare module`, and its answer\n * per plan comes from that plugin's `plan-entitlements` declaration or its\n * `registerPluginEntitlements` defaults rather than from a row core writes —\n * which is why a plugin's gate joins HERE and not above. Every reader keeps reading `OrgFeatureFlags`, and\n * `keyof OrgFeatureFlags` admits a plugin's key the moment it is declared.\n */\nexport interface OrgFeatureFlags\n extends CoreOrgFeatureFlags,\n PluginEntitlementFeatures {}\n\n/**\n * An org's white-label brand identity (White-Label Phase 1). Populated on\n * the org doc (`orgs/{orgId}.brandingProfile`) and applied ONLY when the org\n * carries the `whiteLabel` entitlement (Agency or Enterprise plan, or a\n * per-org override);\n * otherwise every surface falls back to the Aglyn defaults baked into\n * `resolveBrandingProfile`. Every field is optional — a partial profile\n * still resolves, with the Aglyn default filling each gap — so an agency can\n * set just a product name and from-name without supplying logos.\n */\nexport interface OrgBrandingProfile {\n /** Brand name shown in place of \"Aglyn\" (console chrome, emails, badges). */\n productName?: string\n /** Primary/full-color logo URL (light backgrounds, console chrome). */\n logoUrl?: string\n /**\n * The logo for DARK grounds (AGL-3406): the console chrome in dark mode and the\n * published site's attribution badge, which always sits on a dark pill.\n * Unset falls back to {@link logoUrl}.\n */\n logoDarkUrl?: string\n /** Favicon URL for branded surfaces. */\n faviconUrl?: string\n /**\n * The favicon for a browser whose tab strip is dark, emitted as a second\n * `<link rel=\"icon\">` under `(prefers-color-scheme: dark)`. Unset falls back\n * to {@link faviconUrl}.\n */\n faviconDarkUrl?: string\n /** Brand primary color as a CSS color (hex), e.g. `#1a73e8`. */\n primaryColor?: string\n /** Support/help destination linked from branded surfaces and emails. */\n supportUrl?: string\n /** Transactional email from-name (the display name before the address). */\n fromName?: string\n /** Logo URL specifically for the email header (often a hosted PNG). */\n emailLogoUrl?: string\n /** Custom console domain the agency serves the app on (Phase 4 wiring). */\n customConsoleDomain?: string\n}\n\n/**\n * An org's enterprise SSO configuration (AGL-1101, Phase 1). Applied ONLY when\n * the org carries the `ssoEnabled` entitlement. The actual IdP lives in a\n * per-org **GCIP tenant** (`tenantId`) with a SAML/OIDC **provider**\n * (`providerId`); this block is the org-doc mirror the console reads to route\n * sign-in and the staff card edits. A public `ssoDomains/{domain}` doc maps a\n * verified email domain → `{ orgId, tenantId, providerId }` so the\n * pre-auth sign-in page can resolve an SSO org without reading the org doc.\n *\n * Security: a domain reaches `domains[]` ONLY by passing DNS TXT verification\n * (AGL-1210). This is the account-takeover guard, and it is the whole reason\n * self-serve is safe: without it an org could claim a domain it does not own,\n * `ssoDomains/{domain}` would route that domain's sign-ins to its IdP, and it\n * would intercept another company's logins. Claims in flight live in\n * `orgs/{orgId}/ssoDomains/{domain}` (see `OrgSsoDomainClaim`) and are NOT\n * governed until verified.\n *\n * `enforced` blocks password/social login for the governed domains, so it is a\n * lockout risk and is rehearsed (`previewSsoEnforcement`) before it is applied.\n */\nexport interface OrgSsoConfig {\n /** GCIP tenant id that carries this org's IdP provider. */\n tenantId: string\n /** GCIP provider id, e.g. `saml.aglyn-workspace` or `oidc.acme`. */\n providerId: string\n /** IdP protocol. Phase 1 ships SAML; OIDC is Phase 2. */\n protocol: 'saml' | 'oidc'\n /** Human label for the IdP (shown on the SSO button + staff card). */\n displayName?: string\n /**\n * Email domains routed to this IdP (lowercased, no `@`). A domain is added\n * here only after its claim passes DNS TXT verification, and removed the\n * moment re-verification fails — so membership of this array IS the\n * \"ownership proven\" statement. Never write to it from anywhere but the\n * verification path.\n */\n domains: string[]\n /**\n * Retained for `sso-jit`, which gates on it. Kept in lockstep with\n * `domains.length > 0` by the verification path; it was a staff-attested\n * boolean before AGL-1210 and is now derived, never asserted by a human.\n */\n domainVerified: boolean\n /**\n * SAML metadata the CUSTOMER supplies about their IdP. Stored so the pool's\n * provider config can be rebuilt or re-applied without asking again. The\n * X.509 certificate is a public signing certificate, not a secret.\n */\n idp?: {\n entityId: string\n ssoUrl: string\n certificates: string[]\n }\n /**\n * Require SSO for the governed domains — disables password/social login for\n * them (Phase 2). Phase 1 keeps this false so users keep a fallback.\n */\n enforced: boolean\n /** Lifecycle: `configuring` (not live), `active`, or `disabled`. */\n status: 'configuring' | 'active' | 'disabled'\n /**\n * Uid + time of the last config change. Since AGL-1210 this is normally an\n * ORG ADMIN, not staff — the flow is self-serve end to end.\n */\n configuredBy?: string\n configuredAt?: ITimestamp\n}\n\n/**\n * A pending or proven claim on one email domain (AGL-1210), stored at\n * `orgs/{orgId}/ssoDomains/{domain}`.\n *\n * A subcollection rather than a map on the org doc because domains contain\n * dots, and a dotted key in a Firestore map is read as a nested field path by\n * every update helper — writing `sso.domainClaims[\"acme.com\"]` would silently\n * create `{acme: {com: …}}`. A document id has no such ambiguity.\n *\n * The claim is deliberately worthless on its own: holding one grants nothing.\n * Only `verified` moving to true adds the domain to `sso.domains`, and only\n * that array is consulted at sign-in.\n */\nexport interface OrgSsoDomainClaim {\n /** The domain being claimed (lowercased, no `@`); mirrors the document id. */\n domain: string\n /**\n * Random value the org must publish as a DNS TXT record at\n * `_aglyn-challenge.<domain>`. Per org+domain, so two orgs claiming the same\n * domain get different tokens and neither can pass on the other's record.\n */\n token: string\n /** True once a DNS lookup has actually seen `token` at the challenge host. */\n verified: boolean\n createdAt: ITimestamp\n verifiedAt?: ITimestamp\n /** Last lookup attempt, successful or not — drives re-verification. */\n lastCheckedAt?: ITimestamp\n /**\n * TXT records seen on the last FAILED lookup. Shown back to the customer,\n * because \"no record found\" and \"found the wrong value\" are different\n * mistakes and the fix differs.\n */\n lastRecords?: string[]\n}\n\n/**\n * Effective limits/gates for a tenant. Plan defaults come from\n * `PLAN_ENTITLEMENTS` (versioned with the app); per-tenant overrides can be\n * stored on the tenant doc and win over the plan defaults.\n *\n * The CORE half. Read {@link OrgEntitlements} instead: it is this plus\n * whatever the plugins declare. The keys spelled out below predate the seam\n * and move to their plugins with the rest of AGL-3080; every NEW key is\n * declared by whoever owns it.\n */\nexport interface CoreOrgEntitlements {\n hostLimit?: number\n screensPerHost?: number\n sharedLayoutsPerHost?: number\n /** Saved templates per host (AGL-666) — includes marketplace downloads. */\n templatesPerHost?: number\n // NOTE (AGL-658): \"add-on\" means two unrelated things in this codebase.\n // `seatAddons` below are BILLING capacity — extra managers, hosts, seats —\n // surfaced in the UI as \"plan add-ons\". The marketplace sense (installed\n // plugins, the `orgAddons` slot) is a different concept entirely. The\n // marketplace owns the bare word; billing copy always qualifies it.\n // Firestore and Stripe lookup keys stay as they are — they are persisted.\n storagePerHostMb?: number\n /**\n * RETIRED (AGL-2133). Enforced by nothing and unreachable by measurement —\n * see `RETIRED_ENTITLEMENT_KEYS`. It stays on the TYPE because live org\n * documents still carry staff overrides of it and `OrgEntitlements` is the\n * shape those documents are read through; `resolveOrgEntitlements` drops\n * it, and no plan declares it any more, so nothing can resolve a value.\n * @deprecated\n */\n totalSiteSizeMb?: number\n /**\n * Included per-site COLLABORATOR seats (`hosts/{id}/members`,\n * viewer/editor/admin) — console teammates scoped to one site, not\n * end-user member accounts (`siteMembers`), which are unlimited on\n * every plan (AGL-888/889). Legacy key name; persisted, do not rename.\n */\n membersPerHost?: number\n /** Seat model (AGL-112): included tenant-manager seats. */\n managersPerOrg?: number\n /** Hard seat caps incl. purchased addons; beyond these, upgrade the plan. */\n maxManagersPerOrg?: number\n /** Hard per-site collaborator cap incl. addons (see `membersPerHost`). */\n maxMembersPerHost?: number\n bandwidthGb?: number\n /**\n * Saved form DEFINITIONS per host — documents under\n * `hosts/{hostId}/forms`, the entity a submission's `formId` points at.\n *\n * Not the same dimension as `formSubmissionsPerMonth`, and the two are\n * routinely confused: this is how many distinct intake forms an author may\n * build, that is how many replies the site may accept. Only the second is\n * tiered. This one resolves to the same `FORMS_PER_HOST_CEILING` on every\n * plan that has the form entity at all — it bounds a collection rather than\n * selling a tier — and exists as an entitlement so that `checkQuota` can\n * refuse a create, and so a contract can override one org's number.\n *\n * Counts the CATALOG, never the traffic. A `Form` node drawn on a page and\n * left unbound submits without a definition and spends nothing here, which\n * is why a plan resolving to 0 still accepts submissions.\n */\n formsPerHost?: number\n /** Component-builder caps (AGL-99): host variables. */\n variablesPerHost?: number\n /** Component-builder caps (AGL-99): host functions. */\n functionsPerHost?: number\n /** Workflow builder cap (AGL-99/101). */\n workflowsPerHost?: number\n /** Bookable services per host (AGL-159). */\n servicesPerHost?: number\n /** Redirect rules per host (AGL-154). */\n redirectsPerHost?: number\n /**\n * The CRM RECORDS band (AGL-197, widened in AGL-2611): contacts, companies\n * and deals, counted together across the org. The persisted key still says\n * \"contacts\" because it is written on live org documents as a staff\n * override and read back through this type; every customer surface says\n * \"CRM records\". A tier with an `extraContactsUsdPer1k` rate meters past\n * it; a tier with none refuses the next record. Free has no CRM\n * (AGL-2851), so on Free the record its band refuses is a capture. Tasks\n * and activities are not counted — see `CRM_ACTIVITIES_PER_RECORD_CEILING`.\n */\n contactsPerHost?: number\n /**\n * CAMPAIGN emails sendable per calendar month (AGL-161), and campaign\n * emails only (AGL-1438).\n *\n * Transactional mail — password resets, invites, order confirmations,\n * booking reminders, workflow notifications — is never refused by this cap\n * at any tier. It still COUNTS toward the org's email cost meter; it simply\n * cannot be blocked, because a quota that can drop a password reset locks\n * somebody out of their own account and a dropped order confirmation reads\n * to the buyer as a failed order.\n *\n * The name is the narrow thing on purpose. A cap that means something\n * narrower than it says is how AGL-1438 came to exist, so every surface\n * that shows this number says \"campaign\".\n *\n * Sized against shared-domain reputation, which is a different principle\n * from the one behind `crmEmailsPerDay` — see there for why the two ladders\n * are not meant to track each other.\n */\n emailSendsPerMonth?: number\n /** Action runs per calendar month (AGL-148). */\n actionRunsPerMonth?: number\n /** Included customer REST API requests per calendar month (AGL-634);\n * Beyond it, metered overage per 1,000 where the plan prices it. Only\n * Business/Advanced carry `apiAccess`, so lower tiers are 0. */\n apiRequestsPerMonth?: number\n /** Dynamic data caps — org-scoped (AGL-239/240): datasets are shared\n * by every host in the org, so counts and size meter per org. */\n datasetsPerOrg?: number\n /** Hard dataset cap incl. addons (AGL-132/240); beyond it, upgrade. */\n maxDatasetsPerOrg?: number\n recordsPerDataset?: number\n /** Included aggregate dataset storage (MB) across the org (AGL-240);\n * Beyond it, metered overage per GB where the plan prices it. */\n dataStorageMbPerOrg?: number\n /** @deprecated Legacy host-keyed override (pre-AGL-240); resolved into\n * `datasetsPerOrg` by `resolveOrgEntitlements`. */\n datasetsPerHost?: number\n /** @deprecated Legacy host-keyed override (pre-AGL-240); resolved into\n * `maxDatasetsPerOrg` by `resolveOrgEntitlements`. */\n maxDatasetsPerHost?: number\n /** Catalog products per host (AGL-278). */\n productsPerHost?: number\n /** Inventory locations per host (AGL-286). */\n inventoryLocations?: number\n /** Concurrent POS registers (AGL-312); add-ons raise it (AGL-329). */\n posRegisters?: number\n /** Platform fee % on physical storefront sales (Connect app fee). */\n transactionFeePhysicalPct?: number\n /** Platform fee % on digital storefront sales (Connect app fee). */\n transactionFeeDigitalPct?: number\n features?: OrgFeatureFlags\n /**\n * A staff PLAN COMP (AGL-3034) — see `OrgPlanComp`. Not a quota: the\n * resolver's numeric loop skips it, and `ResolvedOrgEntitlements` omits\n * it. Read through `readOrgPlanComp`, never directly.\n */\n planComp?: OrgPlanComp\n}\n\n/**\n * Effective limits and gates for a tenant, the platform's and its plugins'\n * together (AGL-3124).\n *\n * {@link CoreOrgEntitlements} is what `PLAN_ENTITLEMENTS` declares per plan\n * and what `ResolvedOrgEntitlements` keeps exhaustive.\n * {@link PluginEntitlementQuotas} is the plugin half: a plugin declares a\n * key of its own into it with `declare module`, and its figure per plan comes\n * from that plugin's `plan-entitlements` declaration, compiled into\n * `PLAN_ENTITLEMENTS` (AGL-3080), widened by any seat add-on it declares. So\n * `resolveOrgEntitlements` carries a plugin's band, and `keyof\n * OrgEntitlements` admits its key, without this file naming the plugin's\n * domain.\n */\nexport interface OrgEntitlements\n extends CoreOrgEntitlements,\n PluginEntitlementQuotas {}\n\n/**\n * A plan staff GRANTED a workspace that no live subscription pays for\n * (AGL-3034), stored at `orgs/{orgId}.entitlements.planComp`.\n *\n * ## Why a marker of its own, and not the stored `plan`\n *\n * `plan: 'pro'` beside `billingStatus: 'canceled'` is what EVERY customer who\n * canceled can look like: Stripe's mirror is not always walked back to\n * `free`, and a staff override used to write `plan` directly. So a stored\n * paid plan must never outrank a dead subscription — `resolveEffectivePlan`\n * reads it as Free, which is AGL-247's rule and stays it. A comp is a\n * different fact with a different writer: only `/api/admin/org-override`\n * writes this, audited, with a reason. Nothing reads a comp out of `plan`.\n *\n * ## When it applies\n *\n * Only while the subscription is dead or absent — `resolvePlanComp`. A live\n * subscription (and any status the resolver does not read as dead) governs\n * the plan, and the comp waits, dormant, until staff remove it or the\n * subscription ends.\n *\n * ## Why under `entitlements`\n *\n * It sits beside the other staff overrides, on the one map the override\n * route already owns and the rules already deny to every client, staff\n * included (AGL-1795). A top-level key would have needed a new entry in all\n * three of the org rule's deny-lists, in a rules file at its size limit.\n *\n * ## What it is NOT\n *\n * Not a sale. A comp bills nothing, so every metered band on it is a wall —\n * `resolvePlanPricing` — and it is never a card on file or a paying\n * subscription: `isBillingSubscription` stays false, and the AI overage\n * charge path has no rate to bill it at.\n *\n * ## Capped or uncapped (AGL-3049)\n *\n * A comp is CAPPED unless staff lift it: every band is its plan's figure, a\n * hard limit, and a per-org quota override beside the comp raises one band.\n * An UNCAPPED comp (`uncapped: true`) reads every band and quota as\n * unlimited while it is in force — the shape an internal workspace needs —\n * and still bills nothing. The flag is part of the grant: the same route\n * writes it with its own reason and audit row, removing the comp removes it,\n * and a live subscription ignores it along with the rest of the comp.\n *\n * The route states it on every grant, `false` included. The org write is a\n * merge, which writes a nested map key by key, so a capped grant that left\n * the key out would keep the `true` of the uncapped comp it replaced.\n *\n * Typed loosely where foundation cannot import the vocabulary: `reason` is\n * an `OrgOverrideReasonCode` (`app-utils/org-override-reason`), narrowed by\n * `readOrgPlanComp`.\n */\nexport interface OrgPlanComp {\n /** The plan granted. A paid plan; a comp of `free` is not a comp. */\n plan: OrgPlan\n /**\n * Every band and quota reads as unlimited while the comp is in force\n * (AGL-3049). Only a literal `true` lifts them; absent means capped.\n */\n uncapped?: boolean\n /** Why, from the override's fixed reason set (AGL-1652). */\n reason: string\n /** Staff-only rationale; explicit `null` when none was given. */\n note: string | null\n /** The staff uid that granted it. */\n grantedBy: string\n /** When it was granted — the server's clock. */\n grantedAt: ITimestamp | null\n}\n\n/**\n * Paid addon quantities (AGL-112/524) purchased on top of the plan's\n * included allowances, billed as items on the org's Stripe subscription.\n * Seat/dataset kinds resolve as `included + purchased` clamped to the\n * plan's hard max — beyond the max the org must upgrade. Purchases only\n * count while the subscription is alive (they bill on it); staff grants\n * live on `entitlements` overrides instead, so the two never collide.\n */\nexport interface OrgSeatAddons {\n /** Extra tenant-manager seats. */\n managers?: number\n /**\n * Extra per-site collaborator seats. Legacy key name (AGL-888): the Stripe\n * price env suffix is still `EXTRA_MEMBER` and existing org docs carry this\n * key — persisted, do not rename.\n *\n * A POOL, not a raise (AGL-2439) — the same correction `posRegisters`\n * received in AGL-1775, applied to the key that never got it.\n * `membersPerHost` is enforced PER SITE, so folding this org-wide quantity\n * into the org-level entitlement handed every site the whole purchase: one\n * extra collaborator seat bought 20 collaborators on a 20-site org. Since\n * AGL-2439 the quantity here is the size of an org-level pool and\n * `collaboratorAllocations` says which site each purchased seat is assigned\n * to. Nothing reads this as a per-site number; use\n * `checkHostCollaboratorQuota` / `resolveHostCollaboratorCap`.\n */\n members?: number\n /** Extra org datasets (AGL-132/240); billed monthly per dataset. */\n datasets?: number\n /** Extra sites beyond the plan's `hostLimit` (AGL-68/524). */\n hosts?: number\n /**\n * Extra POS registers beyond the plan's `posRegisters` (AGL-329/524).\n *\n * A POOL, not a raise (AGL-1775). `posRegisters` is enforced PER SITE, so\n * folding this quantity into the org-level entitlement handed every site\n * the whole purchase — one $89/mo register bought 20 registers on a\n * 20-site org. Since AGL-1775 the quantity here is the size of an org-level\n * pool and `registerAllocations` says which site each purchased seat is\n * assigned to. Nothing reads this as a per-site number; use\n * `resolveHostRegisterCap`.\n */\n posRegisters?: number\n /** Event Calendar org-wide toggle, 0/1 (AGL-145/524). */\n eventCalendar?: number\n /**\n * Aglyn AI org-wide toggle, 0/1 (AGL-2896). One purchase covers every\n * host in the org, the way Event Calendar does: `resolveOrgEntitlements`\n * reads any quantity >= 1 as one, adds the plan's\n * `AI_ADDON_CREDITS_PER_MONTH` band to `assistCreditsPerMonth`, and\n * switches `features.aiGenerative` and `features.aiAssist` on. Priced per\n * plan at `PlanPricing.aiAddonMonthlyUsd`; `null` there means the plan\n * does not sell it.\n */\n aiAddon?: number\n}\n\n/**\n * Which SITE each purchased POS register seat is assigned to (AGL-1775):\n * `{ [hostId]: seats }`, drawn from the org-level pool\n * `seatAddons.posRegisters`.\n *\n * WHY IT LIVES ON THE ORG DOC. The pool is org-level and the entitlement it\n * modifies is resolved from this same document, so a caller that can resolve\n * entitlements at all already holds the allocation — there is no second read\n * to fail and no separate loading state in which a host could resolve to\n * something other than its plan cap. That property is the point: the\n * `checkQuota(undefined)` = Free-tier lesson inverted, where an absent\n * allocation must mean the PLAN's cap and never the pooled total.\n *\n * AN ENTITLEMENT INPUT, and therefore Admin-SDK-only — it is denied to every\n * client in `cloud/firebase-firestore.rules` alongside `seatAddons` and\n * `entitlements`. A client that could write this could assign itself the\n * whole pool on every site, which is the defect AGL-1775 exists to close.\n *\n * A host id absent from the map holds ZERO purchased seats and resolves to\n * the plan cap alone. Deleting a site deletes its key, which returns its\n * seats to the pool — the pool is `seatAddons.posRegisters` minus the sum of\n * this map, so a released key is available capacity by arithmetic rather\n * than by a separate counter that could drift.\n */\nexport type OrgRegisterAllocations = Record<string, number>\n\n/**\n * Which SITE each purchased COLLABORATOR seat is assigned to (AGL-2439):\n * `{ [hostId]: seats }`, drawn from the org-level pool `seatAddons.members`.\n *\n * The same object as `OrgRegisterAllocations` and for the same reasons —\n * see that type's block, all of which applies verbatim. It is a distinct type\n * because it is a distinct field on the document and the two pools are bought\n * separately; nothing may read one as the other.\n *\n * AN ENTITLEMENT INPUT, and therefore Admin-SDK-only — denied to every client\n * in `cloud/firebase-firestore.rules` alongside `seatAddons`,\n * `registerAllocations` and `entitlements`. A client that could write this\n * could assign itself the whole pool on every site, which is the defect the\n * pool exists to close.\n *\n * A host id absent from the map holds ZERO purchased seats and resolves to\n * the plan cap alone. Deleting a site deletes its key, returning its seats to\n * the pool by arithmetic rather than by a counter that could drift.\n *\n * ONE THING DIFFERS FROM REGISTERS and it is deliberate: collaborator seats\n * clamp to the plan's `maxMembersPerHost` band, because that is how they are\n * sold. Assigning ten pool seats to a Starter site cannot lift it past ten —\n * `resolveHostCollaboratorCap` applies the clamp, so the console must not\n * present an assignment past the band as capacity.\n */\nexport type OrgCollaboratorAllocations = Record<string, number>\n\n/**\n * The monthly storage-overage spend cap an org CHOSE for itself (AGL-1886,\n * corrected 2026-08-18).\n *\n * The ceiling on metered spend is the END USER's control: it exists to prevent\n * a surprise, not to gate the product on a customer's acknowledgement.\n *\n * So this document is **absent by default and absent is the normal state**.\n * Storage past a metered plan's included band bills without it; the customer\n * is warned by `usage-alerts` on approach and at the band. Writing a cap here\n * is a customer opting IN to being stopped, and it is the only thing that can\n * refuse an upload on a plan that meters.\n *\n * NOT a consent record. As an acknowledgement that must exist before a paying\n * org may store a byte past its band, it costs revenue and blocks customers\n * without preventing any bill — `report-usage` bills stored bytes and never\n * reads this document.\n *\n * Still an ENTITLEMENT INPUT in the security sense, in the other direction: a\n * member who could raise their own `capUsd` could raise their own spend\n * ceiling, so the rules deny it to every client and only\n * `/api/billing/storage-overage` (Admin SDK, `billing.manage`) writes it.\n *\n * @see apps/console/utils/storage-overage.ts for the model and what still\n * hard-bands.\n */\nexport interface OrgStorageOverage {\n /**\n * The monthly storage-overage spend the org will not go past. Present means\n * capped; absent means uncapped, which is the default. A present but\n * malformed value resolves to `STORAGE_CAP_FALLBACK_USD` rather than to \"no\n * cap\" — a customer who asked for a ceiling must not be billed past one\n * because the stored number was corrupt.\n */\n capUsd?: number\n /** When the cap was set, for the audit trail. */\n capSetAt?: ITimestamp | null\n /** The uid that set it. */\n capSetBy?: string | null\n /**\n * LEGACY (pre-2026-08-18): the acknowledged-consent pair. Read only, and\n * honoured as a cap of `monthlyCeilingUsd` so a ceiling somebody typed is\n * not silently raised. Never written again.\n */\n acknowledgedAt?: ITimestamp | null\n /** @deprecated legacy consent field; see `acknowledgedAt`. */\n acknowledgedBy?: string | null\n /** @deprecated legacy bound; read as a cap. See `capUsd`. */\n monthlyCeilingUsd?: number\n}\n\n/**\n * The org's own answer to what happens when Aglyn Assist reaches its included\n * band (AGL-2653).\n *\n * Absent by default, and absent is the normal state: assist past the band is\n * SOLD — `reserveAssistMessage` keeps reserving and `report-usage` bills the\n * credits past the band at `PLAN_PRICING.extraAssistCreditsUsdPer1k`. Writing\n * `hardCap: true` is a customer opting IN to being stopped at the band\n * instead, which is what the gate did for every org before the overage was\n * sold. The shape mirrors `OrgStorageOverage`, the same control on storage,\n * for the same reason: a ceiling on metered spend is the END USER's control,\n * offered rather than imposed.\n *\n * An ENTITLEMENT INPUT in the security sense, in both directions at once. A\n * client that could clear it would lift a ceiling the org chose; one that\n * could set it on another org would switch that org's assistant off at the\n * band. So the rules deny it to every client and only\n * `/api/billing/assist-overage` (Admin SDK, `billing.manage`) writes it. Read\n * through `resolveAssistHardCap` in the AI plugin's `usage/assist-credits`,\n * never directly.\n *\n * `report-usage` never reads this map — it bills whatever landed past the\n * band, the way storage bills stored bytes. With the cap on that is at most\n * the one exchange that crossed the line, because an exchange's cost is known\n * only after it is answered; with it off, it is the overage the org chose to\n * buy. Reading the switch at sweep time instead would let a flip on the 1st\n * erase a month's overage, the AGL-2399 shape on a different meter.\n *\n * On a plan whose `extraAssistCreditsUsdPer1k` is `null` the switch changes\n * nothing: there is no rate to sell past the band at, so the band stays the\n * wall it always was, on or off.\n *\n * ## The dollar ceiling beside the switch (AGL-2898)\n *\n * `capUsd` is the second control, and it answers a different question. The\n * switch asks \"sell past the band at all?\"; the ceiling asks \"and if so, how\n * much?\" — a monthly figure in USD of OVERAGE, priced at the plan's rate, past\n * which the assistant refuses for the rest of the month. It is the\n * `storageOverage.capUsd` of this meter: optional, absent by default, and the\n * customer's own number rather than a band the plan sold them. Read through\n * `resolveAssistOverageCapUsd`, and enforced by `assistOverageCapReached`\n * inside the same reservation transaction as the band.\n *\n * The two are independent. With the switch on the ceiling is never reached,\n * because nothing past the band is ever sold; with the switch off and no\n * ceiling the overage is open-ended, bounded only by the plan's message cap.\n * On a plan with no rate the ceiling, like the switch, changes nothing:\n * overage on such a plan prices to zero and zero never reaches a ceiling.\n */\nexport interface OrgAssistOverage {\n /** True stops assist at the included band; absent or false sells past it. */\n hardCap?: boolean\n /** When the switch was last written, for the audit trail. */\n hardCapSetAt?: ITimestamp | null\n /** The uid that wrote it. */\n hardCapSetBy?: string | null\n /**\n * The month's ceiling on AI overage, in USD at the plan's rate. `null` or\n * absent is no ceiling; anything that is not a finite positive number reads\n * as none too, so a corrupt value cannot become a wall of `NaN`.\n */\n capUsd?: number | null\n /** When the ceiling was last written or cleared, for the audit trail. */\n capSetAt?: ITimestamp | null\n /** The uid that wrote it. */\n capSetBy?: string | null\n}\n\n/**\n * A discount applied to the org's OWN Aglyn subscription (AGL-1105) — a\n * comped enterprise deal or a redeemed coupon, mirrored from a Stripe coupon\n * so `orgMonthlyRevenueUsd` can report net-of-discount MRR without a Stripe\n * round-trip. Exactly one of `percentOff` / `amountOffUsd` is set, matching\n * the Stripe coupon it points at (Stripe coupons are one or the other).\n * DISTINCT from a storefront/customer discount — this is Aglyn's own bill.\n */\nexport interface OrgDiscount {\n /** The Stripe coupon id (`co_…`) applied to the subscription. */\n couponId: string\n /** The Stripe promotion code id (`promo_…`), when the coupon has a code. */\n promotionCodeId?: string\n /** The human redemption code (e.g. `LAUNCH25`), when one exists. */\n code?: string\n /** Percentage off, 0–100 (mutually exclusive with `amountOffUsd`). */\n percentOff?: number\n /** Fixed USD off per invoice (mutually exclusive with `percentOff`). */\n amountOffUsd?: number\n /** The staff uid that applied it (audit trail; self-serve reads `system`). */\n appliedBy: UserUid\n /** Free-text why (the enterprise deal, the promotion) — staff-only. */\n reason?: string\n appliedAt: ITimestamp\n}\n\nexport interface OrgSubscription {\n status?:\n | 'active'\n | 'trialing'\n | 'past_due'\n | 'canceled'\n | 'incomplete'\n | 'unpaid'\n /**\n * WHY the subscription ended, mirrored from Stripe's\n * `cancellation_details.reason` on `customer.subscription.deleted`\n * (AGL-1877). `'payment_failed'` is Stripe having exhausted the dunning\n * retries; `'cancellation_requested'` is the customer leaving on purpose.\n *\n * The distinction is not cosmetic — it is the only thing that tells a\n * DELINQUENT workspace apart from one that simply left, and both arrive as\n * the same event with the same `status: 'canceled'` and the same\n * `plan: 'free'` mirror. Without it `shouldAutoLockOrgForBilling` had no\n * reachable delinquent state at all (see `billing-auto-lock.ts`), the\n * churn funnel counted a dunning failure as voluntary churn, and nobody\n * could be told which had happened.\n *\n * Absent on every org whose subscription ended before this shipped, and\n * every consumer must fail CLOSED on that — an unknown reason is not a\n * proven payment failure.\n */\n canceledReason?: string | null\n priceId?: string\n /**\n * Billing interval of the plan item (AGL-532), webhook-mirrored: the\n * Billing page initializes its monthly/annual toggle from it and plan\n * switches keep it unless the toggle says otherwise.\n */\n interval?: 'month' | 'year'\n currentPeriodEnd?: ITimestamp\n /**\n * A downgrade scheduled for the current period end (AGL-1862). Set by\n * `/api/billing/subscription` when a switch walks DOWN the self-serve\n * ladder — the Stripe subscription schedule owns the transition, this is\n * the manager-gated mirror the billing page renders. `null` (not absent)\n * once released, so a merge clears it. Rides inside `subscription` because\n * `pickOrgBillingFields` drops any other top-level key.\n */\n pendingDowngrade?: {\n plan: string\n interval: 'month' | 'year'\n /** ISO timestamp of the period end the schedule flips at. */\n effectiveAt: string | null\n scheduleId: string\n } | null\n /**\n * Negotiated custom price as a **monthly-normalized** USD figure (AGL-1110),\n * set when an enterprise org bills at an ad-hoc amount rather than a plan's\n * list price — e.g. `agency` capability at $2,730/mo. An annual custom deal\n * is stored ÷12 here (the yearly total lives on the Stripe price), so every\n * reader treats it as monthly and never re-divides. Webhook-mirrored from the\n * Stripe subscription's recurring price. When present it is the truth for\n * revenue — `orgListPriceMonthlyUsd`/`orgMonthlyRevenueUsd` use it instead of\n * the plan default (fixes the custom-price MRR under-report, AGL-1110).\n */\n customMonthlyUsd?: number\n}\n\n/**\n * The org's billing/entitlement doc shape — the view of `orgs/{orgId}`\n * that `useCurrentOrg()`, the plugin-page `org` prop, and the\n * entitlement resolvers carry. (Formerly `AglynTenant`; the alias was\n * removed in AGL-444.)\n */\n/**\n * `orgs/{orgId}.bandwidthCap` — the engaged free-plan bandwidth cap\n * (AGL-1967/2070/2155).\n *\n * STAMPED WITH THE MONTH IT IS FOR, and never cleared. A new month simply\n * stops matching, so the cap lifts on its own with no write: a marker whose\n * removal depends on a cron running is a marker that stays engaged when the\n * cron fails, which would take a customer's site down over an infrastructure\n * problem rather than over their traffic.\n *\n * Plain numbers rather than `ITimestamp`, matching `suspendedUntilMs`, so the\n * value survives the tenant's cache serialization unchanged.\n */\nexport interface OrgBandwidthCap {\n /** UTC `YYYY-MM` this cap was engaged for. Only the current month refuses. */\n month: string\n /** When the sweep engaged it. Diagnostic only; nothing gates on it. */\n engagedAt?: number\n /** The org-wide page views measured at engage time. Diagnostic. */\n pageViews?: number\n /** The band those page views were measured against. Diagnostic. */\n includedPageViews?: number\n}\n\n/**\n * The organization document.\n *\n * NOT composed from a plugin-augmented interface, deliberately (AGL-3124).\n * `org-write-deny-coverage.spec.ts` enumerates this interface's fields by\n * reading THIS FILE'S SOURCE, and checks every one of them against the\n * Firestore write-deny rules; a field a plugin declared from its own file\n * would be invisible to that sweep, so composing here would open a rules\n * coverage hole to save a plugin an already-solved problem. A plugin's own\n * settings block goes through `registerPluginConfigSchema` into\n * `pluginSettings/{pluginId}`, which is its own document under its own rule.\n */\nexport interface AglynOrgBilling extends AglynDocument {\n /** The document id, injected by the reader — never a stored field. */\n $id: OrgUid\n /**\n * The org's display name, written by `createOrganization` and renamed\n * through /api/orgs/settings (the name is denormalized onto every\n * membership row, so the rename fans out server-side). Client-writable by\n * an org admin on purpose — see `ORG_CLIENT_WRITABLE_FIELDS`.\n *\n * Spelled `displayName` here until AGL-1355; nothing ever read that, and\n * the document has always carried `name`. The coverage guard derives the\n * org's field set from this interface, so a field that does not exist\n * bought nothing but a blind spot.\n */\n name?: string\n /** Free-text workspace description; nothing gates on it. */\n description?: string\n /**\n * The IANA zone this workspace's published dates read in (AGL-3237) —\n * `America/Chicago`, `Europe/Berlin`. Absent is UTC.\n *\n * What it decides is which CALENDAR DAY an instant is attributed to on a\n * published site, which is an editorial fact about the publisher: a post\n * that went out at 19:30 in Chicago is dated the 21st here and was dated\n * the 22nd while UTC decided it. Sites inherit it; see\n * `resolveSiteTimeZone`.\n *\n * SERVER-OWNED: written through /api/orgs/settings and denied to the\n * client SDK, because `resolveSiteTimeZone` reads it in `app-utils` and\n * the write-deny guard treats a resolver input as server-owned.\n * Nothing bills, gates or routes on it, and it is validated at every read\n * through `isSupportedTimeZone`, so an unusable value renders as UTC rather\n * than throwing inside a page render.\n */\n timeZone?: string\n /**\n * The sites this organization declared ONE SENDER, `{ [groupId]: { name,\n * hostIds } }` (`consent-groups.ts`). Absent, every site is alone.\n *\n * SERVER-OWNED: denied to the client SDK, because `consentGroupForHost`\n * reads it in `app-utils` and decides who a marketing basis covers and whose\n * opt-outs hold a send. The Emails hub's consent group editor changes it\n * through `/api/orgs/consent-groups`, and the one write is the executor's\n * declare step, taken only after every separating site's refusals have\n * been carried both ways (`consent-group-change.ts`).\n */\n consentGroups?: Record<string, { name: string; hostIds: string[] }>\n /**\n * A consent group change in flight (AGL-3320): `{ changeId, phase,\n * hostIds, startedAtMs, declaredAtMs? }`, present exactly while one runs\n * and cleared when it finishes or is canceled. The job it names is\n * `orgs/{orgId}/consentGroupChanges/{changeId}`.\n *\n * SERVER-OWNED: written only by the executor, which relies on it as the\n * lock — a second change, and a deletion of a site it names, are refused\n * while it stands — so a client able to clear it could run two changes at\n * once, and one able to set it could block every change after it.\n */\n consentGroupsChange?: {\n changeId: string\n phase: 'carry' | 'rehome' | 'sweep'\n hostIds: string[]\n startedAtMs: number\n declaredAtMs?: number\n }\n /**\n * Whether a declared group's sites wait for each other's confirmation click\n * (AGL-3316). Absent or `false` is off: a pending confirmation holds only\n * the asking site's mail.\n *\n * SERVER-OWNED: written through /api/orgs/settings and denied to the client\n * SDK, for the reason `consentGroups` is — `consentGroupForHost` resolves it\n * into every group a send path reads.\n */\n consentGroupsAwaitConfirmation?: boolean\n /** The workspace URL segment, reserved through `orgSlugs/{slug}`. */\n slug?: string\n /**\n * The owner's uid. Spelled `ownerId` here until AGL-1355 — every reader\n * has always used `ownerUid` (the key `createOrganization` writes and the\n * rules deny), so the declared name was dead.\n */\n ownerUid?: UserUid\n hosts?: Record<HostUid, true>\n /** Subscription tier; missing/unknown plans resolve as `free`. */\n plan?: OrgPlan\n /** Per-org entitlement overrides (admin console); win over plan defaults. */\n entitlements?: OrgEntitlements\n /**\n * Per-org RELEASE-flag overrides (AGL-1635) — a different axis from\n * `entitlements`, which asks what the org's plan includes. These ask\n * whether an unreleased feature is switched on for this one customer, and\n * win over both the Remote Config value and the rollout bucket.\n *\n * Staff-only, and deliberately narrower than `entitlements`: super staff\n * alone may write it, matching the platform-wide flag editor\n * (`/api/admin/flags` is super-only), because forcing a flag on for an org\n * is the same class of act as flipping it for everyone — just scoped.\n * Read through `parseOrgReleaseFlagOverrides`, never directly.\n *\n * Typed loosely on purpose. The keys ARE `ReleaseFlagKey`, but that union\n * lives in `app-utils/release-flags` and foundation cannot import\n * app-utils (see `platform.types.ts`). Declaring the narrow type here\n * would also overstate what is on disk: this map outlives registry\n * renames, so a retired key is a thing that genuinely exists in Firestore.\n * `parseOrgReleaseFlagOverrides` is what narrows it, dropping unknown keys\n * and non-booleans.\n */\n releaseFlags?: Record<string, boolean>\n /**\n * White-label brand identity (White-Label Phase 1). Applied only when the\n * org carries the `whiteLabel` entitlement; read exclusively through\n * `resolveBrandingProfile`, never directly, so no surface diverges.\n */\n brandingProfile?: OrgBrandingProfile\n /**\n * Enterprise SSO config (AGL-1101). Applied only when the org carries the\n * `ssoEnabled` entitlement; the console routes sign-in through the org's\n * GCIP tenant/provider named here. See `OrgSsoConfig`.\n */\n sso?: OrgSsoConfig\n /** Per-org plugin switchboard (AGL-416); see plugin-manager/enabled-plugins. */\n enabledPlugins?: string[]\n /** Purchased addon seats (AGL-112); billed monthly per seat. */\n seatAddons?: OrgSeatAddons\n /**\n * POS register seats assigned out of the org pool (AGL-1775). An\n * ENTITLEMENT INPUT: it raises a per-site cap, so it is Admin-SDK-only.\n * @see OrgRegisterAllocations\n */\n registerAllocations?: OrgRegisterAllocations\n /**\n * Per-site collaborator seats assigned out of the org pool (AGL-2439). An\n * ENTITLEMENT INPUT: it raises a per-site cap, so it is Admin-SDK-only.\n * @see OrgCollaboratorAllocations\n */\n collaboratorAllocations?: OrgCollaboratorAllocations\n /**\n * The free plan's engaged BANDWIDTH CAP (AGL-1967/2070/2155), denormalized\n * onto this doc by the `usage-alerts` cron so the serving path can refuse a\n * capped site without a read of its own.\n *\n * An ENTITLEMENT INPUT in the strongest sense: it is the only thing standing\n * between a free site that has blown its band and unmetered egress, so a\n * client-writable value would let an org admin lift their own cap by\n * deleting one field. Admin-SDK only; denied in the rules.\n *\n * Read exclusively through `bandwidthCapEngaged` (`app-utils/bandwidth-cap`),\n * never directly — the marker on its own is not the answer. The resolver\n * re-derives the plan on every read, which is what lets an org that upgrades\n * mid-month start serving again without waiting for a cron to clear it.\n */\n bandwidthCap?: OrgBandwidthCap\n /**\n * The org's assist hard-cap switch (AGL-2653) — see `OrgAssistOverage`.\n * Admin-SDK-only, denied in the rules beside `storageOverage`; read\n * through `resolveAssistHardCap`.\n */\n assistOverage?: OrgAssistOverage\n stripeCustomerId?: string\n subscription?: OrgSubscription\n /**\n * Discount on the org's own Aglyn subscription (AGL-1105) — staff-applied\n * enterprise deal or a redeemed coupon. `orgMonthlyRevenueUsd` subtracts it\n * for net-of-discount MRR; `orgListPriceMonthlyUsd` ignores it (list price).\n */\n discount?: OrgDiscount\n /**\n * Explicit **comped enterprise** marker (AGL-1110). Makes the org read as\n * \"Enterprise\" (via `isEnterpriseOrg`) without a negotiated custom price —\n * for internal/dogfood accounts (e.g. Aglyn's own org) that carry full\n * Enterprise capability + SSO but are 100%-discounted, so they collect $0\n * while infra cost is still metered. Staff-set; distinct from a paying\n * custom-priced enterprise, which qualifies via `subscription.customMonthlyUsd`.\n */\n enterprise?: boolean\n /**\n * The bare `subscription.status` word the Stripe webhook mirrors back onto\n * this doc for the AGL-275 dunning banner and `resolveEffectivePlan`\n * (AGL-1028). An ENTITLEMENT INPUT: a dead subscription downgrades a paid\n * plan to free, so a client-writable value would restore the plan.\n */\n billingStatus?: string\n /** Staff suspension (AGL-202): set = all the org's sites serve 503. */\n suspendedAt?: ITimestamp | null\n suspendedReason?: string\n /**\n * Whether the suspension above is IN FORCE, stored so the staff\n * Organizations list can filter by it with an equality (AGL-3416). `false`\n * on every unsuspended org; written beside the `suspended*` family by the\n * lockdown core and cleared on a lapsed timed lock by\n * `settleLapsedSuspensions`. Never an enforcement input: every enforcing\n * reader asks the family itself.\n */\n suspended?: boolean\n /**\n * GDPR erasure request (AGL-206): hard deletion happens ONLY via `eraseOrg`\n * after a 7-day hold from this stamp — reached by the\n * `/api/admin/run-erasures` cron, or by hand with\n * tools/scripts/erase-tenant.mjs, which calls the same function (AGL-1481).\n */\n erasureRequestedAt?: ITimestamp | null\n /**\n * The CRM's organization-wide settings (AGL-2613), written from\n * CRM → Settings by an owner or admin with the client SDK — see\n * `ORG_CLIENT_WRITABLE_FIELDS`. One map rather than a key per setting, so\n * the section can grow without a rules change each time.\n */\n crm?: OrgCrmSettings\n /**\n * The plan the platform team has asked this workspace to move to\n * (AGL-3466) — see `OrgUpgradeProposal`.\n *\n * SERVER-OWNED: written only by /api/admin/org-upgrade-proposal (staff,\n * audited) and cleared by `writeOrgBilling` once a subscription is live,\n * both Admin SDK. Denied to every client in the rules, staff included,\n * because `standingUpgradeProposal` reads it in `app-utils` and the\n * proposal is the platform's statement to the customer, not theirs.\n */\n upgradeProposal?: OrgUpgradeProposal\n createdAt?: ITimestamp\n updatedAt?: ITimestamp\n}\n\n/**\n * The platform team's request that a workspace move to a paid plan\n * (AGL-3466).\n *\n * The separate step that follows an evaluation: a client handed a workspace\n * looks around first, on whatever the workspace already has, and is asked to\n * upgrade only once staff record this. While it stands and no subscription\n * is live, the workspace's billing managers see it on the org home and on\n * Billing, with the plan preselected. Nothing about it grants or bills.\n */\nexport interface OrgUpgradeProposal {\n /** A paid plan the workspace can buy itself — never `free` or `enterprise`. */\n plan: OrgPlan\n /** The staff member who proposed it. */\n proposedBy: string\n /** Epoch millis, so every cache serialization reads it back unchanged. */\n proposedAt: number\n /** An optional line from staff, shown with the proposal. */\n note?: string\n}\n\n/**\n * What the CRM lets an organization decide for every site at once\n * (AGL-2613). Every key is optional and off when absent: a setting that\n * has never been touched behaves as the product did before it existed.\n */\nexport interface OrgCrmSettings {\n /**\n * Create a company from a captured contact's work email domain when no\n * company the capturing site can see carries that domain. Off by default:\n * a company minted from every domain that ever submitted a form is a list\n * nobody asked for. Public mailbox domains never qualify either way.\n */\n autoCreateCompanies?: boolean\n /**\n * Per-site settings, keyed by host id (AGL-2618). On the ORG document\n * rather than on each host document because the reader is the org-level\n * assignment pass — one read of one document answers every site's default\n * — and because the writer is the same owner-or-admin the rest of this\n * map admits, through the same client branch. A host document would need\n * its own rules clause for a key only the CRM reads.\n */\n hosts?: Record<string, OrgCrmHostSettings>\n /**\n * The assignment rules, in the order they are tried (AGL-2618). The first\n * rule whose every condition holds names the owner; the site's default\n * owner is the fallback when none does. Bounded by\n * `CRM_ASSIGNMENT_RULES_MAX` in the section that writes it.\n */\n assignmentRules?: OrgCrmAssignmentRule[]\n /** The round-robin pool and its pointer — see `OrgCrmRoundRobin`. */\n roundRobin?: OrgCrmRoundRobin\n}\n\n/** What one site decides for records captured on it (AGL-2618). */\nexport interface OrgCrmHostSettings {\n /**\n * The member every record captured on this site is handed to when no\n * assignment rule claimed it. Absent: the record stays unassigned, which\n * is what the product did before the setting existed.\n */\n defaultOwnerUid?: string\n}\n\n/**\n * One assignment rule (AGL-2618): `when` every named condition holds for a\n * capture, `assign` the record this way. A condition left out is not a\n * condition — a rule naming only a source matches every capture from that\n * source — and a rule naming nothing matches every capture, which is how a\n * catch-all is written. `source` is a `ContactSource`, typed as a string\n * here because the foundation cannot import the capture vocabulary; the CRM\n * module narrows it.\n */\nexport interface OrgCrmAssignmentRule {\n id: string\n when: {\n source?: string\n formId?: string\n emailDomain?: string\n tag?: string\n }\n assign: { memberUid: string } | { roundRobin: true }\n}\n\n/**\n * The round-robin pool (AGL-2618): the members handed records in turn, and\n * WHO GOT THE LAST ONE. The pointer is a uid rather than an index so that\n * editing the pool — a member added, removed or moved — never skips or\n * repeats anybody: the next record goes to whoever follows the last\n * recipient in the pool as it stands, and to the first member when the last\n * recipient is no longer in it. Advanced only by the server, inside the\n * transaction that writes the owner, so two captures landing together take\n * two different members.\n */\nexport interface OrgCrmRoundRobin {\n memberUids?: string[]\n lastAssignedUid?: string\n}\n\n/**\n * WHO OWNS EACH FIELD OF `orgs/{orgId}` (AGL-1355).\n *\n * AGL-1354 found four server-owned keys — `brandingProfile`, `sso`,\n * `discount`, `enterprise` — that the Firestore rules' org-update key diff\n * had drifted past, so an org admin holding nothing but the Firebase client\n * SDK could write them on any plan. The keys are closed. The MECHANISM that\n * opened them was a hand-maintained deny-list with nothing checking it, and\n * that is what these two maps close.\n *\n * The rule is DEFAULT-DENY, and it is enforced from the interface above:\n * `org-write-deny-coverage.spec.ts` reads every field declared on\n * `AglynOrgBilling`, and one that appears in neither the rules' deny-list nor\n * one of these maps FAILS THE BUILD, naming the field. Adding a field to the\n * interface therefore forces the ownership decision here, at the declaration,\n * on the same commit — instead of shipping client-writable and staying that\n * way until the next audit.\n *\n * So: add a field above, and either add it to the `hasAny([...])` list in\n * `cloud/firebase-firestore.rules` under `match /orgs/{orgId}` (server-owned:\n * anything an entitlement, a price, a routing decision or a staff judgement\n * reads) or add it below WITH A REASON. When in doubt, deny it — a field the\n * server writes through an Admin-SDK route loses nothing by being denied to\n * the client, and that is true of every key AGL-1354 closed.\n *\n * The entries are the fields an org admin may set from the client SDK. The\n * reason is mandatory and is the whole value of the map: it records that\n * someone decided, rather than that someone forgot.\n */\nexport const ORG_CLIENT_WRITABLE_FIELDS: Readonly<Record<string, string>> = {\n name:\n 'The workspace name. Deliberately writable by an org admin — the rules ' +\n 'admit the rename branch and `firestore-rules.test.mjs` asserts it still ' +\n 'succeeds. Cosmetic: no entitlement, price or routing decision reads it. ' +\n '(The console renames through /api/orgs/settings anyway, because the name ' +\n 'is denormalized onto every membership row and the fan-out is a server ' +\n 'job — but the client write is allowed and must stay allowed.)',\n description:\n 'Free-text workspace blurb. Nothing resolves, gates or bills on it, and ' +\n 'no server route owns it.',\n createdAt:\n 'Creation stamp seeded by `createOrganization`. Nothing gates on it; the ' +\n 'staff org list only displays it. Denying it would buy nothing and would ' +\n 'break the merge-writes below, which stamp the pair together.',\n updatedAt:\n 'Last-write stamp. Every client write that IS allowed sets it in the same ' +\n '`setDoc(..., { merge: true })` — the staff suspension, erasure-request ' +\n 'and plan-override cards all do — so denying it would deny those writes.',\n crm:\n 'The CRM settings map (AGL-2613): `autoCreateCompanies`, the per-site ' +\n 'default owner, the assignment rules and the round-robin pool ' +\n '(AGL-2618), and whatever the CRM → Settings section adds beside them. ' +\n 'Deliberately writable by an org owner or admin — the section writes it ' +\n 'client-direct by dotted path. No entitlement, price, routing decision ' +\n 'or staff judgement reads it; its readers are the contact capture door ' +\n 'deciding whether to mint a company record from an email domain and ' +\n 'whom to hand a new record to, which are choices about the org\\'s own ' +\n \"data that the org's managers are the right people to make. The pool's \" +\n 'pointer (`crm.roundRobin.lastAssignedUid`) is advanced by the server ' +\n 'inside the assigning transaction; a client that moved it would only ' +\n 'change who is next, never what anybody may see.',\n}\n\n/**\n * Fields declared on `AglynOrgBilling` that are NOT stored on the document,\n * and so cannot be written by anyone. Separate from the map above because\n * \"the client may set this\" and \"this is not a field\" are different\n * statements, and collapsing them would let a genuinely client-writable field\n * hide behind a synthetic one.\n */\nexport const ORG_UNPERSISTED_FIELDS: Readonly<Record<string, string>> = {\n $id: 'The document id, injected by the reader. Never written as a field.',\n}\n\n"],"names":["ORG_CLIENT_WRITABLE_FIELDS","name","description","createdAt","updatedAt","crm","ORG_UNPERSISTED_FIELDS","$id"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;CAgBC,GA+zCD;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4BC,GACD,OAAO,MAAMA,6BAA+D;IAC1EC,MACE,2EACA,6EACA,6EACA,8EACA,2EACA;IACFC,aACE,4EACA;IACFC,WACE,6EACA,6EACA;IACFC,WACE,8EACA,4EACA;IACFC,KACE,0EACA,kEACA,2EACA,4EACA,2EACA,2EACA,wEACA,0EACA,2EACA,0EACA,yEACA;AACJ,EAAC;AAED;;;;;;CAMC,GACD,OAAO,MAAMC,yBAA2D;IACtEC,KAAK;AACP,EAAC"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/aglyn/src/lib/foundation/definitions/org-billing.types.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * The org's billing/entitlement vocabulary (AGL-443 naming cleanup).\n *\n * HISTORY: these types were spelled `Tenant*` until AGL-444 — they\n * predate the organizations migration (AGL-232..238); the retired\n * `tenants/{uid}` collection's billing shape was mirrored ONTO the org\n * doc and the names came along. The alias is gone: everything here is\n * `Org*` and describes fields of `orgs/{orgId}`. The last persisted\n * tenant spellings (the `users.{uid}.tenants` map, Stripe\n * `metadata[tenantId]`, `host.tenantId`) were retired pre-launch in\n * AGL-445 — billing keys off `metadata[orgId]` now.\n *\n * Convention (see the docs-site glossary): \"organization/org\" is the\n * entity; \"workspace\" is the user-facing word for it; \"tenant\" is\n * reserved for the published-site runtime (`apps/tenant`,\n * `@aglyn/tenant-*` libs).\n */\n\nimport type { ITimestamp } from '@aglyn/shared-util-timestamp'\nimport type {\n AglynDocument,\n HostUid,\n OrgUid,\n UserUid,\n} from './platform.types'\n/*\n * The plugin half of the two composed shapes below (AGL-3124).\n *\n * Each is an EMPTY interface a plugin augments with `declare module`, so the\n * keys a plugin owns are part of `OrgEntitlements` and `OrgFeatureFlags`\n * without the core listing them. The AI credits, the CRM's one-to-one email\n * pace, the form-submission and workflow-run bands, the marketplace's take\n * rate and selling gate and commerce's three gated features are declared that\n * way, each in its plugin's `plan-entitlements` module (AGL-3080).\n *\n * `import type` and nothing else: TypeScript erases it, so the plugin-manager\n * module is not on this module's runtime graph and a published page that\n * reaches the billing types (through the foundation barrel it must not reach\n * anyway) carries nothing new.\n */\nimport type {\n PluginEntitlementFeatures,\n PluginEntitlementQuotas,\n} from '../../plugin-manager/plugin-entitlement-keys'\n\nexport type { OrgUid } from './platform.types'\n\n\n/** Hosted in master catalog */\n/**\n * SaaS subscription tiers (Tenant Billing & SaaS Plans, AGL-38..41).\n * Pricing v3 (2026-07) inserted `scale` between business and advanced to\n * fill the $139→$399 gap, and added `agency` above advanced for\n * high-volume multi-site orgs — see the Pricing Decision Log.\n *\n * `enterprise` (AGL-1118) is a REAL plan, not a display label: it tops the\n * ladder with unlimited capacity plus white-label and SSO, and it is the ONE\n * tier with no list price — it is staff-provisioned per deal (AGL-1110), never\n * self-serve. Surfaces that offer plans for sale iterate `SELF_SERVE_PLANS`,\n * which excludes it; surfaces that merely NAME the org's plan read\n * `PLAN_LABELS` and get \"Enterprise\" for free.\n */\nexport type OrgPlan =\n | 'free'\n | 'starter'\n | 'pro'\n | 'business'\n | 'scale'\n | 'advanced'\n | 'agency'\n | 'enterprise'\n\n/**\n * The CORE boolean feature gates per plan; quotas live beside them as\n * numbers. Read {@link OrgFeatureFlags} instead — this half exists so the\n * plan tables can stay exhaustive over the platform's own gates while a\n * plugin declares its own (AGL-3124).\n */\nexport interface CoreOrgFeatureFlags {\n /** A/B experiments (AGL-252); Business tier. */\n abTesting?: boolean\n versioning?: boolean\n /**\n * Reusable components WITHOUT a count: Starter and above. Since AGL-3615 a\n * component's create is counted against `componentsPerHost` alone — Free\n * saves one — so this flag no longer decides whether a site may make one.\n * It states that the allowance is unlimited, and a per-org override that\n * turns it on lifts `componentsPerHost` with it, which is how an org\n * granted the feature by contract before the count existed keeps it.\n */\n reusableComponents?: boolean\n customDomain?: boolean\n /**\n * Send mail as a domain the CUSTOMER owns — `hello@acme.com` — by publishing\n * our SPF, DKIM and return-path records in their own zone.\n *\n * Distinct from `customDomain`, which is the site's public web address and\n * authorizes nothing about mail; and distinct from `whiteLabel`, which\n * replaces the Aglyn brand — product name, logo, colors, support URL,\n * console chrome, favicon — across every branded surface. Sending as your\n * own name is one narrow consequence of white-labeling rather than the\n * whole of it, so the two cannot share a flag without the wider capability\n * following the narrower one wherever it goes.\n *\n * It is also the CHEAP half of the sending model, which is why it sits low\n * on the ladder. A customer-owned domain costs the platform one provider\n * domain object and NOTHING in our own DNS zone, because the customer\n * publishes the records; the platform subdomain a site is issued costs a\n * provider slot, three records in our zone and a permanent place in the\n * re-verification sweep. Holding the free-to-us option behind the highest\n * paywall makes the expensive one the default at every tier that can send.\n *\n * Read through `checkEntitlement` at the two routes that write a sending\n * identity: the org's domain list, and the per-site selection.\n */\n customSendingDomain?: boolean\n /**\n * Hold a sending subdomain of the PLATFORM's own —\n * `hello@{label}.mail.aglyn.app` — provisioned for one site inside Aglyn's\n * mail apex.\n *\n * The EXPENSIVE half of the sending model, and the reason it is a separate\n * flag from `customSendingDomain`. A domain the customer owns costs one\n * provider domain object and nothing in our zone, because they publish the\n * records. One of these costs a provider slot, THREE records in our own\n * zone, and a permanent place in the re-verification sweep — per site,\n * forever. The two capabilities are decided against different resources, so\n * a shared flag would move a bounded one whenever the unbounded one is\n * re-cut.\n *\n * The flag is the SECOND of two conditions and never the only one. Nothing\n * provisions on entitlement alone: a merchant asks from the sending-identity\n * route, this says whether the org carries what they asked for, and the\n * provider ceiling still refuses beyond the account's allowance. A site\n * refused at any of the three sends on the shared pool rather than not at\n * all.\n *\n * Read through `checkEntitlement` at\n * `libs/aglyn/src/lib/app-utils/dedicated-sending-domain.ts`, which is the\n * only reader — the claim path in `host-sending-domain.ts` goes through it.\n */\n dedicatedSendingDomain?: boolean\n removeBranding?: boolean\n /** Schedule a version to publish at a date/time (tier above versioning). */\n scheduledPublishing?: boolean\n /** AI copy assist in the besigner (AGL-89). */\n aiAssist?: boolean\n /**\n * Generative building and automation (AGL-2896): pages, components,\n * layouts, templates, SEO, email, campaigns, A/B variants, analytics\n * insights, products, CRM records, onboarding and workflows produced by\n * the assistant rather than assembled by hand.\n *\n * Distinct from `aiAssist`, which is the chat guide, copy assist and\n * generate-section rung every tier from Pro up carries. This is the\n * expensive rung: a generated surface carries the node tree, the catalog\n * and the theme in and iterates, so it draws credits by the hundreds where\n * a question draws tens. No self-serve tier includes it; the Aglyn AI\n * add-on (`OrgSeatAddons.aiAddon`) switches it on and adds the credit band\n * that funds it, and `resolveOrgEntitlements` flips this flag together\n * with `aiAssist` when the add-on is present. Enterprise carries it in the\n * agreement.\n */\n aiGenerative?: boolean\n /** No-code workflow builder (AGL-101). */\n workflows?: boolean\n /** Datasets + repeatable components (AGL-102/103). */\n dataStore?: boolean\n /** Video/file uploads in the media manager (AGL-162). */\n videoMedia?: boolean\n /** Appointment bookings (AGL-159). */\n bookings?: boolean\n /**\n * THE CRM (AGL-2611): contacts, leads, companies, the deals pipeline,\n * tasks, reports and custom fields, the CRM automation steps, and the\n * `crm:*` REST resources.\n *\n * The Contacts section included (AGL-2851). Capture still writes a site's\n * audience into the contacts collection on every plan including Free,\n * banded by `contactsPerHost`, and exporting or erasing those people stays\n * on every plan in Settings → Privacy. What this flag gates is the CRM a\n * team works them in, and it is the upgrade motive from Free to the first\n * paid tier rather than a line on top of one: for the small-business buyer\n * the CRM is the reason to pick a platform over a page builder.\n *\n * Read by the console shell through the CRM extension's and its widgets'\n * `featureFlag` (so the shell refuses every CRM section and page before\n * one mounts), by the `crm/*` routes, by the automation executor before a\n * CRM step runs, by the REST dispatcher in front of the CRM resources,\n * contacts included, and, restated, by the Firestore rules on a client's\n * CRM writes. A per-org override on `entitlements.features.crm` works the\n * way every other flag's does.\n */\n crm?: boolean\n /**\n * SEQUENCES (AGL-2974): one-to-one, multi-step email sequences a rep sends\n * from their own connected mailbox, logged on the CRM's records. The key\n * below keeps the name the feature shipped under, because it is stored on\n * the org (AGL-3199).\n *\n * False on EVERY plan, Enterprise included. Which tiers carry sequences\n * and connected mailboxes, and at what caps, is a packaging decision that\n * has not been made, so no plan may claim it. An organization reaches it\n * only through the per-org override on `entitlements.features.outreach`.\n *\n * Read by the console shell through the Sequences extension's\n * `featureFlag`, by the plugin's routes, and, restated, by the Firestore\n * rules in front of the `outreach*` collections. The rules carry no plan\n * list for it because no plan grants it; when packaging lands, the plan\n * table and those rules change together.\n */\n outreach?: boolean\n /**\n * Basic presentational interactions (AGL-577): menu/drawer open-close,\n * element show/hide, class toggles, sticky nav, navigation, site\n * alerts. Included on ALL plans — pure client-side DOM with no server\n * cost. The `actions` flag below gates the powerful automation steps\n * (server dispatch, runJs, analytics, overlays, raw HTML).\n *\n * `true` on all eight tiers, and NO code gates on it — which makes it look\n * dead, and it has now been filed as dead once (AGL-2082). It is not.\n * `tools/marketing/build-pricing-tables.mts` reads its value to emit the\n * \"Interactions\" row on the public /pricing compare table, ticked on every\n * plan, and the Free plan card bullets it. That row is the claim that a\n * hover-to-open menu is not a paid feature — a real competitive statement,\n * and the reason `true` everywhere is a DECISION rather than a default.\n *\n * So: do not delete it as dead weight. Deleting it silently removes a\n * public pricing row while the Figma frames still carry it in all four\n * responsive variants, which is a pricing call and not a cleanup.\n */\n interactions?: boolean\n /** Event → action automation builder (AGL-148). */\n actions?: boolean\n /** Outbound/inbound webhooks (AGL-149). */\n webhooks?: boolean\n /** Customer REST API v1 + API keys (AGL-615); Business tier. */\n apiAccess?: boolean\n /** Whole-site export/backup + restore (AGL-163). */\n siteExport?: boolean\n /** Multilingual sites (AGL-164): locale variants + switcher. */\n multilingual?: boolean\n /** Event Calendar add-on (AGL-145); paid, not part of any base tier. */\n eventCalendar?: boolean\n /** URL redirects manager (AGL-154). */\n redirects?: boolean\n /** Per-screen traffic analytics (AGL-150). */\n screenAnalytics?: boolean\n /** CDN delivery + responsive image variants for media (AGL-175). */\n mediaCdn?: boolean\n /** Announcement bar + promotional popups (AGL-195/196). */\n marketingOverlays?: boolean\n /** Full storefront commerce: catalog, cart, checkout (AGL-278). */\n commerce?: boolean\n /** Console point-of-sale mode (AGL-312). */\n pos?: boolean\n /** Entitlement-gated screens/sections/video paywalls (AGL-309). */\n contentGating?: boolean\n /** Verified-buyer product reviews (AGL-324). */\n productReviews?: boolean\n /** Abandoned checkout recovery emails (AGL-323). */\n abandonedCart?: boolean\n /** Dropship supplier routing on paid orders (AGL-289). */\n dropshipRouting?: boolean\n /**\n * White-label the platform (White-Label Phase 1): replace the Aglyn brand\n * — product name, logo, colors, support URL, transactional email from-name\n * — with the org's own `brandingProfile` across every branded surface.\n * Agency ($799) and Enterprise carry it on the plan (AGL-1118); any other\n * tier needs a per-org `entitlements` override. Strictly broader than\n * `removeBranding`, which only drops the\n * \"Made with Aglyn\" badge on published sites; white-label REPLACES the\n * brand rather than merely hiding it. Every branded surface resolves the\n * effective brand through `resolveBrandingProfile` so it can never drift.\n */\n whiteLabel?: boolean\n /**\n * Enterprise SSO (AGL-1101): the org's console users sign in through the\n * org's own SAML/OIDC IdP, wired as a per-org GCIP tenant (`org.sso`).\n * Distinct from `whiteLabel` — an Agency org can have one without the other.\n * Carried by the `enterprise` plan (AGL-1118) and false on every other base\n * plan; a lower tier needs a per-org `entitlements` override, which is how\n * enterprise orgs provisioned before that plan existed still get it. Gates\n * the staff SSO-config card and the SSO sign-in path; a non-entitled org can\n * neither configure nor use SSO.\n */\n ssoEnabled?: boolean\n}\n\n/**\n * Boolean feature gates per plan, the platform's and its plugins' together.\n *\n * {@link CoreOrgFeatureFlags} is the platform's own set — what\n * `PLAN_ENTITLEMENTS` declares for every plan, and what\n * `Required<CoreOrgFeatureFlags>` keeps exhaustive.\n * {@link PluginEntitlementFeatures} is the plugin half (AGL-3124): a plugin\n * declares a gate of its own into it with `declare module`, and its answer\n * per plan comes from that plugin's `plan-entitlements` declaration or its\n * `registerPluginEntitlements` defaults rather than from a row core writes —\n * which is why a plugin's gate joins HERE and not above. Every reader keeps reading `OrgFeatureFlags`, and\n * `keyof OrgFeatureFlags` admits a plugin's key the moment it is declared.\n */\nexport interface OrgFeatureFlags\n extends CoreOrgFeatureFlags,\n PluginEntitlementFeatures {}\n\n/**\n * An org's white-label brand identity (White-Label Phase 1). Populated on\n * the org doc (`orgs/{orgId}.brandingProfile`) and applied ONLY when the org\n * carries the `whiteLabel` entitlement (Agency or Enterprise plan, or a\n * per-org override);\n * otherwise every surface falls back to the Aglyn defaults baked into\n * `resolveBrandingProfile`. Every field is optional — a partial profile\n * still resolves, with the Aglyn default filling each gap — so an agency can\n * set just a product name and from-name without supplying logos.\n */\nexport interface OrgBrandingProfile {\n /** Brand name shown in place of \"Aglyn\" (console chrome, emails, badges). */\n productName?: string\n /** Primary/full-color logo URL (light backgrounds, console chrome). */\n logoUrl?: string\n /**\n * The logo for DARK grounds (AGL-3406): the console chrome in dark mode and the\n * published site's attribution badge, which always sits on a dark pill.\n * Unset falls back to {@link logoUrl}.\n */\n logoDarkUrl?: string\n /** Favicon URL for branded surfaces. */\n faviconUrl?: string\n /**\n * The favicon for a browser whose tab strip is dark, emitted as a second\n * `<link rel=\"icon\">` under `(prefers-color-scheme: dark)`. Unset falls back\n * to {@link faviconUrl}.\n */\n faviconDarkUrl?: string\n /** Brand primary color as a CSS color (hex), e.g. `#1a73e8`. */\n primaryColor?: string\n /** Support/help destination linked from branded surfaces and emails. */\n supportUrl?: string\n /** Transactional email from-name (the display name before the address). */\n fromName?: string\n /** Logo URL specifically for the email header (often a hosted PNG). */\n emailLogoUrl?: string\n /** Custom console domain the agency serves the app on (Phase 4 wiring). */\n customConsoleDomain?: string\n}\n\n/**\n * An org's enterprise SSO configuration (AGL-1101, Phase 1). Applied ONLY when\n * the org carries the `ssoEnabled` entitlement. The actual IdP lives in a\n * per-org **GCIP tenant** (`tenantId`) with a SAML/OIDC **provider**\n * (`providerId`); this block is the org-doc mirror the console reads to route\n * sign-in and the staff card edits. A public `ssoDomains/{domain}` doc maps a\n * verified email domain → `{ orgId, tenantId, providerId }` so the\n * pre-auth sign-in page can resolve an SSO org without reading the org doc.\n *\n * Security: a domain reaches `domains[]` ONLY by passing DNS TXT verification\n * (AGL-1210). This is the account-takeover guard, and it is the whole reason\n * self-serve is safe: without it an org could claim a domain it does not own,\n * `ssoDomains/{domain}` would route that domain's sign-ins to its IdP, and it\n * would intercept another company's logins. Claims in flight live in\n * `orgs/{orgId}/ssoDomains/{domain}` (see `OrgSsoDomainClaim`) and are NOT\n * governed until verified.\n *\n * `enforced` blocks password/social login for the governed domains, so it is a\n * lockout risk and is rehearsed (`previewSsoEnforcement`) before it is applied.\n */\nexport interface OrgSsoConfig {\n /** GCIP tenant id that carries this org's IdP provider. */\n tenantId: string\n /** GCIP provider id, e.g. `saml.aglyn-workspace` or `oidc.acme`. */\n providerId: string\n /** IdP protocol. Phase 1 ships SAML; OIDC is Phase 2. */\n protocol: 'saml' | 'oidc'\n /** Human label for the IdP (shown on the SSO button + staff card). */\n displayName?: string\n /**\n * Email domains routed to this IdP (lowercased, no `@`). A domain is added\n * here only after its claim passes DNS TXT verification, and removed the\n * moment re-verification fails — so membership of this array IS the\n * \"ownership proven\" statement. Never write to it from anywhere but the\n * verification path.\n */\n domains: string[]\n /**\n * Retained for `sso-jit`, which gates on it. Kept in lockstep with\n * `domains.length > 0` by the verification path; it was a staff-attested\n * boolean before AGL-1210 and is now derived, never asserted by a human.\n */\n domainVerified: boolean\n /**\n * SAML metadata the CUSTOMER supplies about their IdP. Stored so the pool's\n * provider config can be rebuilt or re-applied without asking again. The\n * X.509 certificate is a public signing certificate, not a secret.\n */\n idp?: {\n entityId: string\n ssoUrl: string\n certificates: string[]\n }\n /**\n * Require SSO for the governed domains — disables password/social login for\n * them (Phase 2). Phase 1 keeps this false so users keep a fallback.\n */\n enforced: boolean\n /** Lifecycle: `configuring` (not live), `active`, or `disabled`. */\n status: 'configuring' | 'active' | 'disabled'\n /**\n * Uid + time of the last config change. Since AGL-1210 this is normally an\n * ORG ADMIN, not staff — the flow is self-serve end to end.\n */\n configuredBy?: string\n configuredAt?: ITimestamp\n}\n\n/**\n * A pending or proven claim on one email domain (AGL-1210), stored at\n * `orgs/{orgId}/ssoDomains/{domain}`.\n *\n * A subcollection rather than a map on the org doc because domains contain\n * dots, and a dotted key in a Firestore map is read as a nested field path by\n * every update helper — writing `sso.domainClaims[\"acme.com\"]` would silently\n * create `{acme: {com: …}}`. A document id has no such ambiguity.\n *\n * The claim is deliberately worthless on its own: holding one grants nothing.\n * Only `verified` moving to true adds the domain to `sso.domains`, and only\n * that array is consulted at sign-in.\n */\nexport interface OrgSsoDomainClaim {\n /** The domain being claimed (lowercased, no `@`); mirrors the document id. */\n domain: string\n /**\n * Random value the org must publish as a DNS TXT record at\n * `_aglyn-challenge.<domain>`. Per org+domain, so two orgs claiming the same\n * domain get different tokens and neither can pass on the other's record.\n */\n token: string\n /** True once a DNS lookup has actually seen `token` at the challenge host. */\n verified: boolean\n createdAt: ITimestamp\n verifiedAt?: ITimestamp\n /** Last lookup attempt, successful or not — drives re-verification. */\n lastCheckedAt?: ITimestamp\n /**\n * TXT records seen on the last FAILED lookup. Shown back to the customer,\n * because \"no record found\" and \"found the wrong value\" are different\n * mistakes and the fix differs.\n */\n lastRecords?: string[]\n}\n\n/**\n * Effective limits/gates for a tenant. Plan defaults come from\n * `PLAN_ENTITLEMENTS` (versioned with the app); per-tenant overrides can be\n * stored on the tenant doc and win over the plan defaults.\n *\n * The CORE half. Read {@link OrgEntitlements} instead: it is this plus\n * whatever the plugins declare. The keys spelled out below predate the seam\n * and move to their plugins with the rest of AGL-3080; every NEW key is\n * declared by whoever owns it.\n */\nexport interface CoreOrgEntitlements {\n hostLimit?: number\n screensPerHost?: number\n sharedLayoutsPerHost?: number\n /** Saved templates per host (AGL-666) — includes marketplace downloads. */\n templatesPerHost?: number\n // NOTE (AGL-658): \"add-on\" means two unrelated things in this codebase.\n // `seatAddons` below are BILLING capacity — extra managers, hosts, seats —\n // surfaced in the UI as \"plan add-ons\". The marketplace sense (installed\n // plugins, the `orgAddons` slot) is a different concept entirely. The\n // marketplace owns the bare word; billing copy always qualifies it.\n // Firestore and Stripe lookup keys stay as they are — they are persisted.\n storagePerHostMb?: number\n /**\n * RETIRED (AGL-2133). Enforced by nothing and unreachable by measurement —\n * see `RETIRED_ENTITLEMENT_KEYS`. It stays on the TYPE because live org\n * documents still carry staff overrides of it and `OrgEntitlements` is the\n * shape those documents are read through; `resolveOrgEntitlements` drops\n * it, and no plan declares it any more, so nothing can resolve a value.\n * @deprecated\n */\n totalSiteSizeMb?: number\n /**\n * Included per-site COLLABORATOR seats (`hosts/{id}/members`,\n * viewer/editor/admin) — console teammates scoped to one site, not\n * end-user member accounts (`siteMembers`), which are unlimited on\n * every plan (AGL-888/889). Legacy key name; persisted, do not rename.\n */\n membersPerHost?: number\n /** Seat model (AGL-112): included tenant-manager seats. */\n managersPerOrg?: number\n /** Hard seat caps incl. purchased addons; beyond these, upgrade the plan. */\n maxManagersPerOrg?: number\n /** Hard per-site collaborator cap incl. addons (see `membersPerHost`). */\n maxMembersPerHost?: number\n bandwidthGb?: number\n /**\n * Saved form DEFINITIONS per host — documents under\n * `hosts/{hostId}/forms`, the entity a submission's `formId` points at.\n *\n * Not the same dimension as `formSubmissionsPerMonth`, and the two are\n * routinely confused: this is how many distinct intake forms an author may\n * build, that is how many replies the site may accept. Only the second is\n * tiered. This one resolves to the same `FORMS_PER_HOST_CEILING` on every\n * plan that has the form entity at all — it bounds a collection rather than\n * selling a tier — and exists as an entitlement so that `checkQuota` can\n * refuse a create, and so a contract can override one org's number.\n *\n * Counts the CATALOG, never the traffic. A `Form` node drawn on a page and\n * left unbound submits without a definition and spends nothing here, which\n * is why a plan resolving to 0 still accepts submissions.\n */\n formsPerHost?: number\n /**\n * Reusable component DEFINITIONS per host — documents under\n * `hosts/{hostId}/components` (AGL-3615). Free 1; every paid plan\n * `UNLIMITED`, the allowance it had when components were a boolean\n * feature. Refused at the create only, so a site holding more than its\n * plan now includes keeps every component.\n */\n componentsPerHost?: number\n /** Component-builder caps (AGL-99): host variables. */\n variablesPerHost?: number\n /** Component-builder caps (AGL-99): host functions. */\n functionsPerHost?: number\n /** Workflow builder cap (AGL-99/101). */\n workflowsPerHost?: number\n /** Bookable services per host (AGL-159). */\n servicesPerHost?: number\n /** Redirect rules per host (AGL-154). */\n redirectsPerHost?: number\n /**\n * The CRM RECORDS band (AGL-197, widened in AGL-2611): contacts, companies\n * and deals, counted together across the org. The persisted key still says\n * \"contacts\" because it is written on live org documents as a staff\n * override and read back through this type; every customer surface says\n * \"CRM records\". A tier with an `extraContactsUsdPer1k` rate meters past\n * it; a tier with none refuses the next record. Free has no CRM\n * (AGL-2851), so on Free the record its band refuses is a capture. Tasks\n * and activities are not counted — see `CRM_ACTIVITIES_PER_RECORD_CEILING`.\n */\n contactsPerHost?: number\n /**\n * CAMPAIGN emails sendable per calendar month (AGL-161), and campaign\n * emails only (AGL-1438).\n *\n * Transactional mail — password resets, invites, order confirmations,\n * booking reminders, workflow notifications — is never refused by this cap\n * at any tier. It still COUNTS toward the org's email cost meter; it simply\n * cannot be blocked, because a quota that can drop a password reset locks\n * somebody out of their own account and a dropped order confirmation reads\n * to the buyer as a failed order.\n *\n * The name is the narrow thing on purpose. A cap that means something\n * narrower than it says is how AGL-1438 came to exist, so every surface\n * that shows this number says \"campaign\".\n *\n * Sized against shared-domain reputation, which is a different principle\n * from the one behind `crmEmailsPerDay` — see there for why the two ladders\n * are not meant to track each other.\n */\n emailSendsPerMonth?: number\n /** Action runs per calendar month (AGL-148). */\n actionRunsPerMonth?: number\n /** Included customer REST API requests per calendar month (AGL-634);\n * Beyond it, metered overage per 1,000 where the plan prices it. Only\n * Business/Advanced carry `apiAccess`, so lower tiers are 0. */\n apiRequestsPerMonth?: number\n /** Dynamic data caps — org-scoped (AGL-239/240): datasets are shared\n * by every host in the org, so counts and size meter per org. */\n datasetsPerOrg?: number\n /** Hard dataset cap incl. addons (AGL-132/240); beyond it, upgrade. */\n maxDatasetsPerOrg?: number\n recordsPerDataset?: number\n /** Included aggregate dataset storage (MB) across the org (AGL-240);\n * Beyond it, metered overage per GB where the plan prices it. */\n dataStorageMbPerOrg?: number\n /** @deprecated Legacy host-keyed override (pre-AGL-240); resolved into\n * `datasetsPerOrg` by `resolveOrgEntitlements`. */\n datasetsPerHost?: number\n /** @deprecated Legacy host-keyed override (pre-AGL-240); resolved into\n * `maxDatasetsPerOrg` by `resolveOrgEntitlements`. */\n maxDatasetsPerHost?: number\n /** Catalog products per host (AGL-278). */\n productsPerHost?: number\n /** Inventory locations per host (AGL-286). */\n inventoryLocations?: number\n /** Concurrent POS registers (AGL-312); add-ons raise it (AGL-329). */\n posRegisters?: number\n /** Platform fee % on physical storefront sales (Connect app fee). */\n transactionFeePhysicalPct?: number\n /** Platform fee % on digital storefront sales (Connect app fee). */\n transactionFeeDigitalPct?: number\n features?: OrgFeatureFlags\n /**\n * A staff PLAN COMP (AGL-3034) — see `OrgPlanComp`. Not a quota: the\n * resolver's numeric loop skips it, and `ResolvedOrgEntitlements` omits\n * it. Read through `readOrgPlanComp`, never directly.\n */\n planComp?: OrgPlanComp\n}\n\n/**\n * Effective limits and gates for a tenant, the platform's and its plugins'\n * together (AGL-3124).\n *\n * {@link CoreOrgEntitlements} is what `PLAN_ENTITLEMENTS` declares per plan\n * and what `ResolvedOrgEntitlements` keeps exhaustive.\n * {@link PluginEntitlementQuotas} is the plugin half: a plugin declares a\n * key of its own into it with `declare module`, and its figure per plan comes\n * from that plugin's `plan-entitlements` declaration, compiled into\n * `PLAN_ENTITLEMENTS` (AGL-3080), widened by any seat add-on it declares. So\n * `resolveOrgEntitlements` carries a plugin's band, and `keyof\n * OrgEntitlements` admits its key, without this file naming the plugin's\n * domain.\n */\nexport interface OrgEntitlements\n extends CoreOrgEntitlements,\n PluginEntitlementQuotas {}\n\n/**\n * A plan staff GRANTED a workspace that no live subscription pays for\n * (AGL-3034), stored at `orgs/{orgId}.entitlements.planComp`.\n *\n * ## Why a marker of its own, and not the stored `plan`\n *\n * `plan: 'pro'` beside `billingStatus: 'canceled'` is what EVERY customer who\n * canceled can look like: Stripe's mirror is not always walked back to\n * `free`, and a staff override used to write `plan` directly. So a stored\n * paid plan must never outrank a dead subscription — `resolveEffectivePlan`\n * reads it as Free, which is AGL-247's rule and stays it. A comp is a\n * different fact with a different writer: only `/api/admin/org-override`\n * writes this, audited, with a reason. Nothing reads a comp out of `plan`.\n *\n * ## When it applies\n *\n * Only while the subscription is dead or absent — `resolvePlanComp`. A live\n * subscription (and any status the resolver does not read as dead) governs\n * the plan, and the comp waits, dormant, until staff remove it or the\n * subscription ends.\n *\n * ## Why under `entitlements`\n *\n * It sits beside the other staff overrides, on the one map the override\n * route already owns and the rules already deny to every client, staff\n * included (AGL-1795). A top-level key would have needed a new entry in all\n * three of the org rule's deny-lists, in a rules file at its size limit.\n *\n * ## What it is NOT\n *\n * Not a sale. A comp bills nothing, so every metered band on it is a wall —\n * `resolvePlanPricing` — and it is never a card on file or a paying\n * subscription: `isBillingSubscription` stays false, and the AI overage\n * charge path has no rate to bill it at.\n *\n * ## Capped or uncapped (AGL-3049)\n *\n * A comp is CAPPED unless staff lift it: every band is its plan's figure, a\n * hard limit, and a per-org quota override beside the comp raises one band.\n * An UNCAPPED comp (`uncapped: true`) reads every band and quota as\n * unlimited while it is in force — the shape an internal workspace needs —\n * and still bills nothing. The flag is part of the grant: the same route\n * writes it with its own reason and audit row, removing the comp removes it,\n * and a live subscription ignores it along with the rest of the comp.\n *\n * The route states it on every grant, `false` included. The org write is a\n * merge, which writes a nested map key by key, so a capped grant that left\n * the key out would keep the `true` of the uncapped comp it replaced.\n *\n * Typed loosely where foundation cannot import the vocabulary: `reason` is\n * an `OrgOverrideReasonCode` (`app-utils/org-override-reason`), narrowed by\n * `readOrgPlanComp`.\n */\nexport interface OrgPlanComp {\n /** The plan granted. A paid plan; a comp of `free` is not a comp. */\n plan: OrgPlan\n /**\n * Every band and quota reads as unlimited while the comp is in force\n * (AGL-3049). Only a literal `true` lifts them; absent means capped.\n */\n uncapped?: boolean\n /** Why, from the override's fixed reason set (AGL-1652). */\n reason: string\n /** Staff-only rationale; explicit `null` when none was given. */\n note: string | null\n /** The staff uid that granted it. */\n grantedBy: string\n /** When it was granted — the server's clock. */\n grantedAt: ITimestamp | null\n}\n\n/**\n * Paid addon quantities (AGL-112/524) purchased on top of the plan's\n * included allowances, billed as items on the org's Stripe subscription.\n * Seat/dataset kinds resolve as `included + purchased` clamped to the\n * plan's hard max — beyond the max the org must upgrade. Purchases only\n * count while the subscription is alive (they bill on it); staff grants\n * live on `entitlements` overrides instead, so the two never collide.\n */\nexport interface OrgSeatAddons {\n /** Extra tenant-manager seats. */\n managers?: number\n /**\n * Extra per-site collaborator seats. Legacy key name (AGL-888): the Stripe\n * price env suffix is still `EXTRA_MEMBER` and existing org docs carry this\n * key — persisted, do not rename.\n *\n * A POOL, not a raise (AGL-2439) — the same correction `posRegisters`\n * received in AGL-1775, applied to the key that never got it.\n * `membersPerHost` is enforced PER SITE, so folding this org-wide quantity\n * into the org-level entitlement handed every site the whole purchase: one\n * extra collaborator seat bought 20 collaborators on a 20-site org. Since\n * AGL-2439 the quantity here is the size of an org-level pool and\n * `collaboratorAllocations` says which site each purchased seat is assigned\n * to. Nothing reads this as a per-site number; use\n * `checkHostCollaboratorQuota` / `resolveHostCollaboratorCap`.\n */\n members?: number\n /** Extra org datasets (AGL-132/240); billed monthly per dataset. */\n datasets?: number\n /** Extra sites beyond the plan's `hostLimit` (AGL-68/524). */\n hosts?: number\n /**\n * Extra POS registers beyond the plan's `posRegisters` (AGL-329/524).\n *\n * A POOL, not a raise (AGL-1775). `posRegisters` is enforced PER SITE, so\n * folding this quantity into the org-level entitlement handed every site\n * the whole purchase — one $89/mo register bought 20 registers on a\n * 20-site org. Since AGL-1775 the quantity here is the size of an org-level\n * pool and `registerAllocations` says which site each purchased seat is\n * assigned to. Nothing reads this as a per-site number; use\n * `resolveHostRegisterCap`.\n */\n posRegisters?: number\n /** Event Calendar org-wide toggle, 0/1 (AGL-145/524). */\n eventCalendar?: number\n /**\n * Aglyn AI org-wide toggle, 0/1 (AGL-2896). One purchase covers every\n * host in the org, the way Event Calendar does: `resolveOrgEntitlements`\n * reads any quantity >= 1 as one, adds the plan's\n * `AI_ADDON_CREDITS_PER_MONTH` band to `assistCreditsPerMonth`, and\n * switches `features.aiGenerative` and `features.aiAssist` on. Priced per\n * plan at `PlanPricing.aiAddonMonthlyUsd`; `null` there means the plan\n * does not sell it.\n */\n aiAddon?: number\n}\n\n/**\n * Which SITE each purchased POS register seat is assigned to (AGL-1775):\n * `{ [hostId]: seats }`, drawn from the org-level pool\n * `seatAddons.posRegisters`.\n *\n * WHY IT LIVES ON THE ORG DOC. The pool is org-level and the entitlement it\n * modifies is resolved from this same document, so a caller that can resolve\n * entitlements at all already holds the allocation — there is no second read\n * to fail and no separate loading state in which a host could resolve to\n * something other than its plan cap. That property is the point: the\n * `checkQuota(undefined)` = Free-tier lesson inverted, where an absent\n * allocation must mean the PLAN's cap and never the pooled total.\n *\n * AN ENTITLEMENT INPUT, and therefore Admin-SDK-only — it is denied to every\n * client in `cloud/firebase-firestore.rules` alongside `seatAddons` and\n * `entitlements`. A client that could write this could assign itself the\n * whole pool on every site, which is the defect AGL-1775 exists to close.\n *\n * A host id absent from the map holds ZERO purchased seats and resolves to\n * the plan cap alone. Deleting a site deletes its key, which returns its\n * seats to the pool — the pool is `seatAddons.posRegisters` minus the sum of\n * this map, so a released key is available capacity by arithmetic rather\n * than by a separate counter that could drift.\n */\nexport type OrgRegisterAllocations = Record<string, number>\n\n/**\n * Which SITE each purchased COLLABORATOR seat is assigned to (AGL-2439):\n * `{ [hostId]: seats }`, drawn from the org-level pool `seatAddons.members`.\n *\n * The same object as `OrgRegisterAllocations` and for the same reasons —\n * see that type's block, all of which applies verbatim. It is a distinct type\n * because it is a distinct field on the document and the two pools are bought\n * separately; nothing may read one as the other.\n *\n * AN ENTITLEMENT INPUT, and therefore Admin-SDK-only — denied to every client\n * in `cloud/firebase-firestore.rules` alongside `seatAddons`,\n * `registerAllocations` and `entitlements`. A client that could write this\n * could assign itself the whole pool on every site, which is the defect the\n * pool exists to close.\n *\n * A host id absent from the map holds ZERO purchased seats and resolves to\n * the plan cap alone. Deleting a site deletes its key, returning its seats to\n * the pool by arithmetic rather than by a counter that could drift.\n *\n * ONE THING DIFFERS FROM REGISTERS and it is deliberate: collaborator seats\n * clamp to the plan's `maxMembersPerHost` band, because that is how they are\n * sold. Assigning ten pool seats to a Starter site cannot lift it past ten —\n * `resolveHostCollaboratorCap` applies the clamp, so the console must not\n * present an assignment past the band as capacity.\n */\nexport type OrgCollaboratorAllocations = Record<string, number>\n\n/**\n * The monthly storage-overage spend cap an org CHOSE for itself (AGL-1886,\n * corrected 2026-08-18).\n *\n * The ceiling on metered spend is the END USER's control: it exists to prevent\n * a surprise, not to gate the product on a customer's acknowledgement.\n *\n * So this document is **absent by default and absent is the normal state**.\n * Storage past a metered plan's included band bills without it; the customer\n * is warned by `usage-alerts` on approach and at the band. Writing a cap here\n * is a customer opting IN to being stopped, and it is the only thing that can\n * refuse an upload on a plan that meters.\n *\n * NOT a consent record. As an acknowledgement that must exist before a paying\n * org may store a byte past its band, it costs revenue and blocks customers\n * without preventing any bill — `report-usage` bills stored bytes and never\n * reads this document.\n *\n * Still an ENTITLEMENT INPUT in the security sense, in the other direction: a\n * member who could raise their own `capUsd` could raise their own spend\n * ceiling, so the rules deny it to every client and only\n * `/api/billing/storage-overage` (Admin SDK, `billing.manage`) writes it.\n *\n * @see apps/console/utils/storage-overage.ts for the model and what still\n * hard-bands.\n */\nexport interface OrgStorageOverage {\n /**\n * The monthly storage-overage spend the org will not go past. Present means\n * capped; absent means uncapped, which is the default. A present but\n * malformed value resolves to `STORAGE_CAP_FALLBACK_USD` rather than to \"no\n * cap\" — a customer who asked for a ceiling must not be billed past one\n * because the stored number was corrupt.\n */\n capUsd?: number\n /** When the cap was set, for the audit trail. */\n capSetAt?: ITimestamp | null\n /** The uid that set it. */\n capSetBy?: string | null\n /**\n * LEGACY (pre-2026-08-18): the acknowledged-consent pair. Read only, and\n * honoured as a cap of `monthlyCeilingUsd` so a ceiling somebody typed is\n * not silently raised. Never written again.\n */\n acknowledgedAt?: ITimestamp | null\n /** @deprecated legacy consent field; see `acknowledgedAt`. */\n acknowledgedBy?: string | null\n /** @deprecated legacy bound; read as a cap. See `capUsd`. */\n monthlyCeilingUsd?: number\n}\n\n/**\n * The org's own answer to what happens when Aglyn Assist reaches its included\n * band (AGL-2653).\n *\n * Absent by default, and absent is the normal state: assist past the band is\n * SOLD — `reserveAssistMessage` keeps reserving and `report-usage` bills the\n * credits past the band at `PLAN_PRICING.extraAssistCreditsUsdPer1k`. Writing\n * `hardCap: true` is a customer opting IN to being stopped at the band\n * instead, which is what the gate did for every org before the overage was\n * sold. The shape mirrors `OrgStorageOverage`, the same control on storage,\n * for the same reason: a ceiling on metered spend is the END USER's control,\n * offered rather than imposed.\n *\n * An ENTITLEMENT INPUT in the security sense, in both directions at once. A\n * client that could clear it would lift a ceiling the org chose; one that\n * could set it on another org would switch that org's assistant off at the\n * band. So the rules deny it to every client and only\n * `/api/billing/assist-overage` (Admin SDK, `billing.manage`) writes it. Read\n * through `resolveAssistHardCap` in the AI plugin's `usage/assist-credits`,\n * never directly.\n *\n * `report-usage` never reads this map — it bills whatever landed past the\n * band, the way storage bills stored bytes. With the cap on that is at most\n * the one exchange that crossed the line, because an exchange's cost is known\n * only after it is answered; with it off, it is the overage the org chose to\n * buy. Reading the switch at sweep time instead would let a flip on the 1st\n * erase a month's overage, the AGL-2399 shape on a different meter.\n *\n * On a plan whose `extraAssistCreditsUsdPer1k` is `null` the switch changes\n * nothing: there is no rate to sell past the band at, so the band stays the\n * wall it always was, on or off.\n *\n * ## The dollar ceiling beside the switch (AGL-2898)\n *\n * `capUsd` is the second control, and it answers a different question. The\n * switch asks \"sell past the band at all?\"; the ceiling asks \"and if so, how\n * much?\" — a monthly figure in USD of OVERAGE, priced at the plan's rate, past\n * which the assistant refuses for the rest of the month. It is the\n * `storageOverage.capUsd` of this meter: optional, absent by default, and the\n * customer's own number rather than a band the plan sold them. Read through\n * `resolveAssistOverageCapUsd`, and enforced by `assistOverageCapReached`\n * inside the same reservation transaction as the band.\n *\n * The two are independent. With the switch on the ceiling is never reached,\n * because nothing past the band is ever sold; with the switch off and no\n * ceiling the overage is open-ended, bounded only by the plan's message cap.\n * On a plan with no rate the ceiling, like the switch, changes nothing:\n * overage on such a plan prices to zero and zero never reaches a ceiling.\n */\nexport interface OrgAssistOverage {\n /** True stops assist at the included band; absent or false sells past it. */\n hardCap?: boolean\n /** When the switch was last written, for the audit trail. */\n hardCapSetAt?: ITimestamp | null\n /** The uid that wrote it. */\n hardCapSetBy?: string | null\n /**\n * The month's ceiling on AI overage, in USD at the plan's rate. `null` or\n * absent is no ceiling; anything that is not a finite positive number reads\n * as none too, so a corrupt value cannot become a wall of `NaN`.\n */\n capUsd?: number | null\n /** When the ceiling was last written or cleared, for the audit trail. */\n capSetAt?: ITimestamp | null\n /** The uid that wrote it. */\n capSetBy?: string | null\n}\n\n/**\n * A discount applied to the org's OWN Aglyn subscription (AGL-1105) — a\n * comped enterprise deal or a redeemed coupon, mirrored from a Stripe coupon\n * so `orgMonthlyRevenueUsd` can report net-of-discount MRR without a Stripe\n * round-trip. Exactly one of `percentOff` / `amountOffUsd` is set, matching\n * the Stripe coupon it points at (Stripe coupons are one or the other).\n * DISTINCT from a storefront/customer discount — this is Aglyn's own bill.\n */\nexport interface OrgDiscount {\n /** The Stripe coupon id (`co_…`) applied to the subscription. */\n couponId: string\n /** The Stripe promotion code id (`promo_…`), when the coupon has a code. */\n promotionCodeId?: string\n /** The human redemption code (e.g. `LAUNCH25`), when one exists. */\n code?: string\n /** Percentage off, 0–100 (mutually exclusive with `amountOffUsd`). */\n percentOff?: number\n /** Fixed USD off per invoice (mutually exclusive with `percentOff`). */\n amountOffUsd?: number\n /** The staff uid that applied it (audit trail; self-serve reads `system`). */\n appliedBy: UserUid\n /** Free-text why (the enterprise deal, the promotion) — staff-only. */\n reason?: string\n appliedAt: ITimestamp\n}\n\nexport interface OrgSubscription {\n status?:\n | 'active'\n | 'trialing'\n | 'past_due'\n | 'canceled'\n | 'incomplete'\n | 'unpaid'\n /**\n * WHY the subscription ended, mirrored from Stripe's\n * `cancellation_details.reason` on `customer.subscription.deleted`\n * (AGL-1877). `'payment_failed'` is Stripe having exhausted the dunning\n * retries; `'cancellation_requested'` is the customer leaving on purpose.\n *\n * The distinction is not cosmetic — it is the only thing that tells a\n * DELINQUENT workspace apart from one that simply left, and both arrive as\n * the same event with the same `status: 'canceled'` and the same\n * `plan: 'free'` mirror. Without it `shouldAutoLockOrgForBilling` had no\n * reachable delinquent state at all (see `billing-auto-lock.ts`), the\n * churn funnel counted a dunning failure as voluntary churn, and nobody\n * could be told which had happened.\n *\n * Absent on every org whose subscription ended before this shipped, and\n * every consumer must fail CLOSED on that — an unknown reason is not a\n * proven payment failure.\n */\n canceledReason?: string | null\n priceId?: string\n /**\n * Billing interval of the plan item (AGL-532), webhook-mirrored: the\n * Billing page initializes its monthly/annual toggle from it and plan\n * switches keep it unless the toggle says otherwise.\n */\n interval?: 'month' | 'year'\n currentPeriodEnd?: ITimestamp\n /**\n * A downgrade scheduled for the current period end (AGL-1862). Set by\n * `/api/billing/subscription` when a switch walks DOWN the self-serve\n * ladder — the Stripe subscription schedule owns the transition, this is\n * the manager-gated mirror the billing page renders. `null` (not absent)\n * once released, so a merge clears it. Rides inside `subscription` because\n * `pickOrgBillingFields` drops any other top-level key.\n */\n pendingDowngrade?: {\n plan: string\n interval: 'month' | 'year'\n /** ISO timestamp of the period end the schedule flips at. */\n effectiveAt: string | null\n scheduleId: string\n } | null\n /**\n * Negotiated custom price as a **monthly-normalized** USD figure (AGL-1110),\n * set when an enterprise org bills at an ad-hoc amount rather than a plan's\n * list price — e.g. `agency` capability at $2,730/mo. An annual custom deal\n * is stored ÷12 here (the yearly total lives on the Stripe price), so every\n * reader treats it as monthly and never re-divides. Webhook-mirrored from the\n * Stripe subscription's recurring price. When present it is the truth for\n * revenue — `orgListPriceMonthlyUsd`/`orgMonthlyRevenueUsd` use it instead of\n * the plan default (fixes the custom-price MRR under-report, AGL-1110).\n */\n customMonthlyUsd?: number\n}\n\n/**\n * The org's billing/entitlement doc shape — the view of `orgs/{orgId}`\n * that `useCurrentOrg()`, the plugin-page `org` prop, and the\n * entitlement resolvers carry. (Formerly `AglynTenant`; the alias was\n * removed in AGL-444.)\n */\n/**\n * `orgs/{orgId}.bandwidthCap` — the engaged free-plan bandwidth cap\n * (AGL-1967/2070/2155).\n *\n * STAMPED WITH THE MONTH IT IS FOR, and never cleared. A new month simply\n * stops matching, so the cap lifts on its own with no write: a marker whose\n * removal depends on a cron running is a marker that stays engaged when the\n * cron fails, which would take a customer's site down over an infrastructure\n * problem rather than over their traffic.\n *\n * Plain numbers rather than `ITimestamp`, matching `suspendedUntilMs`, so the\n * value survives the tenant's cache serialization unchanged.\n */\nexport interface OrgBandwidthCap {\n /** UTC `YYYY-MM` this cap was engaged for. Only the current month refuses. */\n month: string\n /** When the sweep engaged it. Diagnostic only; nothing gates on it. */\n engagedAt?: number\n /** The org-wide page views measured at engage time. Diagnostic. */\n pageViews?: number\n /** The band those page views were measured against. Diagnostic. */\n includedPageViews?: number\n}\n\n/**\n * The organization document.\n *\n * NOT composed from a plugin-augmented interface, deliberately (AGL-3124).\n * `org-write-deny-coverage.spec.ts` enumerates this interface's fields by\n * reading THIS FILE'S SOURCE, and checks every one of them against the\n * Firestore write-deny rules; a field a plugin declared from its own file\n * would be invisible to that sweep, so composing here would open a rules\n * coverage hole to save a plugin an already-solved problem. A plugin's own\n * settings block goes through `registerPluginConfigSchema` into\n * `pluginSettings/{pluginId}`, which is its own document under its own rule.\n */\nexport interface AglynOrgBilling extends AglynDocument {\n /** The document id, injected by the reader — never a stored field. */\n $id: OrgUid\n /**\n * The org's display name, written by `createOrganization` and renamed\n * through /api/orgs/settings (the name is denormalized onto every\n * membership row, so the rename fans out server-side). Client-writable by\n * an org admin on purpose — see `ORG_CLIENT_WRITABLE_FIELDS`.\n *\n * Spelled `displayName` here until AGL-1355; nothing ever read that, and\n * the document has always carried `name`. The coverage guard derives the\n * org's field set from this interface, so a field that does not exist\n * bought nothing but a blind spot.\n */\n name?: string\n /** Free-text workspace description; nothing gates on it. */\n description?: string\n /**\n * The IANA zone this workspace's published dates read in (AGL-3237) —\n * `America/Chicago`, `Europe/Berlin`. Absent is UTC.\n *\n * What it decides is which CALENDAR DAY an instant is attributed to on a\n * published site, which is an editorial fact about the publisher: a post\n * that went out at 19:30 in Chicago is dated the 21st here and was dated\n * the 22nd while UTC decided it. Sites inherit it; see\n * `resolveSiteTimeZone`.\n *\n * SERVER-OWNED: written through /api/orgs/settings and denied to the\n * client SDK, because `resolveSiteTimeZone` reads it in `app-utils` and\n * the write-deny guard treats a resolver input as server-owned.\n * Nothing bills, gates or routes on it, and it is validated at every read\n * through `isSupportedTimeZone`, so an unusable value renders as UTC rather\n * than throwing inside a page render.\n */\n timeZone?: string\n /**\n * The sites this organization declared ONE SENDER, `{ [groupId]: { name,\n * hostIds } }` (`consent-groups.ts`). Absent, every site is alone.\n *\n * SERVER-OWNED: denied to the client SDK, because `consentGroupForHost`\n * reads it in `app-utils` and decides who a marketing basis covers and whose\n * opt-outs hold a send. The Emails hub's consent group editor changes it\n * through `/api/orgs/consent-groups`, and the one write is the executor's\n * declare step, taken only after every separating site's refusals have\n * been carried both ways (`consent-group-change.ts`).\n */\n consentGroups?: Record<string, { name: string; hostIds: string[] }>\n /**\n * A consent group change in flight (AGL-3320): `{ changeId, phase,\n * hostIds, startedAtMs, declaredAtMs? }`, present exactly while one runs\n * and cleared when it finishes or is canceled. The job it names is\n * `orgs/{orgId}/consentGroupChanges/{changeId}`.\n *\n * SERVER-OWNED: written only by the executor, which relies on it as the\n * lock — a second change, and a deletion of a site it names, are refused\n * while it stands — so a client able to clear it could run two changes at\n * once, and one able to set it could block every change after it.\n */\n consentGroupsChange?: {\n changeId: string\n phase: 'carry' | 'rehome' | 'sweep'\n hostIds: string[]\n startedAtMs: number\n declaredAtMs?: number\n }\n /**\n * Whether a declared group's sites wait for each other's confirmation click\n * (AGL-3316). Absent or `false` is off: a pending confirmation holds only\n * the asking site's mail.\n *\n * SERVER-OWNED: written through /api/orgs/settings and denied to the client\n * SDK, for the reason `consentGroups` is — `consentGroupForHost` resolves it\n * into every group a send path reads.\n */\n consentGroupsAwaitConfirmation?: boolean\n /** The workspace URL segment, reserved through `orgSlugs/{slug}`. */\n slug?: string\n /**\n * The owner's uid. Spelled `ownerId` here until AGL-1355 — every reader\n * has always used `ownerUid` (the key `createOrganization` writes and the\n * rules deny), so the declared name was dead.\n */\n ownerUid?: UserUid\n hosts?: Record<HostUid, true>\n /** Subscription tier; missing/unknown plans resolve as `free`. */\n plan?: OrgPlan\n /** Per-org entitlement overrides (admin console); win over plan defaults. */\n entitlements?: OrgEntitlements\n /**\n * Per-org RELEASE-flag overrides (AGL-1635) — a different axis from\n * `entitlements`, which asks what the org's plan includes. These ask\n * whether an unreleased feature is switched on for this one customer, and\n * win over both the Remote Config value and the rollout bucket.\n *\n * Staff-only, and deliberately narrower than `entitlements`: super staff\n * alone may write it, matching the platform-wide flag editor\n * (`/api/admin/flags` is super-only), because forcing a flag on for an org\n * is the same class of act as flipping it for everyone — just scoped.\n * Read through `parseOrgReleaseFlagOverrides`, never directly.\n *\n * Typed loosely on purpose. The keys ARE `ReleaseFlagKey`, but that union\n * lives in `app-utils/release-flags` and foundation cannot import\n * app-utils (see `platform.types.ts`). Declaring the narrow type here\n * would also overstate what is on disk: this map outlives registry\n * renames, so a retired key is a thing that genuinely exists in Firestore.\n * `parseOrgReleaseFlagOverrides` is what narrows it, dropping unknown keys\n * and non-booleans.\n */\n releaseFlags?: Record<string, boolean>\n /**\n * White-label brand identity (White-Label Phase 1). Applied only when the\n * org carries the `whiteLabel` entitlement; read exclusively through\n * `resolveBrandingProfile`, never directly, so no surface diverges.\n */\n brandingProfile?: OrgBrandingProfile\n /**\n * Enterprise SSO config (AGL-1101). Applied only when the org carries the\n * `ssoEnabled` entitlement; the console routes sign-in through the org's\n * GCIP tenant/provider named here. See `OrgSsoConfig`.\n */\n sso?: OrgSsoConfig\n /** Per-org plugin switchboard (AGL-416); see plugin-manager/enabled-plugins. */\n enabledPlugins?: string[]\n /** Purchased addon seats (AGL-112); billed monthly per seat. */\n seatAddons?: OrgSeatAddons\n /**\n * POS register seats assigned out of the org pool (AGL-1775). An\n * ENTITLEMENT INPUT: it raises a per-site cap, so it is Admin-SDK-only.\n * @see OrgRegisterAllocations\n */\n registerAllocations?: OrgRegisterAllocations\n /**\n * Per-site collaborator seats assigned out of the org pool (AGL-2439). An\n * ENTITLEMENT INPUT: it raises a per-site cap, so it is Admin-SDK-only.\n * @see OrgCollaboratorAllocations\n */\n collaboratorAllocations?: OrgCollaboratorAllocations\n /**\n * The free plan's engaged BANDWIDTH CAP (AGL-1967/2070/2155), denormalized\n * onto this doc by the `usage-alerts` cron so the serving path can refuse a\n * capped site without a read of its own.\n *\n * An ENTITLEMENT INPUT in the strongest sense: it is the only thing standing\n * between a free site that has blown its band and unmetered egress, so a\n * client-writable value would let an org admin lift their own cap by\n * deleting one field. Admin-SDK only; denied in the rules.\n *\n * Read exclusively through `bandwidthCapEngaged` (`app-utils/bandwidth-cap`),\n * never directly — the marker on its own is not the answer. The resolver\n * re-derives the plan on every read, which is what lets an org that upgrades\n * mid-month start serving again without waiting for a cron to clear it.\n */\n bandwidthCap?: OrgBandwidthCap\n /**\n * The org's assist hard-cap switch (AGL-2653) — see `OrgAssistOverage`.\n * Admin-SDK-only, denied in the rules beside `storageOverage`; read\n * through `resolveAssistHardCap`.\n */\n assistOverage?: OrgAssistOverage\n stripeCustomerId?: string\n subscription?: OrgSubscription\n /**\n * Discount on the org's own Aglyn subscription (AGL-1105) — staff-applied\n * enterprise deal or a redeemed coupon. `orgMonthlyRevenueUsd` subtracts it\n * for net-of-discount MRR; `orgListPriceMonthlyUsd` ignores it (list price).\n */\n discount?: OrgDiscount\n /**\n * Explicit **comped enterprise** marker (AGL-1110). Makes the org read as\n * \"Enterprise\" (via `isEnterpriseOrg`) without a negotiated custom price —\n * for internal/dogfood accounts (e.g. Aglyn's own org) that carry full\n * Enterprise capability + SSO but are 100%-discounted, so they collect $0\n * while infra cost is still metered. Staff-set; distinct from a paying\n * custom-priced enterprise, which qualifies via `subscription.customMonthlyUsd`.\n */\n enterprise?: boolean\n /**\n * The bare `subscription.status` word the Stripe webhook mirrors back onto\n * this doc for the AGL-275 dunning banner and `resolveEffectivePlan`\n * (AGL-1028). An ENTITLEMENT INPUT: a dead subscription downgrades a paid\n * plan to free, so a client-writable value would restore the plan.\n */\n billingStatus?: string\n /** Staff suspension (AGL-202): set = all the org's sites serve 503. */\n suspendedAt?: ITimestamp | null\n suspendedReason?: string\n /**\n * Whether the suspension above is IN FORCE, stored so the staff\n * Organizations list can filter by it with an equality (AGL-3416). `false`\n * on every unsuspended org; written beside the `suspended*` family by the\n * lockdown core and cleared on a lapsed timed lock by\n * `settleLapsedSuspensions`. Never an enforcement input: every enforcing\n * reader asks the family itself.\n */\n suspended?: boolean\n /**\n * GDPR erasure request (AGL-206): hard deletion happens ONLY via `eraseOrg`\n * after a 7-day hold from this stamp — reached by the\n * `/api/admin/run-erasures` cron, or by hand with\n * tools/scripts/erase-tenant.mjs, which calls the same function (AGL-1481).\n */\n erasureRequestedAt?: ITimestamp | null\n /**\n * The CRM's organization-wide settings (AGL-2613), written from\n * CRM → Settings by an owner or admin with the client SDK — see\n * `ORG_CLIENT_WRITABLE_FIELDS`. One map rather than a key per setting, so\n * the section can grow without a rules change each time.\n */\n crm?: OrgCrmSettings\n /**\n * The plan the platform team has asked this workspace to move to\n * (AGL-3466) — see `OrgUpgradeProposal`.\n *\n * SERVER-OWNED: written only by /api/admin/org-upgrade-proposal (staff,\n * audited) and cleared by `writeOrgBilling` once a subscription is live,\n * both Admin SDK. Denied to every client in the rules, staff included,\n * because `standingUpgradeProposal` reads it in `app-utils` and the\n * proposal is the platform's statement to the customer, not theirs.\n */\n upgradeProposal?: OrgUpgradeProposal\n createdAt?: ITimestamp\n updatedAt?: ITimestamp\n}\n\n/**\n * The platform team's request that a workspace move to a paid plan\n * (AGL-3466).\n *\n * The separate step that follows an evaluation: a client handed a workspace\n * looks around first, on whatever the workspace already has, and is asked to\n * upgrade only once staff record this. While it stands and no subscription\n * is live, the workspace's billing managers see it on the org home and on\n * Billing, with the plan preselected. Nothing about it grants or bills.\n */\nexport interface OrgUpgradeProposal {\n /** A paid plan the workspace can buy itself — never `free` or `enterprise`. */\n plan: OrgPlan\n /** The staff member who proposed it. */\n proposedBy: string\n /** Epoch millis, so every cache serialization reads it back unchanged. */\n proposedAt: number\n /** An optional line from staff, shown with the proposal. */\n note?: string\n}\n\n/**\n * What the CRM lets an organization decide for every site at once\n * (AGL-2613). Every key is optional and off when absent: a setting that\n * has never been touched behaves as the product did before it existed.\n */\nexport interface OrgCrmSettings {\n /**\n * Create a company from a captured contact's work email domain when no\n * company the capturing site can see carries that domain. Off by default:\n * a company minted from every domain that ever submitted a form is a list\n * nobody asked for. Public mailbox domains never qualify either way.\n */\n autoCreateCompanies?: boolean\n /**\n * Per-site settings, keyed by host id (AGL-2618). On the ORG document\n * rather than on each host document because the reader is the org-level\n * assignment pass — one read of one document answers every site's default\n * — and because the writer is the same owner-or-admin the rest of this\n * map admits, through the same client branch. A host document would need\n * its own rules clause for a key only the CRM reads.\n */\n hosts?: Record<string, OrgCrmHostSettings>\n /**\n * The assignment rules, in the order they are tried (AGL-2618). The first\n * rule whose every condition holds names the owner; the site's default\n * owner is the fallback when none does. Bounded by\n * `CRM_ASSIGNMENT_RULES_MAX` in the section that writes it.\n */\n assignmentRules?: OrgCrmAssignmentRule[]\n /** The round-robin pool and its pointer — see `OrgCrmRoundRobin`. */\n roundRobin?: OrgCrmRoundRobin\n}\n\n/** What one site decides for records captured on it (AGL-2618). */\nexport interface OrgCrmHostSettings {\n /**\n * The member every record captured on this site is handed to when no\n * assignment rule claimed it. Absent: the record stays unassigned, which\n * is what the product did before the setting existed.\n */\n defaultOwnerUid?: string\n}\n\n/**\n * One assignment rule (AGL-2618): `when` every named condition holds for a\n * capture, `assign` the record this way. A condition left out is not a\n * condition — a rule naming only a source matches every capture from that\n * source — and a rule naming nothing matches every capture, which is how a\n * catch-all is written. `source` is a `ContactSource`, typed as a string\n * here because the foundation cannot import the capture vocabulary; the CRM\n * module narrows it.\n */\nexport interface OrgCrmAssignmentRule {\n id: string\n when: {\n source?: string\n formId?: string\n emailDomain?: string\n tag?: string\n }\n assign: { memberUid: string } | { roundRobin: true }\n}\n\n/**\n * The round-robin pool (AGL-2618): the members handed records in turn, and\n * WHO GOT THE LAST ONE. The pointer is a uid rather than an index so that\n * editing the pool — a member added, removed or moved — never skips or\n * repeats anybody: the next record goes to whoever follows the last\n * recipient in the pool as it stands, and to the first member when the last\n * recipient is no longer in it. Advanced only by the server, inside the\n * transaction that writes the owner, so two captures landing together take\n * two different members.\n */\nexport interface OrgCrmRoundRobin {\n memberUids?: string[]\n lastAssignedUid?: string\n}\n\n/**\n * WHO OWNS EACH FIELD OF `orgs/{orgId}` (AGL-1355).\n *\n * AGL-1354 found four server-owned keys — `brandingProfile`, `sso`,\n * `discount`, `enterprise` — that the Firestore rules' org-update key diff\n * had drifted past, so an org admin holding nothing but the Firebase client\n * SDK could write them on any plan. The keys are closed. The MECHANISM that\n * opened them was a hand-maintained deny-list with nothing checking it, and\n * that is what these two maps close.\n *\n * The rule is DEFAULT-DENY, and it is enforced from the interface above:\n * `org-write-deny-coverage.spec.ts` reads every field declared on\n * `AglynOrgBilling`, and one that appears in neither the rules' deny-list nor\n * one of these maps FAILS THE BUILD, naming the field. Adding a field to the\n * interface therefore forces the ownership decision here, at the declaration,\n * on the same commit — instead of shipping client-writable and staying that\n * way until the next audit.\n *\n * So: add a field above, and either add it to the `hasAny([...])` list in\n * `cloud/firebase-firestore.rules` under `match /orgs/{orgId}` (server-owned:\n * anything an entitlement, a price, a routing decision or a staff judgement\n * reads) or add it below WITH A REASON. When in doubt, deny it — a field the\n * server writes through an Admin-SDK route loses nothing by being denied to\n * the client, and that is true of every key AGL-1354 closed.\n *\n * The entries are the fields an org admin may set from the client SDK. The\n * reason is mandatory and is the whole value of the map: it records that\n * someone decided, rather than that someone forgot.\n */\nexport const ORG_CLIENT_WRITABLE_FIELDS: Readonly<Record<string, string>> = {\n name:\n 'The workspace name. Deliberately writable by an org admin — the rules ' +\n 'admit the rename branch and `firestore-rules.test.mjs` asserts it still ' +\n 'succeeds. Cosmetic: no entitlement, price or routing decision reads it. ' +\n '(The console renames through /api/orgs/settings anyway, because the name ' +\n 'is denormalized onto every membership row and the fan-out is a server ' +\n 'job — but the client write is allowed and must stay allowed.)',\n description:\n 'Free-text workspace blurb. Nothing resolves, gates or bills on it, and ' +\n 'no server route owns it.',\n createdAt:\n 'Creation stamp seeded by `createOrganization`. Nothing gates on it; the ' +\n 'staff org list only displays it. Denying it would buy nothing and would ' +\n 'break the merge-writes below, which stamp the pair together.',\n updatedAt:\n 'Last-write stamp. Every client write that IS allowed sets it in the same ' +\n '`setDoc(..., { merge: true })` — the staff suspension, erasure-request ' +\n 'and plan-override cards all do — so denying it would deny those writes.',\n crm:\n 'The CRM settings map (AGL-2613): `autoCreateCompanies`, the per-site ' +\n 'default owner, the assignment rules and the round-robin pool ' +\n '(AGL-2618), and whatever the CRM → Settings section adds beside them. ' +\n 'Deliberately writable by an org owner or admin — the section writes it ' +\n 'client-direct by dotted path. No entitlement, price, routing decision ' +\n 'or staff judgement reads it; its readers are the contact capture door ' +\n 'deciding whether to mint a company record from an email domain and ' +\n 'whom to hand a new record to, which are choices about the org\\'s own ' +\n \"data that the org's managers are the right people to make. The pool's \" +\n 'pointer (`crm.roundRobin.lastAssignedUid`) is advanced by the server ' +\n 'inside the assigning transaction; a client that moved it would only ' +\n 'change who is next, never what anybody may see.',\n}\n\n/**\n * Fields declared on `AglynOrgBilling` that are NOT stored on the document,\n * and so cannot be written by anyone. Separate from the map above because\n * \"the client may set this\" and \"this is not a field\" are different\n * statements, and collapsing them would let a genuinely client-writable field\n * hide behind a synthetic one.\n */\nexport const ORG_UNPERSISTED_FIELDS: Readonly<Record<string, string>> = {\n $id: 'The document id, injected by the reader. Never written as a field.',\n}\n\n"],"names":["ORG_CLIENT_WRITABLE_FIELDS","name","description","createdAt","updatedAt","crm","ORG_UNPERSISTED_FIELDS","$id"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;CAgBC,GA+0CD;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4BC,GACD,OAAO,MAAMA,6BAA+D;IAC1EC,MACE,2EACA,6EACA,6EACA,8EACA,2EACA;IACFC,aACE,4EACA;IACFC,WACE,6EACA,6EACA;IACFC,WACE,8EACA,4EACA;IACFC,KACE,0EACA,kEACA,2EACA,4EACA,2EACA,2EACA,wEACA,0EACA,2EACA,0EACA,yEACA;AACJ,EAAC;AAED;;;;;;CAMC,GACD,OAAO,MAAMC,yBAA2D;IACtEC,KAAK;AACP,EAAC"}
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
17
|
import type { OrgPermissions } from '../app-utils/org-permissions';
|
|
18
|
+
import type { ReleaseFlagKey } from '../app-utils/release-flags';
|
|
18
19
|
import type { SeoAuditReport } from '../app-utils/seo-audit';
|
|
19
20
|
import type { SeoListingFieldKey } from '../app-utils/seo-listing-fields';
|
|
20
21
|
import type { AglynOrgBilling, OrgFeatureFlags } from '../foundation';
|
|
@@ -583,6 +584,16 @@ export declare const CONSOLE_WIDGET_SLOTS: {
|
|
|
583
584
|
readonly orgDashboard: "orgDashboard";
|
|
584
585
|
/** Host dashboard commerce summary. Props: hostId, org. */
|
|
585
586
|
readonly commerceGlance: "commerceGlance";
|
|
587
|
+
/**
|
|
588
|
+
* A site's Analytics page, below its own traffic cards (AGL-3605): a
|
|
589
|
+
* section of analytics a plugin computes from what the site records — a
|
|
590
|
+
* funnel, say. Props: hostId, orgId (the workspace, or undefined while it
|
|
591
|
+
* loads).
|
|
592
|
+
*
|
|
593
|
+
* A section rather than a glance, so a widget here owns a whole card: its
|
|
594
|
+
* own range picker, its own plan answer and its own empty state.
|
|
595
|
+
*/
|
|
596
|
+
readonly hostAnalytics: "hostAnalytics";
|
|
586
597
|
/** Org Data page body. Props: orgId, org. */
|
|
587
598
|
readonly orgData: "orgData";
|
|
588
599
|
/** Besigner functions (ƒx) panel. Props: hostId. */
|
|
@@ -978,6 +989,15 @@ export declare const CONSOLE_WIDGET_SLOTS: {
|
|
|
978
989
|
* person decides.
|
|
979
990
|
*/
|
|
980
991
|
readonly sitePackageItemPreview: "sitePackageItemPreview";
|
|
992
|
+
/**
|
|
993
|
+
* The media library, beside its Upload media and New folder actions and
|
|
994
|
+
* again in its empty state (AGL-3602): another way to add a file. Props:
|
|
995
|
+
* {@link ConsoleMediaLibraryZoneProps}. The `hostScreens` contract — a
|
|
996
|
+
* widget runs its own flow and writes nothing through the library — with
|
|
997
|
+
* one door back: `onCreated`, which the widget calls with the assets it
|
|
998
|
+
* added so the library shows them, selected.
|
|
999
|
+
*/
|
|
1000
|
+
readonly mediaLibrary: "mediaLibrary";
|
|
981
1001
|
};
|
|
982
1002
|
export type ConsoleWidgetSlot = (typeof CONSOLE_WIDGET_SLOTS)[keyof typeof CONSOLE_WIDGET_SLOTS];
|
|
983
1003
|
/** What the `besignerPageProperties` zone hands each widget (AGL-3475). */
|
|
@@ -1018,6 +1038,23 @@ export type ConsoleHostTemplatesZoneProps = ConsoleHostScreensZoneProps;
|
|
|
1018
1038
|
export type ConsoleHostLayoutsZoneProps = ConsoleHostScreensZoneProps;
|
|
1019
1039
|
/** See {@link ConsoleHostTemplatesZoneProps}. */
|
|
1020
1040
|
export type ConsoleHostComponentsZoneProps = ConsoleHostScreensZoneProps;
|
|
1041
|
+
/** What the `mediaLibrary` zone hands each widget (AGL-3602). */
|
|
1042
|
+
export interface ConsoleMediaLibraryZoneProps {
|
|
1043
|
+
/**
|
|
1044
|
+
* The site whose library is open; for the organization's library, the site
|
|
1045
|
+
* on screen when there is one (a site's Media tab, a picker opened for a
|
|
1046
|
+
* site), else `null`.
|
|
1047
|
+
*/
|
|
1048
|
+
hostId: string | null;
|
|
1049
|
+
/** The org the library belongs to; `undefined` while it resolves. */
|
|
1050
|
+
orgId: string | undefined;
|
|
1051
|
+
/** Which library is open: a site's own, or the organization's. */
|
|
1052
|
+
library: 'host' | 'org';
|
|
1053
|
+
/** The folder open in the library, where new files land; `null` for none. */
|
|
1054
|
+
folderId: string | null;
|
|
1055
|
+
/** Hands the library the assets a widget added, to show and select them. */
|
|
1056
|
+
onCreated: (mediaIds: readonly string[]) => void;
|
|
1057
|
+
}
|
|
1021
1058
|
/** What the `orgSites` zone hands each widget (AGL-2911). */
|
|
1022
1059
|
export interface ConsoleOrgSitesZoneProps {
|
|
1023
1060
|
/** Always `null`: the zone belongs to the organization, not to one site. */
|
|
@@ -1516,8 +1553,56 @@ export interface ConsoleWidget {
|
|
|
1516
1553
|
* part of it. Absent without an upsell, for the reason `permission` gives.
|
|
1517
1554
|
*/
|
|
1518
1555
|
featureFlag?: keyof OrgFeatureFlags;
|
|
1556
|
+
/**
|
|
1557
|
+
* Mount this widget as its own upsell when the ONLY thing missing is the
|
|
1558
|
+
* plan entitlement (AGL-3601).
|
|
1559
|
+
*
|
|
1560
|
+
* Without it a widget whose `featureFlag` the plan lacks is absent, as
|
|
1561
|
+
* above. With it, the shell still mounts it — with `entitled={false}` and
|
|
1562
|
+
* an `upgrade` prop ({@link ConsoleWidgetUpgrade}) — but only when every
|
|
1563
|
+
* other gate passes (the reader's permission, the plugin being on for this
|
|
1564
|
+
* workspace and this site) and the missing flag is one an add-on this
|
|
1565
|
+
* workspace can buy switches on. Where nothing can be bought the widget
|
|
1566
|
+
* stays absent, so the widget never has to decide that itself.
|
|
1567
|
+
*
|
|
1568
|
+
* The widget owns what it draws in that state, and must not open the
|
|
1569
|
+
* feature: the shell has decided the plan does not include it.
|
|
1570
|
+
*/
|
|
1571
|
+
showWhenNotEntitled?: boolean;
|
|
1572
|
+
/**
|
|
1573
|
+
* The release flag this widget is behind, which the shell resolves from the
|
|
1574
|
+
* flags it already loads for every page (AGL-3601) — staff bypass applied,
|
|
1575
|
+
* as the server's own doors apply it. The widget is absent while the flag
|
|
1576
|
+
* is off for this workspace, and while the flags have not settled, so a
|
|
1577
|
+
* control is never drawn and then taken away.
|
|
1578
|
+
*
|
|
1579
|
+
* For a widget that would otherwise have to ask a server door whether its
|
|
1580
|
+
* feature exists before it draws anything.
|
|
1581
|
+
*/
|
|
1582
|
+
releaseFlag?: ReleaseFlagKey;
|
|
1519
1583
|
Component: ComponentType<any>;
|
|
1520
1584
|
}
|
|
1585
|
+
/**
|
|
1586
|
+
* Where a widget mounted by {@link ConsoleWidget.showWhenNotEntitled} sends a
|
|
1587
|
+
* reader to buy what it lacks. The shell builds it, so no extension supplies a
|
|
1588
|
+
* URL the console's own chrome then renders.
|
|
1589
|
+
*/
|
|
1590
|
+
export interface ConsoleWidgetUpgrade {
|
|
1591
|
+
/** The workspace's Billing page, at the section that sells add-ons. */
|
|
1592
|
+
billingHref: string;
|
|
1593
|
+
/** Whether the reader may buy it (`billing.manage`). */
|
|
1594
|
+
canManageBilling: boolean;
|
|
1595
|
+
}
|
|
1596
|
+
/**
|
|
1597
|
+
* The props the shell adds to a widget that declared
|
|
1598
|
+
* {@link ConsoleWidget.showWhenNotEntitled}, beside its zone's own.
|
|
1599
|
+
* `entitled` is `true` when the plan includes the feature, and `upgrade`
|
|
1600
|
+
* is present only when it is `false`.
|
|
1601
|
+
*/
|
|
1602
|
+
export interface ConsoleWidgetEntitlementProps {
|
|
1603
|
+
entitled?: boolean;
|
|
1604
|
+
upgrade?: ConsoleWidgetUpgrade;
|
|
1605
|
+
}
|
|
1521
1606
|
/**
|
|
1522
1607
|
* Console half of the pattern: everything a feature contributes to the
|
|
1523
1608
|
* console shell. Declarative — the shell owns rendering and applies the
|
|
@@ -117,6 +117,15 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
117
117
|
* row, no heading) when no widget survives the gates.
|
|
118
118
|
*/ orgDashboard: 'orgDashboard',
|
|
119
119
|
/** Host dashboard commerce summary. Props: hostId, org. */ commerceGlance: 'commerceGlance',
|
|
120
|
+
/**
|
|
121
|
+
* A site's Analytics page, below its own traffic cards (AGL-3605): a
|
|
122
|
+
* section of analytics a plugin computes from what the site records — a
|
|
123
|
+
* funnel, say. Props: hostId, orgId (the workspace, or undefined while it
|
|
124
|
+
* loads).
|
|
125
|
+
*
|
|
126
|
+
* A section rather than a glance, so a widget here owns a whole card: its
|
|
127
|
+
* own range picker, its own plan answer and its own empty state.
|
|
128
|
+
*/ hostAnalytics: 'hostAnalytics',
|
|
120
129
|
/** Org Data page body. Props: orgId, org. */ orgData: 'orgData',
|
|
121
130
|
/** Besigner functions (ƒx) panel. Props: hostId. */ besignerFunctions: 'besignerFunctions',
|
|
122
131
|
/**
|
|
@@ -483,7 +492,15 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
483
492
|
* or its value list. The widget reads nothing it is not handed beyond what
|
|
484
493
|
* its preview always reads, and never writes: the import writes, after the
|
|
485
494
|
* person decides.
|
|
486
|
-
*/ sitePackageItemPreview: 'sitePackageItemPreview'
|
|
495
|
+
*/ sitePackageItemPreview: 'sitePackageItemPreview',
|
|
496
|
+
/**
|
|
497
|
+
* The media library, beside its Upload media and New folder actions and
|
|
498
|
+
* again in its empty state (AGL-3602): another way to add a file. Props:
|
|
499
|
+
* {@link ConsoleMediaLibraryZoneProps}. The `hostScreens` contract — a
|
|
500
|
+
* widget runs its own flow and writes nothing through the library — with
|
|
501
|
+
* one door back: `onCreated`, which the widget calls with the assets it
|
|
502
|
+
* added so the library shows them, selected.
|
|
503
|
+
*/ mediaLibrary: 'mediaLibrary'
|
|
487
504
|
};
|
|
488
505
|
/**
|
|
489
506
|
* The zones on the STAFF pages (AGL-2939): the staff overview, the staff org
|