@aglyn/aglyn 1.0.0-beta.211 → 1.0.0-beta.213

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/package.json +11 -11
  2. package/src/lib/app-utils/docs-index.generated.js +14 -7
  3. package/src/lib/app-utils/docs-index.generated.js.map +1 -1
  4. package/src/lib/app-utils/plugin-bundle-checks.d.ts +3 -2
  5. package/src/lib/app-utils/plugin-bundle-checks.js +242 -3
  6. package/src/lib/app-utils/plugin-bundle-checks.js.map +1 -1
  7. package/src/lib/app-utils/plugin-host-abi.d.ts +61 -0
  8. package/src/lib/app-utils/plugin-host-abi.js +68 -0
  9. package/src/lib/app-utils/plugin-host-abi.js.map +1 -0
  10. package/src/lib/app-utils/realm-host-surface.generated.d.ts +9 -0
  11. package/src/lib/app-utils/realm-host-surface.generated.js +143 -0
  12. package/src/lib/app-utils/realm-host-surface.generated.js.map +1 -0
  13. package/src/lib/app-utils/search-engine-verification.d.ts +98 -0
  14. package/src/lib/app-utils/search-engine-verification.js +140 -0
  15. package/src/lib/app-utils/search-engine-verification.js.map +1 -0
  16. package/src/lib/app-utils/site-entity-json-ld.d.ts +9 -1
  17. package/src/lib/app-utils/site-entity-json-ld.js +25 -7
  18. package/src/lib/app-utils/site-entity-json-ld.js.map +1 -1
  19. package/src/lib/app-utils/variables.d.ts +18 -19
  20. package/src/lib/app-utils/variables.js +24 -23
  21. package/src/lib/app-utils/variables.js.map +1 -1
  22. package/src/lib/app-utils/webhook-delivery.js +3 -1
  23. package/src/lib/app-utils/webhook-delivery.js.map +1 -1
  24. package/src/lib/foundation/definitions/platform.types.d.ts +24 -0
  25. package/src/lib/foundation/definitions/platform.types.js +2 -1
  26. package/src/lib/foundation/definitions/platform.types.js.map +1 -1
  27. package/src/lib/plugin-manager/enabled-plugins.d.ts +6 -0
  28. package/src/lib/plugin-manager/enabled-plugins.js +5 -0
  29. package/src/lib/plugin-manager/enabled-plugins.js.map +1 -1
  30. package/src/lib/plugin-manager/first-party-plugins.generated.d.ts +6 -0
  31. package/src/lib/plugin-manager/first-party-plugins.generated.js +8 -1
  32. package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
  33. package/src/lib/plugin-manager/plugin-contributions.d.ts +59 -0
  34. package/src/lib/plugin-manager/plugin-contributions.js +113 -0
  35. package/src/lib/plugin-manager/plugin-contributions.js.map +1 -1
  36. package/src/lib/plugin-manager/realm-host-aglyn.generated.d.ts +8 -0
  37. package/src/lib/plugin-manager/realm-host-aglyn.generated.js +101 -0
  38. package/src/lib/plugin-manager/realm-host-aglyn.generated.js.map +1 -0
  39. package/src/lib/plugin-manager/realm-plugins.d.ts +3 -2
  40. package/src/lib/plugin-manager/realm-plugins.js +8 -3
  41. package/src/lib/plugin-manager/realm-plugins.js.map +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/aglyn/src/lib/foundation/definitions/platform.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\nimport type { HttpStatusCode } from '@aglyn/shared-data-enums'\nimport type { HostTheme } from '@aglyn/shared-data-types'\nimport type { ITimestamp } from '@aglyn/shared-util-timestamp'\nimport type {\n AglynNodeSchema,\n FieldComponentType,\n NodeId,\n} from './components.types'\n// Type-only, so this stays a definitions file with no runtime edge into\n// `app-utils`: the two video shapes are DEFINED beside the code that\n// validates them (`normalizeVideoMetadata`, `parseMediaRendition`) so a\n// document field and its validator can never describe different things.\nimport type { MediaEmbeddedMetadata } from '../../app-utils/media-embedded-fields'\nimport type { MediaVideoMetadata } from '../../app-utils/media-metadata'\nimport type { MediaVideoRendition } from '../../app-utils/media-ref'\n\n/**\n * Platform-side definitions: documents, hosts, screens, media, users.\n * The ORG BILLING family (`AglynOrgBilling` and the Org* plan/entitlement\n * vocabulary) lives in `org-billing.types.ts` — see the docs-site glossary\n * for the org/workspace/tenant/host naming convention. (This file was\n * `workspace.types.ts`; renamed in AGL-443 because nothing in it is the\n * workspace/org entity.)\n */\nexport interface AglynDocument {\n [field: string]: any\n}\n\nexport enum HostScreenStatus {\n UNPUBLISHED = 0x1 << 0x1,\n PUBLISHED = 0x1 << 0x2,\n SCHEDULED_TO_PUBLISH_UNPUBLISHED = UNPUBLISHED | (0x1 << 0x3),\n SCHEDULED_TO_UNPUBLISH_PUBLISHED = PUBLISHED | (0x1 << 0x4),\n SCHEDULED_TO_UPDATE_PUBLISHED = PUBLISHED | (0x1 << 0x5),\n SCHEDULED_TO_REVERT_UPDATE_PUBLISHED = PUBLISHED | (0x1 << 0x6),\n}\n\nexport enum HostScreenVisibility {\n PUBLIC = 0x1 << 0x1,\n UNLISTED = PUBLIC | (0x1 << 0x2),\n PRIVATE = 0x1 << 0x3,\n PASSWORD = PRIVATE | (0x1 << 0x4),\n AUTHENTICATED = PRIVATE | (0x1 << 0x5),\n AUTHORIZED = AUTHENTICATED | (0x1 << 0x6),\n}\n\nexport enum HostViewType {\n SCREEN = 0x1,\n LAYOUT = 0x2,\n /** A besigner-designed email document (screen with kind 'email', AGL-395). */\n EMAIL = 0x4,\n}\n\nexport enum HostViewFormat {\n NORMALIZED = 0x1,\n DENORMALIZED = 0x2,\n}\n\nexport enum HostEntityType {\n ORGANIZATION = 0x1,\n PERSON = 0x2,\n}\n\nexport enum HostRedirectParams {\n IGNORE,\n FORWARD = 0x1,\n MATCH = 0x2,\n}\n\n/** Org document id (`orgs/{orgId}`). Formerly TenantUid (AGL-444). */\nexport type OrgUid = string\n\nexport type UserUid = string\n\nexport interface AglynUser extends AglynDocument {\n $id: UserUid\n admin?: boolean\n email?: string\n}\n\n/**\n * Site-wide announcement bar config on the host doc (AGL-195). Text may\n * contain binding tokens; the tenant render resolves them server-side.\n */\nexport interface HostAnnouncementBar {\n enabled?: boolean\n text?: string\n /** Optional link the whole bar navigates to. */\n href?: string\n backgroundColor?: string\n textColor?: string\n /** Visitors may hide the bar; hidden state re-arms when text changes. */\n dismissible?: boolean\n}\n\n/**\n * Promotional popup config on the host doc (AGL-196); one per host,\n * marketingOverlays-gated. Body text may contain binding tokens; the\n * tenant render resolves them server-side.\n */\nexport interface HostPopup {\n enabled?: boolean\n headline?: string\n body?: string\n /** Media-library image URL shown above the copy. */\n imageUrl?: string\n /**\n * Alt text for {@link imageUrl} (AGL-1896). Defaults from the chosen DAM\n * asset's own alt when the author picks through \"Browse media\"; empty\n * still renders `alt=\"\"`, which is correct for a decorative banner.\n */\n imageAlt?: string\n ctaLabel?: string\n ctaHref?: string\n /** Render an email-capture field posting into the forms pipeline. */\n collectEmail?: boolean\n /** How the popup opens. */\n trigger?: 'delay' | 'scroll' | 'exit'\n /** Seconds (delay trigger) or percent 0-100 (scroll trigger). */\n triggerValue?: number\n /**\n * Re-show after this many days once dismissed (localStorage).\n *\n * Ignored when {@link oncePerSession} is set — the two are alternative\n * answers to the same question, and honouring both would mean a popup\n * suppressed for the session AND for seven days after it.\n */\n frequencyDays?: number\n /**\n * Show at most once per browser SESSION (AGL-2174).\n *\n * `/product/marketing` sells \"per-session frequency caps\" in as many\n * words, and the only cap the product had was day-based — a visitor who\n * dismissed a popup did not see it again for a week, which is a\n * different promise. Backed by `sessionStorage`, which is exactly the\n * lifetime the copy describes: it clears when the tab closes.\n */\n oncePerSession?: boolean\n /** Optional showing window (epoch millis; simple to serialize). */\n startAtMs?: number\n endAtMs?: number\n}\n\nexport type ProjectUid = string\nexport type ProjectNumber = number\n\nexport type HostUid = string\nexport type HostPath = string\nexport type HostMediaUid = string\n\n/**\n * Persisted MUI theme customization for a host's published site.\n * Canonical shape lives in `@aglyn/shared-data-types` so UI-scope libs can\n * consume it without depending on this framework lib.\n */\nexport type AglynHostTheme = HostTheme\n\n/** Host-scoped document */\nexport interface AglynHost extends AglynDocument {\n $id: HostUid\n /** Owning org (AGL-233); mirrored into `hostIndex/{hostId}`. */\n orgId?: OrgUid\n /** Membership projection from the org (AGL-233): uid → role tier. */\n memberRoles?: Record<UserUid, string>\n subdomain?: string\n cname?: string\n /** Site-wide announcement bar (AGL-195); marketingOverlays-gated. */\n announcementBar?: HostAnnouncementBar\n /** Promotional popup (AGL-196); marketingOverlays-gated. */\n popup?: HostPopup\n displayName?: string\n /**\n * Site logo URL (AGL-594): the host's own brand mark, shown by the\n * tenant's navigation loader (and future chrome). Distinct from\n * `seo.entity.logo`, which is publisher-semantic JSON-LD.\n */\n logoUrl?: string\n seo?: {\n title?: string\n description?: string\n separator?: string\n favicon?: string\n /**\n * The square mark a visitor installs to their home screen — the `icons`\n * entry of `/manifest.webmanifest`.\n *\n * Distinct from both of its neighbours, and the distinction is what the\n * field exists for. {@link favicon} is drawn at 16–32px in a tab, so it is\n * a glyph rather than artwork. `logoUrl` is the site's LOCKUP — a wordmark\n * on most sites, which is a wide rectangle, and the manifest fell back to\n * it for want of anything else: an installer that trusted a declared size\n * painted a stretched or letterboxed tile on somebody's home screen.\n *\n * A `media:` reference or a plain URL, absolutized before it is written\n * into the manifest because nothing fetches an install icon from the page\n * that linked it. Unset falls back to `logoUrl`, which is what every site\n * installs with today; a site with neither gets no `icons` array at all,\n * so the browser uses a screenshot rather than a broken tile.\n */\n appIcon?: string\n /**\n * Site-wide default social card image (AGL-1337) — used by every page\n * that sets none of its own. A `media:` reference (what the picker\n * writes) or a raw URL; resolved and made absolute by\n * `resolveSocialImage`, never emitted as stored.\n *\n * `''` is a CLEARED field, written directly to Firestore by the picker\n * card rather than through the attributes form stack, which maps `''` to\n * `undefined` and would leave a default nobody could remove (AGL-1191).\n */\n image?: string\n /**\n * Copied from the media record at pick time, and the FALLBACK rather than\n * the answer: a replace rewrites the asset's pair and cannot reach this\n * copy, so the head prefers what the asset's document records when the\n * page is composed (AGL-2850). See `resolveSocialImage`.\n */\n imageWidth?: number\n imageHeight?: number\n /**\n * `og:image:alt` (AGL-2417) — what the card SHOWS, for the screen\n * readers that announce a social preview. Defaulted from the chosen DAM\n * asset's own alt at pick time and editable per surface; stored beside\n * the reference so `resolveSocialImage` emits the description belonging\n * to the image that actually won the precedence list.\n *\n * Blank is never stored: `og:image:alt=\"\"` asserts the image conveys\n * nothing, which is not what an undescribed card means.\n */\n imageAlt?: string\n /**\n * Site-wide \"discourage search engines\" (AGL-1263): `robots.txt` refuses\n * everything, the sitemap goes empty, and every page carries `noindex`.\n * The staged-launch control — a site that is live but not ready to be\n * found, which until now could only be approximated by setting each\n * screen to {@link HostScreenVisibility.UNLISTED} one at a time.\n *\n * Absent means INDEX, and must keep meaning that. Reading a missing field\n * as \"hide\" would let a schema slip de-index every customer at once.\n */\n discourageSearchEngines?: boolean\n entity?: {\n type?: HostEntityType\n name?: string\n logo?: string\n /**\n * What the publisher IS, in a sentence — `Organization.description`\n * (AGL-2716).\n *\n * Distinct from {@link AglynHost.seo.description}, which is the meta\n * description of the site's home page: one describes a business, the\n * other describes a document, and a site whose home page is a product\n * launch should not be telling an AI assistant that the company is a\n * product launch. Unset falls back to the site description, because a\n * fallback that is approximately right beats an entity with no\n * description at all — which is what every site published before this\n * field existed.\n */\n description?: string\n /**\n * The publisher's own canonical address, when it is not this site —\n * a brand site for a company whose main presence is elsewhere. Unset\n * means this site's origin, which is the common case.\n */\n url?: string\n /**\n * Profiles that identify the SAME entity — `schema.org/sameAs`. The\n * company's LinkedIn, its Crunchbase entry, its Wikipedia article. Not\n * a link list: a page that merely mentions the entity is not the\n * entity, and listing one teaches a consumer the wrong identity.\n */\n sameAs?: string[]\n /**\n * How to reach a person — `Organization.contactPoint`.\n *\n * Split into fields rather than stored as a `contactPoint` object\n * because the console collects them as three form controls, and a\n * nested object in a settings document is a shape a partial write can\n * blank. Assembled at serialization; see `siteEntityJsonLd`.\n */\n email?: string\n telephone?: string\n /**\n * Which kind of enquiry that contact answers — `customer support`,\n * `sales`, `technical support`. Free text, because `schema.org` defines\n * the property as text rather than an enumeration.\n */\n contactType?: string\n /**\n * Postal address — `Organization.address`, a `PostalAddress`.\n *\n * Every field optional and every field omitted when blank: a partial\n * address is still a real answer to \"where are they\", while a\n * `PostalAddress` carrying empty strings is a claim that the street is\n * the empty string.\n */\n address?: {\n streetAddress?: string\n addressLocality?: string\n addressRegion?: string\n postalCode?: string\n /** ISO 3166-1 alpha-2, or a country name. */\n addressCountry?: string\n }\n /**\n * The `schema.org` LocalBusiness subtype this entity publishes as —\n * `Plumber`, `Restaurant`, `LocalBusiness` (AGL-3383).\n *\n * Beside `type` rather than a third value of it: `type` still\n * answers Organization or Person for every reader that asks, and a\n * business is an Organization. Only a value on `LOCAL_BUSINESS_TYPES`\n * publishes; anything else, or a Person, publishes as before. The four\n * fields below are published only when this is.\n */\n businessType?: string\n /** Cities or areas the business serves, published as `areaServed`. */\n areaServed?: string[]\n /**\n * Opening hours, one line per set of days — `Mo-Fr 09:00-17:00` — the\n * syntax `schema.org`'s own `openingHours` text uses. Parsed into\n * `openingHoursSpecification` at serialization; a line that does not\n * read is dropped. See `parseOpeningHours`.\n */\n openingHours?: string\n /** Free text, e.g. `$$` or `$100–$500`. */\n priceRange?: string\n /** Free text, e.g. `Cash, credit card, Zelle`. */\n paymentAccepted?: string\n }\n /**\n * Guidance for AI agents, published in `/llms.txt` (AGL-2716).\n *\n * Its own key rather than a field on `entity`, because it is about the\n * SITE's usefulness rather than about who publishes it — and because the\n * two are edited by different people at different times.\n */\n agent?: {\n /**\n * When an agent should reach for this site: the jobs it is the right\n * source for, in the author's own words.\n *\n * Absent is not a gap. `buildLlmsTxt` derives a when-to-use section\n * from what the site demonstrably publishes, so a site whose author\n * writes nothing here still ships checkable guidance; this is the place\n * to say the things that cannot be derived.\n */\n whenToUse?: string\n /** How an agent should call the site — endpoints, etiquette, limits. */\n howToUse?: string\n }\n }\n /**\n * Customer-configured third-party analytics (AGL-138/661). Long written by\n * the console and read by the tenant through `any` casts; declared as of\n * AGL-1498 so the write-deny guard sees it through the type rather than\n * not at all. The id is format-checked (`GA_MEASUREMENT_ID_PATTERN`)\n * before it reaches the inline script, and its LOADING is consent-gated\n * by `visitor-consent.ts` — see `consent` below.\n */\n analytics?: {\n gaMeasurementId?: string\n /**\n * Google Tag Manager container (AGL-2486), `GTM-XXXXXXX`.\n *\n * Declared here for the reason the `consent` comment below spells out at\n * length: a field the console persists and the tenant reads, but that no\n * type knows about, is how a whole-object write silently drops a sibling.\n *\n * Format-checked by `GTM_CONTAINER_ID_PATTERN` before it reaches an\n * inline script, and consent-gated exactly as `gaMeasurementId` is — a\n * container counts as a gated feature in `consentGatedCategories`, so a\n * site running only a container still gets the banner. It has to: a\n * container is the likeliest thing on a page to carry advertising tags,\n * and advertising is opt-in worldwide.\n */\n gtmContainerId?: string\n /**\n * Advertising pixels, vendor id → that vendor's account id (AGL-2486).\n *\n * Declared here for exactly the reason the `gtmContainerId` comment above\n * gives, and it is the field that proved the point: the console persisted\n * it and the tenant read it through `advertising-tags.ts`, while no type\n * knew about it. `aglyn-marketing` carries a live Meta pixel in it today,\n * and a whole-object write to `analytics` would have dropped it silently.\n *\n * The vendor descriptors — which script each id loads, and therefore which\n * hosts the CSP has to admit — live in `advertising-tags.ts`. An id here\n * for a vendor with no descriptor loads nothing.\n */\n adTags?: Record<string, string> | null\n }\n /**\n * Visitor consent tool (AGL-1498). The tool is ACTIVE when this is absent\n * — but only ever on sites that use a consent-gated feature (today:\n * `analytics.gaMeasurementId`); with none configured no consent UI exists\n * and this map decides nothing. `disabled: true` is the host's opt-out\n * (they run their own consent solution, and gated features load ungated).\n * `mode` is the host's posture: `'geo'` (geo-conditional: implied consent\n * with an always-available opt-out in opt-out regions, a prior-consent\n * banner in EU/EEA/UK and unknown regions) or `'strict'` (the banner for\n * everyone). Absent `mode` behaves as `'geo'`. Absent-means-active is the\n * only safe default for the tool itself: a schema slip must fail toward\n * asking, never toward silently tracking. See `visitor-consent.ts`.\n *\n * `advertising` (AGL-1649) is the host's opt-in to ASKING a second\n * question. It was persisted by the console and read by the tenant for four\n * days without being declared here — the same undeclared-field shape the\n * `analytics` comment above says was fixed for AGL-1498, on the very key\n * this issue exists to deliver. The cost is not typos: `consent` is an\n * inline object type, so a whole-object write built against a type missing\n * the key drops a host's advertising opt-in and TypeScript agrees with the\n * drop. `consent-host-schema-coverage.spec.ts` now fails the build on any\n * `consent.*` key that is written or read but not declared.\n */\n consent?: {\n disabled?: boolean\n mode?: 'geo' | 'strict'\n /**\n * Host opt-in to asking visitors about advertising storage (AGL-1649).\n * PERSISTED NAME; absent or `false` — the state of every site that\n * exists — means the visitor is asked about analytics only and\n * `ad_storage`, `ad_user_data` and `ad_personalization` stay denied\n * where AGL-1622 put them.\n *\n * `true` does not grant anything by itself — it makes the category\n * REACHABLE. It adds a second, separate question to the banner and the\n * privacy-choices panel, and where advertising then starts follows the\n * visitor's region: asked-first in the prior-consent set (the EU 27, the\n * EEA/EFTA three, the UK, Gibraltar, the outermost regions, and any\n * visitor whose region cannot be determined), running from the first\n * visit everywhere else on the same implied basis analytics uses. A\n * refusal — declined, opted out, or GPC — denies it everywhere.\n *\n * See `advertisingGrantedByStatus`; this sentence and that function are\n * locked together by `consent-advertising-copy-drift.spec.ts`, and the\n * published Cookie Policy moves before either of them.\n *\n * Read only through `hostAsksAboutAdvertising`, which also requires a\n * configured measurement id and tests `=== true`, so a truthy string out\n * of a hand-edited document cannot turn a consent category on.\n */\n advertising?: boolean\n }\n screens?: Record<ScreenUid, ScreenSlug>\n /** Screen rendered (noindex) for unmatched paths (AGL-87). */\n notFoundScreenId?: ScreenUid\n /**\n * Designable error screens by status (AGL-131). `notFound` supersedes\n * `notFoundScreenId` (kept in sync for back-compat).\n */\n errorScreens?: HostErrorScreens\n /**\n * Designable membership screens (AGL-553): which besigner-built screen\n * renders at `/signin`, `/signup` and `/recover`. An empty slot falls back\n * to the built-in form.\n *\n * ADMIN TIER, not authoring — the one thing that separates it from its\n * sibling `errorScreens`, which is a binding to screens the editor already\n * controls and serves only their own visitors. These three addresses are\n * where a site's members type their password, so the field decides what a\n * sign-in page IS. `enabledPlugins` is denied to an editor one tier down\n * for deciding merely whether those addresses EXIST; the field that\n * decides what they render cannot sit in a weaker tier than the field that\n * opens them.\n *\n * It is also a live-content surface that does not go through the routing\n * map: the tenant loader resolves the slot straight to a screen document,\n * and `screenClaimsToBeAPage` asks only whether that screen is deleted. So\n * `screens` being frozen for an `author` does not reach it, and an author\n * — the role sold as \"edit content but not publish\" — could otherwise put\n * a screen on the site's sign-in address without publishing anything.\n */\n authScreens?: HostAuthScreens\n /** Maintenance mode (AGL-131): every path renders the 503 screen. */\n maintenance?: boolean\n /**\n * STAFF host takedown (AGL-1501) — the host scope of the panic button,\n * for an infected or abusive site. The same field family as the org's\n * AGL-202 suspension: while `suspendedAt` is set (and `suspendedUntilMs`\n * has not passed) the site serves the lockdown notice instead of content.\n * Deliberately separate from `maintenance` above, which is the CUSTOMER'S\n * own switch — a takedown the site's editors could clear from the client\n * SDK would not be a takedown, so all four keys are client-denied in the\n * rules and written only by /api/admin/lockdown. `suspendedMessage` is\n * visitor-facing; timestamps are plain epoch ms (converter-safe).\n * Normalized by `app-utils/lockdown.ts`.\n */\n suspendedAt?: number\n suspendedReasonCode?: string\n suspendedMessage?: string\n suspendedUntilMs?: number\n /**\n * Per-site plugin deny-list (AGL-1014): plugin ids the org has enabled\n * but this site switches OFF. Subtracted from the org's resolved set by\n * `resolveHostEnabledPlugins` — the host can only ever narrow, never\n * widen, and an absent field means every org-enabled plugin runs here.\n * Writable by site ADMINS only (rules). The base component library's id is\n * ignored; a plugin on for every workspace (AI) is switched off for this\n * site like any other (AGL-3028).\n */\n disabledPlugins?: string[]\n /**\n * Per-site plugin OPT-IN list (AGL-2486): the `defaultOffPerSite` plugin\n * ids this site has explicitly turned on. Today that is `accounts` — the\n * user-accounts capability behind `/signin`, `/signup` and `/recover`.\n *\n * The companion of `disabledPlugins` above, not a duplicate of it, because\n * a deny-list cannot express \"off until asked\": absent means ON there, and\n * every published site was therefore serving member pages whether or not\n * it had members. This field is what an absent value means OFF for, and it\n * is read ONLY for ids the catalog marks default-off, so it can never\n * widen a site past its org. Writable by site ADMINS only (rules).\n */\n enabledPlugins?: string[]\n /**\n * External image hosts this site's owner has approved (AGL-1152).\n *\n * The tenant's `img-src` is built from this list, per site, in the\n * middleware — which is the only layer that runs ahead of the ISR cache, and\n * therefore the only place a per-site policy can be set at all.\n *\n * ## Why the list exists rather than a platform-wide allowlist\n *\n * AGL-1726 refused to enforce `img-src` because hotlinking an external image\n * is an ADVERTISED authoring feature (`image.tsx` tells authors to paste a\n * URL), so a first-party-only policy would silently revoke a documented\n * capability from every published site at once. Enforcement against a list\n * the OWNER chose revokes nothing, and the console warns at authoring time\n * so a refusal is never the first anyone hears of it.\n *\n * It is also what gives that issue's condition 6 — \"a rollback that does not\n * need a deploy\" — an answer: this is host data, so widening or emptying it\n * propagates within the verdict TTL with no Vercel build in the path.\n *\n * ⛔ NOT a place to put `firebasestorage.googleapis.com`. That host is PINNED\n * in `security-origins.js` and deliberately not owner-removable: orgs without\n * the paid `mediaCdn` entitlement store absolute download URLs, so an owner\n * who deleted it would blank their own images (AGL-1726 condition 5).\n *\n * Stored as bare hostnames (`cdn.example.com`, or one leading `*.` label).\n * Anything with a scheme, port, path or separator is REFUSED at the parse\n * rather than repaired — `img-src` sources are space-delimited and\n * directives `;`-delimited, so both are injection points. Writable by site\n * ADMINS only (rules).\n */\n approvedImageHosts?: string[]\n /** Site languages (AGL-164), e.g. ['en', 'es']; first is the default\n * unless `defaultLocale` says otherwise. */\n locales?: string[]\n defaultLocale?: string\n /**\n * The IANA zone THIS SITE's published dates read in (AGL-3252) — absent\n * means the workspace's `timeZone`, and absent there means UTC.\n *\n * AGL-3237 put the zone on the organization and said a per-site override\n * was worth its own issue because it would need a second rules change. It\n * does not: the host document's client branch is a deny-list, so a field\n * it does not name is already writable, and what the override actually\n * needed was the classification below. An agency running a Chicago site\n * and a Berlin site out of one workspace is the case it exists for.\n *\n * Read through `resolveSiteTimeZone`, never directly: a stored zone can\n * predate an IANA rename, and an unusable one has to fall back to the\n * workspace rather than throw inside a page render.\n */\n timeZone?: string\n /** Directory of shared layouts by display name (mirrors `screens`). */\n layouts?: Record<LayoutUid, string>\n theme?: AglynHostTheme\n\n // CONCEPT: Redirect screens\n redirects?: Record<RedirectUid, true>\n\n // CONCEPT: Enterprise - Siloed projects\n projectId?: ProjectUid\n projectNumber?: ProjectNumber\n}\n\n/**\n * Host-document fields an editor may legitimately write, each with the reason\n * (AGL-1361). Everything else must be denied in the rules' host key diff.\n *\n * `host-write-deny-coverage.spec.ts` fails the build for any field of this\n * document that is in neither list, so \"I forgot\" is not a reachable state —\n * the answer that costs nothing is always to deny. The reason strings are the\n * point: they record that somebody decided, rather than that somebody did not\n * notice. AGL-1364 is what this catches, found while writing it.\n *\n * The bar for landing here: could a client rewrite change what the platform\n * DECIDES — an entitlement, a price, where a request routes, who may read? If\n * so it belongs in the rules, whatever the console does today.\n */\nexport const HOST_CLIENT_WRITABLE_FIELDS: Readonly<Record<string, string>> = {\n displayName:\n 'The site name shown in the console. Cosmetic, org-scoped, and decides ' +\n 'nothing — the public address is `subdomain`/`cname`, both denied.',\n logoUrl:\n 'The site brand mark rendered by the tenant nav (AGL-594). Authored ' +\n 'content pointing at already-public media; no gate reads it.',\n seo:\n 'Title, description, favicon, app icon, social card and the AGL-1263 ' +\n '`discourageSearchEngines` switch. All of it is authoring: the values ' +\n 'end up in the page the editor is already free to write.',\n // `screens` LEFT this map in AGL-2334 and is now classified as denied,\n // following the `disabledPlugins` precedent: it is TIERED, not freely\n // client-writable. The reason it used to carry — \"an editor who may add a\n // screen may name it\" — is still true of an editor and false of an\n // `author`, and a field one role may write and another may not belongs on\n // the denied side of this partition, where the tier is visible. Registering\n // a path in this map is what makes a page reachable on the live site, so it\n // is the publish surface an author is refused; see `canPublishHostContent`\n // in cloud/firebase-firestore.rules.\n layouts:\n 'The shared-layout directory. A layout has no route of its own, so unlike ' +\n '`screens` this map decides nothing about reachability — it is an index ' +\n 'maintained alongside API-created layout docs. What publishing a layout ' +\n 'moves is the `versionId` pointer on the layout document, which the rules ' +\n 'freeze for an `author` there (AGL-2334), not here.',\n redirects:\n 'The redirect directory. The redirect DOCS are API-create-only so the ' +\n '`redirectsPerHost` quota has somewhere to be enforced; this is the index.',\n notFoundScreenId:\n 'Which screen renders unmatched paths (AGL-87). Points at a screen the ' +\n 'editor already owns, and serves their own visitors only.',\n errorScreens:\n 'Designable error screens by status (AGL-131). Same reasoning as ' +\n '`notFoundScreenId` — a binding to screens this editor already controls.',\n maintenance:\n 'Maintenance mode (AGL-131): every path renders the 503 screen. A ' +\n 'site-wide availability switch, but it only ever takes the editor\\'s OWN ' +\n 'site down, which they can equally do by deleting its screens.',\n locales:\n 'Site languages (AGL-164). The `multilingual` entitlement is re-checked ' +\n 'server-side at page load, so writing this buys nothing a plan forbids.',\n defaultLocale: 'Which of `locales` serves an unprefixed path. Authoring.',\n timeZone:\n 'Which calendar day this site attributes a published instant to ' +\n '(AGL-3252), overriding the workspace zone. Editorial, and it reaches ' +\n 'only this site\\'s own rendered dates — nothing bills, gates or routes ' +\n 'on it, and every read validates it through `isSupportedTimeZone`, so ' +\n 'the worst a rewrite can do is move one publisher\\'s archive by a day. ' +\n 'The org\\'s `timeZone` is denied because /api/orgs/settings owns the ' +\n 'whole org document; this one rides the settings form\\'s client write ' +\n 'beside `seo`, which is read by the same renderer for the same reason.',\n theme:\n 'The persisted MUI theme for the published site. Authoring — the theme ' +\n 'editor writes it directly, and it renders only on this host.',\n themeOverride:\n 'The editor\\'s local diff on top of an installed marketplace theme, ' +\n 'written wholesale by the setup page. Authoring. Its PROVENANCE ' +\n '(`themeInstalledFrom`) is denied instead, which is the half that has to ' +\n 'be true for `isOverrideForCurrentTheme` to mean anything.',\n announcementBar:\n 'Site-wide announcement bar (AGL-195). `marketingOverlays`-gated, but ' +\n 'the gate is enforced where it counts — `site-page-enricher` re-checks ' +\n 'the entitlement at RENDER, so a free plan writing this bar never ships ' +\n 'it. The console gate is the affordance, not the boundary.',\n popup: 'Promotional popup (AGL-196). Identical reasoning to `announcementBar`.',\n business:\n 'Support email, address and social links behind the AGL-1022 host ' +\n 'tokens. Publisher-authored contact detail that renders into their own ' +\n 'pages and emails; undeclared on this interface, so the guard sees it ' +\n 'through the resolver sweep rather than the type.',\n analytics:\n 'The Google Analytics measurement id the host configures for their own ' +\n 'site (AGL-138). Authoring: it only ever tags THEIR pages, the tenant ' +\n 'format-checks it before the inline script, and loading it is ' +\n 'consent-gated (AGL-1498). A rewrite mis-tags the rewriter\\'s own site.',\n consent:\n 'The visitor consent tool switch (AGL-1498). Decides whether the ' +\n 'host\\'s OWN site asks its visitors before loading the analytics the ' +\n 'host configured — their compliance posture on their own pages, exactly ' +\n 'like `seo.discourageSearchEngines`. No platform gate reads it.',\n createdAt:\n 'Stamped by /api/hosts/create. Nothing gates on it — the org owns ' +\n 'billing dates — and console saves touch it freely.',\n updatedAt:\n 'Bumped by console saves to drive \"last edited\" copy. Decides nothing; ' +\n 'a forged value misleads the editor about their own site.',\n}\n\n/**\n * Host-document keys that never reach Firestore, so the rules have no opinion.\n */\nexport const HOST_UNPERSISTED_FIELDS: Readonly<Record<string, string>> = {\n $id: 'The document id, attached on read by the Firestore hooks. Never a field.',\n}\n\n/** Error-screen bindings by HTTP-ish status (AGL-131). */\nexport interface HostErrorScreens {\n notFound?: ScreenUid\n unauthorized?: ScreenUid\n forbidden?: ScreenUid\n unavailable?: ScreenUid\n}\n\n/**\n * The membership screen slots (AGL-553), keyed as the console card and the\n * tenant loader spell them. The names carry the `ScreenId` suffix because\n * that is how they are persisted; the console writes them as dotted field\n * paths (`authScreens.signinScreenId`).\n */\nexport interface HostAuthScreens {\n signinScreenId?: ScreenUid\n signupScreenId?: ScreenUid\n recoveryScreenId?: ScreenUid\n}\n\n/**\n * The error slots, as a value (AGL-2092).\n *\n * The console card enumerated these separately and so did nothing else, which\n * was fine while the list was only a list. It stopped being only a list when\n * `kind: 'error'` made an assigned screen stop counting against\n * `screensPerHost`: the number of slots is now the BOUND on how many screens a\n * host can take off its plan that way, so the bound and the pickers have to be\n * the same four things or the exemption is unbounded in one of them.\n *\n * `ERROR_SCREEN_MAX_PER_HOST` is `.length` of this, deliberately — adding a\n * fifth status here widens the exemption to five, which is correct and is the\n * only way it should ever widen.\n */\nexport const HOST_ERROR_SCREEN_SLOTS = [\n 'notFound',\n 'unauthorized',\n 'forbidden',\n 'unavailable',\n] as const\n\nexport type HostErrorScreenSlot = (typeof HOST_ERROR_SCREEN_SLOTS)[number]\n\nexport type ScreenUid = string\nexport type ScreenSlug = string\nexport type VersionUid = string\nexport type LayoutUid = string\n\n/**\n * Who may see an org-owned resource (AGL-1037): `'org'` is every site in\n * the org, `host:{hostId}` names one. Lives here rather than beside its\n * helpers in `app-utils/scope-tokens` because foundation cannot import\n * app-utils; that module re-exports this type along with the helpers.\n */\nexport type ScopeToken = 'org' | `host:${string}`\n\n/**\n * Uploaded media metadata (AGL-72): the binary lives in Firebase Storage at\n * `hosts/{hostId}/media/{mediaId}`; this doc mirrors it in Firestore at the\n * same logical path so the library can list without Storage list calls.\n */\nexport interface AglynHostMedia {\n $id: HostMediaUid\n fileName?: string\n contentType?: string\n sizeBytes?: number\n /** Download URL captured at upload time; nodes reference this directly. */\n url?: string\n /**\n * Folder doc id in `hosts/{hostId}/mediaFolders` (AGL-171); null/absent\n * = root. Replaces the legacy free-text `folder` string below.\n */\n folderId?: string | null\n /** Legacy AGL-124 free-text folder; read as fallback until migrated. */\n folder?: string\n tags?: string[]\n alt?: string\n description?: string\n /** Auto-captured at upload (AGL-173); best-effort for images only. */\n width?: number\n height?: number\n uploadedBy?: string\n /**\n * CDN delivery (AGL-175 / AGL-829): the stable, mediaId-keyed path\n * (`/api/media/cdn/{scope}/{mediaId}`) served by both apps — location- and\n * replace-independent (references never break; the route revalidates via\n * the `contentHash` ETag). The same route also serves an immutable\n * `…/{mediaId}/{hash}` form. `variants` are the WebP widths from upload.\n */\n cdnPath?: string\n contentHash?: string\n /**\n * The FULL 64-hex sha256 of the served bytes (AGL-1614), written only by\n * the routes that hash the bytes server-side. Additive beside\n * {@link contentHash}, which is unchanged and stays the ETag and the\n * immutable-URL segment.\n *\n * The distinction is load-bearing rather than tidy. `contentHash` is a\n * 16-hex (64-bit) TRUNCATION of one of TWO digests — sha256 on\n * `/api/media/upload` and `/api/media/replace`, GCS's md5 on\n * `/api/media/upload-url` — which is fine for a cache validator (opaque,\n * per-URL, self-consistent) and weak for anything that has to be a\n * SECURITY key. Two md5 files with the same digest are hours of laptop\n * compute to construct, and a truncation to 64 bits inherits that\n * immediately; a quarantine keyed on such a value can be made to refuse an\n * innocent asset. `contentSha256` has neither property.\n *\n * Absent, not null, when the server never held the bytes: the signed-upload\n * route streams client → bucket and only downloads them back for an SVG\n * sanitization pass, so that is the one case there where it is written.\n * Absent means \"no strong key for these bytes\", and every consumer already\n * degrades to `contentHash` and then to the per-asset key.\n */\n contentSha256?: string\n variants?: number[]\n /**\n * Why an eligible asset has no {@link variants}, when generation was\n * attempted and did not complete (AGL-1468).\n *\n * Written by every upload route and by `/api/media/replace`, and declared\n * here late: it has been on production documents since AGL-1468 and was\n * missing from this interface, so every reader either cast or guessed.\n * Absent is the common, correct case — an SVG, a document, a source\n * already narrower than 320px — and must never look like a fault.\n */\n variantsError?: string\n /**\n * The Cloud Storage object path (`{hosts|orgs}/{id}/media/{folders…}/{id}`).\n * Written at upload, rewritten by a folder move, and read back by\n * `serveMediaCdn` — which honors it ONLY inside this scope's media prefix,\n * because it is client-influenced data on an admin-SDK bucket read\n * (AGL-1881). Legacy assets have none and fall back to the flat layout.\n */\n storagePath?: string\n /** Constructs `sanitizeSvgBuffer` removed (AGL-1474). Absent when clean. */\n svgSanitized?: string[]\n /** Uid of whoever last replaced the bytes (`/api/media/replace`). */\n replacedBy?: string\n /**\n * What the uploader's browser measured about a VIDEO (AGL-2742).\n *\n * Client-reported and bounded by `normalizeVideoMetadata` before it lands,\n * which is why it does not widen {@link width}/{@link height}: those are\n * read from the bytes by the server and carry a stronger claim. A reader\n * that folds the two together loses the ability to tell which it has.\n *\n * All three numbers or none — a `VideoObject` emitting a `duration` with\n * no `width` is worse than one emitting neither.\n */\n video?: MediaVideoMetadata\n /**\n * The generated poster still for a video (AGL-2742), served by the CDN as\n * `?poster=1` and — because it genuinely is an `image/webp` — under the\n * shared edge policy rather than the origin-only one video takes.\n *\n * `variants` here are poster widths, generated by the same `sharp` pass\n * that produces an image's, at `{storagePath}__poster__w{n}.webp`.\n */\n poster?: { width: number; height: number; variants: number[] }\n /**\n * Why a video has no {@link poster}: the browser could not decode it,\n * would not paint a frame, or the encoder refused the bytes it sent.\n * Absent on every video that has one, and on every non-video.\n */\n posterError?: string\n /**\n * Compressed delivery copies of a video (AGL-2745), in the order a\n * renderer should emit `<source>` elements — most efficient codec first,\n * because a browser takes the first it can decode.\n *\n * Produced out of band by `tools/scripts/generate-video-renditions.mjs`\n * rather than at upload: there is no decoder in either upload runtime, and\n * a hosted transcoder is a recurring bill this platform's budget does not\n * carry. Absent means the master is the only copy, which is what every\n * video served before this shipped.\n */\n videoRenditions?: MediaVideoRendition[]\n /**\n * User-defined key/value metadata (AGL-822), mirrored onto the Storage\n * object's `customMetadata` by `/api/media/folders` (action\n * `custom-metadata`); this doc copy is the source of truth for display.\n */\n customMetadata?: Record<string, string>\n /**\n * The metadata the file carries INSIDE its bytes (AGL-3331) — EXIF, IPTC\n * and XMP on a photo, a PDF's document info, an Office file's properties,\n * a video's tags — as read by `readMediaEmbeddedMetadata`. Server-written\n * only (at upload, or the first time the Details drawer opens) and locked\n * in the rules; edits go INTO the file through `/api/media/metadata`,\n * which rewrites the bytes and this record together. Distinct from\n * `customMetadata`, which is Aglyn's own and never touches the file.\n */\n embeddedMetadata?: MediaEmbeddedMetadata\n /**\n * Which sites may see this asset (AGL-1037); absent = org-wide until the\n * AGL-1040 backfill stamps it. Only meaningful on the org-scoped library\n * (`orgs/{orgId}/media`) — a host's own library is private already.\n */\n visibleTo?: ScopeToken[]\n /**\n * Not fetchable without a signature (AGL-1051). **Orthogonal to\n * `visibleTo`**, and the distinction is the whole point:\n *\n * - `visibleTo` answers *which sites may USE this asset* — a discovery\n * and authoring control. An asset restricted to one site is still\n * served to anyone who has its URL, because that site's pages are\n * public and the CDN that serves them is unauthenticated.\n * - `private` answers *may the public FETCH these bytes at all*.\n *\n * A private asset carries no {@link cdnPath}, is refused by the pickers\n * that place media into page content, and is reachable only through a\n * short-lived signed URL minted for an authorized caller. Use it for\n * things that are not web assets — a signed contract, unreleased\n * artwork, an embargoed release — never for \"this image is only on one\n * site\", which is what `visibleTo` is for.\n */\n private?: boolean\n createdAt?: ITimestamp\n updatedAt?: ITimestamp\n deletedAt?: ITimestamp\n}\n\n/**\n * Media folder doc (AGL-171): `hosts/{hostId}/mediaFolders/{folderId}`.\n * Hierarchy/validation helpers live in `app-utils/media-folders`.\n */\nexport interface AglynHostMediaFolder {\n $id: string\n name: string\n /** Parent folder id; null = root. Depth capped at 5 (see app-utils). */\n parentId?: string | null\n order?: number\n /**\n * Which sites may see this folder (AGL-1037). Assets carry their own\n * scope — a folder's is applied to them by an explicit write-time\n * cascade (AGL-1042), never inherited at read time.\n */\n visibleTo?: ScopeToken[]\n createdAt?: ITimestamp\n}\n\n/**\n * Pending scheduled publication (AGL-61): when `publishAt` passes, the\n * parent doc's `versionId` pointer moves to `versionId`. Applied lazily by\n * the tenant's ISR revalidation (no dedicated cron needed).\n */\nexport interface PublishSchedule {\n /** Version to make live; unused for `action: 'unpublish'` (AGL-113). */\n versionId?: VersionUid\n /**\n * What happens at `publishAt` (AGL-113): `publish` (default) flips the\n * live version pointer; `unpublish` removes the screen's routing-map\n * entry so its path 404s after the next revalidate.\n */\n action?: 'publish' | 'unpublish'\n publishAt: ITimestamp\n /**\n * `skipped-unentitled` is a terminal refusal (AGL-1185): the schedule came\n * due on a plan without `scheduledPublishing`, so the executor declined it\n * and will not reconsider.\n *\n * It exists because leaving it `pending` meant the refusal was not recorded\n * anywhere — the schedule stayed due forever, and the moment the org upgraded\n * to Business the next beat published it. Content someone scheduled and\n * forgot, months later, surfacing during an upgrade. Recording the refusal is\n * what makes it stop being due.\n *\n * Every reader tests `=== 'pending'`, so adding a member here degrades\n * correctly rather than needing each one updated — checked, not assumed.\n *\n * `skipped-unroutable` is the second terminal refusal (AGL-1589): the\n * publish came due on a screen with no live route and no address the\n * executor could give it — no slug on the screen or one of its ancestors,\n * an address another screen already holds, or an email document, which has\n * no URL at all. One value for the three because they share a remedy (fix\n * the address in the console and schedule it again) and because a terminal\n * state is a thing every reader has to reason about; the console names the\n * causes in its message.\n *\n * It exists for the same reason as the value above: before AGL-1589 that\n * publish reported `applied` while the page kept 404ing, which is a silent\n * failure in the worst direction. A recorded refusal is the difference\n * between a schedule that did not run and one that pretended it had.\n */\n status:\n | 'pending'\n | 'applied'\n | 'canceled'\n | 'skipped-unentitled'\n | 'skipped-unroutable'\n createdAt?: ITimestamp\n}\n\n/** Host-scoped document */\nexport interface AglynScreen extends AglynDocument {\n $id: ScreenUid\n hostId?: HostUid\n parentId?: ScreenUid\n slug?: ScreenSlug\n /** Position among siblings (screens sharing `parentId`) in the screens list. */\n order?: number\n versionId?: VersionUid\n publishSchedule?: PublishSchedule\n /** Password protection (AGL-87): sha256 hex of the visitor password. */\n protection?: { passwordHash?: string }\n /** Language this screen is written in (AGL-164), e.g. 'en'. */\n locale?: string\n /** Translations of this screen: locale → screen id (AGL-164). */\n localeVariants?: Record<string, ScreenUid>\n status?: HostScreenStatus\n createdAt?: ITimestamp\n updatedAt?: ITimestamp\n /**\n * When the screen's route was last published (AGL: date-published). Stamped\n * by `publishScreenRoute` and cleared on unpublish, so it is present only\n * while the screen is reachable — distinct from `createdAt`.\n */\n publishedAt?: ITimestamp\n deletedAt?: ITimestamp\n displayName?: string\n /**\n * Normalized `displayName` for case-insensitive prefix search (AGL-835);\n * see `nameSearchKey`. Stamped by the screen create/import write paths so\n * the besigner switcher can query screens by name instead of loading the\n * whole collection.\n */\n nameLower?: string\n description?: string\n seo?: {\n title?: string\n description?: string\n breadcrumb?: string\n /**\n * This screen's social card image (AGL-1337), overriding the host\n * default. Same storage contract as {@link AglynHost.seo.image}: a\n * `media:` reference or URL, `''` meaning cleared — which is how a screen\n * goes back to inheriting the site default.\n */\n image?: string\n /**\n * Copied from the media record at pick time, and the FALLBACK rather than\n * the answer: a replace rewrites the asset's pair and cannot reach this\n * copy, so the head prefers what the asset's document records when the\n * page is composed (AGL-2850). See `resolveSocialImage`.\n */\n imageWidth?: number\n imageHeight?: number\n /**\n * `og:image:alt` (AGL-2417) — what the card SHOWS, for the screen\n * readers that announce a social preview. Defaulted from the chosen DAM\n * asset's own alt at pick time and editable per surface; stored beside\n * the reference so `resolveSocialImage` emits the description belonging\n * to the image that actually won the precedence list.\n *\n * Blank is never stored: `og:image:alt=\"\"` asserts the image conveys\n * nothing, which is not what an undescribed card means.\n */\n imageAlt?: string\n }\n\n // CONCEPT: Scheduling\n schedule?: {\n startAt?: ITimestamp\n endAt?: ITimestamp\n next?: VersionUid\n previous?: VersionUid\n }\n\n // CONCEPT: Contextual visibility\n visibility?: HostScreenVisibility\n\n // NOTE (AGL-676): `owner` and `contributors` were declared here and NEVER\n // read or written by anything — zero call sites across the whole repo.\n // Removed rather than left dangling: a declared-but-unmaintained field is\n // how the next person builds on something that was never real. Editor\n // attribution lives in `hosts/{hostId}/activity`, which is actually\n // written. If a contributor set is wanted later, add one that is kept up\n // to date.\n\n /** Shared layout this screen renders inside (see {@link AglynLayout}). */\n layoutId?: LayoutUid\n}\n\n/**\n * Host-scoped document.\n * `N` lets higher layers narrow the node schema (e.g. the aglyn SDK\n * instantiates it with its richer `NodeSchema`).\n */\nexport interface AglynScreenVersion<N = AglynNodeSchema>\n extends AglynDocument {\n $id: VersionUid\n hostId?: HostUid\n screenId?: ScreenUid\n createdAt?: ITimestamp\n updatedAt?: ITimestamp\n nodes?: Record<NodeId, N>\n /**\n * Shared layout THIS VERSION renders inside (see {@link AglynLayout}).\n *\n * Key-present wins over the screen document's {@link AglynScreen.layoutId}:\n * a `LayoutUid` binds the version to that layout, an explicit `null` renders\n * the version with no layout, and an absent key inherits the screen's\n * binding. Per-version binding is what lets a scheduled version bring its\n * own chrome live at publish time without reframing the currently served\n * version. On layout versions ({@link AglynLayoutVersion}) this same key is\n * instead the back-pointer to the owning layout document.\n */\n layoutId?: LayoutUid | null\n /**\n * This version's values for the properties of the layouts it renders inside\n * (AGL-2893), stored beside the {@link layoutId} binding: layout id →\n * property name → value, for every layout in the chain.\n *\n * Keyed by layout so that binding a different layout never hands this\n * screen's values to a property of the same name there, and so that a\n * layout nested inside another can be given values of its own. A property\n * the screen leaves unset renders with its default. Read on screen versions\n * only.\n */\n layoutPropValues?: Record<\n LayoutUid,\n Record<string, ReusableComponentPropValue>\n >\n /**\n * This version's restyling of the layouts it renders inside (AGL-3286),\n * stored beside {@link layoutPropValues}: layout id → that layout's own\n * node id → an sx record merged over the element's styling for this page\n * only. The layout's content stays the layout's, and every other screen\n * using it is unaffected. A node id the layout no longer has is ignored.\n * Read on screen versions only — see `layout-style-overrides.ts`.\n */\n layoutStyleOverrides?: Record<\n LayoutUid,\n Record<string, Record<string, unknown>>\n >\n}\n\n/** Unique id of a host-level reusable component definition. */\nexport type ComponentDefUid = string\n\n/**\n * Attribute field kinds that hold no value of their own: they arrange other\n * fields (a sub-form, tabs, a wizard, a repeating field array), decorate one\n * (an input add-on), or only display or trigger something (plain text, a\n * button). A property is one value, so none of these is a property kind —\n * see `NON_VALUE_FIELD_KINDS` for the reason each is listed.\n */\nexport type NonValueFieldKind =\n | FieldComponentType.BUTTON\n | FieldComponentType.BUTTON_GROUP\n | FieldComponentType.FIELD_ARRAY\n | FieldComponentType.INPUT_ADDON_BUTTON_GROUP\n | FieldComponentType.INPUT_ADDON_GROUP\n | FieldComponentType.PLAIN_TEXT\n | FieldComponentType.SUB_FORM\n | FieldComponentType.TAB_ITEM\n | FieldComponentType.TABS\n | FieldComponentType.WIZARD\n\n/**\n * Attribute field kinds whose property kind was named before properties\n * covered every field kind, and keeps that stored name: a text field is\n * `text`, a textarea `richText`, a switch `boolean`, a dropdown `choice`, an\n * icon picker `icon` and a screen picker `href`.\n */\nexport type LegacyNamedFieldKind =\n | FieldComponentType.TEXT_FIELD\n | FieldComponentType.TEXTAREA\n | FieldComponentType.SWITCH\n | FieldComponentType.SELECT\n | FieldComponentType.ICON_PICKER\n | FieldComponentType.SCREEN_SELECT\n\n/**\n * Kind of a declared component or layout property (AGL-1247, AGL-2893): which\n * control edits it, in the Properties dialog and on every page that sets it,\n * and which fields it can be bound to.\n *\n * Derived from the attribute schema's own field kinds rather than listed\n * beside them. Every {@link FieldComponentType} that holds a value is a\n * property kind — under its own stored value (`color-picker`,\n * `css-dimension`, …), or under the name it already had\n * ({@link LegacyNamedFieldKind}). `number` and `image` are the two text-field\n * variants a coded component declares as `type: 'number'` or as a media\n * field. So a field kind added to the attribute schema is a property kind the\n * moment it exists, and `REUSABLE_PROP_KINDS` fails to compile until it says\n * how the kind is edited.\n *\n * These values are stored in component and layout documents. Never rename\n * one.\n */\nexport type ReusableComponentPropType =\n | 'text'\n | 'richText'\n | 'image'\n | 'href'\n | 'number'\n | 'boolean'\n | 'choice'\n | 'icon'\n | `${Exclude<FieldComponentType, NonValueFieldKind | LegacyNamedFieldKind>}`\n\n/**\n * A property's value as it is stored: a default on the declaration, or a\n * page's own value in `propValues` / `layoutPropValues`.\n *\n * The shape follows the kind's control — a Yes / no stores a boolean, a\n * slider a number, a list of answers an array, an icon pick its id and path —\n * and text-shaped kinds store text. Defaults written before the kinds were\n * typed are text (`'true'`, `'28'`), which every reader accepts.\n */\nexport type ReusableComponentPropValue =\n | string\n | number\n | boolean\n | ReadonlyArray<string | number>\n | ReusableComponentIcon\n\n/**\n * One rule of a property's `condition`: the attribute schema's condition\n * (data-driven-forms' `ConditionDefinition`), with `when` naming another\n * property of the same component or layout instead of an attribute.\n *\n * The operators are the schema's own — `is` (one value, or any of a list),\n * `notMatch` to negate `is` or `pattern`, `isEmpty`, `isNotEmpty`, `pattern`\n * with `flags`, and the four comparisons — and the rule is evaluated by\n * data-driven-forms' own parser, in the Attributes panel and at render. Only\n * the serializable part is here: a stored document cannot carry a function.\n */\nexport interface ReusableComponentPropRule {\n /** The name of the property whose value decides. */\n when: string\n is?: string | number | boolean | ReadonlyArray<string | number | boolean>\n notMatch?: boolean\n isEmpty?: boolean\n isNotEmpty?: boolean\n pattern?: string\n flags?: string\n greaterThan?: number\n greaterThanOrEqualTo?: number\n lessThan?: number\n lessThanOrEqualTo?: number\n}\n\n/**\n * When a property shows and applies: one rule, a list that must ALL hold, or\n * the schema's `and` / `or` / `not` over rules.\n */\nexport type ReusableComponentPropCondition =\n | ReusableComponentPropRule\n | { and: ReusableComponentPropCondition[] }\n | { or: ReusableComponentPropCondition[] }\n | { not: ReusableComponentPropCondition | ReusableComponentPropCondition[] }\n\n/**\n * One answer a `choice` prop offers (AGL-2871).\n *\n * Two halves because two people read it: the page author picks the `label`,\n * and the field inside the component receives the `value` — which is why a\n * dropdown bound to the prop needs values it offers itself.\n */\nexport interface ReusableComponentPropOption {\n /** What a field bound to the prop receives. */\n value: string\n /** What the Attributes panel shows; falls back to `value`. */\n label?: string\n}\n\n/**\n * A prop a reusable component declares (AGL-1247), so one definition can\n * render different content per instance instead of being copied per page.\n *\n * Inside the definition the author binds a node prop to it with the\n * existing token syntax — `{{prop.headline}}`, the same mechanism behind\n * `{{entry.*}}` and `{{host.*}}` — and `composeReusableComponentNodes`\n * substitutes each instance's own values as it grafts.\n */\nexport interface ReusableComponentProp {\n /**\n * Token name: `{{prop.<name>}}`. This is the STABLE key instances store\n * their overrides under, so renaming one orphans every existing value —\n * the editor must rename in place rather than remove-and-add.\n */\n name: string\n type?: ReusableComponentPropType\n /** Field label in the Attributes panel; falls back to `name`. */\n label?: string\n /**\n * Help shown beside the property's field wherever a page sets it — the\n * attribute schema's `description`.\n */\n description?: string\n /**\n * Rendered wherever an instance leaves this prop unset, and shown as the\n * Attributes field's placeholder. One field serving both is deliberate:\n * a component that renders its own sensible copy until overridden is the\n * \"shows the placeholder from the component\" behaviour, and it means an\n * unset prop can never collapse a section to empty on a live page.\n */\n defaultValue?: ReusableComponentPropValue\n /**\n * The answers a page picks from, in the order offered, for a kind whose\n * control lists answers (`choice`, `radio`, `toggle-button`,\n * `dual-list-select`, and a `checkbox` that is a list). A `defaultValue`\n * names one of their values, or several for a kind that takes several.\n */\n options?: ReusableComponentPropOption[]\n /**\n * The kind's own settings, named by the kind's `settings` in\n * `REUSABLE_PROP_KINDS`: a slider's range, whether a choice takes several\n * answers, which theme scale a theme-scale field offers. They are the\n * attribute field's own props, so a coded component's attribute and a\n * property of the same kind are configured with the same words.\n */\n settings?: Record<string, string | number | boolean>\n /**\n * When the property shows and applies. While it does not hold, the field is\n * hidden wherever a page sets it, and the property renders as one with no\n * value and no default: text bound to it is empty, a Yes / no is a no, and\n * a field bound to it keeps its own default.\n */\n condition?: ReusableComponentPropCondition | ReusableComponentPropCondition[]\n /**\n * `icon` only: the SVG path of the icon `defaultValue` names, stored beside\n * the id when the default is picked, for the reason\n * {@link ReusableComponentIcon} stores both — a published page never loads\n * the icon catalog, so an id alone draws the empty Icon placeholder. An\n * instance's own pick travels the same way, as a whole\n * {@link ReusableComponentIcon} in its prop values.\n */\n defaultIconPath?: string\n}\n\n/**\n * A reusable component's chosen icon (AGL-1193), stood in for by the generic\n * package glyph when unset.\n *\n * Both halves are stored, for the reason AGL-1212 exists: the MDI catalog is\n * ~2.9 MB and only picker surfaces load it, so anything that had to resolve\n * `iconId` on a render surface got `DEFAULT_ICON` — a real path, so every\n * icon painted a confident \"help\" glyph. The id stays the source of truth;\n * `iconPath` is the denormalized copy that travels with the document.\n */\nexport interface ReusableComponentIcon {\n iconId?: string\n iconPath?: string\n}\n\n/**\n * Where a reusable component is placed (AGL-3287): on the site's pages, or in\n * the emails the site sends.\n *\n * The two are built from different elements — an email is made of the email\n * plugin's mail-safe blocks, a page of everything else — and neither renders\n * the other's, so a component belongs to exactly one. Absent on every\n * component made before emails could reuse one, which is why absent means\n * `site` and is never backfilled.\n */\nexport type ReusableComponentKind = 'site' | 'email'\n\n/**\n * Reusable component definition: a node subtree promoted from a screen,\n * inserted anywhere as an instance node (`componentId: 'reusableInstance'`,\n * `props.refId`) and grafted at render time (see\n * `composeReusableComponentNodes`). Host-scoped document at\n * `hosts/{hostId}/components/{componentId}`.\n */\nexport interface AglynHostComponent<N = AglynNodeSchema>\n extends AglynDocument {\n $id: ComponentDefUid\n hostId?: HostUid\n displayName?: string\n description?: string\n /**\n * Where this component is placed (AGL-3287): `email` for a block reused\n * across emails — a header, a footer — and absent (or `site`) for one\n * placed on pages. It decides which drawer offers the component: an email\n * offers email blocks alone, and a page never offers one.\n *\n * Set when the component is created and carried by every copy made of it\n * — a duplicate, a template, a marketplace install — so a copy lands in\n * the same drawer as its source.\n */\n kind?: ReusableComponentKind\n /**\n * Icon shown wherever an instance of this component is represented in the\n * besigner (AGL-1193) — the hierarchy row, the canvas badge, the element\n * drawer. Absent on every component that predates the picker, which is\n * why it is optional and never defaulted: unset means \"the package glyph\",\n * not \"an icon we failed to read\".\n */\n icon?: ReusableComponentIcon\n /**\n * Definition tree root id within {@link AglynHostComponent.nodes}.\n *\n * `rootId` and `nodes` on THIS doc are the published snapshot — the copy\n * the tenant runtime renders. `getComponents` reads every component in a\n * single collection query on each page render, so they deliberately stay\n * here rather than moving into the version docs below: relocating them\n * would turn one query into N+1 on the hot path of every published site\n * (AGL-679).\n */\n rootId?: NodeId\n nodes?: Record<NodeId, N>\n /**\n * Declared props (AGL-1247), published alongside `rootId`/`nodes` and for\n * the same reason: the tenant reads this doc, so a definition whose props\n * stayed on the version doc would graft with every `{{prop.*}}` token\n * unresolved on the live site.\n */\n props?: ReusableComponentProp[]\n /**\n * Working version pointer (AGL-679). Absent on components that predate\n * the standalone editor — those still render from the fields above, and\n * opening one creates version 1 from them.\n */\n versionId?: VersionUid\n createdAt?: ITimestamp\n updatedAt?: ITimestamp\n deletedAt?: ITimestamp\n}\n\n/**\n * A reusable component's editing history (AGL-679), at\n * `hosts/{hostId}/components/{componentId}/versions/{versionId}`.\n *\n * Same shape as a screen version, with the component's `rootId` carried so\n * publishing is a copy of both fields onto the parent rather than a\n * reconstruction. Edits here are invisible to live sites until published —\n * the same mental model screens already have.\n */\nexport interface AglynHostComponentVersion<N = AglynNodeSchema>\n extends AglynDocument {\n $id: VersionUid\n hostId?: HostUid\n componentId?: ComponentDefUid\n displayName?: string\n rootId?: NodeId\n nodes?: Record<NodeId, N>\n /** Declared props being edited; published onto the parent (AGL-1247). */\n props?: ReusableComponentProp[]\n createdAt?: ITimestamp\n updatedAt?: ITimestamp\n}\n\n/**\n * Shared layout: canvas chrome (appbar, footer, nav) designed once and\n * rendered around every bound screen. Host-scoped document at\n * `hosts/{hostId}/layouts/{layoutId}`.\n */\nexport interface AglynLayout extends AglynDocument {\n $id: LayoutUid\n hostId?: HostUid\n /** Published version pointer; bound screens render this version. */\n versionId?: VersionUid\n publishSchedule?: PublishSchedule\n versions?: Array<VersionUid>\n displayName?: string\n description?: string\n /**\n * Layout this layout renders inside (AGL-703) — the same relationship\n * `AglynScreen.layoutId` expresses, one level up, so shared chrome can\n * sit outside a more specific frame. Must not be this layout, or any\n * layout already below it; see `canNestLayout`.\n */\n layoutId?: LayoutUid\n // `contributors` removed here too — see the note on AglynScreen (AGL-676).\n createdAt?: ITimestamp\n updatedAt?: ITimestamp\n}\n\n/**\n * Shared layout version. Node map has the same shape as a screen version\n * (including compression at rest) plus a LayoutSlot node marking where the\n * bound screen's content is grafted. Hosted at\n * `hosts/{hostId}/layouts/{layoutId}/versions/{versionId}`.\n */\nexport interface AglynLayoutVersion<N = AglynNodeSchema>\n extends AglynScreenVersion<N> {\n layoutId?: LayoutUid\n hostId?: HostUid\n /**\n * The properties this layout declares (AGL-2893), each set by the screens\n * that render inside it. On the version rather than the layout document,\n * beside the nodes that bind them: a layout is served by its version\n * pointer, so a version's properties go live with its tree.\n */\n props?: ReusableComponentProp[]\n}\n\nexport type TemplateUid = string\n\n/** What a template can be instantiated as. */\nexport type TemplateKind = 'page' | 'component' | 'layout'\n\n/**\n * A named substitution offered when the template is instantiated. Values are\n * applied with `resolveNamedTokens` — the same `{{name}}` mechanism that\n * renders collection entry templates — so a template author marks copy as\n * `{{productName}}` and is prompted for it on use.\n */\nexport interface TemplatePlaceholder {\n /** Token name as it appears in the nodes, without the braces. */\n name: string\n /** Prompt label shown when instantiating. */\n label?: string\n defaultValue?: string\n}\n\n/**\n * Where a template came from.\n *\n * SERVER-MANAGED. A client that could write this would be able to stamp\n * `marketplace` provenance on something it authored, and the library shows\n * this to the user as a trust signal.\n */\nexport interface TemplateSource {\n type: 'authored' | 'marketplace' | 'starter'\n /** Marketplace listing this was installed from. */\n listingId?: string\n /** Listing version installed — compared against `latestVersion` to\n * surface \"update available\" without storing anything extra. */\n version?: number | string\n /** First-party starter id from `starter-templates.ts`. */\n starterId?: string\n /**\n * Bundle-level identity for a seeded starter (AGL-687). A multi-page\n * starter seeds one page template per screen, exactly as a marketplace\n * install does; these carry the name/description/order of the bundle the\n * screens belong to so the gallery can present them as the single starter\n * they were authored as, without a second, code-side template source.\n */\n starterName?: string\n starterDescription?: string\n /** Position within the starter; fixes the order pages are created in. */\n starterOrder?: number\n}\n\n/**\n * Reusable starting point for a page, component or layout (AGL-666).\n * Host-scoped document at `hosts/{hostId}/templates/{templateId}`.\n *\n * Distinct from the things it produces: a template is inert until\n * instantiated, so marketplace downloads land here rather than becoming live\n * pages. `nodes` carries the same node-map shape screens, components and\n * layouts already use, which is what lets one collection serve all three\n * kinds.\n */\nexport interface AglynTemplate<N = AglynNodeSchema> extends AglynDocument {\n $id: TemplateUid\n hostId?: HostUid\n kind?: TemplateKind\n displayName?: string\n description?: string\n category?: string\n nodes?: Record<NodeId, N>\n /** Definition tree root — `component` kind, mirroring AglynHostComponent. */\n rootId?: NodeId\n /**\n * The properties a `component` or `layout` template's tree binds to\n * (AGL-2932), carried so what is made from it declares them — without them\n * every `{{prop.*}}` in the tree renders raw.\n */\n props?: ReusableComponentProp[]\n /**\n * A `component` template's {@link AglynHostComponent.kind} (AGL-3287), so\n * the component made from it is offered where its source was: an email\n * block saved as a template makes an email block again. Named apart from\n * {@link kind}, which says what the template makes.\n */\n componentKind?: ReusableComponentKind\n /** Suggested slug — `page` kind; de-conflicted against the host on use. */\n slug?: string\n /** Mirrors AglynScreen.seo — carried through to the created page. */\n seo?: {\n title?: string\n description?: string\n breadcrumb?: string\n image?: string\n imageWidth?: number\n imageHeight?: number\n /** Mirrors `AglynScreen.seo.imageAlt` (AGL-2417). */\n imageAlt?: string\n }\n placeholders?: Array<TemplatePlaceholder>\n /**\n * Theme the template was designed against, carried over from a site\n * template's snapshot. Held rather than applied: applying a theme changes\n * the whole site's appearance, which is exactly the kind of instant,\n * site-wide change installing is not allowed to make (AGL-669).\n */\n theme?: Record<string, unknown>\n source?: TemplateSource\n createdAt?: ITimestamp\n updatedAt?: ITimestamp\n deletedAt?: ITimestamp\n}\n\nexport type RedirectUid = string\n\n/** CONCEPT: Host redirects. Host-scoped document */\nexport interface AglynRedirect extends AglynDocument {\n $id: RedirectUid\n hostId?: HostUid\n sourcePath?: HostPath\n sourceScreen?: ScreenUid\n destinationPath?: HostPath\n destinationScreen?: ScreenUid\n statusCode?: HttpStatusCode\n params?: HostRedirectParams\n flags?: {\n regex?: true\n ignoreSlash?: true\n ignoreCase?: true\n }\n hits?: number\n lastAccess?: ITimestamp\n description?: string\n}\n"],"names":["HostScreenStatus","HostScreenVisibility","HostViewType","HostViewFormat","HostEntityType","HostRedirectParams","HOST_CLIENT_WRITABLE_FIELDS","displayName","logoUrl","seo","layouts","redirects","notFoundScreenId","errorScreens","maintenance","locales","defaultLocale","timeZone","theme","themeOverride","announcementBar","popup","business","analytics","consent","createdAt","updatedAt","HOST_UNPERSISTED_FIELDS","$id","HOST_ERROR_SCREEN_SLOTS"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GA8BD,OAAO,IAAA,AAAKA,0CAAAA;;;;;;;WAAAA;MAOX;AAED,OAAO,IAAA,AAAKC,8CAAAA;;;;;;;WAAAA;MAOX;AAED,OAAO,IAAA,AAAKC,sCAAAA;0CACD;0CACA;IACT,4EAA4E,wCACpE;WAJEA;MAKX;AAED,OAAO,IAAA,AAAKC,wCAAAA;kDACG;oDACE;WAFLA;MAGX;AAED,OAAO,IAAA,AAAKC,wCAAAA;oDACK;8CACN;WAFCA;MAGX;AAED,OAAO,IAAA,AAAKC,4CAAAA;;uDAEA;qDACF;WAHEA;MAIX;AA+fD;;;;;;;;;;;;;CAaC,GACD,OAAO,MAAMC,8BAAgE;IAC3EC,aACE,2EACA;IACFC,SACE,wEACA;IACFC,KACE,yEACA,0EACA;IACF,uEAAuE;IACvE,sEAAsE;IACtE,0EAA0E;IAC1E,mEAAmE;IACnE,0EAA0E;IAC1E,4EAA4E;IAC5E,4EAA4E;IAC5E,2EAA2E;IAC3E,qCAAqC;IACrCC,SACE,8EACA,4EACA,4EACA,8EACA;IACFC,WACE,0EACA;IACFC,kBACE,2EACA;IACFC,cACE,qEACA;IACFC,aACE,sEACA,6EACA;IACFC,SACE,4EACA;IACFC,eAAe;IACfC,UACE,oEACA,0EACA,2EACA,0EACA,2EACA,yEACA,0EACA;IACFC,OACE,2EACA;IACFC,eACE,wEACA,oEACA,6EACA;IACFC,iBACE,0EACA,2EACA,4EACA;IACFC,OAAO;IACPC,UACE,sEACA,2EACA,0EACA;IACFC,WACE,2EACA,0EACA,kEACA;IACFC,SACE,qEACA,yEACA,4EACA;IACFC,WACE,sEACA;IACFC,WACE,2EACA;AACJ,EAAC;AAED;;CAEC,GACD,OAAO,MAAMC,0BAA4D;IACvEC,KAAK;AACP,EAAC;AAsBD;;;;;;;;;;;;;CAaC,GACD,OAAO,MAAMC,0BAA0B;IACrC;IACA;IACA;IACA;CACD,CAAS"}
1
+ {"version":3,"sources":["../../../../../../../libs/aglyn/src/lib/foundation/definitions/platform.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\nimport type { HttpStatusCode } from '@aglyn/shared-data-enums'\nimport type { HostTheme } from '@aglyn/shared-data-types'\nimport type { ITimestamp } from '@aglyn/shared-util-timestamp'\nimport type {\n AglynNodeSchema,\n FieldComponentType,\n NodeId,\n} from './components.types'\n// Type-only, so this stays a definitions file with no runtime edge into\n// `app-utils`: the two video shapes are DEFINED beside the code that\n// validates them (`normalizeVideoMetadata`, `parseMediaRendition`) so a\n// document field and its validator can never describe different things.\nimport type { MediaEmbeddedMetadata } from '../../app-utils/media-embedded-fields'\nimport type { MediaVideoMetadata } from '../../app-utils/media-metadata'\nimport type { MediaVideoRendition } from '../../app-utils/media-ref'\n\n/**\n * Platform-side definitions: documents, hosts, screens, media, users.\n * The ORG BILLING family (`AglynOrgBilling` and the Org* plan/entitlement\n * vocabulary) lives in `org-billing.types.ts` — see the docs-site glossary\n * for the org/workspace/tenant/host naming convention. (This file was\n * `workspace.types.ts`; renamed in AGL-443 because nothing in it is the\n * workspace/org entity.)\n */\nexport interface AglynDocument {\n [field: string]: any\n}\n\nexport enum HostScreenStatus {\n UNPUBLISHED = 0x1 << 0x1,\n PUBLISHED = 0x1 << 0x2,\n SCHEDULED_TO_PUBLISH_UNPUBLISHED = UNPUBLISHED | (0x1 << 0x3),\n SCHEDULED_TO_UNPUBLISH_PUBLISHED = PUBLISHED | (0x1 << 0x4),\n SCHEDULED_TO_UPDATE_PUBLISHED = PUBLISHED | (0x1 << 0x5),\n SCHEDULED_TO_REVERT_UPDATE_PUBLISHED = PUBLISHED | (0x1 << 0x6),\n}\n\nexport enum HostScreenVisibility {\n PUBLIC = 0x1 << 0x1,\n UNLISTED = PUBLIC | (0x1 << 0x2),\n PRIVATE = 0x1 << 0x3,\n PASSWORD = PRIVATE | (0x1 << 0x4),\n AUTHENTICATED = PRIVATE | (0x1 << 0x5),\n AUTHORIZED = AUTHENTICATED | (0x1 << 0x6),\n}\n\nexport enum HostViewType {\n SCREEN = 0x1,\n LAYOUT = 0x2,\n /** A besigner-designed email document (screen with kind 'email', AGL-395). */\n EMAIL = 0x4,\n}\n\nexport enum HostViewFormat {\n NORMALIZED = 0x1,\n DENORMALIZED = 0x2,\n}\n\nexport enum HostEntityType {\n ORGANIZATION = 0x1,\n PERSON = 0x2,\n}\n\nexport enum HostRedirectParams {\n IGNORE,\n FORWARD = 0x1,\n MATCH = 0x2,\n}\n\n/** Org document id (`orgs/{orgId}`). Formerly TenantUid (AGL-444). */\nexport type OrgUid = string\n\nexport type UserUid = string\n\nexport interface AglynUser extends AglynDocument {\n $id: UserUid\n admin?: boolean\n email?: string\n}\n\n/**\n * Site-wide announcement bar config on the host doc (AGL-195). Text may\n * contain binding tokens; the tenant render resolves them server-side.\n */\nexport interface HostAnnouncementBar {\n enabled?: boolean\n text?: string\n /** Optional link the whole bar navigates to. */\n href?: string\n backgroundColor?: string\n textColor?: string\n /** Visitors may hide the bar; hidden state re-arms when text changes. */\n dismissible?: boolean\n}\n\n/**\n * Promotional popup config on the host doc (AGL-196); one per host,\n * marketingOverlays-gated. Body text may contain binding tokens; the\n * tenant render resolves them server-side.\n */\nexport interface HostPopup {\n enabled?: boolean\n headline?: string\n body?: string\n /** Media-library image URL shown above the copy. */\n imageUrl?: string\n /**\n * Alt text for {@link imageUrl} (AGL-1896). Defaults from the chosen DAM\n * asset's own alt when the author picks through \"Browse media\"; empty\n * still renders `alt=\"\"`, which is correct for a decorative banner.\n */\n imageAlt?: string\n ctaLabel?: string\n ctaHref?: string\n /** Render an email-capture field posting into the forms pipeline. */\n collectEmail?: boolean\n /** How the popup opens. */\n trigger?: 'delay' | 'scroll' | 'exit'\n /** Seconds (delay trigger) or percent 0-100 (scroll trigger). */\n triggerValue?: number\n /**\n * Re-show after this many days once dismissed (localStorage).\n *\n * Ignored when {@link oncePerSession} is set — the two are alternative\n * answers to the same question, and honouring both would mean a popup\n * suppressed for the session AND for seven days after it.\n */\n frequencyDays?: number\n /**\n * Show at most once per browser SESSION (AGL-2174).\n *\n * `/product/marketing` sells \"per-session frequency caps\" in as many\n * words, and the only cap the product had was day-based — a visitor who\n * dismissed a popup did not see it again for a week, which is a\n * different promise. Backed by `sessionStorage`, which is exactly the\n * lifetime the copy describes: it clears when the tab closes.\n */\n oncePerSession?: boolean\n /** Optional showing window (epoch millis; simple to serialize). */\n startAtMs?: number\n endAtMs?: number\n}\n\nexport type ProjectUid = string\nexport type ProjectNumber = number\n\nexport type HostUid = string\nexport type HostPath = string\nexport type HostMediaUid = string\n\n/**\n * Persisted MUI theme customization for a host's published site.\n * Canonical shape lives in `@aglyn/shared-data-types` so UI-scope libs can\n * consume it without depending on this framework lib.\n */\nexport type AglynHostTheme = HostTheme\n\n/** Host-scoped document */\nexport interface AglynHost extends AglynDocument {\n $id: HostUid\n /** Owning org (AGL-233); mirrored into `hostIndex/{hostId}`. */\n orgId?: OrgUid\n /** Membership projection from the org (AGL-233): uid → role tier. */\n memberRoles?: Record<UserUid, string>\n subdomain?: string\n cname?: string\n /** Site-wide announcement bar (AGL-195); marketingOverlays-gated. */\n announcementBar?: HostAnnouncementBar\n /** Promotional popup (AGL-196); marketingOverlays-gated. */\n popup?: HostPopup\n displayName?: string\n /**\n * Site logo URL (AGL-594): the host's own brand mark, shown by the\n * tenant's navigation loader (and future chrome). Distinct from\n * `seo.entity.logo`, which is publisher-semantic JSON-LD.\n */\n logoUrl?: string\n /**\n * The site logo for the DARK scheme (AGL-3400). A wordmark drawn for a\n * light ground — dark ink on a transparent PNG — disappears on the dark\n * loader scrim and error screens, so a site may name a second mark for\n * them. Unset means {@link logoUrl} serves both schemes. Read only where a\n * scheme is known; emails, `{{host.logo}}` and the install icon stay on\n * `logoUrl`, because a mail client paints a light ground.\n */\n logoDarkUrl?: string\n seo?: {\n title?: string\n description?: string\n separator?: string\n favicon?: string\n /**\n * The square mark a visitor installs to their home screen — the `icons`\n * entry of `/manifest.webmanifest`.\n *\n * Distinct from both of its neighbours, and the distinction is what the\n * field exists for. {@link favicon} is drawn at 16–32px in a tab, so it is\n * a glyph rather than artwork. `logoUrl` is the site's LOCKUP — a wordmark\n * on most sites, which is a wide rectangle, and the manifest fell back to\n * it for want of anything else: an installer that trusted a declared size\n * painted a stretched or letterboxed tile on somebody's home screen.\n *\n * A `media:` reference or a plain URL, absolutized before it is written\n * into the manifest because nothing fetches an install icon from the page\n * that linked it. Unset falls back to `logoUrl`, which is what every site\n * installs with today; a site with neither gets no `icons` array at all,\n * so the browser uses a screenshot rather than a broken tile.\n */\n appIcon?: string\n /**\n * Site-wide default social card image (AGL-1337) — used by every page\n * that sets none of its own. A `media:` reference (what the picker\n * writes) or a raw URL; resolved and made absolute by\n * `resolveSocialImage`, never emitted as stored.\n *\n * `''` is a CLEARED field, written directly to Firestore by the picker\n * card rather than through the attributes form stack, which maps `''` to\n * `undefined` and would leave a default nobody could remove (AGL-1191).\n */\n image?: string\n /**\n * Copied from the media record at pick time, and the FALLBACK rather than\n * the answer: a replace rewrites the asset's pair and cannot reach this\n * copy, so the head prefers what the asset's document records when the\n * page is composed (AGL-2850). See `resolveSocialImage`.\n */\n imageWidth?: number\n imageHeight?: number\n /**\n * `og:image:alt` (AGL-2417) — what the card SHOWS, for the screen\n * readers that announce a social preview. Defaulted from the chosen DAM\n * asset's own alt at pick time and editable per surface; stored beside\n * the reference so `resolveSocialImage` emits the description belonging\n * to the image that actually won the precedence list.\n *\n * Blank is never stored: `og:image:alt=\"\"` asserts the image conveys\n * nothing, which is not what an undescribed card means.\n */\n imageAlt?: string\n /**\n * Site-wide \"discourage search engines\" (AGL-1263): `robots.txt` refuses\n * everything, the sitemap goes empty, and every page carries `noindex`.\n * The staged-launch control — a site that is live but not ready to be\n * found, which until now could only be approximated by setting each\n * screen to {@link HostScreenVisibility.UNLISTED} one at a time.\n *\n * Absent means INDEX, and must keep meaning that. Reading a missing field\n * as \"hide\" would let a schema slip de-index every customer at once.\n */\n discourageSearchEngines?: boolean\n entity?: {\n type?: HostEntityType\n name?: string\n logo?: string\n /**\n * What the publisher IS, in a sentence — `Organization.description`\n * (AGL-2716).\n *\n * Distinct from {@link AglynHost.seo.description}, which is the meta\n * description of the site's home page: one describes a business, the\n * other describes a document, and a site whose home page is a product\n * launch should not be telling an AI assistant that the company is a\n * product launch. Unset falls back to the site description, because a\n * fallback that is approximately right beats an entity with no\n * description at all — which is what every site published before this\n * field existed.\n */\n description?: string\n /**\n * The publisher's own canonical address, when it is not this site —\n * a brand site for a company whose main presence is elsewhere. Unset\n * means this site's origin, which is the common case.\n */\n url?: string\n /**\n * Profiles that identify the SAME entity — `schema.org/sameAs`. The\n * company's LinkedIn, its Crunchbase entry, its Wikipedia article. Not\n * a link list: a page that merely mentions the entity is not the\n * entity, and listing one teaches a consumer the wrong identity.\n */\n sameAs?: string[]\n /**\n * How to reach a person — `Organization.contactPoint`.\n *\n * Split into fields rather than stored as a `contactPoint` object\n * because the console collects them as three form controls, and a\n * nested object in a settings document is a shape a partial write can\n * blank. Assembled at serialization; see `siteEntityJsonLd`.\n */\n email?: string\n telephone?: string\n /**\n * Which kind of enquiry that contact answers — `customer support`,\n * `sales`, `technical support`. Free text, because `schema.org` defines\n * the property as text rather than an enumeration.\n */\n contactType?: string\n /**\n * Postal address — `Organization.address`, a `PostalAddress`.\n *\n * Every field optional and every field omitted when blank: a partial\n * address is still a real answer to \"where are they\", while a\n * `PostalAddress` carrying empty strings is a claim that the street is\n * the empty string.\n */\n address?: {\n streetAddress?: string\n addressLocality?: string\n addressRegion?: string\n postalCode?: string\n /** ISO 3166-1 alpha-2, or a country name. */\n addressCountry?: string\n }\n /**\n * The `schema.org` LocalBusiness subtype this entity publishes as —\n * `Plumber`, `Restaurant`, `LocalBusiness` (AGL-3383).\n *\n * Beside `type` rather than a third value of it: `type` still\n * answers Organization or Person for every reader that asks, and a\n * business is an Organization. Only a value on `LOCAL_BUSINESS_TYPES`\n * publishes; anything else, or a Person, publishes as before. The four\n * fields below are published only when this is.\n */\n businessType?: string\n /** Cities or areas the business serves, published as `areaServed`. */\n areaServed?: string[]\n /**\n * Opening hours, one line per set of days — `Mo-Fr 09:00-17:00` — the\n * syntax `schema.org`'s own `openingHours` text uses. Parsed into\n * `openingHoursSpecification` at serialization; a line that does not\n * read is dropped. See `parseOpeningHours`.\n */\n openingHours?: string\n /** Free text, e.g. `$$` or `$100–$500`. */\n priceRange?: string\n /** Free text, e.g. `Cash, credit card, Zelle`. */\n paymentAccepted?: string\n }\n /**\n * Guidance for AI agents, published in `/llms.txt` (AGL-2716).\n *\n * Its own key rather than a field on `entity`, because it is about the\n * SITE's usefulness rather than about who publishes it — and because the\n * two are edited by different people at different times.\n */\n agent?: {\n /**\n * When an agent should reach for this site: the jobs it is the right\n * source for, in the author's own words.\n *\n * Absent is not a gap. `buildLlmsTxt` derives a when-to-use section\n * from what the site demonstrably publishes, so a site whose author\n * writes nothing here still ships checkable guidance; this is the place\n * to say the things that cannot be derived.\n */\n whenToUse?: string\n /** How an agent should call the site — endpoints, etiquette, limits. */\n howToUse?: string\n }\n /**\n * Search engine ownership tokens (AGL-3399), emitted by the tenant as\n * `<meta name=\"google-site-verification\">` and `<meta name=\"msvalidate.01\">`\n * in the head of every published page.\n *\n * The HTML-tag method is the only one a site on a platform subdomain can\n * use: it has no DNS of its own and cannot upload a file to the origin.\n * Only the tag's `content` value is stored, and the tenant re-checks it\n * against `SEARCH_ENGINE_VERIFICATION_TOKEN_PATTERN` before emitting, so a\n * stored value is never echoed into the head as written.\n */\n verification?: {\n google?: string\n bing?: string\n }\n }\n /**\n * Customer-configured third-party analytics (AGL-138/661). Long written by\n * the console and read by the tenant through `any` casts; declared as of\n * AGL-1498 so the write-deny guard sees it through the type rather than\n * not at all. The id is format-checked (`GA_MEASUREMENT_ID_PATTERN`)\n * before it reaches the inline script, and its LOADING is consent-gated\n * by `visitor-consent.ts` — see `consent` below.\n */\n analytics?: {\n gaMeasurementId?: string\n /**\n * Google Tag Manager container (AGL-2486), `GTM-XXXXXXX`.\n *\n * Declared here for the reason the `consent` comment below spells out at\n * length: a field the console persists and the tenant reads, but that no\n * type knows about, is how a whole-object write silently drops a sibling.\n *\n * Format-checked by `GTM_CONTAINER_ID_PATTERN` before it reaches an\n * inline script, and consent-gated exactly as `gaMeasurementId` is — a\n * container counts as a gated feature in `consentGatedCategories`, so a\n * site running only a container still gets the banner. It has to: a\n * container is the likeliest thing on a page to carry advertising tags,\n * and advertising is opt-in worldwide.\n */\n gtmContainerId?: string\n /**\n * Advertising pixels, vendor id → that vendor's account id (AGL-2486).\n *\n * Declared here for exactly the reason the `gtmContainerId` comment above\n * gives, and it is the field that proved the point: the console persisted\n * it and the tenant read it through `advertising-tags.ts`, while no type\n * knew about it. `aglyn-marketing` carries a live Meta pixel in it today,\n * and a whole-object write to `analytics` would have dropped it silently.\n *\n * The vendor descriptors — which script each id loads, and therefore which\n * hosts the CSP has to admit — live in `advertising-tags.ts`. An id here\n * for a vendor with no descriptor loads nothing.\n */\n adTags?: Record<string, string> | null\n }\n /**\n * Visitor consent tool (AGL-1498). The tool is ACTIVE when this is absent\n * — but only ever on sites that use a consent-gated feature (today:\n * `analytics.gaMeasurementId`); with none configured no consent UI exists\n * and this map decides nothing. `disabled: true` is the host's opt-out\n * (they run their own consent solution, and gated features load ungated).\n * `mode` is the host's posture: `'geo'` (geo-conditional: implied consent\n * with an always-available opt-out in opt-out regions, a prior-consent\n * banner in EU/EEA/UK and unknown regions) or `'strict'` (the banner for\n * everyone). Absent `mode` behaves as `'geo'`. Absent-means-active is the\n * only safe default for the tool itself: a schema slip must fail toward\n * asking, never toward silently tracking. See `visitor-consent.ts`.\n *\n * `advertising` (AGL-1649) is the host's opt-in to ASKING a second\n * question. It was persisted by the console and read by the tenant for four\n * days without being declared here — the same undeclared-field shape the\n * `analytics` comment above says was fixed for AGL-1498, on the very key\n * this issue exists to deliver. The cost is not typos: `consent` is an\n * inline object type, so a whole-object write built against a type missing\n * the key drops a host's advertising opt-in and TypeScript agrees with the\n * drop. `consent-host-schema-coverage.spec.ts` now fails the build on any\n * `consent.*` key that is written or read but not declared.\n */\n consent?: {\n disabled?: boolean\n mode?: 'geo' | 'strict'\n /**\n * Host opt-in to asking visitors about advertising storage (AGL-1649).\n * PERSISTED NAME; absent or `false` — the state of every site that\n * exists — means the visitor is asked about analytics only and\n * `ad_storage`, `ad_user_data` and `ad_personalization` stay denied\n * where AGL-1622 put them.\n *\n * `true` does not grant anything by itself — it makes the category\n * REACHABLE. It adds a second, separate question to the banner and the\n * privacy-choices panel, and where advertising then starts follows the\n * visitor's region: asked-first in the prior-consent set (the EU 27, the\n * EEA/EFTA three, the UK, Gibraltar, the outermost regions, and any\n * visitor whose region cannot be determined), running from the first\n * visit everywhere else on the same implied basis analytics uses. A\n * refusal — declined, opted out, or GPC — denies it everywhere.\n *\n * See `advertisingGrantedByStatus`; this sentence and that function are\n * locked together by `consent-advertising-copy-drift.spec.ts`, and the\n * published Cookie Policy moves before either of them.\n *\n * Read only through `hostAsksAboutAdvertising`, which also requires a\n * configured measurement id and tests `=== true`, so a truthy string out\n * of a hand-edited document cannot turn a consent category on.\n */\n advertising?: boolean\n }\n screens?: Record<ScreenUid, ScreenSlug>\n /** Screen rendered (noindex) for unmatched paths (AGL-87). */\n notFoundScreenId?: ScreenUid\n /**\n * Designable error screens by status (AGL-131). `notFound` supersedes\n * `notFoundScreenId` (kept in sync for back-compat).\n */\n errorScreens?: HostErrorScreens\n /**\n * Designable membership screens (AGL-553): which besigner-built screen\n * renders at `/signin`, `/signup` and `/recover`. An empty slot falls back\n * to the built-in form.\n *\n * ADMIN TIER, not authoring — the one thing that separates it from its\n * sibling `errorScreens`, which is a binding to screens the editor already\n * controls and serves only their own visitors. These three addresses are\n * where a site's members type their password, so the field decides what a\n * sign-in page IS. `enabledPlugins` is denied to an editor one tier down\n * for deciding merely whether those addresses EXIST; the field that\n * decides what they render cannot sit in a weaker tier than the field that\n * opens them.\n *\n * It is also a live-content surface that does not go through the routing\n * map: the tenant loader resolves the slot straight to a screen document,\n * and `screenClaimsToBeAPage` asks only whether that screen is deleted. So\n * `screens` being frozen for an `author` does not reach it, and an author\n * — the role sold as \"edit content but not publish\" — could otherwise put\n * a screen on the site's sign-in address without publishing anything.\n */\n authScreens?: HostAuthScreens\n /** Maintenance mode (AGL-131): every path renders the 503 screen. */\n maintenance?: boolean\n /**\n * STAFF host takedown (AGL-1501) — the host scope of the panic button,\n * for an infected or abusive site. The same field family as the org's\n * AGL-202 suspension: while `suspendedAt` is set (and `suspendedUntilMs`\n * has not passed) the site serves the lockdown notice instead of content.\n * Deliberately separate from `maintenance` above, which is the CUSTOMER'S\n * own switch — a takedown the site's editors could clear from the client\n * SDK would not be a takedown, so all four keys are client-denied in the\n * rules and written only by /api/admin/lockdown. `suspendedMessage` is\n * visitor-facing; timestamps are plain epoch ms (converter-safe).\n * Normalized by `app-utils/lockdown.ts`.\n */\n suspendedAt?: number\n suspendedReasonCode?: string\n suspendedMessage?: string\n suspendedUntilMs?: number\n /**\n * Per-site plugin deny-list (AGL-1014): plugin ids the org has enabled\n * but this site switches OFF. Subtracted from the org's resolved set by\n * `resolveHostEnabledPlugins` — the host can only ever narrow, never\n * widen, and an absent field means every org-enabled plugin runs here.\n * Writable by site ADMINS only (rules). The base component library's id is\n * ignored; a plugin on for every workspace (AI) is switched off for this\n * site like any other (AGL-3028).\n */\n disabledPlugins?: string[]\n /**\n * Per-site plugin OPT-IN list (AGL-2486): the `defaultOffPerSite` plugin\n * ids this site has explicitly turned on. Today that is `accounts` — the\n * user-accounts capability behind `/signin`, `/signup` and `/recover`.\n *\n * The companion of `disabledPlugins` above, not a duplicate of it, because\n * a deny-list cannot express \"off until asked\": absent means ON there, and\n * every published site was therefore serving member pages whether or not\n * it had members. This field is what an absent value means OFF for, and it\n * is read ONLY for ids the catalog marks default-off, so it can never\n * widen a site past its org. Writable by site ADMINS only (rules).\n */\n enabledPlugins?: string[]\n /**\n * External image hosts this site's owner has approved (AGL-1152).\n *\n * The tenant's `img-src` is built from this list, per site, in the\n * middleware — which is the only layer that runs ahead of the ISR cache, and\n * therefore the only place a per-site policy can be set at all.\n *\n * ## Why the list exists rather than a platform-wide allowlist\n *\n * AGL-1726 refused to enforce `img-src` because hotlinking an external image\n * is an ADVERTISED authoring feature (`image.tsx` tells authors to paste a\n * URL), so a first-party-only policy would silently revoke a documented\n * capability from every published site at once. Enforcement against a list\n * the OWNER chose revokes nothing, and the console warns at authoring time\n * so a refusal is never the first anyone hears of it.\n *\n * It is also what gives that issue's condition 6 — \"a rollback that does not\n * need a deploy\" — an answer: this is host data, so widening or emptying it\n * propagates within the verdict TTL with no Vercel build in the path.\n *\n * ⛔ NOT a place to put `firebasestorage.googleapis.com`. That host is PINNED\n * in `security-origins.js` and deliberately not owner-removable: orgs without\n * the paid `mediaCdn` entitlement store absolute download URLs, so an owner\n * who deleted it would blank their own images (AGL-1726 condition 5).\n *\n * Stored as bare hostnames (`cdn.example.com`, or one leading `*.` label).\n * Anything with a scheme, port, path or separator is REFUSED at the parse\n * rather than repaired — `img-src` sources are space-delimited and\n * directives `;`-delimited, so both are injection points. Writable by site\n * ADMINS only (rules).\n */\n approvedImageHosts?: string[]\n /** Site languages (AGL-164), e.g. ['en', 'es']; first is the default\n * unless `defaultLocale` says otherwise. */\n locales?: string[]\n defaultLocale?: string\n /**\n * The IANA zone THIS SITE's published dates read in (AGL-3252) — absent\n * means the workspace's `timeZone`, and absent there means UTC.\n *\n * AGL-3237 put the zone on the organization and said a per-site override\n * was worth its own issue because it would need a second rules change. It\n * does not: the host document's client branch is a deny-list, so a field\n * it does not name is already writable, and what the override actually\n * needed was the classification below. An agency running a Chicago site\n * and a Berlin site out of one workspace is the case it exists for.\n *\n * Read through `resolveSiteTimeZone`, never directly: a stored zone can\n * predate an IANA rename, and an unusable one has to fall back to the\n * workspace rather than throw inside a page render.\n */\n timeZone?: string\n /** Directory of shared layouts by display name (mirrors `screens`). */\n layouts?: Record<LayoutUid, string>\n theme?: AglynHostTheme\n\n // CONCEPT: Redirect screens\n redirects?: Record<RedirectUid, true>\n\n // CONCEPT: Enterprise - Siloed projects\n projectId?: ProjectUid\n projectNumber?: ProjectNumber\n}\n\n/**\n * Host-document fields an editor may legitimately write, each with the reason\n * (AGL-1361). Everything else must be denied in the rules' host key diff.\n *\n * `host-write-deny-coverage.spec.ts` fails the build for any field of this\n * document that is in neither list, so \"I forgot\" is not a reachable state —\n * the answer that costs nothing is always to deny. The reason strings are the\n * point: they record that somebody decided, rather than that somebody did not\n * notice. AGL-1364 is what this catches, found while writing it.\n *\n * The bar for landing here: could a client rewrite change what the platform\n * DECIDES — an entitlement, a price, where a request routes, who may read? If\n * so it belongs in the rules, whatever the console does today.\n */\nexport const HOST_CLIENT_WRITABLE_FIELDS: Readonly<Record<string, string>> = {\n displayName:\n 'The site name shown in the console. Cosmetic, org-scoped, and decides ' +\n 'nothing — the public address is `subdomain`/`cname`, both denied.',\n logoUrl:\n 'The site brand mark rendered by the tenant nav (AGL-594). Authored ' +\n 'content pointing at already-public media; no gate reads it.',\n logoDarkUrl:\n 'The dark-scheme variant of `logoUrl` (AGL-3400). Same reasoning: ' +\n 'authored content pointing at already-public media; no gate reads it.',\n seo:\n 'Title, description, favicon, app icon, social card, search engine ' +\n 'verification tokens and the AGL-1263 `discourageSearchEngines` switch. ' +\n 'All of it is authoring: the values end up in the page the editor is ' +\n 'already free to write.',\n // `screens` LEFT this map in AGL-2334 and is now classified as denied,\n // following the `disabledPlugins` precedent: it is TIERED, not freely\n // client-writable. The reason it used to carry — \"an editor who may add a\n // screen may name it\" — is still true of an editor and false of an\n // `author`, and a field one role may write and another may not belongs on\n // the denied side of this partition, where the tier is visible. Registering\n // a path in this map is what makes a page reachable on the live site, so it\n // is the publish surface an author is refused; see `canPublishHostContent`\n // in cloud/firebase-firestore.rules.\n layouts:\n 'The shared-layout directory. A layout has no route of its own, so unlike ' +\n '`screens` this map decides nothing about reachability — it is an index ' +\n 'maintained alongside API-created layout docs. What publishing a layout ' +\n 'moves is the `versionId` pointer on the layout document, which the rules ' +\n 'freeze for an `author` there (AGL-2334), not here.',\n redirects:\n 'The redirect directory. The redirect DOCS are API-create-only so the ' +\n '`redirectsPerHost` quota has somewhere to be enforced; this is the index.',\n notFoundScreenId:\n 'Which screen renders unmatched paths (AGL-87). Points at a screen the ' +\n 'editor already owns, and serves their own visitors only.',\n errorScreens:\n 'Designable error screens by status (AGL-131). Same reasoning as ' +\n '`notFoundScreenId` — a binding to screens this editor already controls.',\n maintenance:\n 'Maintenance mode (AGL-131): every path renders the 503 screen. A ' +\n 'site-wide availability switch, but it only ever takes the editor\\'s OWN ' +\n 'site down, which they can equally do by deleting its screens.',\n locales:\n 'Site languages (AGL-164). The `multilingual` entitlement is re-checked ' +\n 'server-side at page load, so writing this buys nothing a plan forbids.',\n defaultLocale: 'Which of `locales` serves an unprefixed path. Authoring.',\n timeZone:\n 'Which calendar day this site attributes a published instant to ' +\n '(AGL-3252), overriding the workspace zone. Editorial, and it reaches ' +\n 'only this site\\'s own rendered dates — nothing bills, gates or routes ' +\n 'on it, and every read validates it through `isSupportedTimeZone`, so ' +\n 'the worst a rewrite can do is move one publisher\\'s archive by a day. ' +\n 'The org\\'s `timeZone` is denied because /api/orgs/settings owns the ' +\n 'whole org document; this one rides the settings form\\'s client write ' +\n 'beside `seo`, which is read by the same renderer for the same reason.',\n theme:\n 'The persisted MUI theme for the published site. Authoring — the theme ' +\n 'editor writes it directly, and it renders only on this host.',\n themeOverride:\n 'The editor\\'s local diff on top of an installed marketplace theme, ' +\n 'written wholesale by the setup page. Authoring. Its PROVENANCE ' +\n '(`themeInstalledFrom`) is denied instead, which is the half that has to ' +\n 'be true for `isOverrideForCurrentTheme` to mean anything.',\n announcementBar:\n 'Site-wide announcement bar (AGL-195). `marketingOverlays`-gated, but ' +\n 'the gate is enforced where it counts — `site-page-enricher` re-checks ' +\n 'the entitlement at RENDER, so a free plan writing this bar never ships ' +\n 'it. The console gate is the affordance, not the boundary.',\n popup: 'Promotional popup (AGL-196). Identical reasoning to `announcementBar`.',\n business:\n 'Support email, address and social links behind the AGL-1022 host ' +\n 'tokens. Publisher-authored contact detail that renders into their own ' +\n 'pages and emails; undeclared on this interface, so the guard sees it ' +\n 'through the resolver sweep rather than the type.',\n analytics:\n 'The Google Analytics measurement id the host configures for their own ' +\n 'site (AGL-138). Authoring: it only ever tags THEIR pages, the tenant ' +\n 'format-checks it before the inline script, and loading it is ' +\n 'consent-gated (AGL-1498). A rewrite mis-tags the rewriter\\'s own site.',\n consent:\n 'The visitor consent tool switch (AGL-1498). Decides whether the ' +\n 'host\\'s OWN site asks its visitors before loading the analytics the ' +\n 'host configured — their compliance posture on their own pages, exactly ' +\n 'like `seo.discourageSearchEngines`. No platform gate reads it.',\n createdAt:\n 'Stamped by /api/hosts/create. Nothing gates on it — the org owns ' +\n 'billing dates — and console saves touch it freely.',\n updatedAt:\n 'Bumped by console saves to drive \"last edited\" copy. Decides nothing; ' +\n 'a forged value misleads the editor about their own site.',\n}\n\n/**\n * Host-document keys that never reach Firestore, so the rules have no opinion.\n */\nexport const HOST_UNPERSISTED_FIELDS: Readonly<Record<string, string>> = {\n $id: 'The document id, attached on read by the Firestore hooks. Never a field.',\n}\n\n/** Error-screen bindings by HTTP-ish status (AGL-131). */\nexport interface HostErrorScreens {\n notFound?: ScreenUid\n unauthorized?: ScreenUid\n forbidden?: ScreenUid\n unavailable?: ScreenUid\n}\n\n/**\n * The membership screen slots (AGL-553), keyed as the console card and the\n * tenant loader spell them. The names carry the `ScreenId` suffix because\n * that is how they are persisted; the console writes them as dotted field\n * paths (`authScreens.signinScreenId`).\n */\nexport interface HostAuthScreens {\n signinScreenId?: ScreenUid\n signupScreenId?: ScreenUid\n recoveryScreenId?: ScreenUid\n}\n\n/**\n * The error slots, as a value (AGL-2092).\n *\n * The console card enumerated these separately and so did nothing else, which\n * was fine while the list was only a list. It stopped being only a list when\n * `kind: 'error'` made an assigned screen stop counting against\n * `screensPerHost`: the number of slots is now the BOUND on how many screens a\n * host can take off its plan that way, so the bound and the pickers have to be\n * the same four things or the exemption is unbounded in one of them.\n *\n * `ERROR_SCREEN_MAX_PER_HOST` is `.length` of this, deliberately — adding a\n * fifth status here widens the exemption to five, which is correct and is the\n * only way it should ever widen.\n */\nexport const HOST_ERROR_SCREEN_SLOTS = [\n 'notFound',\n 'unauthorized',\n 'forbidden',\n 'unavailable',\n] as const\n\nexport type HostErrorScreenSlot = (typeof HOST_ERROR_SCREEN_SLOTS)[number]\n\nexport type ScreenUid = string\nexport type ScreenSlug = string\nexport type VersionUid = string\nexport type LayoutUid = string\n\n/**\n * Who may see an org-owned resource (AGL-1037): `'org'` is every site in\n * the org, `host:{hostId}` names one. Lives here rather than beside its\n * helpers in `app-utils/scope-tokens` because foundation cannot import\n * app-utils; that module re-exports this type along with the helpers.\n */\nexport type ScopeToken = 'org' | `host:${string}`\n\n/**\n * Uploaded media metadata (AGL-72): the binary lives in Firebase Storage at\n * `hosts/{hostId}/media/{mediaId}`; this doc mirrors it in Firestore at the\n * same logical path so the library can list without Storage list calls.\n */\nexport interface AglynHostMedia {\n $id: HostMediaUid\n fileName?: string\n contentType?: string\n sizeBytes?: number\n /** Download URL captured at upload time; nodes reference this directly. */\n url?: string\n /**\n * Folder doc id in `hosts/{hostId}/mediaFolders` (AGL-171); null/absent\n * = root. Replaces the legacy free-text `folder` string below.\n */\n folderId?: string | null\n /** Legacy AGL-124 free-text folder; read as fallback until migrated. */\n folder?: string\n tags?: string[]\n alt?: string\n description?: string\n /** Auto-captured at upload (AGL-173); best-effort for images only. */\n width?: number\n height?: number\n uploadedBy?: string\n /**\n * CDN delivery (AGL-175 / AGL-829): the stable, mediaId-keyed path\n * (`/api/media/cdn/{scope}/{mediaId}`) served by both apps — location- and\n * replace-independent (references never break; the route revalidates via\n * the `contentHash` ETag). The same route also serves an immutable\n * `…/{mediaId}/{hash}` form. `variants` are the WebP widths from upload.\n */\n cdnPath?: string\n contentHash?: string\n /**\n * The FULL 64-hex sha256 of the served bytes (AGL-1614), written only by\n * the routes that hash the bytes server-side. Additive beside\n * {@link contentHash}, which is unchanged and stays the ETag and the\n * immutable-URL segment.\n *\n * The distinction is load-bearing rather than tidy. `contentHash` is a\n * 16-hex (64-bit) TRUNCATION of one of TWO digests — sha256 on\n * `/api/media/upload` and `/api/media/replace`, GCS's md5 on\n * `/api/media/upload-url` — which is fine for a cache validator (opaque,\n * per-URL, self-consistent) and weak for anything that has to be a\n * SECURITY key. Two md5 files with the same digest are hours of laptop\n * compute to construct, and a truncation to 64 bits inherits that\n * immediately; a quarantine keyed on such a value can be made to refuse an\n * innocent asset. `contentSha256` has neither property.\n *\n * Absent, not null, when the server never held the bytes: the signed-upload\n * route streams client → bucket and only downloads them back for an SVG\n * sanitization pass, so that is the one case there where it is written.\n * Absent means \"no strong key for these bytes\", and every consumer already\n * degrades to `contentHash` and then to the per-asset key.\n */\n contentSha256?: string\n variants?: number[]\n /**\n * Why an eligible asset has no {@link variants}, when generation was\n * attempted and did not complete (AGL-1468).\n *\n * Written by every upload route and by `/api/media/replace`, and declared\n * here late: it has been on production documents since AGL-1468 and was\n * missing from this interface, so every reader either cast or guessed.\n * Absent is the common, correct case — an SVG, a document, a source\n * already narrower than 320px — and must never look like a fault.\n */\n variantsError?: string\n /**\n * The Cloud Storage object path (`{hosts|orgs}/{id}/media/{folders…}/{id}`).\n * Written at upload, rewritten by a folder move, and read back by\n * `serveMediaCdn` — which honors it ONLY inside this scope's media prefix,\n * because it is client-influenced data on an admin-SDK bucket read\n * (AGL-1881). Legacy assets have none and fall back to the flat layout.\n */\n storagePath?: string\n /** Constructs `sanitizeSvgBuffer` removed (AGL-1474). Absent when clean. */\n svgSanitized?: string[]\n /** Uid of whoever last replaced the bytes (`/api/media/replace`). */\n replacedBy?: string\n /**\n * What the uploader's browser measured about a VIDEO (AGL-2742).\n *\n * Client-reported and bounded by `normalizeVideoMetadata` before it lands,\n * which is why it does not widen {@link width}/{@link height}: those are\n * read from the bytes by the server and carry a stronger claim. A reader\n * that folds the two together loses the ability to tell which it has.\n *\n * All three numbers or none — a `VideoObject` emitting a `duration` with\n * no `width` is worse than one emitting neither.\n */\n video?: MediaVideoMetadata\n /**\n * The generated poster still for a video (AGL-2742), served by the CDN as\n * `?poster=1` and — because it genuinely is an `image/webp` — under the\n * shared edge policy rather than the origin-only one video takes.\n *\n * `variants` here are poster widths, generated by the same `sharp` pass\n * that produces an image's, at `{storagePath}__poster__w{n}.webp`.\n */\n poster?: { width: number; height: number; variants: number[] }\n /**\n * Why a video has no {@link poster}: the browser could not decode it,\n * would not paint a frame, or the encoder refused the bytes it sent.\n * Absent on every video that has one, and on every non-video.\n */\n posterError?: string\n /**\n * Compressed delivery copies of a video (AGL-2745), in the order a\n * renderer should emit `<source>` elements — most efficient codec first,\n * because a browser takes the first it can decode.\n *\n * Produced out of band by `tools/scripts/generate-video-renditions.mjs`\n * rather than at upload: there is no decoder in either upload runtime, and\n * a hosted transcoder is a recurring bill this platform's budget does not\n * carry. Absent means the master is the only copy, which is what every\n * video served before this shipped.\n */\n videoRenditions?: MediaVideoRendition[]\n /**\n * User-defined key/value metadata (AGL-822), mirrored onto the Storage\n * object's `customMetadata` by `/api/media/folders` (action\n * `custom-metadata`); this doc copy is the source of truth for display.\n */\n customMetadata?: Record<string, string>\n /**\n * The metadata the file carries INSIDE its bytes (AGL-3331) — EXIF, IPTC\n * and XMP on a photo, a PDF's document info, an Office file's properties,\n * a video's tags — as read by `readMediaEmbeddedMetadata`. Server-written\n * only (at upload, or the first time the Details drawer opens) and locked\n * in the rules; edits go INTO the file through `/api/media/metadata`,\n * which rewrites the bytes and this record together. Distinct from\n * `customMetadata`, which is Aglyn's own and never touches the file.\n */\n embeddedMetadata?: MediaEmbeddedMetadata\n /**\n * Which sites may see this asset (AGL-1037); absent = org-wide until the\n * AGL-1040 backfill stamps it. Only meaningful on the org-scoped library\n * (`orgs/{orgId}/media`) — a host's own library is private already.\n */\n visibleTo?: ScopeToken[]\n /**\n * Not fetchable without a signature (AGL-1051). **Orthogonal to\n * `visibleTo`**, and the distinction is the whole point:\n *\n * - `visibleTo` answers *which sites may USE this asset* — a discovery\n * and authoring control. An asset restricted to one site is still\n * served to anyone who has its URL, because that site's pages are\n * public and the CDN that serves them is unauthenticated.\n * - `private` answers *may the public FETCH these bytes at all*.\n *\n * A private asset carries no {@link cdnPath}, is refused by the pickers\n * that place media into page content, and is reachable only through a\n * short-lived signed URL minted for an authorized caller. Use it for\n * things that are not web assets — a signed contract, unreleased\n * artwork, an embargoed release — never for \"this image is only on one\n * site\", which is what `visibleTo` is for.\n */\n private?: boolean\n createdAt?: ITimestamp\n updatedAt?: ITimestamp\n deletedAt?: ITimestamp\n}\n\n/**\n * Media folder doc (AGL-171): `hosts/{hostId}/mediaFolders/{folderId}`.\n * Hierarchy/validation helpers live in `app-utils/media-folders`.\n */\nexport interface AglynHostMediaFolder {\n $id: string\n name: string\n /** Parent folder id; null = root. Depth capped at 5 (see app-utils). */\n parentId?: string | null\n order?: number\n /**\n * Which sites may see this folder (AGL-1037). Assets carry their own\n * scope — a folder's is applied to them by an explicit write-time\n * cascade (AGL-1042), never inherited at read time.\n */\n visibleTo?: ScopeToken[]\n createdAt?: ITimestamp\n}\n\n/**\n * Pending scheduled publication (AGL-61): when `publishAt` passes, the\n * parent doc's `versionId` pointer moves to `versionId`. Applied lazily by\n * the tenant's ISR revalidation (no dedicated cron needed).\n */\nexport interface PublishSchedule {\n /** Version to make live; unused for `action: 'unpublish'` (AGL-113). */\n versionId?: VersionUid\n /**\n * What happens at `publishAt` (AGL-113): `publish` (default) flips the\n * live version pointer; `unpublish` removes the screen's routing-map\n * entry so its path 404s after the next revalidate.\n */\n action?: 'publish' | 'unpublish'\n publishAt: ITimestamp\n /**\n * `skipped-unentitled` is a terminal refusal (AGL-1185): the schedule came\n * due on a plan without `scheduledPublishing`, so the executor declined it\n * and will not reconsider.\n *\n * It exists because leaving it `pending` meant the refusal was not recorded\n * anywhere — the schedule stayed due forever, and the moment the org upgraded\n * to Business the next beat published it. Content someone scheduled and\n * forgot, months later, surfacing during an upgrade. Recording the refusal is\n * what makes it stop being due.\n *\n * Every reader tests `=== 'pending'`, so adding a member here degrades\n * correctly rather than needing each one updated — checked, not assumed.\n *\n * `skipped-unroutable` is the second terminal refusal (AGL-1589): the\n * publish came due on a screen with no live route and no address the\n * executor could give it — no slug on the screen or one of its ancestors,\n * an address another screen already holds, or an email document, which has\n * no URL at all. One value for the three because they share a remedy (fix\n * the address in the console and schedule it again) and because a terminal\n * state is a thing every reader has to reason about; the console names the\n * causes in its message.\n *\n * It exists for the same reason as the value above: before AGL-1589 that\n * publish reported `applied` while the page kept 404ing, which is a silent\n * failure in the worst direction. A recorded refusal is the difference\n * between a schedule that did not run and one that pretended it had.\n */\n status:\n | 'pending'\n | 'applied'\n | 'canceled'\n | 'skipped-unentitled'\n | 'skipped-unroutable'\n createdAt?: ITimestamp\n}\n\n/** Host-scoped document */\nexport interface AglynScreen extends AglynDocument {\n $id: ScreenUid\n hostId?: HostUid\n parentId?: ScreenUid\n slug?: ScreenSlug\n /** Position among siblings (screens sharing `parentId`) in the screens list. */\n order?: number\n versionId?: VersionUid\n publishSchedule?: PublishSchedule\n /** Password protection (AGL-87): sha256 hex of the visitor password. */\n protection?: { passwordHash?: string }\n /** Language this screen is written in (AGL-164), e.g. 'en'. */\n locale?: string\n /** Translations of this screen: locale → screen id (AGL-164). */\n localeVariants?: Record<string, ScreenUid>\n status?: HostScreenStatus\n createdAt?: ITimestamp\n updatedAt?: ITimestamp\n /**\n * When the screen's route was last published (AGL: date-published). Stamped\n * by `publishScreenRoute` and cleared on unpublish, so it is present only\n * while the screen is reachable — distinct from `createdAt`.\n */\n publishedAt?: ITimestamp\n deletedAt?: ITimestamp\n displayName?: string\n /**\n * Normalized `displayName` for case-insensitive prefix search (AGL-835);\n * see `nameSearchKey`. Stamped by the screen create/import write paths so\n * the besigner switcher can query screens by name instead of loading the\n * whole collection.\n */\n nameLower?: string\n description?: string\n seo?: {\n title?: string\n description?: string\n breadcrumb?: string\n /**\n * This screen's social card image (AGL-1337), overriding the host\n * default. Same storage contract as {@link AglynHost.seo.image}: a\n * `media:` reference or URL, `''` meaning cleared — which is how a screen\n * goes back to inheriting the site default.\n */\n image?: string\n /**\n * Copied from the media record at pick time, and the FALLBACK rather than\n * the answer: a replace rewrites the asset's pair and cannot reach this\n * copy, so the head prefers what the asset's document records when the\n * page is composed (AGL-2850). See `resolveSocialImage`.\n */\n imageWidth?: number\n imageHeight?: number\n /**\n * `og:image:alt` (AGL-2417) — what the card SHOWS, for the screen\n * readers that announce a social preview. Defaulted from the chosen DAM\n * asset's own alt at pick time and editable per surface; stored beside\n * the reference so `resolveSocialImage` emits the description belonging\n * to the image that actually won the precedence list.\n *\n * Blank is never stored: `og:image:alt=\"\"` asserts the image conveys\n * nothing, which is not what an undescribed card means.\n */\n imageAlt?: string\n }\n\n // CONCEPT: Scheduling\n schedule?: {\n startAt?: ITimestamp\n endAt?: ITimestamp\n next?: VersionUid\n previous?: VersionUid\n }\n\n // CONCEPT: Contextual visibility\n visibility?: HostScreenVisibility\n\n // NOTE (AGL-676): `owner` and `contributors` were declared here and NEVER\n // read or written by anything — zero call sites across the whole repo.\n // Removed rather than left dangling: a declared-but-unmaintained field is\n // how the next person builds on something that was never real. Editor\n // attribution lives in `hosts/{hostId}/activity`, which is actually\n // written. If a contributor set is wanted later, add one that is kept up\n // to date.\n\n /** Shared layout this screen renders inside (see {@link AglynLayout}). */\n layoutId?: LayoutUid\n}\n\n/**\n * Host-scoped document.\n * `N` lets higher layers narrow the node schema (e.g. the aglyn SDK\n * instantiates it with its richer `NodeSchema`).\n */\nexport interface AglynScreenVersion<N = AglynNodeSchema>\n extends AglynDocument {\n $id: VersionUid\n hostId?: HostUid\n screenId?: ScreenUid\n createdAt?: ITimestamp\n updatedAt?: ITimestamp\n nodes?: Record<NodeId, N>\n /**\n * Shared layout THIS VERSION renders inside (see {@link AglynLayout}).\n *\n * Key-present wins over the screen document's {@link AglynScreen.layoutId}:\n * a `LayoutUid` binds the version to that layout, an explicit `null` renders\n * the version with no layout, and an absent key inherits the screen's\n * binding. Per-version binding is what lets a scheduled version bring its\n * own chrome live at publish time without reframing the currently served\n * version. On layout versions ({@link AglynLayoutVersion}) this same key is\n * instead the back-pointer to the owning layout document.\n */\n layoutId?: LayoutUid | null\n /**\n * This version's values for the properties of the layouts it renders inside\n * (AGL-2893), stored beside the {@link layoutId} binding: layout id →\n * property name → value, for every layout in the chain.\n *\n * Keyed by layout so that binding a different layout never hands this\n * screen's values to a property of the same name there, and so that a\n * layout nested inside another can be given values of its own. A property\n * the screen leaves unset renders with its default. Read on screen versions\n * only.\n */\n layoutPropValues?: Record<\n LayoutUid,\n Record<string, ReusableComponentPropValue>\n >\n /**\n * This version's restyling of the layouts it renders inside (AGL-3286),\n * stored beside {@link layoutPropValues}: layout id → that layout's own\n * node id → an sx record merged over the element's styling for this page\n * only. The layout's content stays the layout's, and every other screen\n * using it is unaffected. A node id the layout no longer has is ignored.\n * Read on screen versions only — see `layout-style-overrides.ts`.\n */\n layoutStyleOverrides?: Record<\n LayoutUid,\n Record<string, Record<string, unknown>>\n >\n}\n\n/** Unique id of a host-level reusable component definition. */\nexport type ComponentDefUid = string\n\n/**\n * Attribute field kinds that hold no value of their own: they arrange other\n * fields (a sub-form, tabs, a wizard, a repeating field array), decorate one\n * (an input add-on), or only display or trigger something (plain text, a\n * button). A property is one value, so none of these is a property kind —\n * see `NON_VALUE_FIELD_KINDS` for the reason each is listed.\n */\nexport type NonValueFieldKind =\n | FieldComponentType.BUTTON\n | FieldComponentType.BUTTON_GROUP\n | FieldComponentType.FIELD_ARRAY\n | FieldComponentType.INPUT_ADDON_BUTTON_GROUP\n | FieldComponentType.INPUT_ADDON_GROUP\n | FieldComponentType.PLAIN_TEXT\n | FieldComponentType.SUB_FORM\n | FieldComponentType.TAB_ITEM\n | FieldComponentType.TABS\n | FieldComponentType.WIZARD\n\n/**\n * Attribute field kinds whose property kind was named before properties\n * covered every field kind, and keeps that stored name: a text field is\n * `text`, a textarea `richText`, a switch `boolean`, a dropdown `choice`, an\n * icon picker `icon` and a screen picker `href`.\n */\nexport type LegacyNamedFieldKind =\n | FieldComponentType.TEXT_FIELD\n | FieldComponentType.TEXTAREA\n | FieldComponentType.SWITCH\n | FieldComponentType.SELECT\n | FieldComponentType.ICON_PICKER\n | FieldComponentType.SCREEN_SELECT\n\n/**\n * Kind of a declared component or layout property (AGL-1247, AGL-2893): which\n * control edits it, in the Properties dialog and on every page that sets it,\n * and which fields it can be bound to.\n *\n * Derived from the attribute schema's own field kinds rather than listed\n * beside them. Every {@link FieldComponentType} that holds a value is a\n * property kind — under its own stored value (`color-picker`,\n * `css-dimension`, …), or under the name it already had\n * ({@link LegacyNamedFieldKind}). `number` and `image` are the two text-field\n * variants a coded component declares as `type: 'number'` or as a media\n * field. So a field kind added to the attribute schema is a property kind the\n * moment it exists, and `REUSABLE_PROP_KINDS` fails to compile until it says\n * how the kind is edited.\n *\n * These values are stored in component and layout documents. Never rename\n * one.\n */\nexport type ReusableComponentPropType =\n | 'text'\n | 'richText'\n | 'image'\n | 'href'\n | 'number'\n | 'boolean'\n | 'choice'\n | 'icon'\n | `${Exclude<FieldComponentType, NonValueFieldKind | LegacyNamedFieldKind>}`\n\n/**\n * A property's value as it is stored: a default on the declaration, or a\n * page's own value in `propValues` / `layoutPropValues`.\n *\n * The shape follows the kind's control — a Yes / no stores a boolean, a\n * slider a number, a list of answers an array, an icon pick its id and path —\n * and text-shaped kinds store text. Defaults written before the kinds were\n * typed are text (`'true'`, `'28'`), which every reader accepts.\n */\nexport type ReusableComponentPropValue =\n | string\n | number\n | boolean\n | ReadonlyArray<string | number>\n | ReusableComponentIcon\n\n/**\n * One rule of a property's `condition`: the attribute schema's condition\n * (data-driven-forms' `ConditionDefinition`), with `when` naming another\n * property of the same component or layout instead of an attribute.\n *\n * The operators are the schema's own — `is` (one value, or any of a list),\n * `notMatch` to negate `is` or `pattern`, `isEmpty`, `isNotEmpty`, `pattern`\n * with `flags`, and the four comparisons — and the rule is evaluated by\n * data-driven-forms' own parser, in the Attributes panel and at render. Only\n * the serializable part is here: a stored document cannot carry a function.\n */\nexport interface ReusableComponentPropRule {\n /** The name of the property whose value decides. */\n when: string\n is?: string | number | boolean | ReadonlyArray<string | number | boolean>\n notMatch?: boolean\n isEmpty?: boolean\n isNotEmpty?: boolean\n pattern?: string\n flags?: string\n greaterThan?: number\n greaterThanOrEqualTo?: number\n lessThan?: number\n lessThanOrEqualTo?: number\n}\n\n/**\n * When a property shows and applies: one rule, a list that must ALL hold, or\n * the schema's `and` / `or` / `not` over rules.\n */\nexport type ReusableComponentPropCondition =\n | ReusableComponentPropRule\n | { and: ReusableComponentPropCondition[] }\n | { or: ReusableComponentPropCondition[] }\n | { not: ReusableComponentPropCondition | ReusableComponentPropCondition[] }\n\n/**\n * One answer a `choice` prop offers (AGL-2871).\n *\n * Two halves because two people read it: the page author picks the `label`,\n * and the field inside the component receives the `value` — which is why a\n * dropdown bound to the prop needs values it offers itself.\n */\nexport interface ReusableComponentPropOption {\n /** What a field bound to the prop receives. */\n value: string\n /** What the Attributes panel shows; falls back to `value`. */\n label?: string\n}\n\n/**\n * A prop a reusable component declares (AGL-1247), so one definition can\n * render different content per instance instead of being copied per page.\n *\n * Inside the definition the author binds a node prop to it with the\n * existing token syntax — `{{prop.headline}}`, the same mechanism behind\n * `{{entry.*}}` and `{{host.*}}` — and `composeReusableComponentNodes`\n * substitutes each instance's own values as it grafts.\n */\nexport interface ReusableComponentProp {\n /**\n * Token name: `{{prop.<name>}}`. This is the STABLE key instances store\n * their overrides under, so renaming one orphans every existing value —\n * the editor must rename in place rather than remove-and-add.\n */\n name: string\n type?: ReusableComponentPropType\n /** Field label in the Attributes panel; falls back to `name`. */\n label?: string\n /**\n * Help shown beside the property's field wherever a page sets it — the\n * attribute schema's `description`.\n */\n description?: string\n /**\n * Rendered wherever an instance leaves this prop unset, and shown as the\n * Attributes field's placeholder. One field serving both is deliberate:\n * a component that renders its own sensible copy until overridden is the\n * \"shows the placeholder from the component\" behaviour, and it means an\n * unset prop can never collapse a section to empty on a live page.\n */\n defaultValue?: ReusableComponentPropValue\n /**\n * The answers a page picks from, in the order offered, for a kind whose\n * control lists answers (`choice`, `radio`, `toggle-button`,\n * `dual-list-select`, and a `checkbox` that is a list). A `defaultValue`\n * names one of their values, or several for a kind that takes several.\n */\n options?: ReusableComponentPropOption[]\n /**\n * The kind's own settings, named by the kind's `settings` in\n * `REUSABLE_PROP_KINDS`: a slider's range, whether a choice takes several\n * answers, which theme scale a theme-scale field offers. They are the\n * attribute field's own props, so a coded component's attribute and a\n * property of the same kind are configured with the same words.\n */\n settings?: Record<string, string | number | boolean>\n /**\n * When the property shows and applies. While it does not hold, the field is\n * hidden wherever a page sets it, and the property renders as one with no\n * value and no default: text bound to it is empty, a Yes / no is a no, and\n * a field bound to it keeps its own default.\n */\n condition?: ReusableComponentPropCondition | ReusableComponentPropCondition[]\n /**\n * `icon` only: the SVG path of the icon `defaultValue` names, stored beside\n * the id when the default is picked, for the reason\n * {@link ReusableComponentIcon} stores both — a published page never loads\n * the icon catalog, so an id alone draws the empty Icon placeholder. An\n * instance's own pick travels the same way, as a whole\n * {@link ReusableComponentIcon} in its prop values.\n */\n defaultIconPath?: string\n}\n\n/**\n * A reusable component's chosen icon (AGL-1193), stood in for by the generic\n * package glyph when unset.\n *\n * Both halves are stored, for the reason AGL-1212 exists: the MDI catalog is\n * ~2.9 MB and only picker surfaces load it, so anything that had to resolve\n * `iconId` on a render surface got `DEFAULT_ICON` — a real path, so every\n * icon painted a confident \"help\" glyph. The id stays the source of truth;\n * `iconPath` is the denormalized copy that travels with the document.\n */\nexport interface ReusableComponentIcon {\n iconId?: string\n iconPath?: string\n}\n\n/**\n * Where a reusable component is placed (AGL-3287): on the site's pages, or in\n * the emails the site sends.\n *\n * The two are built from different elements — an email is made of the email\n * plugin's mail-safe blocks, a page of everything else — and neither renders\n * the other's, so a component belongs to exactly one. Absent on every\n * component made before emails could reuse one, which is why absent means\n * `site` and is never backfilled.\n */\nexport type ReusableComponentKind = 'site' | 'email'\n\n/**\n * Reusable component definition: a node subtree promoted from a screen,\n * inserted anywhere as an instance node (`componentId: 'reusableInstance'`,\n * `props.refId`) and grafted at render time (see\n * `composeReusableComponentNodes`). Host-scoped document at\n * `hosts/{hostId}/components/{componentId}`.\n */\nexport interface AglynHostComponent<N = AglynNodeSchema>\n extends AglynDocument {\n $id: ComponentDefUid\n hostId?: HostUid\n displayName?: string\n description?: string\n /**\n * Where this component is placed (AGL-3287): `email` for a block reused\n * across emails — a header, a footer — and absent (or `site`) for one\n * placed on pages. It decides which drawer offers the component: an email\n * offers email blocks alone, and a page never offers one.\n *\n * Set when the component is created and carried by every copy made of it\n * — a duplicate, a template, a marketplace install — so a copy lands in\n * the same drawer as its source.\n */\n kind?: ReusableComponentKind\n /**\n * Icon shown wherever an instance of this component is represented in the\n * besigner (AGL-1193) — the hierarchy row, the canvas badge, the element\n * drawer. Absent on every component that predates the picker, which is\n * why it is optional and never defaulted: unset means \"the package glyph\",\n * not \"an icon we failed to read\".\n */\n icon?: ReusableComponentIcon\n /**\n * Definition tree root id within {@link AglynHostComponent.nodes}.\n *\n * `rootId` and `nodes` on THIS doc are the published snapshot — the copy\n * the tenant runtime renders. `getComponents` reads every component in a\n * single collection query on each page render, so they deliberately stay\n * here rather than moving into the version docs below: relocating them\n * would turn one query into N+1 on the hot path of every published site\n * (AGL-679).\n */\n rootId?: NodeId\n nodes?: Record<NodeId, N>\n /**\n * Declared props (AGL-1247), published alongside `rootId`/`nodes` and for\n * the same reason: the tenant reads this doc, so a definition whose props\n * stayed on the version doc would graft with every `{{prop.*}}` token\n * unresolved on the live site.\n */\n props?: ReusableComponentProp[]\n /**\n * Working version pointer (AGL-679). Absent on components that predate\n * the standalone editor — those still render from the fields above, and\n * opening one creates version 1 from them.\n */\n versionId?: VersionUid\n createdAt?: ITimestamp\n updatedAt?: ITimestamp\n deletedAt?: ITimestamp\n}\n\n/**\n * A reusable component's editing history (AGL-679), at\n * `hosts/{hostId}/components/{componentId}/versions/{versionId}`.\n *\n * Same shape as a screen version, with the component's `rootId` carried so\n * publishing is a copy of both fields onto the parent rather than a\n * reconstruction. Edits here are invisible to live sites until published —\n * the same mental model screens already have.\n */\nexport interface AglynHostComponentVersion<N = AglynNodeSchema>\n extends AglynDocument {\n $id: VersionUid\n hostId?: HostUid\n componentId?: ComponentDefUid\n displayName?: string\n rootId?: NodeId\n nodes?: Record<NodeId, N>\n /** Declared props being edited; published onto the parent (AGL-1247). */\n props?: ReusableComponentProp[]\n createdAt?: ITimestamp\n updatedAt?: ITimestamp\n}\n\n/**\n * Shared layout: canvas chrome (appbar, footer, nav) designed once and\n * rendered around every bound screen. Host-scoped document at\n * `hosts/{hostId}/layouts/{layoutId}`.\n */\nexport interface AglynLayout extends AglynDocument {\n $id: LayoutUid\n hostId?: HostUid\n /** Published version pointer; bound screens render this version. */\n versionId?: VersionUid\n publishSchedule?: PublishSchedule\n versions?: Array<VersionUid>\n displayName?: string\n description?: string\n /**\n * Layout this layout renders inside (AGL-703) — the same relationship\n * `AglynScreen.layoutId` expresses, one level up, so shared chrome can\n * sit outside a more specific frame. Must not be this layout, or any\n * layout already below it; see `canNestLayout`.\n */\n layoutId?: LayoutUid\n // `contributors` removed here too — see the note on AglynScreen (AGL-676).\n createdAt?: ITimestamp\n updatedAt?: ITimestamp\n}\n\n/**\n * Shared layout version. Node map has the same shape as a screen version\n * (including compression at rest) plus a LayoutSlot node marking where the\n * bound screen's content is grafted. Hosted at\n * `hosts/{hostId}/layouts/{layoutId}/versions/{versionId}`.\n */\nexport interface AglynLayoutVersion<N = AglynNodeSchema>\n extends AglynScreenVersion<N> {\n layoutId?: LayoutUid\n hostId?: HostUid\n /**\n * The properties this layout declares (AGL-2893), each set by the screens\n * that render inside it. On the version rather than the layout document,\n * beside the nodes that bind them: a layout is served by its version\n * pointer, so a version's properties go live with its tree.\n */\n props?: ReusableComponentProp[]\n}\n\nexport type TemplateUid = string\n\n/** What a template can be instantiated as. */\nexport type TemplateKind = 'page' | 'component' | 'layout'\n\n/**\n * A named substitution offered when the template is instantiated. Values are\n * applied with `resolveNamedTokens` — the same `{{name}}` mechanism that\n * renders collection entry templates — so a template author marks copy as\n * `{{productName}}` and is prompted for it on use.\n */\nexport interface TemplatePlaceholder {\n /** Token name as it appears in the nodes, without the braces. */\n name: string\n /** Prompt label shown when instantiating. */\n label?: string\n defaultValue?: string\n}\n\n/**\n * Where a template came from.\n *\n * SERVER-MANAGED. A client that could write this would be able to stamp\n * `marketplace` provenance on something it authored, and the library shows\n * this to the user as a trust signal.\n */\nexport interface TemplateSource {\n type: 'authored' | 'marketplace' | 'starter'\n /** Marketplace listing this was installed from. */\n listingId?: string\n /** Listing version installed — compared against `latestVersion` to\n * surface \"update available\" without storing anything extra. */\n version?: number | string\n /** First-party starter id from `starter-templates.ts`. */\n starterId?: string\n /**\n * Bundle-level identity for a seeded starter (AGL-687). A multi-page\n * starter seeds one page template per screen, exactly as a marketplace\n * install does; these carry the name/description/order of the bundle the\n * screens belong to so the gallery can present them as the single starter\n * they were authored as, without a second, code-side template source.\n */\n starterName?: string\n starterDescription?: string\n /** Position within the starter; fixes the order pages are created in. */\n starterOrder?: number\n}\n\n/**\n * Reusable starting point for a page, component or layout (AGL-666).\n * Host-scoped document at `hosts/{hostId}/templates/{templateId}`.\n *\n * Distinct from the things it produces: a template is inert until\n * instantiated, so marketplace downloads land here rather than becoming live\n * pages. `nodes` carries the same node-map shape screens, components and\n * layouts already use, which is what lets one collection serve all three\n * kinds.\n */\nexport interface AglynTemplate<N = AglynNodeSchema> extends AglynDocument {\n $id: TemplateUid\n hostId?: HostUid\n kind?: TemplateKind\n displayName?: string\n description?: string\n category?: string\n nodes?: Record<NodeId, N>\n /** Definition tree root — `component` kind, mirroring AglynHostComponent. */\n rootId?: NodeId\n /**\n * The properties a `component` or `layout` template's tree binds to\n * (AGL-2932), carried so what is made from it declares them — without them\n * every `{{prop.*}}` in the tree renders raw.\n */\n props?: ReusableComponentProp[]\n /**\n * A `component` template's {@link AglynHostComponent.kind} (AGL-3287), so\n * the component made from it is offered where its source was: an email\n * block saved as a template makes an email block again. Named apart from\n * {@link kind}, which says what the template makes.\n */\n componentKind?: ReusableComponentKind\n /** Suggested slug — `page` kind; de-conflicted against the host on use. */\n slug?: string\n /** Mirrors AglynScreen.seo — carried through to the created page. */\n seo?: {\n title?: string\n description?: string\n breadcrumb?: string\n image?: string\n imageWidth?: number\n imageHeight?: number\n /** Mirrors `AglynScreen.seo.imageAlt` (AGL-2417). */\n imageAlt?: string\n }\n placeholders?: Array<TemplatePlaceholder>\n /**\n * Theme the template was designed against, carried over from a site\n * template's snapshot. Held rather than applied: applying a theme changes\n * the whole site's appearance, which is exactly the kind of instant,\n * site-wide change installing is not allowed to make (AGL-669).\n */\n theme?: Record<string, unknown>\n source?: TemplateSource\n createdAt?: ITimestamp\n updatedAt?: ITimestamp\n deletedAt?: ITimestamp\n}\n\nexport type RedirectUid = string\n\n/** CONCEPT: Host redirects. Host-scoped document */\nexport interface AglynRedirect extends AglynDocument {\n $id: RedirectUid\n hostId?: HostUid\n sourcePath?: HostPath\n sourceScreen?: ScreenUid\n destinationPath?: HostPath\n destinationScreen?: ScreenUid\n statusCode?: HttpStatusCode\n params?: HostRedirectParams\n flags?: {\n regex?: true\n ignoreSlash?: true\n ignoreCase?: true\n }\n hits?: number\n lastAccess?: ITimestamp\n description?: string\n}\n"],"names":["HostScreenStatus","HostScreenVisibility","HostViewType","HostViewFormat","HostEntityType","HostRedirectParams","HOST_CLIENT_WRITABLE_FIELDS","displayName","logoUrl","logoDarkUrl","seo","layouts","redirects","notFoundScreenId","errorScreens","maintenance","locales","defaultLocale","timeZone","theme","themeOverride","announcementBar","popup","business","analytics","consent","createdAt","updatedAt","HOST_UNPERSISTED_FIELDS","$id","HOST_ERROR_SCREEN_SLOTS"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GA8BD,OAAO,IAAA,AAAKA,0CAAAA;;;;;;;WAAAA;MAOX;AAED,OAAO,IAAA,AAAKC,8CAAAA;;;;;;;WAAAA;MAOX;AAED,OAAO,IAAA,AAAKC,sCAAAA;0CACD;0CACA;IACT,4EAA4E,wCACpE;WAJEA;MAKX;AAED,OAAO,IAAA,AAAKC,wCAAAA;kDACG;oDACE;WAFLA;MAGX;AAED,OAAO,IAAA,AAAKC,wCAAAA;oDACK;8CACN;WAFCA;MAGX;AAED,OAAO,IAAA,AAAKC,4CAAAA;;uDAEA;qDACF;WAHEA;MAIX;AAuhBD;;;;;;;;;;;;;CAaC,GACD,OAAO,MAAMC,8BAAgE;IAC3EC,aACE,2EACA;IACFC,SACE,wEACA;IACFC,aACE,sEACA;IACFC,KACE,uEACA,4EACA,yEACA;IACF,uEAAuE;IACvE,sEAAsE;IACtE,0EAA0E;IAC1E,mEAAmE;IACnE,0EAA0E;IAC1E,4EAA4E;IAC5E,4EAA4E;IAC5E,2EAA2E;IAC3E,qCAAqC;IACrCC,SACE,8EACA,4EACA,4EACA,8EACA;IACFC,WACE,0EACA;IACFC,kBACE,2EACA;IACFC,cACE,qEACA;IACFC,aACE,sEACA,6EACA;IACFC,SACE,4EACA;IACFC,eAAe;IACfC,UACE,oEACA,0EACA,2EACA,0EACA,2EACA,yEACA,0EACA;IACFC,OACE,2EACA;IACFC,eACE,wEACA,oEACA,6EACA;IACFC,iBACE,0EACA,2EACA,4EACA;IACFC,OAAO;IACPC,UACE,sEACA,2EACA,0EACA;IACFC,WACE,2EACA,0EACA,kEACA;IACFC,SACE,qEACA,yEACA,4EACA;IACFC,WACE,sEACA;IACFC,WACE,2EACA;AACJ,EAAC;AAED;;CAEC,GACD,OAAO,MAAMC,0BAA4D;IACvEC,KAAK;AACP,EAAC;AAsBD;;;;;;;;;;;;;CAaC,GACD,OAAO,MAAMC,0BAA0B;IACrC;IACA;IACA;IACA;CACD,CAAS"}
@@ -27,6 +27,12 @@
27
27
  * generated from; this module holds the row's shape and the resolvers.
28
28
  */
29
29
  import { FIRST_PARTY_PLUGINS, PUBLISHED_SITE_IMPACT } from './first-party-plugins.generated';
30
+ /**
31
+ * The first-party elements that run a site function (AGL-3393), compiled from
32
+ * each plugin's `contributes.site.functionBindings`. Compose merges it with the
33
+ * installed marketplace plugins' declarations via `mergeFunctionBindings`.
34
+ */
35
+ export { FIRST_PARTY_FUNCTION_BINDINGS } from './first-party-plugins.generated';
30
36
  /**
31
37
  * One quick link on a live site's admin edit bar, declared by the plugin whose
32
38
  * console page it opens (AGL-3080).
@@ -25,6 +25,11 @@
25
25
  * `plugins.config.json`, beside the package names the loader manifests are
26
26
  * generated from; this module holds the row's shape and the resolvers.
27
27
  */ import { FIRST_PARTY_PLUGINS, PLUGIN_EDIT_BAR_LINKS, PUBLISHED_SITE_IMPACT } from "./first-party-plugins.generated.js";
28
+ /**
29
+ * The first-party elements that run a site function (AGL-3393), compiled from
30
+ * each plugin's `contributes.site.functionBindings`. Compose merges it with the
31
+ * installed marketplace plugins' declarations via `mergeFunctionBindings`.
32
+ */ export { FIRST_PARTY_FUNCTION_BINDINGS } from "./first-party-plugins.generated.js";
28
33
  /**
29
34
  * Every catalog id classified, from the `publishedSiteImpact` each plugin
30
35
  * declares on its own row. The generator refuses a row whose verdict
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/enabled-plugins.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 * Per-org plugin enablement (AGL-416): `org.enabledPlugins` is the\n * switchboard that decides which plugins LOAD for a workspace — the loader\n * (AGL-417) dynamically imports only these. It composes with (not replaces)\n * the existing gates: a surface renders when its plugin is enabled AND its\n * `featureFlag` entitlement resolves; marketplace/marketplace listings keep\n * their per-host/org `installs` docs on top.\n *\n * The catalog's rows are declared by the plugins themselves, in\n * `plugins.config.json`, beside the package names the loader manifests are\n * generated from; this module holds the row's shape and the resolvers.\n */\n\nimport {\n FIRST_PARTY_PLUGINS,\n PLUGIN_EDIT_BAR_LINKS,\n PUBLISHED_SITE_IMPACT,\n} from './first-party-plugins.generated'\n\n/**\n * One quick link on a live site's admin edit bar, declared by the plugin whose\n * console page it opens (AGL-3080).\n *\n * The bar is drawn by the TENANT server, which never loads a plugin's console\n * code, so this is DATA compiled from `plugins.config.json` rather than a\n * runtime registration — a registry that process had not filled would drop the\n * link silently instead of failing, which is the AGL-3025 shape. It is also\n * why the label is written here rather than taken from the plugin's nav item:\n * the nav item lives in the console bundle, and \"Orders\" is a section of\n * Commerce rather than the plugin's own name.\n *\n * The link is drawn only where the SITE runs the plugin. The bar asks the same\n * resolved plugin set the rest of the edit context is built from, so a site\n * with Commerce switched off is not offered its Orders page.\n */\nexport interface PluginEditBarLink {\n /** The plugin that owns the page — also the id the site's set is checked against. */\n pluginId: string\n /** Where it sits in the bar; every declared order is distinct. */\n order: number\n /** What the bar says. A SECTION's name where that is the honest one. */\n label: string\n /**\n * Console path beneath the site, beginning with `/` and carrying no query —\n * `/inbox`, `/products/orders`. Joined onto `/{orgSlug}/hosts/{host}`.\n */\n path: string\n}\n\nexport interface FirstPartyPlugin {\n /**\n * Stable plugin id — persisted in `org.enabledPlugins`,\n * `host.disabledPlugins` and `host.enabledPlugins`, and the document id\n * under every `pluginSettings` collection.\n *\n * Renaming one is therefore a data change, not a code change, and it is\n * done in two halves that must both ship: the old id is read through\n * {@link canonicalPluginId} so every stored list still resolves, and a\n * backfill rewrites the stored documents so the alias can be retired once\n * it reads nothing.\n */\n id: string\n /** Console-facing display name. */\n label: string\n /**\n * On for every workspace AND every site, with no switch anywhere: the base\n * component library the canvas cannot render without.\n */\n alwaysOn?: boolean\n /**\n * On for every WORKSPACE, and switchable for one SITE (AGL-3028, AGL-3029).\n *\n * The workspace half is what `alwaysOn` gives: the id is unioned into the\n * org's resolved set whatever `org.enabledPlugins` stores, so a list saved\n * before the plugin existed still runs it, and no workspace switch is\n * offered. The site half is ordinary: the id is subtracted by a site's\n * `disabledPlugins` deny-list like any other, so an absent host field means\n * ON and nothing needs migrating.\n *\n * It exists for capabilities whose workspace-level half must never stop.\n * AI carries the add-on, credits, allotments and overage billing, none of\n * which belongs to a site; Forms carries the catalog and the submissions\n * already stored. A workspace switch would take those down with the\n * site-facing half, so the only switch is the site's.\n */\n alwaysOnForWorkspace?: boolean\n /**\n * What switching this plugin off for ONE site stops, and what it leaves\n * running (AGL-3028, AGL-3029) — the copy the site's Admin › Plugins page\n * states beside the switch, so the decision is made knowing both halves.\n *\n * `confirm` makes the site switch ask before it applies, naming what it is\n * about to break on published pages.\n */\n siteOff?: {\n /** What stops on the site, stated as the consequence. */\n stops: string\n /** What keeps running, because it carries no site. */\n keeps: string\n /** Ask before applying. */\n confirm?: boolean\n /**\n * How the confirmation names the site's published pages the switch\n * reaches (AGL-3029): the sentence above the list, and the one that\n * stands in for an empty list when the scan read everything.\n */\n pages?: { heading: string; none: string }\n }\n /** One-line description for the org-settings toggle list. */\n description?: string\n /**\n * Release flag gating this plugin platform-wide (AGL-422). A flagged-off\n * plugin is subtracted from every workspace's effective set — console\n * loader, published sites, and API dispatch — unless the subject is\n * staff. Always-on plugins carry no flag.\n */\n releaseFlag?: string\n /**\n * OFF for a site until that site turns it on (AGL-2486) — the inversion of\n * this switchboard's default, and the only field here that changes what an\n * absent host doc means.\n *\n * The per-host field is a DENY-list: a site records what it switches off,\n * which makes \"absent means on\" the default for all twelve other bundles.\n * That is right for a capability whose worst case is an unused nav tab, and\n * wrong for one whose worst case is a PAGE — `/signin`, `/signup`,\n * `/recover` were served by every published site on the platform,\n * including marketing sites whose real sign-in is somewhere else entirely.\n * A sign-in-shaped page on a brand's own domain that is not that brand's\n * sign-in is a credential-confusion hazard, so this one defaults closed.\n *\n * A site un-defaults it by listing the id in `host.enabledPlugins`. That\n * list is scoped to default-off ids and cannot widen anything else, so the\n * AGL-1014 invariant survives intact: a site still can never reach past\n * what its org enables.\n */\n defaultOffPerSite?: boolean\n /**\n * Plugin ids this one cannot function without (AGL-2486) — DECLARED, never\n * inferred.\n *\n * The switchboard has always let a workspace turn off a plugin another one\n * is built on, and said nothing. `accounts` is the case that proves it: the\n * Members blocks and every `membership/*` API handler ship inside the\n * COMMERCE bundle, so switching Commerce off leaves a site advertising\n * `/signin` while nothing can answer the login POST.\n *\n * Declared rather than derived on purpose. The couplings that matter are\n * exactly the ones no static read can see — which bundle happens to\n * register whose components — so a graph inferred from imports or from the\n * registry would miss the real edges while looking authoritative. An\n * incomplete warning that presents itself as complete is worse than none,\n * so this list is the contract, and {@link resolveDisableCascade} is honest\n * about covering only what is on it.\n */\n requires?: readonly string[]\n}\n\n/**\n * What a DISABLE does to a site that is already published (AGL-2486).\n *\n * The cascade dialog has to state a consequence, and there are two very\n * different ones. \"These blocks will no longer be offered\" is a different\n * decision from \"parts of your live pages go blank\", and one generic sentence\n * for both would be a lie in one direction or the other.\n *\n * - `elements` — the plugin registers site components. The tenant loads only\n * the site's enabled bundles, so elements ALREADY PLACED on published pages\n * stop rendering. Pre-existing AGL-1014 behaviour.\n * - `routes` — the plugin registers no components, but a published site\n * stops serving something: `accounts` gates `/signin`, `/signup`,\n * `/recover`; `redirects` stops applying its rules; `workflows` stops\n * answering its hooks.\n * - `console-only` — nothing a visitor can reach changes; the plugin leaves\n * navigation and the editor.\n */\nexport type PublishedSiteImpact = 'elements' | 'routes' | 'console-only'\n\n/**\n * Every catalog id classified, from the `publishedSiteImpact` each plugin\n * declares on its own row. The generator refuses a row whose verdict\n * disagrees with its `register.site`, so a site-registering bundle cannot be\n * added without declaring its consequence.\n */\nexport { PUBLISHED_SITE_IMPACT }\n\n/**\n * The user-accounts capability (AGL-2486): visitor sign-in, sign-up and\n * password recovery on a published site.\n *\n * It carries no loader manifest entry, and that is deliberate rather than an\n * oversight. The member blocks and the `membership/*` API handlers already\n * ship inside the commerce bundle, so there is no separate package to load;\n * what this id contributes is the SWITCH — the thing the tenant route gate,\n * the console card and the sitemap all ask. Several catalog ids already have\n * no tenant bundle (`contacts`, `data`, `logic`), so a manifest-less entry is\n * the existing shape, not a new one.\n */\nexport const ACCOUNTS_PLUGIN_ID = 'accounts'\n\n/**\n * The Forms capability's id, named in core (AGL-3029).\n *\n * A form's server half is core — `/api/forms/submit`, the publish-time\n * contract check, the published page's render — and core may not import a\n * plugin, so each asks the site's plugin set about this id rather than asking\n * the Forms plugin anything.\n */\nexport const FORMS_PLUGIN_ID = 'forms'\n\n/**\n * The switchboard catalog. The core holds no row of it: each plugin declares\n * its own in `plugins.config.json` (`catalog`, and one per `capabilities`\n * entry), and the generator compiles them here in their declared order, so\n * adding a plugin edits no core file.\n *\n * Compiled in rather than registered at runtime, because every resolver below\n * is synchronous and runs in each bundle of both apps; a registry that one of\n * those bundles had not filled would read as every plugin switched OFF.\n */\nexport { FIRST_PARTY_PLUGINS }\n\n/**\n * The admin edit bar's quick links that THIS SITE runs, in the order the bar\n * draws them (AGL-3080).\n *\n * `enabledPluginIds` is the site's resolved plugin set — the same one the rest\n * of the edit context is built from, already narrowed by the org's switchboard,\n * the site's own deny-list and the release flags. A link whose plugin is not\n * in it is not offered, which is what stops the bar linking a site with\n * Commerce switched off to its Orders page.\n *\n * Compiled in rather than registered, for the reason the catalog above is:\n * the tenant server draws this bar and never loads a plugin's console code,\n * so a registry it had not filled would drop every link with nothing red.\n */\nexport function pluginEditBarLinks(\n enabledPluginIds: readonly string[] | undefined,\n): readonly PluginEditBarLink[] {\n const enabled = new Set(enabledPluginIds ?? [])\n return PLUGIN_EDIT_BAR_LINKS.filter((link) => enabled.has(link.pluginId))\n}\n\n/** Ids loaded for orgs that have never touched the switchboard. */\nexport const DEFAULT_ENABLED_PLUGINS: readonly string[] =\n FIRST_PARTY_PLUGINS.map((plugin) => plugin.id)\n\n/**\n * The id a stored plugin id means today — the seam a rename re-arms.\n *\n * A first-party id is written into every org's `enabledPlugins` and every\n * site's `disabledPlugins` the moment somebody touches the switchboard, so a\n * rename in this file alone would silently turn the plugin OFF for every\n * workspace that had listed it under the old name — the stored id would match\n * nothing in the catalog and fall through as a marketplace listing id. Every\n * reader of a stored list runs it through here first, so a document written\n * before a rename keeps meaning what it meant while the backfill runs.\n *\n * Nothing is aliased today. `contacts` read as `crm` from the CRM's rename\n * (AGL-2595) until its backfill reported zero documents carrying the old id,\n * and the alias was retired (AGL-2614) — an alias that outlives its backfill\n * is a second name for the plugin that nothing writes and every reader must\n * keep honoring. The next rename adds its pair here, ships a backfill over\n * `org.enabledPlugins`, `host.disabledPlugins`, `host.enabledPlugins` and the\n * `pluginSettings` document ids, and removes the pair the same way.\n */\nexport function canonicalPluginId(pluginId: string): string {\n return pluginId\n}\n\n/** A stored list, read through the alias seam and de-duplicated. */\nfunction canonicalPluginIds(pluginIds: readonly unknown[]): string[] {\n return Array.from(\n new Set(pluginIds.map((id) => canonicalPluginId(String(id)))),\n )\n}\n\n/** On everywhere, a site's deny-list included: the base component library. */\nconst ALWAYS_ON: readonly string[] = FIRST_PARTY_PLUGINS.filter(\n (plugin) => plugin.alwaysOn,\n).map((plugin) => plugin.id)\n\n/**\n * Unioned into every workspace's set whatever the org stored: {@link ALWAYS_ON}\n * plus the ids that are on for every workspace and switchable per site.\n */\nconst ALWAYS_ON_FOR_WORKSPACE: readonly string[] = FIRST_PARTY_PLUGINS.filter(\n (plugin) => plugin.alwaysOn || plugin.alwaysOnForWorkspace,\n).map((plugin) => plugin.id)\n\n/**\n * Whether the WORKSPACE switch for this plugin is locked on (AGL-3028): the\n * base library, and every plugin that is on for every workspace and\n * switchable only per site. The org switchboard renders these on and inert.\n */\nexport function isLockedOnForWorkspace(pluginId: string): boolean {\n return ALWAYS_ON_FOR_WORKSPACE.includes(pluginId)\n}\n\n/**\n * Whether the SITE switch for this plugin is locked on: the base library\n * alone. Every other plugin a workspace runs can be switched off for one site.\n */\nexport function isLockedOnForSite(pluginId: string): boolean {\n return ALWAYS_ON.includes(pluginId)\n}\n\nconst FIRST_PARTY_IDS: ReadonlySet<string> = new Set(\n FIRST_PARTY_PLUGINS.map((plugin) => plugin.id),\n)\n\n/**\n * Ids a SITE does not get until it asks (AGL-2486). See\n * {@link FirstPartyPlugin.defaultOffPerSite} for why this inversion exists.\n */\nexport const DEFAULT_OFF_PER_SITE_PLUGIN_IDS: ReadonlySet<string> = new Set(\n FIRST_PARTY_PLUGINS.filter((plugin) => plugin.defaultOffPerSite).map(\n (plugin) => plugin.id,\n ),\n)\n\n/** Whether a plugin id is off for a site until that site opts in. */\nexport function isDefaultOffPerSite(pluginId: string): boolean {\n return DEFAULT_OFF_PER_SITE_PLUGIN_IDS.has(pluginId)\n}\n\n/**\n * Applies a host's `enabledPlugins` OPT-IN list (AGL-2486): subtracts every\n * default-off id the host has not explicitly asked for.\n *\n * Narrow-only, like its deny-list sibling. The list can only ever REMOVE the\n * default-off subtraction for an id the org already enables — listing an\n * ordinary id buys nothing, and listing one the org switched off buys\n * nothing either, because this runs against the org's resolved set.\n */\nexport function applyDefaultOffOptIn(\n pluginIds: readonly string[],\n optedIn?: readonly string[] | null,\n): string[] {\n if (!DEFAULT_OFF_PER_SITE_PLUGIN_IDS.size) return [...pluginIds]\n const asked = new Set(\n Array.isArray(optedIn) ? canonicalPluginIds(optedIn) : [],\n )\n return pluginIds.filter(\n (id) => !DEFAULT_OFF_PER_SITE_PLUGIN_IDS.has(id) || asked.has(id),\n )\n}\n\n/**\n * Whether an `enabledPlugins` id is a first-party BUNDLE (vs a marketplace\n * listing id) — AGL-777. `enabledPlugins` is a flat mix of the two: bundle\n * ids are the short, stable names in {@link FIRST_PARTY_PLUGINS}; marketplace\n * installs ride the same field under their Firestore listing doc id. This is\n * the single classifier both writers and readers use so the two kinds never\n * get confused — e.g. an install sync must never add/remove a bundle id.\n */\nexport function isFirstPartyPlugin(pluginId: string): boolean {\n return FIRST_PARTY_IDS.has(pluginId)\n}\n\n/**\n * Splits a mixed `enabledPlugins` array into first-party bundle ids and\n * marketplace listing ids (AGL-777). The field stays a single flat list;\n * this just names the two kinds for callers that need to treat them apart.\n */\nexport function classifyEnabledPlugins(pluginIds: readonly string[]): {\n bundles: string[]\n listings: string[]\n} {\n const bundles: string[] = []\n const listings: string[] = []\n for (const id of pluginIds) {\n if (isFirstPartyPlugin(id)) bundles.push(id)\n else listings.push(id)\n }\n return { bundles, listings }\n}\n\n/**\n * The org's effective enabled-plugin set. Absent field → every first-party\n * plugin (existing orgs keep working untouched); always-on ids — and the ids\n * that are on for every workspace and switchable only per site — are unioned\n * in, so no stored list can switch them off for a workspace, including one\n * saved before the plugin existed. Unknown ids are kept — realm-trusted\n * marketplace plugins (AGL-420) ride the same field.\n */\nexport function resolveEnabledPlugins(\n org?: { enabledPlugins?: string[] } | null,\n): string[] {\n const configured = org?.enabledPlugins\n const base = Array.isArray(configured)\n ? canonicalPluginIds(configured)\n : [...DEFAULT_ENABLED_PLUGINS]\n return Array.from(new Set([...ALWAYS_ON_FOR_WORKSPACE, ...base]))\n}\n\n/**\n * Subtracts a host's per-site deny-list from an enabled set (AGL-1014).\n * Always-on ids survive — the base component library cannot be switched\n * off per site any more than per org. A plugin that is on for every\n * workspace (`alwaysOnForWorkspace`) does NOT survive: its site switch is\n * the one switch it has. Order of the surviving ids is kept.\n */\nexport function subtractDisabledPlugins(\n pluginIds: readonly string[],\n disabledPlugins?: readonly string[] | null,\n): string[] {\n if (!Array.isArray(disabledPlugins) || !disabledPlugins.length)\n return [...pluginIds]\n const disabled = new Set(canonicalPluginIds(disabledPlugins))\n return pluginIds.filter(\n (id) => ALWAYS_ON.includes(id) || !disabled.has(id),\n )\n}\n\n/**\n * A HOST's effective enabled-plugin set (AGL-1014): the org's resolved set\n * minus the host's `disabledPlugins` deny-list. This is the single source\n * of truth for per-site enablement — console navigation, the editor,\n * published sites, and API dispatch must all read it, or a \"disabled\"\n * plugin is merely hidden, not off.\n *\n * Semantics are narrow-only by construction: a host stores what it turns\n * OFF, so it can never widen beyond what the org enables, and an absent\n * field means every org-enabled plugin runs (newly installed plugins\n * default to enabled per site until a host admin disables them).\n *\n * ONE class of id reads the other way (AGL-2486): a `defaultOffPerSite`\n * plugin is subtracted unless the host names it in `enabledPlugins`. That\n * list un-defaults; it does not grant. Both fields are still bounded by the\n * org's set, and an explicit deny still beats an explicit opt-in — the two\n * are applied in that order below, so the safe reading wins whenever a\n * hand-edited or stale doc sets both.\n */\nexport function resolveHostEnabledPlugins(\n org?: { enabledPlugins?: string[] } | null,\n host?: { disabledPlugins?: string[]; enabledPlugins?: string[] } | null,\n): string[] {\n return subtractDisabledPlugins(\n applyDefaultOffOptIn(resolveEnabledPlugins(org), host?.enabledPlugins),\n host?.disabledPlugins,\n )\n}\n\n/**\n * Whether ONE plugin runs on this site — the host-aware counterpart of\n * {@link isPluginEnabled}, and the form every route gate wants.\n */\nexport function isHostPluginEnabled(\n org: { enabledPlugins?: string[] } | null | undefined,\n host: { disabledPlugins?: string[]; enabledPlugins?: string[] } | null | undefined,\n pluginId: string,\n): boolean {\n return resolveHostEnabledPlugins(org, host).includes(pluginId)\n}\n\nexport function isPluginEnabled(\n org: { enabledPlugins?: string[] } | null | undefined,\n pluginId: string,\n): boolean {\n return resolveEnabledPlugins(org).includes(pluginId)\n}\n\n/**\n * Where one plugin stands ON ONE SITE (AGL-1014, AGL-2486).\n *\n * Three surfaces need this answer and they must not each derive it: the site\n * plugin page's \"Where it runs\" card, that page's dependency list — where the\n * same question is asked about a NEIGHBOR, and where the answer is the whole\n * point, because a dependency the workspace enables and this site has switched\n * off is a broken plugin behind a healthy-looking workspace page — and the\n * per-site switchboard. A second derivation is a second chance to disagree\n * with `resolveHostEnabledPlugins`, which is what actually runs.\n *\n * - `always-on` — the base library; it cannot be switched off anywhere.\n * A plugin that is on for every workspace but\n * switchable per site is NOT this: it reads\n * `runs-here` or `off-for-site` like any other.\n * - `off-for-workspace` — the org has it off, so it runs on none of its sites\n * and this site cannot turn it on.\n * - `runs-here` — the effective host set contains it.\n * - `awaiting-opt-in` — a `defaultOffPerSite` plugin the org enables and\n * this site has not asked for. Off, but off by\n * DEFAULT rather than by a decision, which is a\n * different thing to tell an operator.\n * - `off-for-site` — the org enables it and this site switched it off.\n */\nexport type PluginSiteState =\n | 'always-on'\n | 'off-for-workspace'\n | 'runs-here'\n | 'awaiting-opt-in'\n | 'off-for-site'\n\nexport function resolvePluginSiteState(\n org: { enabledPlugins?: string[] } | null | undefined,\n host: { disabledPlugins?: string[]; enabledPlugins?: string[] } | null | undefined,\n pluginId: string,\n): PluginSiteState {\n if (ALWAYS_ON.includes(pluginId)) return 'always-on'\n if (!resolveEnabledPlugins(org).includes(pluginId)) return 'off-for-workspace'\n if (resolveHostEnabledPlugins(org, host).includes(pluginId)) return 'runs-here'\n // A default-off plugin this site has not named is off because nobody asked\n // for it; naming it in the deny-list is a decision, and an explicit deny\n // beats an opt-in, so the deny-list is checked first.\n const denied = Array.isArray(host?.disabledPlugins)\n ? host.disabledPlugins.map(String).includes(pluginId)\n : false\n return !denied && isDefaultOffPerSite(pluginId)\n ? 'awaiting-opt-in'\n : 'off-for-site'\n}\n\n/**\n * The declared dependency graph, both directions, built once (AGL-2486).\n *\n * `requires` on the catalog is the forward edge — what a plugin cannot run\n * without — and every consumer so far wanted the reverse one. Building both\n * here keeps the cascade, the console's dependency card and the server-side\n * refusal reading the SAME edges: a graph assembled a second time is a graph\n * that can disagree with itself about which plugins are coupled, and the\n * disagreement would show up as a warning shown on one surface and not on the\n * one that writes.\n *\n * `extraRequirements` extends the catalog and cannot shrink it, exactly as it\n * does for {@link resolveDisableCascade} — a third-party id can add edges of\n * its own but can never declare away a first-party one.\n */\nfunction dependencyEdges(\n extraRequirements?: Readonly<Record<string, readonly string[]>>,\n): {\n /** Keyed by a required id; holds the ids that depend on it. */\n dependents: Map<string, string[]>\n /** Keyed by a dependent id; holds the ids it requires. */\n requires: Map<string, string[]>\n} {\n const dependents = new Map<string, string[]>()\n const requires = new Map<string, string[]>()\n const addEdge = (dependent: string, required: string) => {\n const reverse = dependents.get(required)\n if (reverse) reverse.push(dependent)\n else dependents.set(required, [dependent])\n const forward = requires.get(dependent)\n if (forward) forward.push(required)\n else requires.set(dependent, [required])\n }\n for (const plugin of FIRST_PARTY_PLUGINS)\n for (const required of plugin.requires ?? []) addEdge(plugin.id, required)\n for (const [dependent, required] of Object.entries(extraRequirements ?? {}))\n for (const one of required ?? []) addEdge(dependent, String(one))\n return { dependents, requires }\n}\n\n/** What `pluginId` declares it cannot run without — DIRECT edges only. */\nexport function pluginRequirements(\n pluginId: string,\n extraRequirements?: Readonly<Record<string, readonly string[]>>,\n): string[] {\n return [...(dependencyEdges(extraRequirements).requires.get(pluginId) ?? [])]\n}\n\n/** What declares it cannot run without `pluginId` — DIRECT edges only. */\nexport function pluginDependents(\n pluginId: string,\n extraRequirements?: Readonly<Record<string, readonly string[]>>,\n): string[] {\n return [...(dependencyEdges(extraRequirements).dependents.get(pluginId) ?? [])]\n}\n\n/**\n * Which of `enabledIds` would be left running with a requirement switched off\n * (AGL-2486) — the same question the cascade dialog asks, phrased so a WRITER\n * can refuse instead of a reader warning.\n *\n * The dialog is a courtesy: it stands in front of the console's own switches\n * and nothing else. A direct API call, a stale tab that posts an older set, or\n * a second console surface that forgot to ask can still store a set where\n * User Accounts is on and the Commerce bundle that answers its sign-in POST is\n * not. This is what a write path calls to reject that set outright, so the\n * boundary does not live in a dialog anyone can skip.\n *\n * Only DECLARED requirements are checked, with the same limit\n * {@link PLUGIN_CASCADE_IS_DECLARED_ONLY} states: an undeclared coupling is\n * invisible here too. A requirement naming an id outside the catalog is\n * ignored rather than treated as unmet — a stale edge must not lock a\n * workspace out of its own switchboard.\n */\nexport function strandedDependents(\n enabledIds: readonly string[],\n extraRequirements?: Readonly<Record<string, readonly string[]>>,\n): Array<{ pluginId: string; missing: string[] }> {\n const enabled = new Set(enabledIds.map(String))\n const known = new Set<string>([\n ...FIRST_PARTY_PLUGINS.map((plugin) => plugin.id),\n ...enabled,\n ])\n const { requires } = dependencyEdges(extraRequirements)\n const stranded: Array<{ pluginId: string; missing: string[] }> = []\n for (const pluginId of enabled) {\n const missing = (requires.get(pluginId) ?? []).filter(\n (required) => known.has(required) && !enabled.has(required),\n )\n if (missing.length) stranded.push({ pluginId, missing })\n }\n return stranded\n}\n\n/**\n * Everything that must ALSO be switched off when `pluginId` is (AGL-2486) —\n * the transitive closure over reverse `requires` edges, restricted to what is\n * currently on.\n *\n * Pure and surface-agnostic: the org switchboard and the per-site card both\n * call it with their own \"currently enabled\" set, so the same graph answers\n * both, and the org's wider blast radius comes from the set it passes, not\n * from a second implementation.\n *\n * `extraRequirements` is how a marketplace listing joins the graph. Third-party\n * ids ride the same `enabledPlugins` field, so a listing whose manifest\n * declares `requires` is cascaded exactly like a bundle. It EXTENDS the\n * catalog graph and cannot shrink it — a listing cannot declare away a\n * first-party edge.\n *\n * ⚠️ The result is only ever as complete as what has been DECLARED. A\n * marketplace plugin that uses first-party components without saying so in its\n * manifest will not appear here, and callers must not present the list as\n * exhaustive. See `PLUGIN_CASCADE_IS_DECLARED_ONLY`.\n */\nexport function resolveDisableCascade(\n pluginId: string,\n enabledIds: readonly string[],\n extraRequirements?: Readonly<Record<string, readonly string[]>>,\n): string[] {\n const enabled = new Set(enabledIds.map(String))\n const dependents = dependencyEdges(extraRequirements).dependents\n\n // Breadth-first over reverse edges. `seen` is seeded with the origin so a\n // cycle back to it terminates and the plugin being disabled is never listed\n // among its own dependents.\n const seen = new Set<string>([pluginId])\n const cascade: string[] = []\n const queue: string[] = [pluginId]\n while (queue.length) {\n const current = queue.shift() as string\n for (const dependent of dependents.get(current) ?? []) {\n if (seen.has(dependent)) continue\n seen.add(dependent)\n // Walk THROUGH an already-off dependent — something may depend on it in\n // turn — but do not claim it is being turned off.\n if (enabled.has(dependent)) cascade.push(dependent)\n queue.push(dependent)\n }\n }\n return cascade\n}\n\n/**\n * Why the cascade list must never be presented as exhaustive (AGL-2486).\n *\n * Exported as copy rather than left to each caller to paraphrase: the whole\n * value of the warning rests on it being honest about its own limits, and two\n * surfaces wording that differently is how one of them ends up overclaiming.\n *\n * It says built-in ONLY, and that is the current truth rather than a hedge.\n * `PluginManifest` carries no `requires` field, so a publisher cannot declare\n * a dependency even if they want to, and `extraRequirements` above — the seam\n * that would carry them — is supplied by nothing outside tests. An earlier\n * draft said \"a plugin that uses this one WITHOUT declaring it cannot be\n * detected\", which quietly implied declaring was possible and would have made\n * this the exact overclaiming warning it exists to avoid. Adding the manifest\n * field is a separate change: it needs a publish form, validation and a line\n * on the install screen, or it is a field written by nobody and read by\n * nothing. Update this sentence in the same change, not before.\n */\nexport const PLUGIN_CASCADE_IS_DECLARED_ONLY =\n 'This covers built-in plugins only. A marketplace plugin has no way to ' +\n 'declare that it depends on another one yet, so none are listed here — ' +\n 'check any third-party plugins you rely on before continuing.'\n\n/** Reverse lookup: which first-party plugin a release flag gates, if any. */\nexport function pluginForReleaseFlag(\n flagKey: string,\n): FirstPartyPlugin | undefined {\n return FIRST_PARTY_PLUGINS.find((plugin) => plugin.releaseFlag === flagKey)\n}\n\n/**\n * Subtracts release-flagged-off plugins from an effective set (AGL-422).\n * Pure — the caller supplies the verdict source (client: the activated\n * Remote Config hook state; server: the cached admin-SDK template read),\n * so the same policy runs identically on every surface:\n *\n * - unknown ids (marketplace/realm installs) and always-on ids pass;\n * - a first-party id with a `releaseFlag` passes only when the flag is on\n * for the subject, or `staffBypass` is set (staff preview keeps working\n * while a feature is dark).\n */\nexport function filterPluginsByReleaseFlags(\n pluginIds: readonly string[],\n isFlagOn: (flagKey: string) => boolean,\n options?: { staffBypass?: boolean },\n): string[] {\n if (options?.staffBypass) return [...pluginIds]\n const catalog = new Map(\n FIRST_PARTY_PLUGINS.map((plugin) => [plugin.id, plugin]),\n )\n return pluginIds.filter((pluginId) => {\n const plugin = catalog.get(pluginId)\n if (!plugin?.releaseFlag || plugin.alwaysOn) return true\n return isFlagOn(plugin.releaseFlag)\n })\n}\n"],"names":["FIRST_PARTY_PLUGINS","PLUGIN_EDIT_BAR_LINKS","PUBLISHED_SITE_IMPACT","ACCOUNTS_PLUGIN_ID","FORMS_PLUGIN_ID","pluginEditBarLinks","enabledPluginIds","enabled","Set","filter","link","has","pluginId","DEFAULT_ENABLED_PLUGINS","map","plugin","id","canonicalPluginId","canonicalPluginIds","pluginIds","Array","from","String","ALWAYS_ON","alwaysOn","ALWAYS_ON_FOR_WORKSPACE","alwaysOnForWorkspace","isLockedOnForWorkspace","includes","isLockedOnForSite","FIRST_PARTY_IDS","DEFAULT_OFF_PER_SITE_PLUGIN_IDS","defaultOffPerSite","isDefaultOffPerSite","applyDefaultOffOptIn","optedIn","size","asked","isArray","isFirstPartyPlugin","classifyEnabledPlugins","bundles","listings","push","resolveEnabledPlugins","org","configured","enabledPlugins","base","subtractDisabledPlugins","disabledPlugins","length","disabled","resolveHostEnabledPlugins","host","isHostPluginEnabled","isPluginEnabled","resolvePluginSiteState","denied","dependencyEdges","extraRequirements","dependents","Map","requires","addEdge","dependent","required","reverse","get","set","forward","Object","entries","one","pluginRequirements","pluginDependents","strandedDependents","enabledIds","known","stranded","missing","resolveDisableCascade","seen","cascade","queue","current","shift","add","PLUGIN_CASCADE_IS_DECLARED_ONLY","pluginForReleaseFlag","flagKey","find","releaseFlag","filterPluginsByReleaseFlags","isFlagOn","options","staffBypass","catalog"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/enabled-plugins.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 * Per-org plugin enablement (AGL-416): `org.enabledPlugins` is the\n * switchboard that decides which plugins LOAD for a workspace — the loader\n * (AGL-417) dynamically imports only these. It composes with (not replaces)\n * the existing gates: a surface renders when its plugin is enabled AND its\n * `featureFlag` entitlement resolves; marketplace/marketplace listings keep\n * their per-host/org `installs` docs on top.\n *\n * The catalog's rows are declared by the plugins themselves, in\n * `plugins.config.json`, beside the package names the loader manifests are\n * generated from; this module holds the row's shape and the resolvers.\n */\n\nimport {\n FIRST_PARTY_PLUGINS,\n PLUGIN_EDIT_BAR_LINKS,\n PUBLISHED_SITE_IMPACT,\n} from './first-party-plugins.generated'\n\n/**\n * The first-party elements that run a site function (AGL-3393), compiled from\n * each plugin's `contributes.site.functionBindings`. Compose merges it with the\n * installed marketplace plugins' declarations via `mergeFunctionBindings`.\n */\nexport { FIRST_PARTY_FUNCTION_BINDINGS } from './first-party-plugins.generated'\n\n/**\n * One quick link on a live site's admin edit bar, declared by the plugin whose\n * console page it opens (AGL-3080).\n *\n * The bar is drawn by the TENANT server, which never loads a plugin's console\n * code, so this is DATA compiled from `plugins.config.json` rather than a\n * runtime registration — a registry that process had not filled would drop the\n * link silently instead of failing, which is the AGL-3025 shape. It is also\n * why the label is written here rather than taken from the plugin's nav item:\n * the nav item lives in the console bundle, and \"Orders\" is a section of\n * Commerce rather than the plugin's own name.\n *\n * The link is drawn only where the SITE runs the plugin. The bar asks the same\n * resolved plugin set the rest of the edit context is built from, so a site\n * with Commerce switched off is not offered its Orders page.\n */\nexport interface PluginEditBarLink {\n /** The plugin that owns the page — also the id the site's set is checked against. */\n pluginId: string\n /** Where it sits in the bar; every declared order is distinct. */\n order: number\n /** What the bar says. A SECTION's name where that is the honest one. */\n label: string\n /**\n * Console path beneath the site, beginning with `/` and carrying no query —\n * `/inbox`, `/products/orders`. Joined onto `/{orgSlug}/hosts/{host}`.\n */\n path: string\n}\n\nexport interface FirstPartyPlugin {\n /**\n * Stable plugin id — persisted in `org.enabledPlugins`,\n * `host.disabledPlugins` and `host.enabledPlugins`, and the document id\n * under every `pluginSettings` collection.\n *\n * Renaming one is therefore a data change, not a code change, and it is\n * done in two halves that must both ship: the old id is read through\n * {@link canonicalPluginId} so every stored list still resolves, and a\n * backfill rewrites the stored documents so the alias can be retired once\n * it reads nothing.\n */\n id: string\n /** Console-facing display name. */\n label: string\n /**\n * On for every workspace AND every site, with no switch anywhere: the base\n * component library the canvas cannot render without.\n */\n alwaysOn?: boolean\n /**\n * On for every WORKSPACE, and switchable for one SITE (AGL-3028, AGL-3029).\n *\n * The workspace half is what `alwaysOn` gives: the id is unioned into the\n * org's resolved set whatever `org.enabledPlugins` stores, so a list saved\n * before the plugin existed still runs it, and no workspace switch is\n * offered. The site half is ordinary: the id is subtracted by a site's\n * `disabledPlugins` deny-list like any other, so an absent host field means\n * ON and nothing needs migrating.\n *\n * It exists for capabilities whose workspace-level half must never stop.\n * AI carries the add-on, credits, allotments and overage billing, none of\n * which belongs to a site; Forms carries the catalog and the submissions\n * already stored. A workspace switch would take those down with the\n * site-facing half, so the only switch is the site's.\n */\n alwaysOnForWorkspace?: boolean\n /**\n * What switching this plugin off for ONE site stops, and what it leaves\n * running (AGL-3028, AGL-3029) — the copy the site's Admin › Plugins page\n * states beside the switch, so the decision is made knowing both halves.\n *\n * `confirm` makes the site switch ask before it applies, naming what it is\n * about to break on published pages.\n */\n siteOff?: {\n /** What stops on the site, stated as the consequence. */\n stops: string\n /** What keeps running, because it carries no site. */\n keeps: string\n /** Ask before applying. */\n confirm?: boolean\n /**\n * How the confirmation names the site's published pages the switch\n * reaches (AGL-3029): the sentence above the list, and the one that\n * stands in for an empty list when the scan read everything.\n */\n pages?: { heading: string; none: string }\n }\n /** One-line description for the org-settings toggle list. */\n description?: string\n /**\n * Release flag gating this plugin platform-wide (AGL-422). A flagged-off\n * plugin is subtracted from every workspace's effective set — console\n * loader, published sites, and API dispatch — unless the subject is\n * staff. Always-on plugins carry no flag.\n */\n releaseFlag?: string\n /**\n * OFF for a site until that site turns it on (AGL-2486) — the inversion of\n * this switchboard's default, and the only field here that changes what an\n * absent host doc means.\n *\n * The per-host field is a DENY-list: a site records what it switches off,\n * which makes \"absent means on\" the default for all twelve other bundles.\n * That is right for a capability whose worst case is an unused nav tab, and\n * wrong for one whose worst case is a PAGE — `/signin`, `/signup`,\n * `/recover` were served by every published site on the platform,\n * including marketing sites whose real sign-in is somewhere else entirely.\n * A sign-in-shaped page on a brand's own domain that is not that brand's\n * sign-in is a credential-confusion hazard, so this one defaults closed.\n *\n * A site un-defaults it by listing the id in `host.enabledPlugins`. That\n * list is scoped to default-off ids and cannot widen anything else, so the\n * AGL-1014 invariant survives intact: a site still can never reach past\n * what its org enables.\n */\n defaultOffPerSite?: boolean\n /**\n * Plugin ids this one cannot function without (AGL-2486) — DECLARED, never\n * inferred.\n *\n * The switchboard has always let a workspace turn off a plugin another one\n * is built on, and said nothing. `accounts` is the case that proves it: the\n * Members blocks and every `membership/*` API handler ship inside the\n * COMMERCE bundle, so switching Commerce off leaves a site advertising\n * `/signin` while nothing can answer the login POST.\n *\n * Declared rather than derived on purpose. The couplings that matter are\n * exactly the ones no static read can see — which bundle happens to\n * register whose components — so a graph inferred from imports or from the\n * registry would miss the real edges while looking authoritative. An\n * incomplete warning that presents itself as complete is worse than none,\n * so this list is the contract, and {@link resolveDisableCascade} is honest\n * about covering only what is on it.\n */\n requires?: readonly string[]\n}\n\n/**\n * What a DISABLE does to a site that is already published (AGL-2486).\n *\n * The cascade dialog has to state a consequence, and there are two very\n * different ones. \"These blocks will no longer be offered\" is a different\n * decision from \"parts of your live pages go blank\", and one generic sentence\n * for both would be a lie in one direction or the other.\n *\n * - `elements` — the plugin registers site components. The tenant loads only\n * the site's enabled bundles, so elements ALREADY PLACED on published pages\n * stop rendering. Pre-existing AGL-1014 behaviour.\n * - `routes` — the plugin registers no components, but a published site\n * stops serving something: `accounts` gates `/signin`, `/signup`,\n * `/recover`; `redirects` stops applying its rules; `workflows` stops\n * answering its hooks.\n * - `console-only` — nothing a visitor can reach changes; the plugin leaves\n * navigation and the editor.\n */\nexport type PublishedSiteImpact = 'elements' | 'routes' | 'console-only'\n\n/**\n * Every catalog id classified, from the `publishedSiteImpact` each plugin\n * declares on its own row. The generator refuses a row whose verdict\n * disagrees with its `register.site`, so a site-registering bundle cannot be\n * added without declaring its consequence.\n */\nexport { PUBLISHED_SITE_IMPACT }\n\n/**\n * The user-accounts capability (AGL-2486): visitor sign-in, sign-up and\n * password recovery on a published site.\n *\n * It carries no loader manifest entry, and that is deliberate rather than an\n * oversight. The member blocks and the `membership/*` API handlers already\n * ship inside the commerce bundle, so there is no separate package to load;\n * what this id contributes is the SWITCH — the thing the tenant route gate,\n * the console card and the sitemap all ask. Several catalog ids already have\n * no tenant bundle (`contacts`, `data`, `logic`), so a manifest-less entry is\n * the existing shape, not a new one.\n */\nexport const ACCOUNTS_PLUGIN_ID = 'accounts'\n\n/**\n * The Forms capability's id, named in core (AGL-3029).\n *\n * A form's server half is core — `/api/forms/submit`, the publish-time\n * contract check, the published page's render — and core may not import a\n * plugin, so each asks the site's plugin set about this id rather than asking\n * the Forms plugin anything.\n */\nexport const FORMS_PLUGIN_ID = 'forms'\n\n/**\n * The switchboard catalog. The core holds no row of it: each plugin declares\n * its own in `plugins.config.json` (`catalog`, and one per `capabilities`\n * entry), and the generator compiles them here in their declared order, so\n * adding a plugin edits no core file.\n *\n * Compiled in rather than registered at runtime, because every resolver below\n * is synchronous and runs in each bundle of both apps; a registry that one of\n * those bundles had not filled would read as every plugin switched OFF.\n */\nexport { FIRST_PARTY_PLUGINS }\n\n/**\n * The admin edit bar's quick links that THIS SITE runs, in the order the bar\n * draws them (AGL-3080).\n *\n * `enabledPluginIds` is the site's resolved plugin set — the same one the rest\n * of the edit context is built from, already narrowed by the org's switchboard,\n * the site's own deny-list and the release flags. A link whose plugin is not\n * in it is not offered, which is what stops the bar linking a site with\n * Commerce switched off to its Orders page.\n *\n * Compiled in rather than registered, for the reason the catalog above is:\n * the tenant server draws this bar and never loads a plugin's console code,\n * so a registry it had not filled would drop every link with nothing red.\n */\nexport function pluginEditBarLinks(\n enabledPluginIds: readonly string[] | undefined,\n): readonly PluginEditBarLink[] {\n const enabled = new Set(enabledPluginIds ?? [])\n return PLUGIN_EDIT_BAR_LINKS.filter((link) => enabled.has(link.pluginId))\n}\n\n/** Ids loaded for orgs that have never touched the switchboard. */\nexport const DEFAULT_ENABLED_PLUGINS: readonly string[] =\n FIRST_PARTY_PLUGINS.map((plugin) => plugin.id)\n\n/**\n * The id a stored plugin id means today — the seam a rename re-arms.\n *\n * A first-party id is written into every org's `enabledPlugins` and every\n * site's `disabledPlugins` the moment somebody touches the switchboard, so a\n * rename in this file alone would silently turn the plugin OFF for every\n * workspace that had listed it under the old name — the stored id would match\n * nothing in the catalog and fall through as a marketplace listing id. Every\n * reader of a stored list runs it through here first, so a document written\n * before a rename keeps meaning what it meant while the backfill runs.\n *\n * Nothing is aliased today. `contacts` read as `crm` from the CRM's rename\n * (AGL-2595) until its backfill reported zero documents carrying the old id,\n * and the alias was retired (AGL-2614) — an alias that outlives its backfill\n * is a second name for the plugin that nothing writes and every reader must\n * keep honoring. The next rename adds its pair here, ships a backfill over\n * `org.enabledPlugins`, `host.disabledPlugins`, `host.enabledPlugins` and the\n * `pluginSettings` document ids, and removes the pair the same way.\n */\nexport function canonicalPluginId(pluginId: string): string {\n return pluginId\n}\n\n/** A stored list, read through the alias seam and de-duplicated. */\nfunction canonicalPluginIds(pluginIds: readonly unknown[]): string[] {\n return Array.from(\n new Set(pluginIds.map((id) => canonicalPluginId(String(id)))),\n )\n}\n\n/** On everywhere, a site's deny-list included: the base component library. */\nconst ALWAYS_ON: readonly string[] = FIRST_PARTY_PLUGINS.filter(\n (plugin) => plugin.alwaysOn,\n).map((plugin) => plugin.id)\n\n/**\n * Unioned into every workspace's set whatever the org stored: {@link ALWAYS_ON}\n * plus the ids that are on for every workspace and switchable per site.\n */\nconst ALWAYS_ON_FOR_WORKSPACE: readonly string[] = FIRST_PARTY_PLUGINS.filter(\n (plugin) => plugin.alwaysOn || plugin.alwaysOnForWorkspace,\n).map((plugin) => plugin.id)\n\n/**\n * Whether the WORKSPACE switch for this plugin is locked on (AGL-3028): the\n * base library, and every plugin that is on for every workspace and\n * switchable only per site. The org switchboard renders these on and inert.\n */\nexport function isLockedOnForWorkspace(pluginId: string): boolean {\n return ALWAYS_ON_FOR_WORKSPACE.includes(pluginId)\n}\n\n/**\n * Whether the SITE switch for this plugin is locked on: the base library\n * alone. Every other plugin a workspace runs can be switched off for one site.\n */\nexport function isLockedOnForSite(pluginId: string): boolean {\n return ALWAYS_ON.includes(pluginId)\n}\n\nconst FIRST_PARTY_IDS: ReadonlySet<string> = new Set(\n FIRST_PARTY_PLUGINS.map((plugin) => plugin.id),\n)\n\n/**\n * Ids a SITE does not get until it asks (AGL-2486). See\n * {@link FirstPartyPlugin.defaultOffPerSite} for why this inversion exists.\n */\nexport const DEFAULT_OFF_PER_SITE_PLUGIN_IDS: ReadonlySet<string> = new Set(\n FIRST_PARTY_PLUGINS.filter((plugin) => plugin.defaultOffPerSite).map(\n (plugin) => plugin.id,\n ),\n)\n\n/** Whether a plugin id is off for a site until that site opts in. */\nexport function isDefaultOffPerSite(pluginId: string): boolean {\n return DEFAULT_OFF_PER_SITE_PLUGIN_IDS.has(pluginId)\n}\n\n/**\n * Applies a host's `enabledPlugins` OPT-IN list (AGL-2486): subtracts every\n * default-off id the host has not explicitly asked for.\n *\n * Narrow-only, like its deny-list sibling. The list can only ever REMOVE the\n * default-off subtraction for an id the org already enables — listing an\n * ordinary id buys nothing, and listing one the org switched off buys\n * nothing either, because this runs against the org's resolved set.\n */\nexport function applyDefaultOffOptIn(\n pluginIds: readonly string[],\n optedIn?: readonly string[] | null,\n): string[] {\n if (!DEFAULT_OFF_PER_SITE_PLUGIN_IDS.size) return [...pluginIds]\n const asked = new Set(\n Array.isArray(optedIn) ? canonicalPluginIds(optedIn) : [],\n )\n return pluginIds.filter(\n (id) => !DEFAULT_OFF_PER_SITE_PLUGIN_IDS.has(id) || asked.has(id),\n )\n}\n\n/**\n * Whether an `enabledPlugins` id is a first-party BUNDLE (vs a marketplace\n * listing id) — AGL-777. `enabledPlugins` is a flat mix of the two: bundle\n * ids are the short, stable names in {@link FIRST_PARTY_PLUGINS}; marketplace\n * installs ride the same field under their Firestore listing doc id. This is\n * the single classifier both writers and readers use so the two kinds never\n * get confused — e.g. an install sync must never add/remove a bundle id.\n */\nexport function isFirstPartyPlugin(pluginId: string): boolean {\n return FIRST_PARTY_IDS.has(pluginId)\n}\n\n/**\n * Splits a mixed `enabledPlugins` array into first-party bundle ids and\n * marketplace listing ids (AGL-777). The field stays a single flat list;\n * this just names the two kinds for callers that need to treat them apart.\n */\nexport function classifyEnabledPlugins(pluginIds: readonly string[]): {\n bundles: string[]\n listings: string[]\n} {\n const bundles: string[] = []\n const listings: string[] = []\n for (const id of pluginIds) {\n if (isFirstPartyPlugin(id)) bundles.push(id)\n else listings.push(id)\n }\n return { bundles, listings }\n}\n\n/**\n * The org's effective enabled-plugin set. Absent field → every first-party\n * plugin (existing orgs keep working untouched); always-on ids — and the ids\n * that are on for every workspace and switchable only per site — are unioned\n * in, so no stored list can switch them off for a workspace, including one\n * saved before the plugin existed. Unknown ids are kept — realm-trusted\n * marketplace plugins (AGL-420) ride the same field.\n */\nexport function resolveEnabledPlugins(\n org?: { enabledPlugins?: string[] } | null,\n): string[] {\n const configured = org?.enabledPlugins\n const base = Array.isArray(configured)\n ? canonicalPluginIds(configured)\n : [...DEFAULT_ENABLED_PLUGINS]\n return Array.from(new Set([...ALWAYS_ON_FOR_WORKSPACE, ...base]))\n}\n\n/**\n * Subtracts a host's per-site deny-list from an enabled set (AGL-1014).\n * Always-on ids survive — the base component library cannot be switched\n * off per site any more than per org. A plugin that is on for every\n * workspace (`alwaysOnForWorkspace`) does NOT survive: its site switch is\n * the one switch it has. Order of the surviving ids is kept.\n */\nexport function subtractDisabledPlugins(\n pluginIds: readonly string[],\n disabledPlugins?: readonly string[] | null,\n): string[] {\n if (!Array.isArray(disabledPlugins) || !disabledPlugins.length)\n return [...pluginIds]\n const disabled = new Set(canonicalPluginIds(disabledPlugins))\n return pluginIds.filter(\n (id) => ALWAYS_ON.includes(id) || !disabled.has(id),\n )\n}\n\n/**\n * A HOST's effective enabled-plugin set (AGL-1014): the org's resolved set\n * minus the host's `disabledPlugins` deny-list. This is the single source\n * of truth for per-site enablement — console navigation, the editor,\n * published sites, and API dispatch must all read it, or a \"disabled\"\n * plugin is merely hidden, not off.\n *\n * Semantics are narrow-only by construction: a host stores what it turns\n * OFF, so it can never widen beyond what the org enables, and an absent\n * field means every org-enabled plugin runs (newly installed plugins\n * default to enabled per site until a host admin disables them).\n *\n * ONE class of id reads the other way (AGL-2486): a `defaultOffPerSite`\n * plugin is subtracted unless the host names it in `enabledPlugins`. That\n * list un-defaults; it does not grant. Both fields are still bounded by the\n * org's set, and an explicit deny still beats an explicit opt-in — the two\n * are applied in that order below, so the safe reading wins whenever a\n * hand-edited or stale doc sets both.\n */\nexport function resolveHostEnabledPlugins(\n org?: { enabledPlugins?: string[] } | null,\n host?: { disabledPlugins?: string[]; enabledPlugins?: string[] } | null,\n): string[] {\n return subtractDisabledPlugins(\n applyDefaultOffOptIn(resolveEnabledPlugins(org), host?.enabledPlugins),\n host?.disabledPlugins,\n )\n}\n\n/**\n * Whether ONE plugin runs on this site — the host-aware counterpart of\n * {@link isPluginEnabled}, and the form every route gate wants.\n */\nexport function isHostPluginEnabled(\n org: { enabledPlugins?: string[] } | null | undefined,\n host: { disabledPlugins?: string[]; enabledPlugins?: string[] } | null | undefined,\n pluginId: string,\n): boolean {\n return resolveHostEnabledPlugins(org, host).includes(pluginId)\n}\n\nexport function isPluginEnabled(\n org: { enabledPlugins?: string[] } | null | undefined,\n pluginId: string,\n): boolean {\n return resolveEnabledPlugins(org).includes(pluginId)\n}\n\n/**\n * Where one plugin stands ON ONE SITE (AGL-1014, AGL-2486).\n *\n * Three surfaces need this answer and they must not each derive it: the site\n * plugin page's \"Where it runs\" card, that page's dependency list — where the\n * same question is asked about a NEIGHBOR, and where the answer is the whole\n * point, because a dependency the workspace enables and this site has switched\n * off is a broken plugin behind a healthy-looking workspace page — and the\n * per-site switchboard. A second derivation is a second chance to disagree\n * with `resolveHostEnabledPlugins`, which is what actually runs.\n *\n * - `always-on` — the base library; it cannot be switched off anywhere.\n * A plugin that is on for every workspace but\n * switchable per site is NOT this: it reads\n * `runs-here` or `off-for-site` like any other.\n * - `off-for-workspace` — the org has it off, so it runs on none of its sites\n * and this site cannot turn it on.\n * - `runs-here` — the effective host set contains it.\n * - `awaiting-opt-in` — a `defaultOffPerSite` plugin the org enables and\n * this site has not asked for. Off, but off by\n * DEFAULT rather than by a decision, which is a\n * different thing to tell an operator.\n * - `off-for-site` — the org enables it and this site switched it off.\n */\nexport type PluginSiteState =\n | 'always-on'\n | 'off-for-workspace'\n | 'runs-here'\n | 'awaiting-opt-in'\n | 'off-for-site'\n\nexport function resolvePluginSiteState(\n org: { enabledPlugins?: string[] } | null | undefined,\n host: { disabledPlugins?: string[]; enabledPlugins?: string[] } | null | undefined,\n pluginId: string,\n): PluginSiteState {\n if (ALWAYS_ON.includes(pluginId)) return 'always-on'\n if (!resolveEnabledPlugins(org).includes(pluginId)) return 'off-for-workspace'\n if (resolveHostEnabledPlugins(org, host).includes(pluginId)) return 'runs-here'\n // A default-off plugin this site has not named is off because nobody asked\n // for it; naming it in the deny-list is a decision, and an explicit deny\n // beats an opt-in, so the deny-list is checked first.\n const denied = Array.isArray(host?.disabledPlugins)\n ? host.disabledPlugins.map(String).includes(pluginId)\n : false\n return !denied && isDefaultOffPerSite(pluginId)\n ? 'awaiting-opt-in'\n : 'off-for-site'\n}\n\n/**\n * The declared dependency graph, both directions, built once (AGL-2486).\n *\n * `requires` on the catalog is the forward edge — what a plugin cannot run\n * without — and every consumer so far wanted the reverse one. Building both\n * here keeps the cascade, the console's dependency card and the server-side\n * refusal reading the SAME edges: a graph assembled a second time is a graph\n * that can disagree with itself about which plugins are coupled, and the\n * disagreement would show up as a warning shown on one surface and not on the\n * one that writes.\n *\n * `extraRequirements` extends the catalog and cannot shrink it, exactly as it\n * does for {@link resolveDisableCascade} — a third-party id can add edges of\n * its own but can never declare away a first-party one.\n */\nfunction dependencyEdges(\n extraRequirements?: Readonly<Record<string, readonly string[]>>,\n): {\n /** Keyed by a required id; holds the ids that depend on it. */\n dependents: Map<string, string[]>\n /** Keyed by a dependent id; holds the ids it requires. */\n requires: Map<string, string[]>\n} {\n const dependents = new Map<string, string[]>()\n const requires = new Map<string, string[]>()\n const addEdge = (dependent: string, required: string) => {\n const reverse = dependents.get(required)\n if (reverse) reverse.push(dependent)\n else dependents.set(required, [dependent])\n const forward = requires.get(dependent)\n if (forward) forward.push(required)\n else requires.set(dependent, [required])\n }\n for (const plugin of FIRST_PARTY_PLUGINS)\n for (const required of plugin.requires ?? []) addEdge(plugin.id, required)\n for (const [dependent, required] of Object.entries(extraRequirements ?? {}))\n for (const one of required ?? []) addEdge(dependent, String(one))\n return { dependents, requires }\n}\n\n/** What `pluginId` declares it cannot run without — DIRECT edges only. */\nexport function pluginRequirements(\n pluginId: string,\n extraRequirements?: Readonly<Record<string, readonly string[]>>,\n): string[] {\n return [...(dependencyEdges(extraRequirements).requires.get(pluginId) ?? [])]\n}\n\n/** What declares it cannot run without `pluginId` — DIRECT edges only. */\nexport function pluginDependents(\n pluginId: string,\n extraRequirements?: Readonly<Record<string, readonly string[]>>,\n): string[] {\n return [...(dependencyEdges(extraRequirements).dependents.get(pluginId) ?? [])]\n}\n\n/**\n * Which of `enabledIds` would be left running with a requirement switched off\n * (AGL-2486) — the same question the cascade dialog asks, phrased so a WRITER\n * can refuse instead of a reader warning.\n *\n * The dialog is a courtesy: it stands in front of the console's own switches\n * and nothing else. A direct API call, a stale tab that posts an older set, or\n * a second console surface that forgot to ask can still store a set where\n * User Accounts is on and the Commerce bundle that answers its sign-in POST is\n * not. This is what a write path calls to reject that set outright, so the\n * boundary does not live in a dialog anyone can skip.\n *\n * Only DECLARED requirements are checked, with the same limit\n * {@link PLUGIN_CASCADE_IS_DECLARED_ONLY} states: an undeclared coupling is\n * invisible here too. A requirement naming an id outside the catalog is\n * ignored rather than treated as unmet — a stale edge must not lock a\n * workspace out of its own switchboard.\n */\nexport function strandedDependents(\n enabledIds: readonly string[],\n extraRequirements?: Readonly<Record<string, readonly string[]>>,\n): Array<{ pluginId: string; missing: string[] }> {\n const enabled = new Set(enabledIds.map(String))\n const known = new Set<string>([\n ...FIRST_PARTY_PLUGINS.map((plugin) => plugin.id),\n ...enabled,\n ])\n const { requires } = dependencyEdges(extraRequirements)\n const stranded: Array<{ pluginId: string; missing: string[] }> = []\n for (const pluginId of enabled) {\n const missing = (requires.get(pluginId) ?? []).filter(\n (required) => known.has(required) && !enabled.has(required),\n )\n if (missing.length) stranded.push({ pluginId, missing })\n }\n return stranded\n}\n\n/**\n * Everything that must ALSO be switched off when `pluginId` is (AGL-2486) —\n * the transitive closure over reverse `requires` edges, restricted to what is\n * currently on.\n *\n * Pure and surface-agnostic: the org switchboard and the per-site card both\n * call it with their own \"currently enabled\" set, so the same graph answers\n * both, and the org's wider blast radius comes from the set it passes, not\n * from a second implementation.\n *\n * `extraRequirements` is how a marketplace listing joins the graph. Third-party\n * ids ride the same `enabledPlugins` field, so a listing whose manifest\n * declares `requires` is cascaded exactly like a bundle. It EXTENDS the\n * catalog graph and cannot shrink it — a listing cannot declare away a\n * first-party edge.\n *\n * ⚠️ The result is only ever as complete as what has been DECLARED. A\n * marketplace plugin that uses first-party components without saying so in its\n * manifest will not appear here, and callers must not present the list as\n * exhaustive. See `PLUGIN_CASCADE_IS_DECLARED_ONLY`.\n */\nexport function resolveDisableCascade(\n pluginId: string,\n enabledIds: readonly string[],\n extraRequirements?: Readonly<Record<string, readonly string[]>>,\n): string[] {\n const enabled = new Set(enabledIds.map(String))\n const dependents = dependencyEdges(extraRequirements).dependents\n\n // Breadth-first over reverse edges. `seen` is seeded with the origin so a\n // cycle back to it terminates and the plugin being disabled is never listed\n // among its own dependents.\n const seen = new Set<string>([pluginId])\n const cascade: string[] = []\n const queue: string[] = [pluginId]\n while (queue.length) {\n const current = queue.shift() as string\n for (const dependent of dependents.get(current) ?? []) {\n if (seen.has(dependent)) continue\n seen.add(dependent)\n // Walk THROUGH an already-off dependent — something may depend on it in\n // turn — but do not claim it is being turned off.\n if (enabled.has(dependent)) cascade.push(dependent)\n queue.push(dependent)\n }\n }\n return cascade\n}\n\n/**\n * Why the cascade list must never be presented as exhaustive (AGL-2486).\n *\n * Exported as copy rather than left to each caller to paraphrase: the whole\n * value of the warning rests on it being honest about its own limits, and two\n * surfaces wording that differently is how one of them ends up overclaiming.\n *\n * It says built-in ONLY, and that is the current truth rather than a hedge.\n * `PluginManifest` carries no `requires` field, so a publisher cannot declare\n * a dependency even if they want to, and `extraRequirements` above — the seam\n * that would carry them — is supplied by nothing outside tests. An earlier\n * draft said \"a plugin that uses this one WITHOUT declaring it cannot be\n * detected\", which quietly implied declaring was possible and would have made\n * this the exact overclaiming warning it exists to avoid. Adding the manifest\n * field is a separate change: it needs a publish form, validation and a line\n * on the install screen, or it is a field written by nobody and read by\n * nothing. Update this sentence in the same change, not before.\n */\nexport const PLUGIN_CASCADE_IS_DECLARED_ONLY =\n 'This covers built-in plugins only. A marketplace plugin has no way to ' +\n 'declare that it depends on another one yet, so none are listed here — ' +\n 'check any third-party plugins you rely on before continuing.'\n\n/** Reverse lookup: which first-party plugin a release flag gates, if any. */\nexport function pluginForReleaseFlag(\n flagKey: string,\n): FirstPartyPlugin | undefined {\n return FIRST_PARTY_PLUGINS.find((plugin) => plugin.releaseFlag === flagKey)\n}\n\n/**\n * Subtracts release-flagged-off plugins from an effective set (AGL-422).\n * Pure — the caller supplies the verdict source (client: the activated\n * Remote Config hook state; server: the cached admin-SDK template read),\n * so the same policy runs identically on every surface:\n *\n * - unknown ids (marketplace/realm installs) and always-on ids pass;\n * - a first-party id with a `releaseFlag` passes only when the flag is on\n * for the subject, or `staffBypass` is set (staff preview keeps working\n * while a feature is dark).\n */\nexport function filterPluginsByReleaseFlags(\n pluginIds: readonly string[],\n isFlagOn: (flagKey: string) => boolean,\n options?: { staffBypass?: boolean },\n): string[] {\n if (options?.staffBypass) return [...pluginIds]\n const catalog = new Map(\n FIRST_PARTY_PLUGINS.map((plugin) => [plugin.id, plugin]),\n )\n return pluginIds.filter((pluginId) => {\n const plugin = catalog.get(pluginId)\n if (!plugin?.releaseFlag || plugin.alwaysOn) return true\n return isFlagOn(plugin.releaseFlag)\n })\n}\n"],"names":["FIRST_PARTY_PLUGINS","PLUGIN_EDIT_BAR_LINKS","PUBLISHED_SITE_IMPACT","FIRST_PARTY_FUNCTION_BINDINGS","ACCOUNTS_PLUGIN_ID","FORMS_PLUGIN_ID","pluginEditBarLinks","enabledPluginIds","enabled","Set","filter","link","has","pluginId","DEFAULT_ENABLED_PLUGINS","map","plugin","id","canonicalPluginId","canonicalPluginIds","pluginIds","Array","from","String","ALWAYS_ON","alwaysOn","ALWAYS_ON_FOR_WORKSPACE","alwaysOnForWorkspace","isLockedOnForWorkspace","includes","isLockedOnForSite","FIRST_PARTY_IDS","DEFAULT_OFF_PER_SITE_PLUGIN_IDS","defaultOffPerSite","isDefaultOffPerSite","applyDefaultOffOptIn","optedIn","size","asked","isArray","isFirstPartyPlugin","classifyEnabledPlugins","bundles","listings","push","resolveEnabledPlugins","org","configured","enabledPlugins","base","subtractDisabledPlugins","disabledPlugins","length","disabled","resolveHostEnabledPlugins","host","isHostPluginEnabled","isPluginEnabled","resolvePluginSiteState","denied","dependencyEdges","extraRequirements","dependents","Map","requires","addEdge","dependent","required","reverse","get","set","forward","Object","entries","one","pluginRequirements","pluginDependents","strandedDependents","enabledIds","known","stranded","missing","resolveDisableCascade","seen","cascade","queue","current","shift","add","PLUGIN_CASCADE_IS_DECLARED_ONLY","pluginForReleaseFlag","flagKey","find","releaseFlag","filterPluginsByReleaseFlags","isFlagOn","options","staffBypass","catalog"],"mappings":"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"}