@aglyn/aglyn 1.0.0-beta.229 → 1.0.0-beta.231

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 (123) hide show
  1. package/package.json +11 -11
  2. package/src/lib/app-utils/analytics-events.d.ts +18 -0
  3. package/src/lib/app-utils/analytics-events.js +2 -0
  4. package/src/lib/app-utils/analytics-events.js.map +1 -1
  5. package/src/lib/app-utils/crm.d.ts +14 -1
  6. package/src/lib/app-utils/crm.js +19 -2
  7. package/src/lib/app-utils/crm.js.map +1 -1
  8. package/src/lib/app-utils/docs-help.generated.d.ts +111 -9
  9. package/src/lib/app-utils/docs-help.generated.js +272 -3
  10. package/src/lib/app-utils/docs-help.generated.js.map +1 -1
  11. package/src/lib/app-utils/docs-index.generated.js +810 -61
  12. package/src/lib/app-utils/docs-index.generated.js.map +1 -1
  13. package/src/lib/app-utils/host-status.d.ts +85 -0
  14. package/src/lib/app-utils/host-status.js +115 -0
  15. package/src/lib/app-utils/host-status.js.map +1 -0
  16. package/src/lib/app-utils/lockdown.js +1 -1
  17. package/src/lib/app-utils/lockdown.js.map +1 -1
  18. package/src/lib/app-utils/media-filter.d.ts +136 -0
  19. package/src/lib/app-utils/media-filter.js +400 -0
  20. package/src/lib/app-utils/media-filter.js.map +1 -0
  21. package/src/lib/app-utils/mobile-push.d.ts +98 -0
  22. package/src/lib/app-utils/mobile-push.js +97 -0
  23. package/src/lib/app-utils/mobile-push.js.map +1 -0
  24. package/src/lib/app-utils/notification-push.d.ts +38 -0
  25. package/src/lib/app-utils/notification-push.js +54 -0
  26. package/src/lib/app-utils/notification-push.js.map +1 -0
  27. package/src/lib/app-utils/notifications.d.ts +7 -0
  28. package/src/lib/app-utils/notifications.js.map +1 -1
  29. package/src/lib/app-utils/organizations.js +5 -2
  30. package/src/lib/app-utils/organizations.js.map +1 -1
  31. package/src/lib/app-utils/plan-entitlements.js +20 -0
  32. package/src/lib/app-utils/plan-entitlements.js.map +1 -1
  33. package/src/lib/app-utils/plugin-host-events.generated.d.ts +1 -1
  34. package/src/lib/app-utils/plugin-host-events.generated.js +164 -0
  35. package/src/lib/app-utils/plugin-host-events.generated.js.map +1 -1
  36. package/src/lib/app-utils/plugin-release-flags.generated.d.ts +1 -1
  37. package/src/lib/app-utils/plugin-release-flags.generated.js +7 -0
  38. package/src/lib/app-utils/plugin-release-flags.generated.js.map +1 -1
  39. package/src/lib/app-utils/realm-host-surface.generated.js +3 -0
  40. package/src/lib/app-utils/realm-host-surface.generated.js.map +1 -1
  41. package/src/lib/app-utils/release-flags.js +6 -2
  42. package/src/lib/app-utils/release-flags.js.map +1 -1
  43. package/src/lib/app-utils/scope-tokens.d.ts +16 -1
  44. package/src/lib/app-utils/scope-tokens.js +15 -1
  45. package/src/lib/app-utils/scope-tokens.js.map +1 -1
  46. package/src/lib/app-utils/site-journey.d.ts +143 -0
  47. package/src/lib/app-utils/site-journey.js +282 -0
  48. package/src/lib/app-utils/site-journey.js.map +1 -0
  49. package/src/lib/app-utils/site-list-query.d.ts +47 -0
  50. package/src/lib/app-utils/site-list-query.js +142 -0
  51. package/src/lib/app-utils/site-list-query.js.map +1 -0
  52. package/src/lib/app-utils/site-wide-outbox.d.ts +95 -0
  53. package/src/lib/app-utils/site-wide-outbox.js +117 -0
  54. package/src/lib/app-utils/site-wide-outbox.js.map +1 -0
  55. package/src/lib/app-utils/transfer-launcher-context.d.ts +6 -0
  56. package/src/lib/app-utils/transfer-launcher-context.js.map +1 -1
  57. package/src/lib/app-utils/upload-inspection.js +7 -0
  58. package/src/lib/app-utils/upload-inspection.js.map +1 -1
  59. package/src/lib/app-utils/webhook-delivery.js +4 -1
  60. package/src/lib/app-utils/webhook-delivery.js.map +1 -1
  61. package/src/lib/foundation/definitions/org-billing.types.d.ts +24 -0
  62. package/src/lib/foundation/definitions/org-billing.types.js.map +1 -1
  63. package/src/lib/foundation/definitions/organization.types.d.ts +18 -7
  64. package/src/lib/foundation/definitions/organization.types.js.map +1 -1
  65. package/src/lib/foundation/definitions/write-deny-coverage.util.d.ts +4 -1
  66. package/src/lib/foundation/definitions/write-deny-coverage.util.js +12 -2
  67. package/src/lib/foundation/definitions/write-deny-coverage.util.js.map +1 -1
  68. package/src/lib/plugin-manager/enabled-plugins.js +4 -2
  69. package/src/lib/plugin-manager/enabled-plugins.js.map +1 -1
  70. package/src/lib/plugin-manager/feature-plugins.d.ts +173 -0
  71. package/src/lib/plugin-manager/feature-plugins.js +62 -1
  72. package/src/lib/plugin-manager/feature-plugins.js.map +1 -1
  73. package/src/lib/plugin-manager/first-party-plugins.generated.js +251 -2
  74. package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
  75. package/src/lib/plugin-manager/plugin-ai-capabilities.d.ts +192 -0
  76. package/src/lib/plugin-manager/plugin-ai-capabilities.js +157 -0
  77. package/src/lib/plugin-manager/plugin-ai-capabilities.js.map +1 -0
  78. package/src/lib/plugin-manager/plugin-checkout-extras.d.ts +168 -0
  79. package/src/lib/plugin-manager/plugin-checkout-extras.js +172 -0
  80. package/src/lib/plugin-manager/plugin-checkout-extras.js.map +1 -0
  81. package/src/lib/plugin-manager/plugin-contributions.d.ts +7 -0
  82. package/src/lib/plugin-manager/plugin-contributions.js +1 -1
  83. package/src/lib/plugin-manager/plugin-contributions.js.map +1 -1
  84. package/src/lib/plugin-manager/plugin-domain-events.d.ts +138 -0
  85. package/src/lib/plugin-manager/plugin-domain-events.js +148 -0
  86. package/src/lib/plugin-manager/plugin-domain-events.js.map +1 -0
  87. package/src/lib/plugin-manager/plugin-events.d.ts +51 -0
  88. package/src/lib/plugin-manager/plugin-events.js +4 -0
  89. package/src/lib/plugin-manager/plugin-events.js.map +1 -1
  90. package/src/lib/plugin-manager/plugin-fulfillment-providers.d.ts +101 -0
  91. package/src/lib/plugin-manager/plugin-fulfillment-providers.js +83 -0
  92. package/src/lib/plugin-manager/plugin-fulfillment-providers.js.map +1 -0
  93. package/src/lib/plugin-manager/plugin-permissions.js +21 -5
  94. package/src/lib/plugin-manager/plugin-permissions.js.map +1 -1
  95. package/src/lib/plugin-manager/plugin-person-records.d.ts +90 -0
  96. package/src/lib/plugin-manager/plugin-person-records.js +26 -0
  97. package/src/lib/plugin-manager/plugin-person-records.js.map +1 -1
  98. package/src/lib/plugin-manager/plugin-product-catalog.d.ts +204 -0
  99. package/src/lib/plugin-manager/plugin-product-catalog.js +43 -0
  100. package/src/lib/plugin-manager/plugin-product-catalog.js.map +1 -0
  101. package/src/lib/plugin-manager/plugin-shipment-records.d.ts +210 -0
  102. package/src/lib/plugin-manager/plugin-shipment-records.js +63 -0
  103. package/src/lib/plugin-manager/plugin-shipment-records.js.map +1 -0
  104. package/src/lib/plugin-manager/plugin-shipping-rates.d.ts +151 -0
  105. package/src/lib/plugin-manager/plugin-shipping-rates.js +62 -0
  106. package/src/lib/plugin-manager/plugin-shipping-rates.js.map +1 -0
  107. package/src/lib/plugin-manager/plugin-sms-messaging.d.ts +103 -0
  108. package/src/lib/plugin-manager/plugin-sms-messaging.js +39 -0
  109. package/src/lib/plugin-manager/plugin-sms-messaging.js.map +1 -0
  110. package/src/lib/plugin-manager/plugin-stock-levels.d.ts +81 -0
  111. package/src/lib/plugin-manager/plugin-stock-levels.js +32 -0
  112. package/src/lib/plugin-manager/plugin-stock-levels.js.map +1 -0
  113. package/src/lib/plugin-manager/plugin-tax-profile.d.ts +154 -0
  114. package/src/lib/plugin-manager/plugin-tax-profile.js +56 -0
  115. package/src/lib/plugin-manager/plugin-tax-profile.js.map +1 -1
  116. package/src/lib/plugin-manager/plugin-theme-font-catalog.d.ts +59 -0
  117. package/src/lib/plugin-manager/plugin-theme-font-catalog.js +40 -0
  118. package/src/lib/plugin-manager/plugin-theme-font-catalog.js.map +1 -0
  119. package/src/lib/plugin-manager/plugin-tracking-pages.d.ts +55 -0
  120. package/src/lib/plugin-manager/plugin-tracking-pages.js +76 -0
  121. package/src/lib/plugin-manager/plugin-tracking-pages.js.map +1 -0
  122. package/src/lib/plugin-manager/realm-host-aglyn.generated.js +3 -0
  123. package/src/lib/plugin-manager/realm-host-aglyn.generated.js.map +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/feature-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 * Feature-plugin pattern (AGL-277, AGL-395). Each feature ships as one lib\n * under `libs/plugins/{feature}` (moved out of the old `.../ui/` nesting)\n * that owns both halves and never merges into `plugins-mui` (which stays\n * pure component/theme definitions):\n *\n * - UI half → besigner/host components. Builds its bundle with\n * `defineUiFeatureBundle` (which depends on the mui bundle so\n * primitives/theming resolve first) and registers it with\n * `Aglyn.plugins.addDependency`, exactly like the mui bundle itself.\n * Registered per-editor via `register{Feature}Plugin()`.\n * - Console half → a `ConsoleExtension` registered with\n * `registerConsoleExtension` via a separate `register{Feature}Console()`\n * entry point (so app-load registration pulls no canvas code). The\n * console shell renders nav items + their pages, dashboard cards, and\n * settings sections from the registry, gated by the feature flag.\n *\n * This module is pure (no registry singletons) per app-utils layering;\n * the plugin libs close the loop by passing `Aglyn.components` in.\n * Reference implementation: events-calendar (AGL-313/394); commerce and\n * email follow the same shape (AGL-290/346, relocated in AGL-395).\n */\n\nimport { runInAction } from 'mobx'\nimport type { OrgPermissions } from '../app-utils/org-permissions'\nimport type { SeoAuditReport } from '../app-utils/seo-audit'\nimport type { SeoListingFieldKey } from '../app-utils/seo-listing-fields'\nimport type { AglynOrgBilling, OrgFeatureFlags } from '../foundation'\nimport type {\n ComponentSchema,\n MdiIconProps,\n PresetSchema,\n} from '../types/nodes'\nimport type { Plugin, PluginId } from './plugin-manager'\nimport type { HostThemeSource } from '../app-utils/site-theme'\nimport type { HostTheme, HostThemeScheme } from '@aglyn/shared-data-types'\nimport type { ComponentType } from 'react'\nexport type {\n ConsoleImportMappingZoneProps,\n ConsoleRecordEmailZoneProps,\n ConsoleRecordInsightsZoneProps,\n} from './record-zone-props'\n\n/** The mui bundle id every UI feature bundle depends on. */\nexport const MUI_BUNDLE_ID: PluginId = 'mui'\n\nexport interface FeatureBundleEntry {\n component: any\n schema: ComponentSchema<any>\n presets?: PresetSchema[]\n}\n\n/** The slice of ComponentManager a feature bundle needs (structural). */\nexport interface ComponentRegistrar {\n registerComponent(component: any, schema: ComponentSchema<any>): void\n registerPreset(presets: PresetSchema[]): void\n unregisterComponent(componentId: string): void\n unregisterPreset(presetIds: string[]): void\n}\n\nexport interface UiFeatureBundleOptions {\n /** Stable bundle id — persisted as `pluginId` in screen docs; never rename. */\n bundleId: PluginId\n displayName: string\n description?: string\n icon?: MdiIconProps\n /** Extra bundle ids this feature needs beyond mui. */\n dependsOn?: PluginId[]\n components: FeatureBundleEntry[]\n}\n\n/**\n * UI half of the pattern: a plugin-registry bundle whose load/destroy\n * register the feature's components + presets against the given\n * registrar (`Aglyn.components` in apps), declared as depending on the\n * mui bundle so the registry loads mui first.\n */\nexport function defineUiFeatureBundle(\n options: UiFeatureBundleOptions,\n registrar: ComponentRegistrar,\n): Plugin {\n const dependencies: Record<PluginId, true> = { [MUI_BUNDLE_ID]: true }\n for (const id of options.dependsOn ?? []) dependencies[id] = true\n return {\n $id: options.bundleId,\n displayName: options.displayName,\n title: options.displayName,\n description: options.description,\n icon: options.icon,\n dependencies,\n load(): void {\n // One mobx transaction per bundle (AGL-371): observers (component\n // drawer, canvas) re-render once instead of once per registration.\n runInAction(() => {\n for (const entry of options.components) {\n registrar.registerComponent(entry.component, entry.schema)\n }\n for (const entry of options.components) {\n if (entry.presets?.length) registrar.registerPreset(entry.presets)\n }\n })\n },\n destroy(): void {\n runInAction(() => {\n for (const entry of options.components) {\n if (entry.presets?.length) {\n registrar.unregisterPreset(\n entry.presets.map((preset) => preset.$id),\n )\n }\n }\n for (const entry of options.components) {\n registrar.unregisterComponent(entry.schema.$id)\n }\n })\n },\n }\n}\n\n/**\n * One of the organization's sites, as the shell hands them to a surface\n * mounted at the ORGANIZATION level (AGL-2630).\n *\n * The three facts a cross-site surface needs and a record never carries: a\n * record holds a host DOCUMENT ID, a console URL under `/hosts/[host]` takes\n * the SUBDOMAIN, and a person reads the NAME. `subdomain` is null for a site\n * whose document did not answer — such a site is still named (the\n * relationship is real) and never linked (the route would not exist).\n */\nexport interface ConsolePluginOrgHost {\n id: string\n name: string\n subdomain: string | null\n}\n\n/**\n * The organization a surface is mounted under when it is mounted at the\n * org level rather than under a site (AGL-2630).\n *\n * The CRM exists at two levels: the site hub, where `hostId` names the site\n * and every read is scoped to it, and `/[orgSlug]/crm`, where an org-wide\n * member sees every site's records at once. At the org level there is no\n * site, so the shell hands the org's own site list instead — the pickers a\n * create needs (a record is always captured BY a site) and the names a\n * cross-site fact is shown under. `hostsReady` separates \"no sites\" from\n * \"not yet\".\n */\nexport interface ConsolePluginOrgMount {\n orgId: string\n /**\n * The organization's URL slug (AGL-3080) — the segment its console pages\n * hang under, and what a surface needs to link to one of its siblings.\n *\n * Carried because plugins were reconstructing it: from `hostsPath` by\n * splitting a string, or by resolving a site's org through an async lookup\n * that can come back empty and leave a link unbuilt (AGL-867). The shell\n * has it synchronously; every plugin paying for it again, differently, is\n * the cost of not handing it over.\n */\n orgSlug: string\n hosts: readonly ConsolePluginOrgHost[]\n hostsReady: boolean\n /**\n * The console path every site's own hub hangs beneath — `/[orgSlug]/hosts`\n * — so a cross-site fact can link a person into the site that holds them:\n * a site's CRM is `${hostsPath}/${subdomain}/crm`. A path rather than a\n * builder because the mount is data the shell hands over and a plugin\n * cannot import the console's route table.\n */\n hostsPath: string\n /**\n * Where this organization manages its PLAN — the console's billing page\n * for the org (AGL-3080).\n *\n * Here for the same reason `hostsPath` is: a plugin cannot import the\n * console's route table, and a surface that has to say \"on a paid plan the\n * cut is lower\" is useless without somewhere to send the person who just\n * read it. The shell's own upgrade notice covers an ENTITLEMENT refusal,\n * where the surface never renders; this covers the case the surface renders\n * fine and the plan is still the answer — a marketplace publisher seeing\n * the free-plan fee on a listing they are about to price.\n *\n * OPTIONAL, and a plugin must branch on it rather than assume it: a\n * deployment that bills nobody has no such page, and a self-hoster who\n * removed it should not get a plugin's link into a 404. The shell's own\n * upgrade notice makes the same allowance. `resolveOrgMount` supplies it\n * for every mount this console builds.\n *\n * A plugin renders a link to it or does not, and never parses it.\n */\n billingPath?: string\n}\n\n/**\n * Props every plugin-contributed console page receives from the shell's\n * generic host route. The shell owns auth + chrome + flag gating and\n * passes the resolved host and entitlement state in, so plugin pages stay\n * free of console-app hooks.\n */\nexport interface ConsolePluginPageProps {\n /**\n * The site this surface is mounted under — or `null` when it is mounted at\n * the ORGANIZATION level, where there is no site and {@link orgMount} says\n * which org (AGL-2630). Two things mount there: the CRM's own org route,\n * and every {@link ConsoleExtension.orgNavItems} surface through the\n * generic org route (AGL-2974). A surface reached through a site route\n * always receives a string.\n */\n hostId: string | null\n /** Present only at an org-level mount — see {@link ConsolePluginOrgMount}. */\n orgMount?: ConsolePluginOrgMount\n /**\n * Every workspace the SIGNED-IN PERSON belongs to, as the org switcher\n * already names them (AGL-3080) — not the mounted org's siblings, and\n * nothing about what any of them contain.\n *\n * For a surface whose subject crosses workspaces. The marketplace's\n * licences panel is the case: a purchase licenses one organization, so\n * \"I bought this once — which workspace did the licence land in?\" is a\n * question about the BUYER, and the answer is a name the reader already\n * sees in the switcher. Resolving it from ids would be a read per row of\n * documents the shell is already holding.\n *\n * Absent when the shell has not resolved them, and a surface must name the\n * id rather than wait: this is a label, and a row with a raw id in it is\n * worse than nothing only if the row does not appear at all.\n */\n viewerOrgs?: readonly { id: string; name: string }[]\n /** True when the org holds the extension's `featureFlag` entitlement. */\n entitled: boolean\n /**\n * The absolute console path this surface is mounted at — the nav item's\n * `href` under the active org and site, e.g. `/acme/hosts/shop/products`\n * (AGL-2501).\n *\n * A plugin page is handed a host DOC ID and nothing else, so building a\n * link to itself meant resolving the org slug and subdomain from Firestore\n * — two `getDoc`s that answer `null` on first paint, which for a section\n * rail means drawing it without hrefs. The shell already knows this string\n * synchronously; the alternative is paying for it again, later, per page.\n */\n basePath?: string\n /**\n * The nav item's declared {@link ConsoleNavItem.sections}, resolved: an\n * absolute `href` per section, and the release-flag verdict already applied\n * to `visible` (AGL-2501).\n *\n * The plugin DECLARES sections; the shell RESOLVES them. Release flags live\n * in `scope:app` and a `scope:lib` plugin may not import the hooks that read\n * them, so a page that filtered its own rail could only do it by guessing —\n * and a rail offering a link into the shell's own \"coming soon\" notice is\n * the guess going wrong. Feed this straight to `HubSections`.\n */\n sections?: readonly ResolvedConsoleNavSection[]\n /**\n * The id of the section the URL names, or undefined on the nav item's own\n * href (AGL-2501).\n *\n * Always one of the declared `sections` — the shell 404s an id it does not\n * recognize rather than passing it down, so a page may switch on this\n * without a fallback branch for a section it does not have.\n */\n section?: string\n /**\n * Path segments beneath `basePath`, `[]` on the nav item's own href\n * (AGL-2501). `segments[0]` is `section`; anything after it is the section's\n * own, so a section can own deeper routes (`…/orders/ord_123`) without a\n * further registry change.\n */\n segments?: readonly string[]\n /**\n * The ORG billing doc (`orgs/{orgId}`) the shell already loaded to\n * compute `entitled` (prop renamed from `tenant` in AGL-444). Passed\n * through so a plugin page can run its own `checkEntitlement`/\n * `checkQuota` (e.g. per-plan service limits) without reaching for the\n * console-app org/session hooks.\n */\n org?: Partial<AglynOrgBilling>\n /**\n * The signed-in user's resolved org permissions (AGL-395), passed through\n * so a plugin page can gate actions (e.g. install/publish) without the\n * console-app session/permission hooks.\n */\n /**\n * Widened past the legacy six (AGL-2474): plugin-declared keys such as\n * commerce's `managePos` are resolved into the same map, and typing this\n * `Partial<OrgPermissions>` meant a plugin could not read its OWN\n * permission without a cast — the declared key was not assignable.\n */\n permissions?: Partial<OrgPermissions> & Record<string, boolean | undefined>\n /**\n * The verdict for the release flag that governs this surface (AGL-1662),\n * resolved by the shell from the nav item's `navTabId` — the same flag\n * `FeatureGate` applies around the page body.\n *\n * `FeatureGate` admits staff with the flag OFF, so a plugin page can be\n * looking at an org that does not have the feature and is not being\n * billed for it. Anything the page says about MONEY has to follow the\n * flag rather than the viewer, and this is how a plugin page gets that\n * answer without reaching for the console-app release-flag hooks (which\n * live in `scope:app` and are off-limits to a `scope:lib` plugin).\n */\n releaseFlag?: {\n /**\n * The rollout verdict for this ORG — staff bypass deliberately NOT\n * applied. `visible` is what decides who sees a page; this is what\n * decides what the invoice carries, and staff opening a page must not\n * put a line on a customer's bill.\n */\n released: boolean\n /**\n * True once the flag verdict has settled. Release flags are default-off\n * before Remote Config activation, so an ungated claim asserts the\n * withheld case for one paint on an org that may well be billed.\n */\n ready: boolean\n }\n /**\n * What the viewer's role on THIS SITE lets them do, for a surface that\n * publishes.\n *\n * The `author` host role edits content and may not make it live; that is\n * enforced in the Firestore rules and by the promotion routes, and the\n * console's job is to say no with a reason rather than let a click come\n * back as a bare `permission-denied`. Resolving it needs the org member\n * document and the host-access predicate over it, which the shell already\n * reaches for — a plugin cannot, for the same reason it cannot read a\n * release flag.\n *\n * `loaded` separates \"no\" from \"not yet\", so a surface disables with a\n * reason rather than hiding a control that is about to be allowed. Read it\n * the safe way round: `canPublish` is false until the read lands.\n */\n hostRole?: {\n canPublish: boolean\n loaded: boolean\n }\n}\n\nexport type ConsolePluginPage = ComponentType<ConsolePluginPageProps>\n\n/**\n * One routed section of a plugin console page (AGL-2501).\n *\n * A section is a real URL beneath the nav item's `href`, not a panel: it is\n * linkable, the back button walks sections, and the page mounts the one being\n * read. That last part is the reason this exists — a six-panel hub subscribes\n * every panel's queries on load, and the reader is looking at one.\n */\nexport interface ConsoleNavSection {\n /**\n * URL segment beneath the nav item's `href`, and the id the shell hands the\n * page as `section`. Appears in links people keep — treat it as persisted.\n */\n id: string\n label: string\n /**\n * Release-flag nav-tab id gating THIS section, when it ships on a different\n * schedule than the surface around it. Omit to inherit the nav item's gate,\n * which is the common case.\n *\n * Declaring one NARROWS, never widens: the nav item's own gate is applied\n * outside this one, so a section of a flagged-off surface stays unreachable\n * whatever it declares. A section gated by its own flag is refused on a deep\n * link exactly as it is hidden from the rail — one verdict, both places.\n */\n navTabId?: string\n /**\n * Entitlement flag gating THIS section, when the org's plan may include\n * the surface and not the whole of it (AGL-2611). Omit to inherit the\n * extension's `featureFlag`, which is the common case.\n *\n * Composes by AND with the extension's, the way `navTabId` composes with\n * the nav item's release gate: the shell answers the extension's flag\n * first and this one inside it, so a section can only ever be NARROWER\n * than the surface holding it. A section this refuses is resolved\n * `locked` for the rail and refused on a deep link with the shell's own\n * upgrade notice — one verdict, both places — and the page body never\n * mounts, which is the whole of the shell's promise about entitlements.\n *\n * The case it exists for is a hub whose first section ships on every\n * plan and whose others do not: the CRM's contacts list is on Free, and\n * the sales suite built on that list starts at Starter.\n */\n featureFlag?: keyof OrgFeatureFlags\n /**\n * Permission key gating THIS section, when the surface is open to every\n * member and part of it is not (AGL-3080). Omit to inherit the\n * extension's and the nav item's, which is the common case.\n *\n * The third gate, composed the way the other two are: ANDed with what the\n * extension and the nav item already require, so a section can only ever\n * be narrower than the surface holding it. A section this refuses is not\n * drawn in the rail at all — unlike a `featureFlag` refusal, which draws\n * locked and links to the notice that sells it, because a permission is\n * not something the reader can buy — and a deep link to it is answered\n * with the shell's refusal instead of its body. One verdict, both places.\n *\n * ⚠️ NOT a replacement for the server's rule. The rules and the plugin's\n * own handlers enforce this regardless of what renders; this keeps a\n * reader from being offered a page that is about to refuse them.\n *\n * The case it exists for is a hub most of a workspace uses and whose\n * seller half only a publisher does: the Marketplace's browse, installed\n * and licences sections are every member's, and listings, upload, sales\n * and payouts read the organization's revenue.\n */\n permission?: string\n /**\n * Query keys that land a BARE hub URL on this section instead of on the\n * first one the reader may open (AGL-3080).\n *\n * The case it exists for is a return URL held by somebody else. Stripe\n * bakes `?connect=` into account-onboarding links and `?purchase=` into\n * checkout sessions, so a seller part-way through onboarding is carrying\n * one right now — in a third party's records, not ours, and unfixable from\n * this side once it lands somewhere that means nothing to them. A seller\n * coming back from Connect wants Payouts; a buyer coming back from\n * checkout wants what they now own.\n *\n * The key's VALUE is not read, only its presence: these are markers, and a\n * marker nothing routes on still survives the hop, which is what makes it\n * safe for anyone to add one. The gates still apply — a section this\n * claims but the reader may not open is not landed on, and the bare rule\n * takes over.\n */\n landsOnQuery?: readonly string[]\n}\n\n/** A {@link ConsoleNavSection} with the shell's answers filled in. */\nexport interface ResolvedConsoleNavSection {\n id: string\n label: string\n /** Absolute console path — `${basePath}/${id}`. */\n href: string\n /** False when this section's release flag hides it from this viewer. */\n visible: boolean\n /**\n * True when the org's SETTLED plan does not carry the section's\n * `featureFlag` (AGL-2611). The rail draws it locked and still links it —\n * the notice behind the link is the way to buy it — and the shell refuses\n * the body. Never true while the org read is pending: an unsettled plan\n * is not a refusal, and a lock that appeared for one paint on a paying\n * workspace would be the AGL-1380 defect in a new place.\n */\n locked?: boolean\n /**\n * True when this section's own `permission` refuses this reader\n * (AGL-3080) — the reason it is not `visible`, kept apart from the\n * release flag's so a deep link is answered with the refusal that\n * applies rather than with \"coming soon\". Never true while the member\n * read is pending: the permission map answers as an admin's until it\n * lands, so an unsettled read is neither a grant nor a refusal.\n */\n refused?: boolean\n /** The section's declared {@link ConsoleNavSection.landsOnQuery}, carried\n * through so the shell's landing rule can read it (AGL-3080). */\n landsOnQuery?: readonly string[]\n}\n\nexport interface ConsoleNavItem {\n label: string\n /**\n * Host-relative console route (e.g. '/events'). The shell mounts it\n * under the active host ('/[hostId]/events') via its generic plugin\n * route, so the same string keys both the nav link and the page.\n */\n href: string\n icon?: MdiIconProps\n /**\n * Release-flag nav-tab id (e.g. 'nav-tab-events'). Lets the shell apply\n * the same staff-preview gating hardcoded tabs get; omit for always-on.\n */\n navTabId?: string\n /**\n * The permission THIS surface requires, when it is narrower than the\n * extension's own {@link ConsoleExtension.permission}.\n *\n * Declaring one NARROWS, never widens: the extension's requirement is\n * applied alongside this one and both must be held, so a surface cannot\n * escape its extension's gate by naming a key its reader happens to have.\n * The composition is the release-flag one a nav item and its section\n * already have, for the same reason.\n *\n * The granularity exists because one extension can register surfaces with\n * genuinely different answers — a catalog anyone who edits the site may\n * open, beside a register that takes money.\n */\n permission?: string\n /**\n * Page body rendered by the shell's generic host route. When present,\n * the plugin owns the whole surface — no core page file needed.\n */\n Component?: ConsolePluginPage\n /**\n * Routed sections of this page (AGL-2501). Each becomes a URL at\n * `${href}/${section.id}`, and the shell tells the page which one it is on.\n *\n * Optional, and omitting it is not a lesser option — it means the surface is\n * ONE page, which is what every plugin surface was before this existed and\n * what most should stay. A nav item without sections resolves exactly as it\n * always has: its own href and nothing beneath it, so a path under it is a\n * 404 rather than this page rendered again.\n */\n sections?: readonly ConsoleNavSection[]\n /**\n * Whether this nav item claims every path beneath its own href, with no\n * declared section naming them.\n *\n * The case is a surface whose deeper URLs are ENTITIES rather than\n * sections: `/forms` is a list, `/forms/{formId}` is one of its rows, and\n * the set of ids is a property of the workspace's data, so no static\n * `sections` list could enumerate them. Without this a nav item matches its\n * own href and nothing else, and every row's page is a 404.\n *\n * The trade is deliberate and is why it must be asked for. A surface that\n * owns its subtree can no longer distinguish a typo'd path from an entity\n * id — `/forms/bogus` reaches the page rather than the shell's 404 — so it\n * takes on the duty of saying \"no such thing\" itself, which a list-detail\n * surface has to be able to do anyway for an id that was deleted while a\n * link to it was still in someone's inbox.\n *\n * Sections win where both are declared: an id the `sections` list names is\n * resolved as a section, and this only widens what happens when none\n * matches.\n */\n ownsSubtree?: boolean\n /**\n * The browser tab's noun on a RECORD beneath a surface that\n * {@link ownsSubtree} (AGL-3596), where the surface's `label` would stand\n * otherwise: `AI jobs` lists a site's jobs at `/ai-jobs`, and one job's\n * page at `/ai-jobs/{jobId}` is `Building your site`.\n *\n * The tab title is built on the server from the URL alone, which never\n * reads the record, so this is one fixed string per surface — the noun for\n * what a record's page is, not the record's name. It is read from the\n * plugin's source by `tools/scripts/generate-plugin-manifests.mjs` into the\n * titles manifest, and so must be a string literal beside a literal `href`.\n */\n recordTitle?: string\n /**\n * A page with an address and no tab (AGL-3594): served at its `href` like\n * any nav item, and left off the site's tab strip. For a surface a person\n * is SENT to — the page a flow lands on, the page a notification opens —\n * rather than one they browse to; the gates and the matching are the same.\n */\n unlisted?: boolean\n /**\n * Hrefs this nav item answered to before it moved (AGL-2595).\n *\n * A console path is something people keep — a bookmark, a docs link, an\n * email from the console itself — and a nav item that changes its `href`\n * would otherwise turn every one of them into the shell's \"not available\"\n * notice. Matching here is identical to matching on `href` (the same\n * sections, the same subtree rule), and the resolved page carries\n * `legacy: true` so the shell can replace the address with the current one\n * rather than leave a moved page living at two.\n */\n legacyHrefs?: readonly string[]\n /**\n * Where this item's tab sits among the plugin tabs of its strip\n * (AGL-3294): lower first, absent is 0, and a tie keeps registration\n * order.\n *\n * Registration order is otherwise the order, and it is not a tab's to\n * choose: it follows the plugin registry, which also orders the staff\n * strip, the providers and every widget zone the plugin fills — so moving\n * one tab by moving its plugin would move everything else it registers.\n * This moves the tab and nothing else. The shell's own tabs are not in the\n * comparison; the plugin tabs sit as one block between them.\n */\n tabOrder?: number\n /**\n * Dashboard header for the plugin page (title + icon), and the docs topic\n * its help `?` explains.\n *\n * `docsTopic` is a plain string rather than the console's\n * `DocsHelpTopicKey` because that registry lives in `apps/console` and a\n * lib cannot import from an app. The console validates it and falls back\n * to the marketplace topic when it does not resolve — which is not just\n * defensive: a third-party plugin can name any string, and the alternative\n * to a fallback is a help button that throws on hover (AGL-1074).\n *\n * Every surface mounted by the shell's generic plugin route shares one\n * `help=` prop, so a surface that omits this is not \"help-less\" — it\n * inherits Plugins & Marketplace, which reads as if it were its own.\n *\n * `docsAnchor` deep-links the help to one heading of that topic's page\n * (`#at-the-organization-level`), for a surface whose page explains this\n * mount under a heading of its own. It is validated the same way: an\n * anchor the topic's page does not carry is dropped, and the help opens the\n * top of the page.\n */\n header?: {\n title: string\n icon?: MdiIconProps\n docsTopic?: string\n docsAnchor?: string\n }\n}\n\nexport interface ConsoleDashboardCard {\n /** Card registry key the dashboard resolves to a component. */\n cardId: string\n title: string\n}\n\nexport interface ConsoleSettingsSection {\n sectionId: string\n title: string\n /** Rendered inside the org/host settings surface when present (AGL-419). */\n Component?: ComponentType<ConsolePluginPageProps>\n}\n\n/**\n * The injection-zone catalog (AGL-433, Strapi injection-zone parity):\n * every named slot the console shell renders through `PluginWidgetSlot`,\n * with what the slot receives. `slot` stays an open string so apps can\n * add custom zones without a core release; these are the guaranteed ones.\n */\nexport const CONSOLE_WIDGET_SLOTS = {\n /** Host dashboard + screen view activity column. Props: hostId. */\n hostActivity: 'hostActivity',\n /**\n * The host dashboard's glance row — one card per capability the site\n * actually has. Props: hostId.\n *\n * The dashboard's own cards used to be imported by the page, which made\n * enablement a decision nobody was making: `New site users` rendered on a\n * site that has never turned member accounts on, and `Last campaign` on a\n * workspace with the email plugin switched off. A card that answers a\n * question about a capability belongs to the capability, so it registers\n * here and the shell's entitlement + enablement gate decides.\n */\n hostDashboard: 'hostDashboard',\n /**\n * The organization's sites page, above the site grid (AGL-2636): the\n * org-level twin of `hostDashboard`, for a card that totals the\n * organization rather than one site. Props: `hostId` (always `null`),\n * `orgMount` (the org and its sites — the {@link ConsolePluginOrgMount}\n * the org-level hub page hands its plugin page), `basePath` (that hub's\n * own path, `/[orgSlug]/crm`, for the links a widget builds when there is\n * no site to derive them from).\n *\n * Every widget here reads ACROSS the host boundary — an org-wide total\n * carries no scope clause — which is the one read a site collaborator may\n * never make, so the page gates the whole row on the org hub's own access\n * verdict before any widget mounts, and renders nothing at all (no empty\n * row, no heading) when no widget survives the gates.\n */\n orgDashboard: 'orgDashboard',\n /** Host dashboard commerce summary. Props: hostId, org. */\n commerceGlance: 'commerceGlance',\n /** Org Data page body. Props: orgId, org. */\n orgData: 'orgData',\n /** Besigner functions (ƒx) panel. Props: hostId. */\n besignerFunctions: 'besignerFunctions',\n /**\n * Wherever a console page offers to publish something it holds (AGL-3080).\n * Props: {@link ConsoleArtifactPublishZoneProps}.\n *\n * A layouts page, a components page and the org publish panel each held a\n * dialog that posted to `marketplace/publish-layout` and read the\n * marketplace's price floor. The console knows it has a layout; where a\n * layout can be PUBLISHED, what a listing costs at the least, and what to\n * call the thing in the copy all belong to whatever sells it.\n *\n * ── The page says what it has, never where it goes ───────────────────────\n *\n * The zone is handed a {@link ConsolePublishableArtifact} in the console's\n * own vocabulary — a kind, the site or org it belongs to, the document id —\n * and the widget decides the endpoint, the noun and the form. A kind the\n * widget does not publish is one it draws nothing for, which is the same\n * answer as no widget at all.\n *\n * ── An offer with nowhere to go must not be made ─────────────────────────\n *\n * The control that OPENS this — a menu item, a button — belongs to the page\n * and is not a widget, because it is one entry of a list the page builds.\n * So a page offering it asks `useSlotWidgets` whether this zone has a\n * widget at all and leaves the entry out when it does not. Drawing it\n * anyway would be a menu item that opens nothing on a workspace with no\n * marketplace.\n */\n hostArtifactPublish: 'hostArtifactPublish',\n /*\n * `marketplaceListing`, `marketplaceCapability`, `orgMarketplace` and\n * `orgAddons` were here until AGL-3080, and are gone rather than deprecated.\n *\n * Each existed for one reason: a CONSOLE ROUTE had to show marketplace UI\n * and an app may not import a plugin. AGL-3080 moved those routes into the\n * marketplace plugin, where the components are plain imports — so the four\n * zones had no drawer left, and a zone nothing draws is a contract nothing\n * can be held to. `plugin-widget-slot-zones.spec.ts` is what noticed, which\n * is the whole reason that inventory exists.\n *\n * `pluginSiteSet` and `hostArtifactPublish` stayed, and the difference is\n * the test: both are drawn by a console page that is NOT the marketplace —\n * the installation detail page and a site's layouts list — offering a\n * marketplace action in passing.\n */\n /**\n * The org Plugins page, above the built-in plugins (AGL-3080): the code a\n * plugin has INSTALLED into this workspace, one row per installation.\n * Props: {@link ConsoleOrgPluginInstallsZoneProps}.\n *\n * The Plugins page is the workspace's inventory — what it runs, wherever it\n * came from. The built-in half is the shell's own: the switchboard catalog\n * says what ships. The installed half is not: where an installation is\n * pinned, what version it runs, whether a newer one may be installed and\n * whether its publisher's kill switch is thrown are all facts held by the\n * plugin that installed it, in collections that are its own. So the page\n * hands over the sites it can see and the plugin draws its installations.\n *\n * Every row links to `/[orgSlug]/plugins/[pluginRef]`, the shell's\n * installation page, keyed by whatever id the installing plugin pins by —\n * that page is the one place an installation is managed, whoever drew the\n * row.\n */\n orgPluginInstalls: 'orgPluginInstalls',\n /**\n * The installation page of a plugin some plugin INSTALLED, above where it\n * runs (AGL-3080): what the installer says about the version this\n * workspace runs. Props: {@link ConsolePluginInstallStatusZoneProps}.\n *\n * Drawn only for an installation that exists — a first-party plugin has no\n * version to be behind, and a page for code installed nowhere says so\n * itself. A widget here reports; it never installs. Applying an update is\n * the installing plugin's own surface, which the widget may link to.\n */\n pluginInstallStatus: 'pluginInstallStatus',\n /**\n * The template gallery — \"Start from a template\" on a site's Screens,\n * Layouts and Components pages — below the site's own templates and the\n * starters (AGL-3080): a shelf of templates a plugin offers to INSTALL.\n * Props: {@link ConsoleTemplateGalleryZoneProps}.\n *\n * The gallery is the shell's: what the site holds and what ships with the\n * platform. Templates offered from elsewhere are the offering plugin's —\n * what it lists, how it is searched, what it costs and the route that\n * installs one, with every check that route makes — so the dialog hands\n * over the kind it picks and the word typed in its search, and the plugin\n * draws its own shelf. An install lands in the site's library and\n * publishes nothing; the widget calls `onInstalled` so the gallery closes.\n *\n * The dialog's \"nothing matches\" line covers every shelf, so a widget\n * reports whether its shelf is loading, empty or showing something through\n * `reportShelf`. A widget that never reports is a shelf the line ignores;\n * with no widget at all the gallery is the site's own and the starters.\n */\n templateGallery: 'templateGallery',\n /**\n * One row of a site's Templates library whose template a plugin INSTALLED,\n * beside its Source badge (AGL-3080): what the installer says about the\n * copy the site holds — that a newer version can be installed, and the\n * control that installs it. Props: {@link ConsoleTemplateInstallStatusZoneProps}.\n *\n * Drawn once per such row, and never for a template saved here or a\n * starter. A widget draws nothing for a template it did not install, and\n * nothing when there is nothing to say. Applying an update goes through\n * the installing plugin's own route, which keeps all its checks; the\n * library's rows re-read the templates it replaced.\n */\n templateInstallStatus: 'templateInstallStatus',\n /** Bottom of the host dashboard. Props: hostId, org. (AGL-433) */\n dashboardFooter: 'dashboardFooter',\n /** Org settings page, below the tabbed cards. Props: orgId, org. */\n orgSettings: 'orgSettings',\n /** Host setup page, below the built-in cards. Props: hostId, org. */\n hostSettings: 'hostSettings',\n /**\n * The top of the page a newly created site lands on (AGL-2918): where a\n * widget may offer to START the site for the person rather than leave them\n * an empty one. Props: {@link ConsoleHostFirstRunZoneProps}.\n *\n * ── The blank path is the default, not the fallback ──────────────────────\n *\n * The site already exists, blank, and the page beneath this zone is the\n * ordinary one every site gets. A widget here is an OFFER on top of that\n * page: it asks the person what they want, it builds only what they then\n * confirm, and everything it makes is a draft the site does not serve.\n *\n * So a zone with no widget — no plugin loaded, a feature not released, a\n * reader without the permission — is not a degraded state. It is the blank\n * path, unchanged and complete, which is why nothing on the page below\n * depends on anything here.\n *\n * ── A widget here MUST offer `startBlank` ────────────────────────────────\n *\n * A guided start that a person cannot leave turns creating a site into a\n * funnel, so `startBlank` is handed down rather than left to each widget to\n * invent: a widget draws it where its questions START, not at the end of\n * them, and taking it leaves the person on this same page with nothing\n * begun behind them. The shell remembers the choice for this site and stops\n * asking.\n *\n * A widget that takes the screen rather than sitting on the page — a full\n * screen dialog, an overlay — owes the same exit in every shape it has one:\n * a close control, and the key a person presses to dismiss it. Each of them\n * is `startBlank`, because a takeover somebody can only dismiss BACK INTO is\n * the funnel this zone exists to refuse, and nothing here is a half-answered\n * state worth returning to.\n */\n hostFirstRun: 'hostFirstRun',\n /**\n * The host setup Theme section, between the Theme picker and the editor\n * (AGL-2938). Props: {@link ConsoleHostThemeZoneProps} — the\n * site, the theme the editor shows, where that theme came from, the\n * editor's own preview, and `proposeDraft`, which puts a theme in the\n * editor as unsaved changes.\n *\n * A widget here proposes and never writes. The person saves what it\n * proposed through the editor's own Save — the guarded write that stores\n * an installed theme's edits as its override patch — or discards it. A\n * palette importer, a brand kit and a generator are the same shape of\n * widget.\n */\n hostTheme: 'hostTheme',\n /**\n * The staff overview, among its platform-wide cards (AGL-3080). No props:\n * the overview is about the platform, not one org, so a widget here reads\n * what its plugin holds across every workspace through its own staff\n * route. A staff zone — see {@link CONSOLE_STAFF_WIDGET_SLOTS}.\n */\n staffOverview: 'staffOverview',\n /**\n * Staff admin org detail (staff-only surfaces). Props: orgId. A staff\n * zone — see {@link CONSOLE_STAFF_WIDGET_SLOTS}.\n */\n adminOrgDetail: 'adminOrgDetail',\n /**\n * Billing → Usage, below the meters (AGL-2940). Props: `orgId`, `org` (the\n * billing-merged org doc), `canManage` (the reader holds\n * `billing.manage`). A card here explains or controls consumption the\n * meters above it show.\n */\n orgBillingUsage: 'orgBillingUsage',\n /**\n * Billing → Overview, among the plan and add-on cards (AGL-2940). Props:\n * `orgId`, `org`, `plan` (the page's own defaulted plan), `canManage`.\n */\n orgBillingOverview: 'orgBillingOverview',\n /**\n * The staff org page, among its cards (AGL-2940). Props: `orgId`. Staff\n * only — the page is behind `StaffOnly`, and a widget here may read the\n * staff-only routes. A staff zone — see {@link CONSOLE_STAFF_WIDGET_SLOTS}.\n */\n staffOrg: 'staffOrg',\n /**\n * The staff user page, below the account's activity. Props: `uid`. A\n * staff zone — see {@link CONSOLE_STAFF_WIDGET_SLOTS}.\n */\n staffUser: 'staffUser',\n /**\n * The staff site page, below its own cards (AGL-3379). Props: `hostId`,\n * `orgId` (the site's organization, `''` for none) and `host` (the site\n * document as the page read it, or `undefined` while it loads). What a\n * plugin holds for one site — its automations, its sends — is shown here,\n * by the plugin that owns it. A staff zone — see\n * {@link CONSOLE_STAFF_WIDGET_SLOTS}.\n */\n staffSite: 'staffSite',\n /**\n * A COLUMN of the staff Organizations list (AGL-2984) — see\n * {@link ConsoleWidget.column}. The list renders the widget's component\n * once per row with `{ row, orgId, orgIds }`: the row as the list route\n * serves it, that row's org id, and every org id on the page, so a column\n * that reads figures of its own asks once for the page rather than once\n * per row. Its `Header` receives `{ orgIds }` beside the sort props. A\n * staff zone — see {@link CONSOLE_STAFF_WIDGET_SLOTS}.\n */\n staffOrgsListColumn: 'staffOrgsListColumn',\n /**\n * The staff org usage table (AGL-2984): the monthly rollups on the staff\n * org page and in the Organizations list's usage dialog. A widget with a\n * `column` is a column of the table, between Forms and Cost, rendered once\n * per month with `{ month, orgId }` — `month` is the rollup row as\n * `/api/admin/org-usage` serves it. A widget without one renders above\n * the table with `{ orgId, org }`, where `org` is the org document when the\n * surface holds one and `undefined` when it does not. A staff zone — see\n * {@link CONSOLE_STAFF_WIDGET_SLOTS}.\n */\n staffOrgUsageColumn: 'staffOrgUsageColumn',\n /**\n * The org's team member detail page, below the member's activity\n * (AGL-2940). Props: `orgId`, `orgSlug`, `uid`, `member` (the org member\n * document as the page loaded it), `hosts` (the org's sites, for naming\n * them), `canManage` (the reader may manage the org).\n */\n orgMember: 'orgMember',\n /**\n * A COLUMN of the org Team table (AGL-2940) — see\n * {@link ConsoleWidget.column}: the widget declares the header and the\n * shell renders its component once per row with `{ member, orgId,\n * canManage }`. A widget on this slot without a `column` renders nothing.\n */\n orgMembersListColumn: 'orgMembersListColumn',\n /**\n * The site collaborators card (AGL-2940). A widget with a `column` is a\n * column of its table, rendered per row with `{ member, orgId, hostId,\n * canManage }` — the owner's row too, with `member` carrying the owner's\n * `uid` and `role: 'owner'`; a widget without one renders beneath the\n * table with `{ hostId, canManage }`.\n */\n hostMembers: 'hostMembers',\n /**\n * One visitor account's drawer on a site's Users page (AGL-546), under the\n * account's password controls and above its saved addresses: what a plugin\n * holds about the person behind the account — what they bought, what they\n * subscribe to. Props: {@link ConsoleSiteMemberZoneProps}. Each widget is\n * one section of the drawer's own column, which spaces it; the drawer\n * draws the account itself, its suspension and its password help.\n */\n siteMember: 'siteMember',\n /**\n * The console dock (AGL-2940): the one position above every route boundary\n * in both the `(app)` and `(editor)` shells, where a floating panel — an\n * assistant, a helper — survives a navigation. Props:\n * {@link ConsoleDockZoneProps}. Named for the position, not for what a\n * plugin puts there (AGL-3080: it was `assistPanel`).\n */\n consoleDock: 'consoleDock',\n /**\n * The console's top bar, among its own status controls just ahead of the\n * notifications bell (AGL-3593), in both shells: a compact indicator a\n * plugin keeps in view on every page — work in progress, something waiting\n * on the reader — that opens the plugin's own surface when pressed. Props:\n * {@link ConsoleTopBarZoneProps}, the dock's answers, because both sit\n * above every route and answer the same questions. A widget here is one\n * control in a row the bar spaces; it draws nothing at all when it has\n * nothing to say, and never more than one small control.\n */\n consoleTopBar: 'consoleTopBar',\n /**\n * A section at the bottom of the besigner's Attributes panel (AGL-2940),\n * under the selected element's own fields. Props: `hostId` (`null` on an\n * editor that names no site), and `node`, the selected element (AGL-2984)\n * — present wherever the designer draws the panel for a selection.\n */\n besignerInspector: 'besignerInspector',\n /**\n * The besigner's Interactions section, on every editor that offers one\n * (AGL-3080): the section experiments a plugin runs on a page's elements.\n * Props: {@link ConsoleBesignerInteractionsZoneProps}.\n *\n * The section is the designer's, and so are an element's own interactions:\n * they live on its node and ride the document's save. A section experiment\n * is not a node's: it is a record of whichever plugin runs experiments,\n * stored where that plugin keeps them. So a widget here draws nothing. It\n * reads its own records and REPORTS them, and the section badges an element\n * and offers to start one from what was reported. With no widget the\n * section offers none, which is a workspace with no plugin that runs them.\n */\n besignerInteractions: 'besignerInteractions',\n /**\n * Inside a SEARCH LISTING editor (AGL-2910), under its fields. Props:\n * {@link ConsoleSeoFieldsZoneProps} — what the listing describes, the\n * fields the editor edits and what they hold, and `proposeValues`, which\n * stages values in those fields as unsaved edits.\n *\n * Two editors host it: the screen detail page's SEO card, and the commerce\n * product editor's search engine listing, which draws it through\n * `useConsoleWidgetSlot` because a plugin's dialog cannot mount the shell's\n * slot itself. A widget here proposes and never writes: the editor's own\n * Save is the write, with the guards that write carries. A keyword checker,\n * a translation memory and a generator are the same shape of widget.\n */\n seoFields: 'seoFields',\n /**\n * The host setup SEO section, under the site's SEO check and above the site\n * SEO form (AGL-2910). Props: {@link ConsoleHostSeoZoneProps} — the site,\n * its stored SEO settings, the check's last report, and `proposeDraft`,\n * which puts values in the form as unsaved edits. The form's Update stores\n * them; nothing a widget proposes reaches the published site before that.\n */\n hostSeo: 'hostSeo',\n /** {@link ConsoleRecordInsightsZoneProps} */\n recordInsights: 'recordInsights',\n /** {@link ConsoleRecordEmailZoneProps} */\n recordEmail: 'recordEmail',\n /** {@link ConsoleImportMappingZoneProps} */\n importMapping: 'importMapping',\n /**\n * The besigner's secondary toolbar, after the undo and redo controls\n * (AGL-2984), on every editor the designer opens: screens, layouts,\n * components, forms, templates and email designs. A control here acts on\n * the document in the editor. Props: `hostId` (`null` on an editor that\n * names no site).\n */\n besignerToolbar: 'besignerToolbar',\n /**\n * A section at the foot of the besigner's Page Properties drawer\n * (AGL-3475), under the page's publishing, layout, SEO and password\n * sections: what a plugin makes of the PAGE itself, such as serving it once\n * per record. Props: {@link ConsoleBesignerPagePropertiesZoneProps}. The\n * drawer's column spaces each widget as one of its sections; a widget saves\n * through its own routes, never through the drawer's buttons.\n */\n besignerPageProperties: 'besignerPageProperties',\n /**\n * Inside one row of a site's Pages list (AGL-3475), beside the page's name:\n * a chip a plugin draws about that page — that it is a record template,\n * and how many pages it serves. Props:\n * {@link ConsoleHostScreenRowZoneProps}. Drawn once per row, so a widget\n * reads what it needs once for the site and answers each row from that.\n */\n hostScreenRow: 'hostScreenRow',\n /**\n * A site's Screens page, beside its Templates and Create New Screen actions\n * (AGL-2907): another way to start a screen. Props:\n * {@link ConsoleHostScreensZoneProps}. A widget here runs its own flow and\n * writes nothing through the page; the screens list shows what it makes once\n * it exists.\n */\n hostScreens: 'hostScreens',\n /**\n * A site's Templates page, beside its Create Template action (AGL-3043):\n * another way to start a template. Props:\n * {@link ConsoleHostTemplatesZoneProps}. The `hostScreens` contract: a\n * widget here runs its own flow and writes nothing through the page, and\n * the template list shows what it makes once it exists.\n */\n hostTemplates: 'hostTemplates',\n /**\n * A site's Layouts page, beside its Templates and Create New Layout actions\n * (AGL-3043): another way to start a layout. Props:\n * {@link ConsoleHostLayoutsZoneProps}, on the `hostScreens` contract.\n */\n hostLayouts: 'hostLayouts',\n /**\n * A site's Components page, beside its Templates and Create Component\n * actions (AGL-3051): another way to start a reusable component. Props:\n * {@link ConsoleHostComponentsZoneProps}, on the `hostScreens` contract.\n */\n hostComponents: 'hostComponents',\n /**\n * The organization's sites page, beside the sites themselves (AGL-2911):\n * an action a member takes across MANY of the org's sites at once, rather\n * than a card totaling them. Props: {@link ConsoleOrgSitesZoneProps}.\n *\n * Distinct from `orgDashboard`, which is on the same page and gated on the\n * org CRM hub's reach verdict because every card there reads across the\n * host boundary. A widget here reads nothing of the kind: the sites it acts\n * on are the ones the page already resolved for this reader, and its own\n * door proves the reader's permission on each of them again. So the zone\n * carries no gate beyond the slot's own — enablement, entitlement, and the\n * widget's declared permission.\n */\n orgSites: 'orgSites',\n /**\n * One side of one item in a site package import's Changes step, beside\n * the other side (AGL-3545): the item drawn the way its owner previews it.\n * Props: {@link ConsoleSitePackageItemPreviewZoneProps}.\n *\n * A package carries items of many kinds, and how one looks belongs to\n * whoever keeps that kind — a form through the form's own preview, a site\n * email through the email preview. So a widget here names the kinds it\n * draws in its `itemKinds`, the import draws it for those and nothing else,\n * and an item of a kind no widget names keeps the console's own rendering\n * or its value list. The widget reads nothing it is not handed beyond what\n * its preview always reads, and never writes: the import writes, after the\n * person decides.\n */\n sitePackageItemPreview: 'sitePackageItemPreview',\n} as const\n\nexport type ConsoleWidgetSlot =\n (typeof CONSOLE_WIDGET_SLOTS)[keyof typeof CONSOLE_WIDGET_SLOTS]\n\n/** What the `besignerPageProperties` zone hands each widget (AGL-3475). */\nexport interface ConsoleBesignerPagePropertiesZoneProps {\n hostId: string\n /** The org the page names; `undefined` while it resolves. */\n orgId: string | undefined\n /** The page in the editor. */\n screenId: string\n /** The page's `kind` as stored: `'template'` for a template, absent for a page. */\n screenKind?: string\n}\n\n/** What the `hostScreenRow` zone hands each widget (AGL-3475). */\nexport interface ConsoleHostScreenRowZoneProps {\n hostId: string\n /** The org the page names; `undefined` while it resolves. */\n orgId: string | undefined\n /** The row's page. */\n screenId: string\n /** The page's `kind` as stored. */\n screenKind?: string\n}\n\n/** What the `hostScreens` zone hands each widget (AGL-2907). */\nexport interface ConsoleHostScreensZoneProps {\n hostId: string\n /** The org the page names; `undefined` while it resolves. */\n orgId: string | undefined\n}\n\n/**\n * What the `hostTemplates` and `hostLayouts` zones (AGL-3043) and the\n * `hostComponents` zone (AGL-3051) hand each widget: the site and its org, as\n * `hostScreens` hands them. A plugin that hosts a resource page of its own\n * hands the same contract from a zone it declares (the forms plugin's\n * `hostForms`).\n */\nexport type ConsoleHostTemplatesZoneProps = ConsoleHostScreensZoneProps\n/** See {@link ConsoleHostTemplatesZoneProps}. */\nexport type ConsoleHostLayoutsZoneProps = ConsoleHostScreensZoneProps\n/** See {@link ConsoleHostTemplatesZoneProps}. */\nexport type ConsoleHostComponentsZoneProps = ConsoleHostScreensZoneProps\n\n/** What the `orgSites` zone hands each widget (AGL-2911). */\nexport interface ConsoleOrgSitesZoneProps {\n /** Always `null`: the zone belongs to the organization, not to one site. */\n hostId: null\n /** The org and the sites the page resolved for this reader. */\n orgMount: ConsolePluginOrgMount\n /** The sites page's own path, for the links a widget builds. */\n basePath: string\n}\n\n\n/** What the `hostFirstRun` zone hands each widget (AGL-2918). */\nexport interface ConsoleHostFirstRunZoneProps {\n hostId: string\n /** The org the page names; `undefined` while it resolves. */\n orgId: string | undefined\n /** Path slug for building `/[orgSlug]/…` links. */\n orgSlug: string\n /** The site's subdomain, which is what a console URL names a site by. */\n host: string | null\n /**\n * Leaves the guided start for a blank site: this same page, with nothing\n * begun. The shell records the choice for this site, draws the zone no more,\n * and gives a site born for the guided start its starter — the published\n * Home page and layout every other new site is born with (AGL-3594).\n *\n * Required of every widget on this zone, drawn where its questions start\n * rather than after them, and — for a widget that takes the screen — what\n * every way of dismissing it does BEFORE it has started anything. See\n * `hostFirstRun` in {@link CONSOLE_WIDGET_SLOTS}.\n */\n startBlank: () => void\n /**\n * Closes the zone after its widget STARTED the site some other way — a\n * guided start whose job is running (AGL-3594). The shell records that the\n * site was asked and draws the zone no more, and writes no starter: the\n * site's pages are the job's to build. A widget that started nothing calls\n * `startBlank` instead. Optional, so a shell that predates it still closes\n * the zone through `startBlank`.\n */\n leave?: () => void\n}\n\n/**\n * Something a console page holds that somebody may want to publish\n * (AGL-3080).\n *\n * The console's own vocabulary and nothing else: a `kind` naming what the\n * thing IS, the scope that holds it, and the document. Where it can be\n * published to, what a listing of it is called and what it may cost are the\n * publishing plugin's, which is the whole point of handing this over rather\n * than building a request.\n *\n * `kind` is an open string for the reason a plugin's zone ids are: core\n * listing every publishable noun would be core holding the catalog again. A\n * widget that does not publish a kind draws nothing for it.\n */\nexport interface ConsolePublishableArtifact {\n /** What the thing is: `layout`, `component`, `theme`, `site`, … */\n kind: string\n /** The site it belongs to, where the kind belongs to one. */\n hostId?: string | null\n /** The organization, for a kind held by the org rather than a site. */\n orgId?: string | null\n /**\n * The document, in the holding surface's terms. Absent for a kind that IS\n * the site or the org — a theme, a whole site template — where the scope\n * above already names it.\n */\n artifactId?: string | null\n /** Seeds the listing's name; the person may change it. */\n displayName?: string\n description?: string\n}\n\n/** One site of the workspace, as the console names it. */\nexport interface ConsoleZoneSite {\n id: string\n label: string\n}\n\n/** What the `orgPluginInstalls` zone hands each widget (AGL-3080). */\nexport interface ConsoleOrgPluginInstallsZoneProps {\n /** The organization whose inventory the page is. */\n orgId: string\n /** Path slug for building `/[orgSlug]/…` links. */\n orgSlug: string\n /**\n * The sites of the organization the reader can see. An installation may be\n * pinned to some of them rather than to the whole organization, so a widget\n * reads each site's pins through this list rather than listing sites\n * itself.\n */\n hosts: ReadonlyArray<ConsoleZoneSite>\n}\n\n/** What the `pluginInstallStatus` zone hands each widget (AGL-3080). */\nexport interface ConsolePluginInstallStatusZoneProps {\n /** Path slug for building `/[orgSlug]/…` links. */\n orgSlug: string\n /** The installation's id: the segment of the page, which is the pin's key. */\n pluginRef: string\n /**\n * One pin of the installation — org-wide if there is one, else any site's.\n * Every pin carries the same version and manifest, which is what a status\n * is about.\n */\n pin: Readonly<Record<string, unknown>>\n}\n\n/** A release flag's verdict as the console applies it: the staff bypass included. */\nexport interface ConsoleReleaseVerdict {\n /** Released, or the reader is staff. */\n visible: boolean\n /** Visible ONLY because the reader is staff. */\n staffPreview: boolean\n}\n\n/** What the `consoleDock` zone hands each widget (AGL-2940, AGL-3080). */\nexport interface ConsoleDockZoneProps {\n orgId?: string\n org?: unknown\n orgReady: boolean\n /**\n * The org a widget may speak for, act as, and be METERED against — or\n * `undefined` where the page named none, or where the membership positively\n * contradicts the URL (AGL-1130, AGL-1916, AGL-1934).\n */\n scopedOrgId?: string\n /** Path slug for building `/[orgSlug]/…` links. */\n orgSlug: string\n /** The site in view, or null off a host route. */\n hostId: string | null\n /** The product's name as this org reads it (AGL-2319). */\n productName: string\n /**\n * The verdict for any release flag the widget names, staff bypass applied.\n * The shell names no plugin's flag; a widget asks for its own.\n */\n releaseVerdict: (key: string) => ConsoleReleaseVerdict\n isStaff: boolean\n /** The reader's verdict for every declared permission key on the site in view. */\n permissionsOnHost?: { loaded: boolean; granted: Readonly<Record<string, boolean>> }\n}\n\n/**\n * What the `consoleTopBar` zone hands each widget (AGL-3593): the same\n * answers the console dock gets, from the same shell resolution.\n */\nexport type ConsoleTopBarZoneProps = ConsoleDockZoneProps\n\n/** How one plugin's shelf of the template gallery stands (AGL-3080). */\nexport type ConsoleTemplateGalleryShelfState = 'loading' | 'empty' | 'shown'\n\n/** What the `templateGallery` zone hands each widget (AGL-3080). */\nexport interface ConsoleTemplateGalleryZoneProps {\n /** The site a template installs into. */\n hostId: string\n /**\n * The kind of template the gallery picks: `page` on the Screens page,\n * `layout` and `component` on theirs. A shelf offering only one kind\n * draws nothing for the others.\n */\n kind: 'page' | 'component' | 'layout'\n /** The word typed in the gallery's search box, `''` for none. */\n search: string\n /** Closes the gallery once an install has landed in the library. */\n onInstalled: () => void\n /**\n * Says how this shelf stands, keyed by a name the widget chooses and keeps,\n * so the gallery's \"nothing matches\" line counts it. Report `empty` rather\n * than nothing once a read answers with nothing.\n */\n reportShelf: (shelfId: string, state: ConsoleTemplateGalleryShelfState) => void\n}\n\n/** What the `siteMember` zone hands each widget (AGL-3080). */\nexport interface ConsoleSiteMemberZoneProps {\n /** The site whose visitor account it is. */\n hostId: string\n /**\n * The account's `siteMembers` document as the drawer holds it, `$id`\n * included. Its `email` is how a plugin finds what the person did on the\n * site.\n */\n member: Readonly<Record<string, unknown>> & { $id: string }\n}\n\n/** One section experiment, as the besigner's Interactions section lists it (AGL-3080). */\nexport interface ConsoleBesignerSectionExperiment {\n id: string\n name?: string\n /** The element the experiment varies. */\n nodeId: string\n status?: string\n}\n\n/** What one plugin reports to the besigner's Interactions section (AGL-3080). */\nexport interface ConsoleBesignerSectionExperiments {\n experiments: ConsoleBesignerSectionExperiment[]\n /**\n * Starts a draft experiment on an element. Reported only where the editor's\n * document is a page (`screenId` is set): a layout or a component is no\n * page for an experiment to run on.\n */\n create?: (options: { nodeId: string }) => void\n}\n\n/** What the `besignerInteractions` zone hands each widget (AGL-3080). */\nexport interface ConsoleBesignerInteractionsZoneProps {\n /** The site whose editor this is. */\n hostId: string\n /** The page under edit, or `null` on a layout or a component. */\n screenId: string | null\n /**\n * Hands the section what this plugin runs, keyed by a name the widget\n * chooses and keeps; `null` withdraws it. Call it from an effect.\n */\n reportSectionExperiments: (\n reporterId: string,\n report: ConsoleBesignerSectionExperiments | null,\n ) => void\n}\n\n/** What the `templateInstallStatus` zone hands each widget (AGL-3080). */\nexport interface ConsoleTemplateInstallStatusZoneProps {\n /** The site whose library the row is in. */\n hostId: string\n /**\n * The row's template document as the library read it, `$id` included:\n * its server-managed `source` and `installedFrom` stamps say who installed\n * it, and `editedAt` whether the site has edited its copy since.\n */\n template: Readonly<Record<string, unknown>>\n}\n\n/** What the `sitePackageItemPreview` zone hands each widget (AGL-3545). */\nexport interface ConsoleSitePackageItemPreviewZoneProps {\n /** The site the package is being imported into. */\n hostId: string\n /** Which side this is: the site's copy, or the file's. */\n side: 'site' | 'file'\n /** The item's package key, `<kind>/<id>`. */\n itemKey: string\n /** One of the kinds the widget names in `itemKinds`. */\n kind: string\n /** The document the side is: the site's item, or the id the file's would land under. */\n itemId: string\n /** The item as an import would write it: the document, without `$id`. */\n content: unknown\n /** What the import calls the item. */\n title: string\n}\n\n/** What the `hostArtifactPublish` zone hands each widget (AGL-3080). */\nexport interface ConsoleArtifactPublishZoneProps {\n /**\n * What the page asked to publish, or `null` while nothing is open. The\n * page owns the open/closed state because the control that opens this is\n * the page's own.\n */\n artifact: ConsolePublishableArtifact | null\n /** Closes it, however it was closed — cancelled, published, or refused. */\n onClose: () => void\n}\n\n/** What the `hostTheme` zone hands each widget (AGL-2938). */\nexport interface ConsoleHostThemeZoneProps {\n hostId: string\n /** The org the page names; `undefined` while it resolves. */\n orgId: string | undefined\n /** Path slug for building `/[orgSlug]/…` links. */\n orgSlug: string\n /** The site's subdomain, which is what a console URL names a site by. */\n host: string | null\n /** The theme the editor shows: the site's theme with its overrides resolved. */\n theme: HostTheme | undefined\n /** Where that theme came from, which decides how an edit to it is stored. */\n themeSource: HostThemeSource\n /** The editor's own preview, which renders a theme over the brand base. */\n ThemePreview: ComponentType<{ theme: HostTheme; scheme: HostThemeScheme }>\n /**\n * Puts `theme` in the editor as unsaved changes under `key`. A new key\n * replaces the previous draft; the same key again changes nothing until\n * the editor has saved or discarded it.\n */\n proposeDraft: (theme: HostTheme, key: string) => void\n}\n\n/**\n * The zones on the STAFF pages (AGL-2939): the staff overview, the staff org\n * page, its detail zone, and the staff user page.\n *\n * No workspace names the plugin set there. A staff page is ABOUT an org or\n * an account, and the reader's own memberships have nothing to do with what\n * it shows, so the console reads these zones from the plugins it loads for\n * the staff area — every plugin that declares a `staff` register surface —\n * and consults neither a widget's entitlement nor its permission: both are\n * answers about a workspace, and the staff area's guard is what admits the\n * reader.\n */\nexport const CONSOLE_STAFF_WIDGET_SLOTS: readonly ConsoleWidgetSlot[] = [\n CONSOLE_WIDGET_SLOTS.staffOverview,\n CONSOLE_WIDGET_SLOTS.adminOrgDetail,\n CONSOLE_WIDGET_SLOTS.staffOrg,\n CONSOLE_WIDGET_SLOTS.staffUser,\n CONSOLE_WIDGET_SLOTS.staffSite,\n CONSOLE_WIDGET_SLOTS.staffOrgsListColumn,\n CONSOLE_WIDGET_SLOTS.staffOrgUsageColumn,\n]\n\n/** Whether a slot is one of the {@link CONSOLE_STAFF_WIDGET_SLOTS}. */\nexport function isConsoleStaffWidgetSlot(slot: string): boolean {\n return (CONSOLE_STAFF_WIDGET_SLOTS as readonly string[]).includes(slot)\n}\n\n/**\n * The zones whose HOST decides who reads them (AGL-3554): each widget draws\n * only the props the host hands it, and the host page is already gated on\n * what its own route requires — a site package import's side-by-side diff,\n * opened by whoever may import a package into the site. So the console asks\n * the widget's own `permission` there and not its extension's: an\n * extension's permission guards the extension's own surfaces and reads (the\n * Email plugin's `data.manage`, for the audiences its page lists), and\n * would otherwise hide a preview from the very person importing the item.\n * The extension's plan feature is still asked.\n */\nexport const CONSOLE_HOST_GATED_WIDGET_SLOTS: readonly ConsoleWidgetSlot[] = [\n CONSOLE_WIDGET_SLOTS.sitePackageItemPreview,\n]\n\n/** Whether a slot is one of the {@link CONSOLE_HOST_GATED_WIDGET_SLOTS}. */\nexport function isConsoleHostGatedWidgetSlot(slot: string): boolean {\n return (CONSOLE_HOST_GATED_WIDGET_SLOTS as readonly string[]).includes(slot)\n}\n\n/** Search listing values by field; a field the editor does not hold is absent. */\nexport type ConsoleSeoFieldValues = Partial<Record<SeoListingFieldKey, string>>\n\n/**\n * What a search listing describes (AGL-2910). A screen is read from its own\n * documents by whoever needs more than its name; a product travels with its\n * name and description, because the product document is the commerce\n * plugin's and nothing else reads it.\n */\nexport type ConsoleSeoFieldsSubject =\n | {\n kind: 'screen'\n /** The screen document id. */\n id: string\n /** The version the page is showing, whose content the listing is about. */\n versionId: string | null\n name: string\n }\n | {\n kind: 'product'\n /** `null` for a product that has not been saved yet. */\n id: string | null\n name: string\n description: string\n }\n\n/** What the `seoFields` zone hands each widget (AGL-2910). */\nexport interface ConsoleSeoFieldsZoneProps {\n hostId: string\n /** The org the page names; `undefined` while it resolves. */\n orgId: string | undefined\n /** Path slug for building `/[orgSlug]/…` links. */\n orgSlug: string\n subject: ConsoleSeoFieldsSubject\n /**\n * The fields this editor edits, in its order — the only ones a widget may\n * propose. Keys of the `seo-listing-fields` catalog, which carries each\n * field's label and length.\n */\n fields: readonly SeoListingFieldKey[]\n /** What each field holds as the editor shows it: saved, or staged and unsaved. */\n values: ConsoleSeoFieldValues\n /** Whether the listing has a social image — an image description needs one. */\n hasImage: boolean\n /**\n * Stages `values` in the editor as unsaved edits under `key`. Fields the\n * editor does not edit are ignored. The editor's own Save is what stores\n * them; a widget never writes the listing itself.\n */\n proposeValues: (values: ConsoleSeoFieldValues, key: string) => void\n}\n\n/**\n * The site's SEO check as the SEO section last ran it: the platform's\n * findings (`app-utils/seo-audit`), which the section draws for every site\n * owner, and the target keyword lines the check was run with.\n */\nexport interface ConsoleSeoCheck {\n report: SeoAuditReport\n /** The keyword lines as typed, one page a line: `/pricing: plans, pricing`. */\n keywords: string\n}\n\n/** What the `hostSeo` zone hands each widget (AGL-2910). */\nexport interface ConsoleHostSeoZoneProps {\n hostId: string\n /** The org the page names; `undefined` while it resolves. */\n orgId: string | undefined\n /** Path slug for building `/[orgSlug]/…` links. */\n orgSlug: string\n /** The site's subdomain, which is what a console URL names a site by. */\n host: string | null\n /** The site's stored `seo` settings, as the form was seeded with them. */\n seo: Record<string, unknown> | undefined\n /**\n * Puts `values` in the site SEO form as unsaved edits, keyed by the form's\n * field names (`seo.entity.description`, `seo.agent.whenToUse`). Proposals\n * land in the form's draft beside what was typed, the same `key` twice\n * applies once, and the form's Update is what stores them.\n */\n proposeDraft: (values: Record<string, string>, key: string) => void\n /**\n * The SEO check the section drew above the zone, once somebody has run it\n * this visit; `null` before. The section lists the findings: a widget adds\n * what it has for them — a proposed fix, say — and never lists them again.\n */\n check: ConsoleSeoCheck | null\n}\n\n/**\n * A column a widget contributes to a shell-owned table (AGL-2940) — the org\n * Team table and the site collaborators table read these. The widget's\n * `Component` is the CELL renderer, mounted once per row with the row as a\n * prop beside the slot's own props; the header and the sort key are the\n * table's to draw.\n */\nexport interface ConsoleWidgetColumn {\n /** The header cell's text, and the column's name wherever it is listed. */\n header: string\n /**\n * The row field a table that sorts orders this column by. Carried for\n * every column contract so a sortable table can honor it; the two member\n * tables render in fetch order and do not read it today.\n */\n sortKey?: string\n align?: 'left' | 'right' | 'center'\n /**\n * The header cell's content when a plain `header` is not enough\n * (AGL-2939): a hint, or a sort over values only the plugin can read.\n * Mounted once per table with the slot's props beside\n * {@link ConsoleWidgetColumnHeaderProps}; without it the table draws\n * `header` as text.\n */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n Header?: ComponentType<any>\n}\n\n/**\n * What a column's own header receives beside the slot's props (AGL-2939).\n * The table keeps one sort at a time, so a column that sorts replaces\n * another's order.\n */\nexport interface ConsoleWidgetColumnHeaderProps {\n /**\n * Hands the table a row comparator, or `null` to put the rows back in the\n * table's own order. Stable for the life of the table.\n */\n onSort: (compare: ((a: never, b: never) => number) | null) => void\n /** Whether the rows are in this column's order. */\n sorted: boolean\n}\n\n/**\n * A component a plugin renders into a NAMED console slot (AGL-419/433) —\n * see {@link CONSOLE_WIDGET_SLOTS} for the guaranteed zones and their\n * props. The shell owns placement; the plugin owns the UI.\n */\nexport interface ConsoleWidget {\n slot: string\n /**\n * Present when the widget is a table COLUMN rather than a card (AGL-2940)\n * — see {@link ConsoleWidgetColumn}. Only the slots documented as column\n * slots read it; elsewhere it is ignored.\n */\n column?: ConsoleWidgetColumn\n /**\n * The kinds of item the widget draws, for a zone that draws one item of\n * many kinds (`sitePackageItemPreview`, AGL-3545). Only the slots\n * documented as reading it do; elsewhere it is ignored.\n */\n itemKinds?: readonly string[]\n /**\n * Stable identity for this widget, unique within the plugin per slot.\n * The id names the CARD, not its placement: the same card registered on\n * a second slot — the CRM's glance on the host dashboard and again on the\n * org's sites page — carries one id on both.\n *\n * A PERSISTED IDENTIFIER wherever the shell lets someone arrange the\n * surface it lands on — the console stores dashboard cards a reader has\n * switched off by this string, and reads it back sessions later. Giving a\n * retired id to a different card therefore shows that reader an\n * arrangement they never chose. Retire an id by leaving it reserved and\n * minting a new one, never by reusing it.\n */\n widgetId: string\n /**\n * What to call this widget where it is LISTED rather than rendered — the\n * console's dashboard customize dialog is the one such place today.\n *\n * Match the card's own heading: the two names sit a click apart, and a\n * switch labeled differently from the card it controls reads as a switch\n * for something else. Omitting it falls back to the extension's\n * `displayName`, which is right for a plugin contributing one card and\n * ambiguous for one contributing several.\n */\n title?: string\n /**\n * The permission a reader must hold for this card, when it is narrower\n * than its extension's own {@link ConsoleExtension.permission}.\n *\n * Composes by AND with the extension's, like a nav item's does: a widget\n * cannot escape its extension's gate by naming a key its reader happens to\n * hold, and declaring nothing here inherits the extension's requirement\n * rather than clearing it.\n *\n * A card is a surface the reader never asked for — the shell drops it onto\n * a page they opened for something else — so there is nowhere in it to put\n * an upsell or a refusal, and a widget its reader may not have is simply\n * absent. That is the same treatment the entitlement gate gives a widget,\n * and for the same reason.\n */\n permission?: string\n /**\n * The entitlement THIS card needs, when it is narrower than its\n * extension's {@link ConsoleExtension.featureFlag} (AGL-2611).\n *\n * Composes by AND with the extension's, exactly as `permission` above\n * does: a card cannot escape its extension's gate by declaring nothing,\n * and declaring one here can only narrow. The case is an extension whose\n * surface ships on every plan while one of its cards belongs to a paid\n * part of it. Absent without an upsell, for the reason `permission` gives.\n */\n featureFlag?: keyof OrgFeatureFlags\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n Component: ComponentType<any>\n}\n\n/**\n * Console half of the pattern: everything a feature contributes to the\n * console shell. Declarative — the shell owns rendering and applies the\n * feature-flag gate, so extensions cannot bypass entitlements.\n *\n * That sentence describes `apps/console/utils/extension-entitlement.ts`,\n * which the plugin route and `PluginWidgetSlot` both call before they mount\n * anything an extension registered (AGL-2484). It was an aspiration until\n * then: the route resolved the entitlement and handed it to the extension as\n * the `entitled` PROP, and the widget slot did not resolve it at all, so\n * enforcement rested on each extension policing itself — which one\n * first-party page did not do.\n *\n * What the gate covers, exactly: a `featureFlag` refuses to RENDER the\n * extension's page body and its widgets. Nav items stay visible on purpose.\n * Hiding the tab would hide the only route most workspaces have to the page\n * that sells the feature, and a nav entry leading to the shell's own upgrade\n * notice bypasses nothing.\n *\n * TWO QUESTIONS, BOTH ANSWERED BY THE SHELL. `featureFlag` is about the\n * organization's plan; `permission` is about the person reading, and it is\n * enforced in the same place and the same way — resolved from the member's\n * own permission map, never from anything the extension supplies, and read\n * before the surface is constructed. An extension declares what it requires\n * and the shell decides whether the requirement is met, so the sentence\n * above holds for authorization as well as for entitlements.\n */\n/**\n * What a blocked org is told about a feature it does not hold, in the\n * extension's own words.\n *\n * PRESENTATION ONLY. The shell decides entitlement from the org billing doc\n * and this extension's `featureFlag`, and reads this object solely to render\n * a refusal it has ALREADY decided on. Nothing here is an input to that\n * decision, and an extension supplying it gains no access — the surface it\n * describes stays unmounted either way.\n *\n * It exists because the shell's own copy can only speak in generalities. An\n * entitlement that no plan grants — one sold as a per-organization add-on —\n * is described exactly wrong by \"not included in your current plan\", which\n * sends the reader to compare plan tiers that would not have helped.\n */\nexport interface ConsoleUpgradeNotice {\n /**\n * The sentence a blocked org reads. Plain text: the shell renders it as a\n * string, never as markup, and a third-party extension writes this.\n */\n message: string\n /**\n * Which card on the billing page sells it, as a bare fragment id\n * ('addons'). The console validates it against the anchors that page\n * actually has and drops it otherwise — the `docsTopic` treatment, for the\n * same reason: an extension can name any string, and an unrecognized one\n * must degrade to the plain Billing link rather than build a dead URL.\n *\n * Deliberately not an href. A full URL from an extension would be an\n * open redirect rendered by the console's own chrome; the shell keeps\n * ownership of the route and accepts only which part of it to scroll to.\n */\n billingAnchor?: string\n}\n\n/** What the console's generic staff route hands a staff page. */\nexport interface ConsoleStaffPageProps {\n /** The page's own console path, `/admin/{id}`. */\n basePath: string\n /**\n * Path segments beneath {@link basePath}, `[]` on the page's own URL\n * (AGL-3080). Only ever non-empty for a page that declared\n * {@link ConsoleStaffPage.ownsSubtree}.\n *\n * The staff twin of {@link ConsolePluginPageProps.segments}, and for the\n * same case: a queue is a list, and a row of it is a page. A staff page\n * without this could only ever BE the list.\n */\n segments?: readonly string[]\n /**\n * The viewer's staff ROLE, or `null` while the claim is still resolving\n * (AGL-3080).\n *\n * Not every staff act is open to every staff role — six are `super`-only\n * on the server — and a page that cannot tell renders live controls to a\n * `support` engineer who clicks them and gets a raw 403. Pass it to\n * `resolveStaffRoleGate` and render the verdict with\n * `BlockedControl`; `null` must never be treated as a refusal, or every\n * page flashes a disabled button at the people who may use it.\n *\n * ⚠️ NOT the boundary. The routes verify the decoded token per request and\n * refuse regardless of what rendered. This exists so the console stops\n * promising what the server will refuse.\n */\n staffRole?: string | null\n /**\n * Console destinations a staff page may link ACROSS to, built by the shell\n * (AGL-3080) — the staff twin of {@link ConsolePluginOrgMount}'s paths,\n * and for the same reason: a plugin cannot import the console's route\n * table, and a plugin that rebuilt one of these from a string would break\n * silently the day the console moved it.\n */\n staffPaths?: ConsoleStaffPagePaths\n}\n\nexport interface ConsoleStaffPagePaths {\n /**\n * The staff console's page for one workspace. `undefined` on a deployment\n * that has no such page, which a caller renders as no link rather than a\n * dead one.\n */\n orgDetail(orgId: string): string | undefined\n}\n\n/**\n * A page a plugin adds to the STAFF area (AGL-2939): a tab in the staff\n * strip and a page at `/admin/{id}`, rendered by the console's generic staff\n * route. The shell owns the layout, the header, the breadcrumbs, the staff\n * guard and the tab; the plugin owns the body.\n *\n * Staff pages load with the staff area's plugins — those declaring a `staff`\n * register surface — not with a workspace's, because no org names the plugin\n * set on `/admin`. They are admitted by the staff claim alone, so the\n * extension's `featureFlag` and `permission` do not apply, and every read a\n * staff page makes is refused server-side to a caller without the claim.\n */\nexport interface ConsoleStaffPage {\n /**\n * The URL segment under `/admin`, and the page's identity. The console's\n * own staff routes win a segment they use, so pick one they do not. It is\n * in links staff keep — treat it as persisted.\n */\n id: string\n /** The tab's label in the staff strip. */\n label: string\n /**\n * The page header (title and icon), and the docs topic its help `?`\n * explains — a plain string for the reason {@link ConsoleNavItem.header}\n * gives, validated by the console.\n */\n header?: { title: string; icon?: MdiIconProps; docsTopic?: string }\n /**\n * Whether this page claims every path beneath `/admin/{id}` too\n * (AGL-3080) — the staff twin of {@link ConsoleNavItem.ownsSubtree}, with\n * the same trade and the same duty.\n *\n * The case is a staff QUEUE: the list is the page, and each row opens one\n * submission. The set of ids is a property of the data, so no static list\n * could enumerate them, and without this every row's URL is a 404.\n *\n * A page that claims its subtree can no longer tell a typo from an id, so\n * it takes on saying \"no such thing\" itself — which a queue has to be able\n * to do anyway for a submission withdrawn while a link to it was still in\n * a reviewer's inbox.\n *\n * The console's own staff routes keep winning their segments either way:\n * a static path beats a dynamic one segment by segment, so `/admin/orgs/1`\n * is still the orgs route and never a staff page's subtree.\n */\n ownsSubtree?: boolean\n Component: ComponentType<ConsoleStaffPageProps>\n}\n\nexport interface ConsoleExtension {\n pluginId: PluginId\n displayName: string\n /** Entitlement flag gating every surface this extension registers. */\n featureFlag?: keyof OrgFeatureFlags\n /**\n * The permission a reader must hold for every surface this extension\n * registers — the AUTHORIZATION half of the sentence above `featureFlag`.\n *\n * `featureFlag` answers what the ORGANIZATION bought; this answers what\n * the PERSON reading may open, and the two are independent: an org can\n * hold a feature that most of its members have no business using.\n *\n * A key in the console's permission vocabulary, which is two spaces and\n * they are not interchangeable. Either a dotted {@link OrgPermission} from\n * the built-in catalog ('data.manage'), which the shell answers from the\n * member's resolved granular map; or a key some plugin declared through\n * `registerPluginPermissions` ('managePos'), which the shell answers from\n * the resolved permission map that carries those keys. A key belonging to\n * NEITHER space refuses the surface rather than passing it — a requirement\n * nothing can answer is not a requirement that has been met.\n *\n * Declared here rather than checked inside the page for the reason the\n * entitlement gate moved out of the pages: a check the extension performs\n * on itself is enforcement only for as long as every extension remembers\n * to perform it, and the surface has already mounted and opened its\n * listeners by the time it runs.\n *\n * Omit for a surface every member of the workspace may open.\n */\n permission?: string\n /**\n * Refusal copy for an org that does not hold `featureFlag`, rendered by\n * the shell in place of the surface. Omit to get the shell's generic\n * plan-tier sentence.\n */\n upgradeNotice?: ConsoleUpgradeNotice\n navItems?: ConsoleNavItem[]\n /**\n * Surfaces mounted at the ORGANIZATION level rather than under a site\n * (AGL-2974): each is served at `/[orgSlug]{href}` by the console's generic\n * org route and listed on the organization's tab strip.\n *\n * A separate list rather than a flag on {@link ConsoleNavItem}, because the\n * two levels are read by different consumers. The site strip, the site\n * route and every title and section lookup iterate `navItems`, and a scope\n * field on one of those entries would reach each of them as a site surface\n * until every one of them learned to skip it. Nothing that reads `navItems`\n * sees these.\n *\n * The same contract a site nav item has, with the site taken away: the\n * page receives `hostId: null` and an `orgMount` naming the organization\n * and its sites, sections resolve and gate the same way, and the\n * extension's `featureFlag` and `permission` apply unchanged. The shell\n * admits only a reader whose reach is the whole organization, because a\n * surface with no site has no scope to narrow a site collaborator to.\n *\n * The href must not name one of the console's own organization routes\n * (`/hosts`, `/team`, `/settings`, `/crm` and the rest): a named route\n * always wins over the generic one, so such a surface would never render.\n */\n orgNavItems?: ConsoleNavItem[]\n dashboardCards?: ConsoleDashboardCard[]\n settingsSections?: ConsoleSettingsSection[]\n /** Slot-addressed components the shell renders in place (AGL-419). */\n widgets?: ConsoleWidget[]\n /**\n * Built-in themes this plugin adds to every site's theme picker\n * (AGL-3404) — see {@link ConsoleThemePreset}. Loaded with the plugin at\n * the {@link THEME_PRESETS_LOAD_POINT}, which the plugin declares among its\n * `console.slots`.\n */\n themePresets?: readonly ConsoleThemePreset[]\n /**\n * The kinds of record this plugin lets the console's search find — see\n * {@link ConsoleSearchSource}. Loaded with the plugin at the\n * {@link CONSOLE_SEARCH_LOAD_POINT}, which the plugin declares among its\n * `console.slots`.\n */\n searchSources?: readonly ConsoleSearchSource[]\n /**\n * Pages in the STAFF area (AGL-2939) — see {@link ConsoleStaffPage}.\n * Neither `featureFlag` nor `permission` applies to them.\n */\n staffPages?: readonly ConsoleStaffPage[]\n /**\n * App-level providers the shell mounts around every console page\n * (AGL-419) — e.g. the marketplace plugin's AI-assist provider.\n */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n providers?: Array<ComponentType<any>>\n}\n\nconst consoleExtensions = new Map<PluginId, ConsoleExtension>()\n\n/** Idempotent by pluginId — re-registration replaces the previous entry. */\nexport function registerConsoleExtension(extension: ConsoleExtension): void {\n consoleExtensions.set(extension.pluginId, extension)\n}\n\nexport function unregisterConsoleExtension(pluginId: PluginId): void {\n consoleExtensions.delete(pluginId)\n}\n\n/**\n * Registration-ordered extensions; the console shell filters by flag.\n *\n * AGL-758: the registry is a module-global that only ever grows — nothing\n * outside tests unregisters, and loaded chunks can't unload — so after\n * visiting two workspaces it holds the UNION of both plugin sets. Every\n * read is therefore scoped by the caller's effective enabled set; pass the\n * current org's plugin ids so one workspace never serves another's nav\n * items, widgets, pages or providers. Omitting the argument keeps the\n * unfiltered union (tests, and non-org surfaces that have no such set).\n */\nexport function listConsoleExtensions(\n enabledPluginIds?: readonly PluginId[],\n): ConsoleExtension[] {\n const all = Array.from(consoleExtensions.values())\n if (!enabledPluginIds) return all\n const enabled = new Set(enabledPluginIds)\n return all.filter((extension) => enabled.has(extension.pluginId))\n}\n\n/** A nav item flattened with its owning extension's id + entitlement flag. */\nexport interface ConsoleNavEntry extends ConsoleNavItem {\n pluginId: PluginId\n featureFlag?: keyof OrgFeatureFlags\n}\n\n/**\n * Every registered nav item, flattened for the shell's nav strip. The\n * shell appends these to its static tabs, so a plugin adds a menu item\n * by registering here — no edit to the console's nav constants.\n */\nexport function listConsoleNavItems(\n enabledPluginIds?: readonly PluginId[],\n): ConsoleNavEntry[] {\n return inTabOrder(\n listConsoleExtensions(enabledPluginIds).flatMap((extension) =>\n (extension.navItems ?? []).filter((navItem) => !navItem.unlisted).map((navItem) => ({\n ...navItem,\n pluginId: extension.pluginId,\n featureFlag: extension.featureFlag,\n })),\n ),\n (entry) => entry.tabOrder,\n )\n}\n\n/**\n * A strip's plugin entries by {@link ConsoleNavItem.tabOrder}, lower first,\n * a tie in the order they arrived — which is registration order.\n */\nfunction inTabOrder<T>(\n entries: readonly T[],\n tabOrderOf: (entry: T) => number | undefined,\n): T[] {\n return entries\n .map((entry, index) => ({ entry, index, order: tabOrderOf(entry) ?? 0 }))\n .sort((a, b) => a.order - b.order || a.index - b.index)\n .map(({ entry }) => entry)\n}\n\n/** What {@link resolveConsolePluginPage} answers for a matched href. */\nexport interface ResolvedConsolePluginPage {\n extension: ConsoleExtension\n navItem: ConsoleNavItem\n /**\n * The section the href names, when the nav item declares sections and the\n * href reaches past its own. Undefined on the nav item's own href.\n */\n section?: ConsoleNavSection\n /** Path segments beneath `navItem.href`; `[]` on the nav item's own href. */\n segments: readonly string[]\n /**\n * The href was one of the nav item's `legacyHrefs`, not its current one.\n * The shell redirects to `navItem.href` plus the same section and segments.\n */\n legacy?: boolean\n}\n\n/**\n * One nav item against one href: exact, a declared section beneath it, or —\n * for a nav item that asks — anything beneath it.\n *\n * A nav item that declares neither `sections` nor `ownsSubtree` matches its\n * own href and nothing else. That is what keeps every plugin written before\n * AGL-2501 behaving as it did: without it, prefix matching would quietly hand\n * `/products/anything` to the Products page, which is the \"it opened the wrong\n * page\" report rather than a 404.\n */\nfunction matchNavItem(\n navItem: ConsoleNavItem,\n href: string,\n): { section?: ConsoleNavSection; segments: readonly string[]; legacy?: boolean } | undefined {\n const current = matchNavItemHref(navItem, navItem.href, href)\n if (current) return current\n for (const legacyHref of navItem.legacyHrefs ?? []) {\n const match = matchNavItemHref(navItem, legacyHref, href)\n if (match) return { ...match, legacy: true }\n }\n return undefined\n}\n\nfunction matchNavItemHref(\n navItem: ConsoleNavItem,\n itemHref: string,\n href: string,\n): { section?: ConsoleNavSection; segments: readonly string[] } | undefined {\n if (itemHref === href) return { segments: [] }\n if (!navItem.sections?.length && !navItem.ownsSubtree) return undefined\n // On a separator boundary, so `/products` cannot claim `/products-archive`.\n if (!href.startsWith(`${itemHref}/`)) return undefined\n const segments = href.slice(itemHref.length + 1).split('/').filter(Boolean)\n const section = navItem.sections?.find((item) => item.id === segments[0])\n if (section) return { section, segments }\n // An id the nav item never declared is NOT this page — unless the surface\n // claimed the subtree, in which case the deeper segments are its own\n // entity ids and it answers for them. Returning the nav item otherwise\n // would render the surface's default section under a URL naming a\n // different one, which reads to the person who typed it as the wrong page\n // opening rather than as a typo.\n return navItem.ownsSubtree ? { segments } : undefined\n}\n\n/**\n * Resolves a host-relative href (e.g. '/events', '/products/orders') to the\n * extension + nav item that owns a renderable page for it, and the section\n * within it. The shell's generic host route uses this to render plugin pages\n * without a per-plugin page file.\n *\n * ## Which registration wins (AGL-2501)\n *\n * LONGEST declared `href` wins, and an exact match therefore always beats a\n * section match — an exact `href` spans the whole path, so nothing matching a\n * prefix of it can be longer. `/products/orders` goes to a plugin that\n * declares that path over one that declares `/products` with an `orders`\n * section, and a prefix only matches on a SEGMENT boundary, so `/products`\n * never claims `/products-archive`.\n *\n * A TIE REFUSES. Two enabled plugins matching the same path at the same length\n * resolve to nothing, and the console 404s. Registry insertion order is an\n * accident of which chunk loaded first, so picking from it means one\n * workspace serves plugin A's page at a URL where another serves plugin B's —\n * silently, and differently per session. Nobody can debug that from the\n * symptom, so it is refused and logged instead. Two nav items of the SAME\n * extension are not a tie: that order is authored, and the first wins as it\n * always has.\n *\n * This is a rule rather than an accident because the registry is a\n * session-wide UNION across plugins from different authors (AGL-758) — one\n * plugin registering `/products` and another `/products/orders` is two\n * workspaces' code meeting in one module-global, not one author's tidiness\n * problem. Scoping is unchanged and load-bearing: every candidate still comes\n * from `listConsoleExtensions(enabledPluginIds)`, so a plugin the current org\n * has not enabled cannot win a path — or collide with one.\n */\nexport function resolveConsolePluginPage(\n href: string,\n enabledPluginIds?: readonly PluginId[],\n): ResolvedConsolePluginPage | undefined {\n return resolvePluginPageAmong(\n href,\n enabledPluginIds,\n (extension) => extension.navItems,\n )\n}\n\n/** One organization-level nav item with the extension that declared it. */\nexport interface ConsoleOrgNavEntry {\n extension: ConsoleExtension\n navItem: ConsoleNavItem\n}\n\n/**\n * Every registered {@link ConsoleExtension.orgNavItems} entry, in\n * {@link ConsoleNavItem.tabOrder} and then registration order, for the\n * organization's tab strip (AGL-2974).\n *\n * Carries the extension whole rather than a flattened copy of two of its\n * fields: a tab for a surface the reader cannot open is hidden, and deciding\n * that takes the extension's `permission`, `featureFlag` and\n * `upgradeNotice`, which is the same set the org route reads.\n */\nexport function listConsoleOrgNavItems(\n enabledPluginIds?: readonly PluginId[],\n): ConsoleOrgNavEntry[] {\n return inTabOrder(\n listConsoleExtensions(enabledPluginIds).flatMap((extension) =>\n (extension.orgNavItems ?? []).map((navItem) => ({ extension, navItem })),\n ),\n (entry) => entry.navItem.tabOrder,\n )\n}\n\n/**\n * {@link resolveConsolePluginPage} for the ORGANIZATION level (AGL-2974): an\n * org-relative href (`/outreach/sequences`) against every enabled\n * extension's `orgNavItems`, with the same matching, the same longest-href\n * rule and the same refusal of a tie between two plugins. Site nav items are\n * never candidates, so a surface registered under a site cannot be opened\n * without one.\n */\nexport function resolveConsoleOrgPluginPage(\n href: string,\n enabledPluginIds?: readonly PluginId[],\n): ResolvedConsolePluginPage | undefined {\n return resolvePluginPageAmong(\n href,\n enabledPluginIds,\n (extension) => extension.orgNavItems,\n )\n}\n\n/** The resolver both levels share; `navItemsOf` picks which list is read. */\nfunction resolvePluginPageAmong(\n href: string,\n enabledPluginIds: readonly PluginId[] | undefined,\n navItemsOf: (extension: ConsoleExtension) => ConsoleNavItem[] | undefined,\n): ResolvedConsolePluginPage | undefined {\n let best: ResolvedConsolePluginPage | undefined\n /** Extensions matching at `best`'s length — more than one is the tie. */\n let contenders: PluginId[] = []\n for (const extension of listConsoleExtensions(enabledPluginIds)) {\n for (const navItem of navItemsOf(extension) ?? []) {\n if (!navItem.Component) continue\n const match = matchNavItem(navItem, href)\n if (!match) continue\n const bestLength = best?.navItem.href.length ?? -1\n if (navItem.href.length > bestLength) {\n best = { extension, navItem, ...match }\n contenders = [extension.pluginId]\n continue\n }\n // Same length, different plugin: ambiguous. Same plugin: authored order,\n // and the first nav item keeps the path.\n if (\n navItem.href.length === bestLength &&\n !contenders.includes(extension.pluginId)\n ) {\n contenders.push(extension.pluginId)\n }\n }\n }\n if (contenders.length > 1) {\n // Loud, because the symptom — a 404 on a page that is plainly installed —\n // names neither plugin. This line is the only place the collision is\n // visible, so it carries both ids and the path they are fighting over.\n console.error(\n `[aglyn] console page path \"${href}\" is claimed by more than one ` +\n `enabled plugin (${contenders.join(', ')}); refusing to guess which ` +\n 'one owns it. Change one plugin\\'s nav item href.',\n )\n return undefined\n }\n return best\n}\n\n/** A staff page flattened with its owning extension's id. */\nexport interface ConsoleStaffPageEntry extends ConsoleStaffPage {\n pluginId: PluginId\n}\n\n/**\n * Every registered staff page, in registration order — the staff strip's\n * plugin tabs, after the console's own.\n */\nexport function listConsoleStaffPages(\n enabledPluginIds?: readonly PluginId[],\n): ConsoleStaffPageEntry[] {\n return listConsoleExtensions(enabledPluginIds).flatMap((extension) =>\n (extension.staffPages ?? []).map((page) => ({\n ...page,\n pluginId: extension.pluginId,\n })),\n )\n}\n\n/**\n * The staff page at `/admin/{id}` (AGL-2939), or `undefined`.\n *\n * Two plugins claiming one id resolve to nothing, and say so: registry order\n * is an accident of which chunk loaded first, and a staff page that is one\n * plugin's on one load and another's on the next cannot be debugged from the\n * symptom — the same rule {@link resolveConsolePluginPage} applies to paths.\n */\nexport function resolveConsoleStaffPage(\n id: string,\n enabledPluginIds?: readonly PluginId[],\n): ConsoleStaffPageEntry | undefined {\n const matches = listConsoleStaffPages(enabledPluginIds).filter(\n (page) => page.id === id,\n )\n const owners = [...new Set(matches.map((page) => page.pluginId))]\n if (owners.length > 1) {\n console.error(\n `[aglyn] staff page \"/admin/${id}\" is claimed by more than one plugin ` +\n `(${owners.join(', ')}); refusing to guess which one owns it. ` +\n \"Change one plugin's staff page id.\",\n )\n return undefined\n }\n return matches[0]\n}\n\n/** Widgets registered for a slot, across every extension (AGL-419). */\nexport function listConsoleWidgets(\n slot: string,\n enabledPluginIds?: readonly PluginId[],\n): Array<{ extension: ConsoleExtension; widget: ConsoleWidget }> {\n const out: Array<{ extension: ConsoleExtension; widget: ConsoleWidget }> = []\n for (const extension of listConsoleExtensions(enabledPluginIds)) {\n for (const widget of extension.widgets ?? []) {\n if (widget.slot === slot) out.push({ extension, widget })\n }\n }\n return out\n}\n\n/**\n * A built-in theme a plugin contributes (AGL-3404): a complete, JSON\n * {@link HostTheme} a site can pick on Setup → Theme.\n *\n * Picking one COPIES it onto the site, and the site's edits are an override\n * on top, so a preset is never modified by a site and a later version of the\n * plugin never repaints a site that did not pick it again. That is also why a\n * preset needs no server surface: the console sends the picked theme with the\n * request.\n */\nexport interface ConsoleThemePreset {\n /**\n * Unique across every plugin, and persisted in the site's selection — so\n * namespace it with the plugin (`themes.bootstrap`) and never rename it.\n */\n id: string\n /** The name in the picker. */\n name: string\n /** One line under the name: what the theme looks like. */\n description?: string\n theme: HostTheme\n}\n\n/**\n * The load point a plugin contributing theme presets declares in its\n * `console.slots`, and the theme page loads before it lists them — so the\n * presets' code is fetched there and nowhere else.\n */\nexport const THEME_PRESETS_LOAD_POINT = 'hostThemePresets'\n\n/**\n * Every built-in theme the enabled plugins contribute, in registration order,\n * with the plugin each came from. A second preset with an id already listed\n * is dropped rather than shown twice, so two plugins cannot make one entry of\n * the picker ambiguous.\n */\nexport function listConsoleThemePresets(\n enabledPluginIds?: readonly PluginId[],\n): Array<ConsoleThemePreset & { pluginId: PluginId }> {\n const seen = new Set<string>()\n const out: Array<ConsoleThemePreset & { pluginId: PluginId }> = []\n for (const extension of listConsoleExtensions(enabledPluginIds)) {\n for (const preset of extension.themePresets ?? []) {\n if (!preset?.id || seen.has(preset.id)) continue\n seen.add(preset.id)\n out.push({ ...preset, pluginId: extension.pluginId })\n }\n }\n return out\n}\n\n/**\n * A kind of record a plugin lets the console's search find (AGL-3080): the\n * collection its rows are read from, the fields a row is named and matched\n * by, and where a row opens.\n *\n * The palette reads a source the way it reads the console's own groups — one\n * capped window per collection, ordered by document id and matched in the\n * browser — so a source describes a read the Firestore rules already admit\n * and needs no index of its own. `host` reads `hosts/{hostId}/{collection}`\n * under the site that is open; `orgData` reads `orgs/{orgId}/{collection}`,\n * the organization's shared data, through the reader's `visibleTo` tokens\n * under a site (the predicate the rules evaluate) and unfiltered at the\n * organization level, where only an org-wide member is offered it.\n *\n * Gated as every surface the extension registers is: the plugin must be on\n * for the workspace and the site, and the extension's `featureFlag` and\n * `permission` compose with the source's own. A source the reader may not\n * open is never read, because a row that links to a page the reader is\n * refused is a dead row.\n */\nexport interface ConsoleSearchSource {\n /**\n * The group's id, unique across the palette: its rows, its cache and its\n * key in the rendered list are held under it. Name it by the plugin's own\n * words (`contacts`, `products`) and keep it stable.\n */\n id: string\n /** The heading above the group's rows. */\n group: string\n /** The kind in a sentence: \"Only the first 30 {noun} were searched.\" */\n noun: string\n /** Where the collection hangs, which decides the path and who may read it. */\n scope: 'host' | 'orgData'\n /** The collection's name under the scope's root. */\n collection: string\n /** The field holding a row's human-readable name. */\n nameField: string\n /** The field a row is labeled by when `nameField` is empty. */\n fallbackNameField?: string\n /** Further fields a reader may find a row by (an address, a slug). */\n extraFields?: readonly string[]\n /**\n * A plan quota that must be non-zero for the group to be read at all: a\n * collection the organization cannot hold costs a read to render nothing.\n */\n entitlementKey?: string\n /** A plan flag this group needs beyond the extension's `featureFlag`. */\n featureFlag?: keyof OrgFeatureFlags\n /** A permission this group needs beyond the extension's `permission`. */\n permission?: string\n /**\n * Where the group is listed, ascending. The console's own groups hold 10\n * (sites), 20 (pages), 30 (emails) and 100 to 140 (components, layouts,\n * templates, content, authors); equal numbers keep registration order.\n */\n order: number\n /**\n * Where one row opens, or `null` when it cannot be addressed from here — a\n * row with nowhere to go is dropped rather than drawn dead. `host` is the\n * open site's subdomain, or `null` at the organization level.\n */\n href(\n row: Readonly<Record<string, unknown>>,\n context: ConsoleSearchLinkContext,\n ): string | null\n}\n\n/** The two route params a search row is linked from. */\nexport interface ConsoleSearchLinkContext {\n orgSlug: string\n host: string | null\n}\n\n/**\n * The load point a plugin contributing search sources declares in its\n * `console.slots`, and the palette loads before it lists them.\n */\nexport const CONSOLE_SEARCH_LOAD_POINT = 'consoleSearch'\n\n/**\n * Every search source the enabled plugins contribute, with the extension\n * each came from so the caller can apply its gates. A second source with an\n * id already listed is dropped, so two plugins cannot share one group.\n */\nexport function listConsoleSearchSources(\n enabledPluginIds?: readonly PluginId[],\n): Array<{ extension: ConsoleExtension; source: ConsoleSearchSource }> {\n const seen = new Set<string>()\n const out: Array<{ extension: ConsoleExtension; source: ConsoleSearchSource }> = []\n for (const extension of listConsoleExtensions(enabledPluginIds)) {\n for (const source of extension.searchSources ?? []) {\n if (!source?.id || seen.has(source.id)) continue\n seen.add(source.id)\n out.push({ extension, source })\n }\n }\n return out\n}\n\n/** Providers registered by every extension, in registration order. */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function listConsoleProviders(\n enabledPluginIds?: readonly PluginId[],\n): Array<ComponentType<any>> {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const out: Array<ComponentType<any>> = []\n for (const extension of listConsoleExtensions(enabledPluginIds)) {\n out.push(...(extension.providers ?? []))\n }\n return out\n}\n"],"names":["runInAction","MUI_BUNDLE_ID","defineUiFeatureBundle","options","registrar","dependencies","id","dependsOn","$id","bundleId","displayName","title","description","icon","load","entry","components","registerComponent","component","schema","presets","length","registerPreset","destroy","unregisterPreset","map","preset","unregisterComponent","CONSOLE_WIDGET_SLOTS","hostActivity","hostDashboard","orgDashboard","commerceGlance","orgData","besignerFunctions","hostArtifactPublish","orgPluginInstalls","pluginInstallStatus","templateGallery","templateInstallStatus","dashboardFooter","orgSettings","hostSettings","hostFirstRun","hostTheme","staffOverview","adminOrgDetail","orgBillingUsage","orgBillingOverview","staffOrg","staffUser","staffSite","staffOrgsListColumn","staffOrgUsageColumn","orgMember","orgMembersListColumn","hostMembers","siteMember","consoleDock","consoleTopBar","besignerInspector","besignerInteractions","seoFields","hostSeo","recordInsights","recordEmail","importMapping","besignerToolbar","besignerPageProperties","hostScreenRow","hostScreens","hostTemplates","hostLayouts","hostComponents","orgSites","sitePackageItemPreview","CONSOLE_STAFF_WIDGET_SLOTS","isConsoleStaffWidgetSlot","slot","includes","CONSOLE_HOST_GATED_WIDGET_SLOTS","isConsoleHostGatedWidgetSlot","consoleExtensions","Map","registerConsoleExtension","extension","set","pluginId","unregisterConsoleExtension","delete","listConsoleExtensions","enabledPluginIds","all","Array","from","values","enabled","Set","filter","has","listConsoleNavItems","inTabOrder","flatMap","navItems","navItem","unlisted","featureFlag","tabOrder","entries","tabOrderOf","index","order","sort","a","b","matchNavItem","href","current","matchNavItemHref","legacyHref","legacyHrefs","match","legacy","undefined","itemHref","segments","sections","ownsSubtree","startsWith","slice","split","Boolean","section","find","item","resolveConsolePluginPage","resolvePluginPageAmong","listConsoleOrgNavItems","orgNavItems","resolveConsoleOrgPluginPage","navItemsOf","best","contenders","Component","bestLength","push","console","error","join","listConsoleStaffPages","staffPages","page","resolveConsoleStaffPage","matches","owners","listConsoleWidgets","out","widget","widgets","THEME_PRESETS_LOAD_POINT","listConsoleThemePresets","seen","themePresets","add","CONSOLE_SEARCH_LOAD_POINT","listConsoleSearchSources","source","searchSources","listConsoleProviders","providers"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/feature-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 * Feature-plugin pattern (AGL-277, AGL-395). Each feature ships as one lib\n * under `libs/plugins/{feature}` (moved out of the old `.../ui/` nesting)\n * that owns both halves and never merges into `plugins-mui` (which stays\n * pure component/theme definitions):\n *\n * - UI half → besigner/host components. Builds its bundle with\n * `defineUiFeatureBundle` (which depends on the mui bundle so\n * primitives/theming resolve first) and registers it with\n * `Aglyn.plugins.addDependency`, exactly like the mui bundle itself.\n * Registered per-editor via `register{Feature}Plugin()`.\n * - Console half → a `ConsoleExtension` registered with\n * `registerConsoleExtension` via a separate `register{Feature}Console()`\n * entry point (so app-load registration pulls no canvas code). The\n * console shell renders nav items + their pages, dashboard cards, and\n * settings sections from the registry, gated by the feature flag.\n *\n * This module is pure (no registry singletons) per app-utils layering;\n * the plugin libs close the loop by passing `Aglyn.components` in.\n * Reference implementation: events-calendar (AGL-313/394); commerce and\n * email follow the same shape (AGL-290/346, relocated in AGL-395).\n */\n\nimport { runInAction } from 'mobx'\nimport type { OrgPermissions } from '../app-utils/org-permissions'\nimport type { ReleaseFlagKey } from '../app-utils/release-flags'\nimport type { SeoAuditReport } from '../app-utils/seo-audit'\nimport type { SeoListingFieldKey } from '../app-utils/seo-listing-fields'\nimport type { AglynOrgBilling, OrgFeatureFlags } from '../foundation'\nimport type {\n ComponentSchema,\n MdiIconProps,\n PresetSchema,\n} from '../types/nodes'\nimport type { Plugin, PluginId } from './plugin-manager'\nimport type { HostThemeSource } from '../app-utils/site-theme'\nimport type { HostTheme, HostThemeScheme } from '@aglyn/shared-data-types'\nimport type { ComponentType } from 'react'\nexport type {\n ConsoleImportMappingZoneProps,\n ConsoleRecordEmailZoneProps,\n ConsoleRecordInsightsZoneProps,\n} from './record-zone-props'\n\n/** The mui bundle id every UI feature bundle depends on. */\nexport const MUI_BUNDLE_ID: PluginId = 'mui'\n\nexport interface FeatureBundleEntry {\n component: any\n schema: ComponentSchema<any>\n presets?: PresetSchema[]\n}\n\n/** The slice of ComponentManager a feature bundle needs (structural). */\nexport interface ComponentRegistrar {\n registerComponent(component: any, schema: ComponentSchema<any>): void\n registerPreset(presets: PresetSchema[]): void\n unregisterComponent(componentId: string): void\n unregisterPreset(presetIds: string[]): void\n}\n\nexport interface UiFeatureBundleOptions {\n /** Stable bundle id — persisted as `pluginId` in screen docs; never rename. */\n bundleId: PluginId\n displayName: string\n description?: string\n icon?: MdiIconProps\n /** Extra bundle ids this feature needs beyond mui. */\n dependsOn?: PluginId[]\n components: FeatureBundleEntry[]\n}\n\n/**\n * UI half of the pattern: a plugin-registry bundle whose load/destroy\n * register the feature's components + presets against the given\n * registrar (`Aglyn.components` in apps), declared as depending on the\n * mui bundle so the registry loads mui first.\n */\nexport function defineUiFeatureBundle(\n options: UiFeatureBundleOptions,\n registrar: ComponentRegistrar,\n): Plugin {\n const dependencies: Record<PluginId, true> = { [MUI_BUNDLE_ID]: true }\n for (const id of options.dependsOn ?? []) dependencies[id] = true\n return {\n $id: options.bundleId,\n displayName: options.displayName,\n title: options.displayName,\n description: options.description,\n icon: options.icon,\n dependencies,\n load(): void {\n // One mobx transaction per bundle (AGL-371): observers (component\n // drawer, canvas) re-render once instead of once per registration.\n runInAction(() => {\n for (const entry of options.components) {\n registrar.registerComponent(entry.component, entry.schema)\n }\n for (const entry of options.components) {\n if (entry.presets?.length) registrar.registerPreset(entry.presets)\n }\n })\n },\n destroy(): void {\n runInAction(() => {\n for (const entry of options.components) {\n if (entry.presets?.length) {\n registrar.unregisterPreset(\n entry.presets.map((preset) => preset.$id),\n )\n }\n }\n for (const entry of options.components) {\n registrar.unregisterComponent(entry.schema.$id)\n }\n })\n },\n }\n}\n\n/**\n * One of the organization's sites, as the shell hands them to a surface\n * mounted at the ORGANIZATION level (AGL-2630).\n *\n * The three facts a cross-site surface needs and a record never carries: a\n * record holds a host DOCUMENT ID, a console URL under `/hosts/[host]` takes\n * the SUBDOMAIN, and a person reads the NAME. `subdomain` is null for a site\n * whose document did not answer — such a site is still named (the\n * relationship is real) and never linked (the route would not exist).\n */\nexport interface ConsolePluginOrgHost {\n id: string\n name: string\n subdomain: string | null\n}\n\n/**\n * The organization a surface is mounted under when it is mounted at the\n * org level rather than under a site (AGL-2630).\n *\n * The CRM exists at two levels: the site hub, where `hostId` names the site\n * and every read is scoped to it, and `/[orgSlug]/crm`, where an org-wide\n * member sees every site's records at once. At the org level there is no\n * site, so the shell hands the org's own site list instead — the pickers a\n * create needs (a record is always captured BY a site) and the names a\n * cross-site fact is shown under. `hostsReady` separates \"no sites\" from\n * \"not yet\".\n */\nexport interface ConsolePluginOrgMount {\n orgId: string\n /**\n * The organization's URL slug (AGL-3080) — the segment its console pages\n * hang under, and what a surface needs to link to one of its siblings.\n *\n * Carried because plugins were reconstructing it: from `hostsPath` by\n * splitting a string, or by resolving a site's org through an async lookup\n * that can come back empty and leave a link unbuilt (AGL-867). The shell\n * has it synchronously; every plugin paying for it again, differently, is\n * the cost of not handing it over.\n */\n orgSlug: string\n hosts: readonly ConsolePluginOrgHost[]\n hostsReady: boolean\n /**\n * The console path every site's own hub hangs beneath — `/[orgSlug]/hosts`\n * — so a cross-site fact can link a person into the site that holds them:\n * a site's CRM is `${hostsPath}/${subdomain}/crm`. A path rather than a\n * builder because the mount is data the shell hands over and a plugin\n * cannot import the console's route table.\n */\n hostsPath: string\n /**\n * Where this organization manages its PLAN — the console's billing page\n * for the org (AGL-3080).\n *\n * Here for the same reason `hostsPath` is: a plugin cannot import the\n * console's route table, and a surface that has to say \"on a paid plan the\n * cut is lower\" is useless without somewhere to send the person who just\n * read it. The shell's own upgrade notice covers an ENTITLEMENT refusal,\n * where the surface never renders; this covers the case the surface renders\n * fine and the plan is still the answer — a marketplace publisher seeing\n * the free-plan fee on a listing they are about to price.\n *\n * OPTIONAL, and a plugin must branch on it rather than assume it: a\n * deployment that bills nobody has no such page, and a self-hoster who\n * removed it should not get a plugin's link into a 404. The shell's own\n * upgrade notice makes the same allowance. `resolveOrgMount` supplies it\n * for every mount this console builds.\n *\n * A plugin renders a link to it or does not, and never parses it.\n */\n billingPath?: string\n}\n\n/**\n * Props every plugin-contributed console page receives from the shell's\n * generic host route. The shell owns auth + chrome + flag gating and\n * passes the resolved host and entitlement state in, so plugin pages stay\n * free of console-app hooks.\n */\nexport interface ConsolePluginPageProps {\n /**\n * The site this surface is mounted under — or `null` when it is mounted at\n * the ORGANIZATION level, where there is no site and {@link orgMount} says\n * which org (AGL-2630). Two things mount there: the CRM's own org route,\n * and every {@link ConsoleExtension.orgNavItems} surface through the\n * generic org route (AGL-2974). A surface reached through a site route\n * always receives a string.\n */\n hostId: string | null\n /** Present only at an org-level mount — see {@link ConsolePluginOrgMount}. */\n orgMount?: ConsolePluginOrgMount\n /**\n * Every workspace the SIGNED-IN PERSON belongs to, as the org switcher\n * already names them (AGL-3080) — not the mounted org's siblings, and\n * nothing about what any of them contain.\n *\n * For a surface whose subject crosses workspaces. The marketplace's\n * licences panel is the case: a purchase licenses one organization, so\n * \"I bought this once — which workspace did the licence land in?\" is a\n * question about the BUYER, and the answer is a name the reader already\n * sees in the switcher. Resolving it from ids would be a read per row of\n * documents the shell is already holding.\n *\n * Absent when the shell has not resolved them, and a surface must name the\n * id rather than wait: this is a label, and a row with a raw id in it is\n * worse than nothing only if the row does not appear at all.\n */\n viewerOrgs?: readonly { id: string; name: string }[]\n /** True when the org holds the extension's `featureFlag` entitlement. */\n entitled: boolean\n /**\n * The absolute console path this surface is mounted at — the nav item's\n * `href` under the active org and site, e.g. `/acme/hosts/shop/products`\n * (AGL-2501).\n *\n * A plugin page is handed a host DOC ID and nothing else, so building a\n * link to itself meant resolving the org slug and subdomain from Firestore\n * — two `getDoc`s that answer `null` on first paint, which for a section\n * rail means drawing it without hrefs. The shell already knows this string\n * synchronously; the alternative is paying for it again, later, per page.\n */\n basePath?: string\n /**\n * The nav item's declared {@link ConsoleNavItem.sections}, resolved: an\n * absolute `href` per section, and the release-flag verdict already applied\n * to `visible` (AGL-2501).\n *\n * The plugin DECLARES sections; the shell RESOLVES them. Release flags live\n * in `scope:app` and a `scope:lib` plugin may not import the hooks that read\n * them, so a page that filtered its own rail could only do it by guessing —\n * and a rail offering a link into the shell's own \"coming soon\" notice is\n * the guess going wrong. Feed this straight to `HubSections`.\n */\n sections?: readonly ResolvedConsoleNavSection[]\n /**\n * The id of the section the URL names, or undefined on the nav item's own\n * href (AGL-2501).\n *\n * Always one of the declared `sections` — the shell 404s an id it does not\n * recognize rather than passing it down, so a page may switch on this\n * without a fallback branch for a section it does not have.\n */\n section?: string\n /**\n * Path segments beneath `basePath`, `[]` on the nav item's own href\n * (AGL-2501). `segments[0]` is `section`; anything after it is the section's\n * own, so a section can own deeper routes (`…/orders/ord_123`) without a\n * further registry change.\n */\n segments?: readonly string[]\n /**\n * The ORG billing doc (`orgs/{orgId}`) the shell already loaded to\n * compute `entitled` (prop renamed from `tenant` in AGL-444). Passed\n * through so a plugin page can run its own `checkEntitlement`/\n * `checkQuota` (e.g. per-plan service limits) without reaching for the\n * console-app org/session hooks.\n */\n org?: Partial<AglynOrgBilling>\n /**\n * The signed-in user's resolved org permissions (AGL-395), passed through\n * so a plugin page can gate actions (e.g. install/publish) without the\n * console-app session/permission hooks.\n */\n /**\n * Widened past the legacy six (AGL-2474): plugin-declared keys such as\n * commerce's `managePos` are resolved into the same map, and typing this\n * `Partial<OrgPermissions>` meant a plugin could not read its OWN\n * permission without a cast — the declared key was not assignable.\n */\n permissions?: Partial<OrgPermissions> & Record<string, boolean | undefined>\n /**\n * The verdict for the release flag that governs this surface (AGL-1662),\n * resolved by the shell from the nav item's `navTabId` — the same flag\n * `FeatureGate` applies around the page body.\n *\n * `FeatureGate` admits staff with the flag OFF, so a plugin page can be\n * looking at an org that does not have the feature and is not being\n * billed for it. Anything the page says about MONEY has to follow the\n * flag rather than the viewer, and this is how a plugin page gets that\n * answer without reaching for the console-app release-flag hooks (which\n * live in `scope:app` and are off-limits to a `scope:lib` plugin).\n */\n releaseFlag?: {\n /**\n * The rollout verdict for this ORG — staff bypass deliberately NOT\n * applied. `visible` is what decides who sees a page; this is what\n * decides what the invoice carries, and staff opening a page must not\n * put a line on a customer's bill.\n */\n released: boolean\n /**\n * True once the flag verdict has settled. Release flags are default-off\n * before Remote Config activation, so an ungated claim asserts the\n * withheld case for one paint on an org that may well be billed.\n */\n ready: boolean\n }\n /**\n * What the viewer's role on THIS SITE lets them do, for a surface that\n * publishes.\n *\n * The `author` host role edits content and may not make it live; that is\n * enforced in the Firestore rules and by the promotion routes, and the\n * console's job is to say no with a reason rather than let a click come\n * back as a bare `permission-denied`. Resolving it needs the org member\n * document and the host-access predicate over it, which the shell already\n * reaches for — a plugin cannot, for the same reason it cannot read a\n * release flag.\n *\n * `loaded` separates \"no\" from \"not yet\", so a surface disables with a\n * reason rather than hiding a control that is about to be allowed. Read it\n * the safe way round: `canPublish` is false until the read lands.\n */\n hostRole?: {\n canPublish: boolean\n loaded: boolean\n }\n}\n\nexport type ConsolePluginPage = ComponentType<ConsolePluginPageProps>\n\n/**\n * One routed section of a plugin console page (AGL-2501).\n *\n * A section is a real URL beneath the nav item's `href`, not a panel: it is\n * linkable, the back button walks sections, and the page mounts the one being\n * read. That last part is the reason this exists — a six-panel hub subscribes\n * every panel's queries on load, and the reader is looking at one.\n */\nexport interface ConsoleNavSection {\n /**\n * URL segment beneath the nav item's `href`, and the id the shell hands the\n * page as `section`. Appears in links people keep — treat it as persisted.\n */\n id: string\n label: string\n /**\n * Release-flag nav-tab id gating THIS section, when it ships on a different\n * schedule than the surface around it. Omit to inherit the nav item's gate,\n * which is the common case.\n *\n * Declaring one NARROWS, never widens: the nav item's own gate is applied\n * outside this one, so a section of a flagged-off surface stays unreachable\n * whatever it declares. A section gated by its own flag is refused on a deep\n * link exactly as it is hidden from the rail — one verdict, both places.\n */\n navTabId?: string\n /**\n * Entitlement flag gating THIS section, when the org's plan may include\n * the surface and not the whole of it (AGL-2611). Omit to inherit the\n * extension's `featureFlag`, which is the common case.\n *\n * Composes by AND with the extension's, the way `navTabId` composes with\n * the nav item's release gate: the shell answers the extension's flag\n * first and this one inside it, so a section can only ever be NARROWER\n * than the surface holding it. A section this refuses is resolved\n * `locked` for the rail and refused on a deep link with the shell's own\n * upgrade notice — one verdict, both places — and the page body never\n * mounts, which is the whole of the shell's promise about entitlements.\n *\n * The case it exists for is a hub whose first section ships on every\n * plan and whose others do not: the CRM's contacts list is on Free, and\n * the sales suite built on that list starts at Starter.\n */\n featureFlag?: keyof OrgFeatureFlags\n /**\n * Permission key gating THIS section, when the surface is open to every\n * member and part of it is not (AGL-3080). Omit to inherit the\n * extension's and the nav item's, which is the common case.\n *\n * The third gate, composed the way the other two are: ANDed with what the\n * extension and the nav item already require, so a section can only ever\n * be narrower than the surface holding it. A section this refuses is not\n * drawn in the rail at all — unlike a `featureFlag` refusal, which draws\n * locked and links to the notice that sells it, because a permission is\n * not something the reader can buy — and a deep link to it is answered\n * with the shell's refusal instead of its body. One verdict, both places.\n *\n * ⚠️ NOT a replacement for the server's rule. The rules and the plugin's\n * own handlers enforce this regardless of what renders; this keeps a\n * reader from being offered a page that is about to refuse them.\n *\n * The case it exists for is a hub most of a workspace uses and whose\n * seller half only a publisher does: the Marketplace's browse, installed\n * and licences sections are every member's, and listings, upload, sales\n * and payouts read the organization's revenue.\n */\n permission?: string\n /**\n * Query keys that land a BARE hub URL on this section instead of on the\n * first one the reader may open (AGL-3080).\n *\n * The case it exists for is a return URL held by somebody else. Stripe\n * bakes `?connect=` into account-onboarding links and `?purchase=` into\n * checkout sessions, so a seller part-way through onboarding is carrying\n * one right now — in a third party's records, not ours, and unfixable from\n * this side once it lands somewhere that means nothing to them. A seller\n * coming back from Connect wants Payouts; a buyer coming back from\n * checkout wants what they now own.\n *\n * The key's VALUE is not read, only its presence: these are markers, and a\n * marker nothing routes on still survives the hop, which is what makes it\n * safe for anyone to add one. The gates still apply — a section this\n * claims but the reader may not open is not landed on, and the bare rule\n * takes over.\n */\n landsOnQuery?: readonly string[]\n}\n\n/** A {@link ConsoleNavSection} with the shell's answers filled in. */\nexport interface ResolvedConsoleNavSection {\n id: string\n label: string\n /** Absolute console path — `${basePath}/${id}`. */\n href: string\n /** False when this section's release flag hides it from this viewer. */\n visible: boolean\n /**\n * True when the org's SETTLED plan does not carry the section's\n * `featureFlag` (AGL-2611). The rail draws it locked and still links it —\n * the notice behind the link is the way to buy it — and the shell refuses\n * the body. Never true while the org read is pending: an unsettled plan\n * is not a refusal, and a lock that appeared for one paint on a paying\n * workspace would be the AGL-1380 defect in a new place.\n */\n locked?: boolean\n /**\n * True when this section's own `permission` refuses this reader\n * (AGL-3080) — the reason it is not `visible`, kept apart from the\n * release flag's so a deep link is answered with the refusal that\n * applies rather than with \"coming soon\". Never true while the member\n * read is pending: the permission map answers as an admin's until it\n * lands, so an unsettled read is neither a grant nor a refusal.\n */\n refused?: boolean\n /** The section's declared {@link ConsoleNavSection.landsOnQuery}, carried\n * through so the shell's landing rule can read it (AGL-3080). */\n landsOnQuery?: readonly string[]\n}\n\nexport interface ConsoleNavItem {\n label: string\n /**\n * Host-relative console route (e.g. '/events'). The shell mounts it\n * under the active host ('/[hostId]/events') via its generic plugin\n * route, so the same string keys both the nav link and the page.\n */\n href: string\n icon?: MdiIconProps\n /**\n * Release-flag nav-tab id (e.g. 'nav-tab-events'). Lets the shell apply\n * the same staff-preview gating hardcoded tabs get; omit for always-on.\n */\n navTabId?: string\n /**\n * The permission THIS surface requires, when it is narrower than the\n * extension's own {@link ConsoleExtension.permission}.\n *\n * Declaring one NARROWS, never widens: the extension's requirement is\n * applied alongside this one and both must be held, so a surface cannot\n * escape its extension's gate by naming a key its reader happens to have.\n * The composition is the release-flag one a nav item and its section\n * already have, for the same reason.\n *\n * The granularity exists because one extension can register surfaces with\n * genuinely different answers — a catalog anyone who edits the site may\n * open, beside a register that takes money.\n */\n permission?: string\n /**\n * Page body rendered by the shell's generic host route. When present,\n * the plugin owns the whole surface — no core page file needed.\n */\n Component?: ConsolePluginPage\n /**\n * Routed sections of this page (AGL-2501). Each becomes a URL at\n * `${href}/${section.id}`, and the shell tells the page which one it is on.\n *\n * Optional, and omitting it is not a lesser option — it means the surface is\n * ONE page, which is what every plugin surface was before this existed and\n * what most should stay. A nav item without sections resolves exactly as it\n * always has: its own href and nothing beneath it, so a path under it is a\n * 404 rather than this page rendered again.\n */\n sections?: readonly ConsoleNavSection[]\n /**\n * Whether this nav item claims every path beneath its own href, with no\n * declared section naming them.\n *\n * The case is a surface whose deeper URLs are ENTITIES rather than\n * sections: `/forms` is a list, `/forms/{formId}` is one of its rows, and\n * the set of ids is a property of the workspace's data, so no static\n * `sections` list could enumerate them. Without this a nav item matches its\n * own href and nothing else, and every row's page is a 404.\n *\n * The trade is deliberate and is why it must be asked for. A surface that\n * owns its subtree can no longer distinguish a typo'd path from an entity\n * id — `/forms/bogus` reaches the page rather than the shell's 404 — so it\n * takes on the duty of saying \"no such thing\" itself, which a list-detail\n * surface has to be able to do anyway for an id that was deleted while a\n * link to it was still in someone's inbox.\n *\n * Sections win where both are declared: an id the `sections` list names is\n * resolved as a section, and this only widens what happens when none\n * matches.\n */\n ownsSubtree?: boolean\n /**\n * The browser tab's noun on a RECORD beneath a surface that\n * {@link ownsSubtree} (AGL-3596), where the surface's `label` would stand\n * otherwise: `AI jobs` lists a site's jobs at `/ai-jobs`, and one job's\n * page at `/ai-jobs/{jobId}` is `Building your site`.\n *\n * The tab title is built on the server from the URL alone, which never\n * reads the record, so this is one fixed string per surface — the noun for\n * what a record's page is, not the record's name. It is read from the\n * plugin's source by `tools/scripts/generate-plugin-manifests.mjs` into the\n * titles manifest, and so must be a string literal beside a literal `href`.\n */\n recordTitle?: string\n /**\n * A page with an address and no tab (AGL-3594): served at its `href` like\n * any nav item, and left off the site's tab strip. For a surface a person\n * is SENT to — the page a flow lands on, the page a notification opens —\n * rather than one they browse to; the gates and the matching are the same.\n */\n unlisted?: boolean\n /**\n * Hrefs this nav item answered to before it moved (AGL-2595).\n *\n * A console path is something people keep — a bookmark, a docs link, an\n * email from the console itself — and a nav item that changes its `href`\n * would otherwise turn every one of them into the shell's \"not available\"\n * notice. Matching here is identical to matching on `href` (the same\n * sections, the same subtree rule), and the resolved page carries\n * `legacy: true` so the shell can replace the address with the current one\n * rather than leave a moved page living at two.\n */\n legacyHrefs?: readonly string[]\n /**\n * Where this item's tab sits among the plugin tabs of its strip\n * (AGL-3294): lower first, absent is 0, and a tie keeps registration\n * order.\n *\n * Registration order is otherwise the order, and it is not a tab's to\n * choose: it follows the plugin registry, which also orders the staff\n * strip, the providers and every widget zone the plugin fills — so moving\n * one tab by moving its plugin would move everything else it registers.\n * This moves the tab and nothing else. The shell's own tabs are not in the\n * comparison; the plugin tabs sit as one block between them.\n */\n tabOrder?: number\n /**\n * Dashboard header for the plugin page (title + icon), and the docs topic\n * its help `?` explains.\n *\n * `docsTopic` is a plain string rather than the console's\n * `DocsHelpTopicKey` because that registry lives in `apps/console` and a\n * lib cannot import from an app. The console validates it and falls back\n * to the marketplace topic when it does not resolve — which is not just\n * defensive: a third-party plugin can name any string, and the alternative\n * to a fallback is a help button that throws on hover (AGL-1074).\n *\n * Every surface mounted by the shell's generic plugin route shares one\n * `help=` prop, so a surface that omits this is not \"help-less\" — it\n * inherits Plugins & Marketplace, which reads as if it were its own.\n *\n * `docsAnchor` deep-links the help to one heading of that topic's page\n * (`#at-the-organization-level`), for a surface whose page explains this\n * mount under a heading of its own. It is validated the same way: an\n * anchor the topic's page does not carry is dropped, and the help opens the\n * top of the page.\n */\n header?: {\n title: string\n icon?: MdiIconProps\n docsTopic?: string\n docsAnchor?: string\n }\n}\n\nexport interface ConsoleDashboardCard {\n /** Card registry key the dashboard resolves to a component. */\n cardId: string\n title: string\n}\n\nexport interface ConsoleSettingsSection {\n sectionId: string\n title: string\n /** Rendered inside the org/host settings surface when present (AGL-419). */\n Component?: ComponentType<ConsolePluginPageProps>\n}\n\n/**\n * The injection-zone catalog (AGL-433, Strapi injection-zone parity):\n * every named slot the console shell renders through `PluginWidgetSlot`,\n * with what the slot receives. `slot` stays an open string so apps can\n * add custom zones without a core release; these are the guaranteed ones.\n */\nexport const CONSOLE_WIDGET_SLOTS = {\n /** Host dashboard + screen view activity column. Props: hostId. */\n hostActivity: 'hostActivity',\n /**\n * The host dashboard's glance row — one card per capability the site\n * actually has. Props: hostId.\n *\n * The dashboard's own cards used to be imported by the page, which made\n * enablement a decision nobody was making: `New site users` rendered on a\n * site that has never turned member accounts on, and `Last campaign` on a\n * workspace with the email plugin switched off. A card that answers a\n * question about a capability belongs to the capability, so it registers\n * here and the shell's entitlement + enablement gate decides.\n */\n hostDashboard: 'hostDashboard',\n /**\n * The organization's sites page, above the site grid (AGL-2636): the\n * org-level twin of `hostDashboard`, for a card that totals the\n * organization rather than one site. Props: `hostId` (always `null`),\n * `orgMount` (the org and its sites — the {@link ConsolePluginOrgMount}\n * the org-level hub page hands its plugin page), `basePath` (that hub's\n * own path, `/[orgSlug]/crm`, for the links a widget builds when there is\n * no site to derive them from).\n *\n * Every widget here reads ACROSS the host boundary — an org-wide total\n * carries no scope clause — which is the one read a site collaborator may\n * never make, so the page gates the whole row on the org hub's own access\n * verdict before any widget mounts, and renders nothing at all (no empty\n * row, no heading) when no widget survives the gates.\n */\n orgDashboard: 'orgDashboard',\n /** Host dashboard commerce summary. Props: hostId, org. */\n commerceGlance: 'commerceGlance',\n /**\n * A site's Analytics page, below its own traffic cards (AGL-3605): a\n * section of analytics a plugin computes from what the site records — a\n * funnel, say. Props: hostId, orgId (the workspace, or undefined while it\n * loads).\n *\n * A section rather than a glance, so a widget here owns a whole card: its\n * own range picker, its own plan answer and its own empty state.\n */\n hostAnalytics: 'hostAnalytics',\n /** Org Data page body. Props: orgId, org. */\n orgData: 'orgData',\n /** Besigner functions (ƒx) panel. Props: hostId. */\n besignerFunctions: 'besignerFunctions',\n /**\n * Wherever a console page offers to publish something it holds (AGL-3080).\n * Props: {@link ConsoleArtifactPublishZoneProps}.\n *\n * A layouts page, a components page and the org publish panel each held a\n * dialog that posted to `marketplace/publish-layout` and read the\n * marketplace's price floor. The console knows it has a layout; where a\n * layout can be PUBLISHED, what a listing costs at the least, and what to\n * call the thing in the copy all belong to whatever sells it.\n *\n * ── The page says what it has, never where it goes ───────────────────────\n *\n * The zone is handed a {@link ConsolePublishableArtifact} in the console's\n * own vocabulary — a kind, the site or org it belongs to, the document id —\n * and the widget decides the endpoint, the noun and the form. A kind the\n * widget does not publish is one it draws nothing for, which is the same\n * answer as no widget at all.\n *\n * ── An offer with nowhere to go must not be made ─────────────────────────\n *\n * The control that OPENS this — a menu item, a button — belongs to the page\n * and is not a widget, because it is one entry of a list the page builds.\n * So a page offering it asks `useSlotWidgets` whether this zone has a\n * widget at all and leaves the entry out when it does not. Drawing it\n * anyway would be a menu item that opens nothing on a workspace with no\n * marketplace.\n */\n hostArtifactPublish: 'hostArtifactPublish',\n /*\n * `marketplaceListing`, `marketplaceCapability`, `orgMarketplace` and\n * `orgAddons` were here until AGL-3080, and are gone rather than deprecated.\n *\n * Each existed for one reason: a CONSOLE ROUTE had to show marketplace UI\n * and an app may not import a plugin. AGL-3080 moved those routes into the\n * marketplace plugin, where the components are plain imports — so the four\n * zones had no drawer left, and a zone nothing draws is a contract nothing\n * can be held to. `plugin-widget-slot-zones.spec.ts` is what noticed, which\n * is the whole reason that inventory exists.\n *\n * `pluginSiteSet` and `hostArtifactPublish` stayed, and the difference is\n * the test: both are drawn by a console page that is NOT the marketplace —\n * the installation detail page and a site's layouts list — offering a\n * marketplace action in passing.\n */\n /**\n * The org Plugins page, above the built-in plugins (AGL-3080): the code a\n * plugin has INSTALLED into this workspace, one row per installation.\n * Props: {@link ConsoleOrgPluginInstallsZoneProps}.\n *\n * The Plugins page is the workspace's inventory — what it runs, wherever it\n * came from. The built-in half is the shell's own: the switchboard catalog\n * says what ships. The installed half is not: where an installation is\n * pinned, what version it runs, whether a newer one may be installed and\n * whether its publisher's kill switch is thrown are all facts held by the\n * plugin that installed it, in collections that are its own. So the page\n * hands over the sites it can see and the plugin draws its installations.\n *\n * Every row links to `/[orgSlug]/plugins/[pluginRef]`, the shell's\n * installation page, keyed by whatever id the installing plugin pins by —\n * that page is the one place an installation is managed, whoever drew the\n * row.\n */\n orgPluginInstalls: 'orgPluginInstalls',\n /**\n * The installation page of a plugin some plugin INSTALLED, above where it\n * runs (AGL-3080): what the installer says about the version this\n * workspace runs. Props: {@link ConsolePluginInstallStatusZoneProps}.\n *\n * Drawn only for an installation that exists — a first-party plugin has no\n * version to be behind, and a page for code installed nowhere says so\n * itself. A widget here reports; it never installs. Applying an update is\n * the installing plugin's own surface, which the widget may link to.\n */\n pluginInstallStatus: 'pluginInstallStatus',\n /**\n * The template gallery — \"Start from a template\" on a site's Screens,\n * Layouts and Components pages — below the site's own templates and the\n * starters (AGL-3080): a shelf of templates a plugin offers to INSTALL.\n * Props: {@link ConsoleTemplateGalleryZoneProps}.\n *\n * The gallery is the shell's: what the site holds and what ships with the\n * platform. Templates offered from elsewhere are the offering plugin's —\n * what it lists, how it is searched, what it costs and the route that\n * installs one, with every check that route makes — so the dialog hands\n * over the kind it picks and the word typed in its search, and the plugin\n * draws its own shelf. An install lands in the site's library and\n * publishes nothing; the widget calls `onInstalled` so the gallery closes.\n *\n * The dialog's \"nothing matches\" line covers every shelf, so a widget\n * reports whether its shelf is loading, empty or showing something through\n * `reportShelf`. A widget that never reports is a shelf the line ignores;\n * with no widget at all the gallery is the site's own and the starters.\n */\n templateGallery: 'templateGallery',\n /**\n * One row of a site's Templates library whose template a plugin INSTALLED,\n * beside its Source badge (AGL-3080): what the installer says about the\n * copy the site holds — that a newer version can be installed, and the\n * control that installs it. Props: {@link ConsoleTemplateInstallStatusZoneProps}.\n *\n * Drawn once per such row, and never for a template saved here or a\n * starter. A widget draws nothing for a template it did not install, and\n * nothing when there is nothing to say. Applying an update goes through\n * the installing plugin's own route, which keeps all its checks; the\n * library's rows re-read the templates it replaced.\n */\n templateInstallStatus: 'templateInstallStatus',\n /** Bottom of the host dashboard. Props: hostId, org. (AGL-433) */\n dashboardFooter: 'dashboardFooter',\n /** Org settings page, below the tabbed cards. Props: orgId, org. */\n orgSettings: 'orgSettings',\n /** Host setup page, below the built-in cards. Props: hostId, org. */\n hostSettings: 'hostSettings',\n /**\n * The top of the page a newly created site lands on (AGL-2918): where a\n * widget may offer to START the site for the person rather than leave them\n * an empty one. Props: {@link ConsoleHostFirstRunZoneProps}.\n *\n * ── The blank path is the default, not the fallback ──────────────────────\n *\n * The site already exists, blank, and the page beneath this zone is the\n * ordinary one every site gets. A widget here is an OFFER on top of that\n * page: it asks the person what they want, it builds only what they then\n * confirm, and everything it makes is a draft the site does not serve.\n *\n * So a zone with no widget — no plugin loaded, a feature not released, a\n * reader without the permission — is not a degraded state. It is the blank\n * path, unchanged and complete, which is why nothing on the page below\n * depends on anything here.\n *\n * ── A widget here MUST offer `startBlank` ────────────────────────────────\n *\n * A guided start that a person cannot leave turns creating a site into a\n * funnel, so `startBlank` is handed down rather than left to each widget to\n * invent: a widget draws it where its questions START, not at the end of\n * them, and taking it leaves the person on this same page with nothing\n * begun behind them. The shell remembers the choice for this site and stops\n * asking.\n *\n * A widget that takes the screen rather than sitting on the page — a full\n * screen dialog, an overlay — owes the same exit in every shape it has one:\n * a close control, and the key a person presses to dismiss it. Each of them\n * is `startBlank`, because a takeover somebody can only dismiss BACK INTO is\n * the funnel this zone exists to refuse, and nothing here is a half-answered\n * state worth returning to.\n */\n hostFirstRun: 'hostFirstRun',\n /**\n * The host setup Theme section, between the Theme picker and the editor\n * (AGL-2938). Props: {@link ConsoleHostThemeZoneProps} — the\n * site, the theme the editor shows, where that theme came from, the\n * editor's own preview, and `proposeDraft`, which puts a theme in the\n * editor as unsaved changes.\n *\n * A widget here proposes and never writes. The person saves what it\n * proposed through the editor's own Save — the guarded write that stores\n * an installed theme's edits as its override patch — or discards it. A\n * palette importer, a brand kit and a generator are the same shape of\n * widget.\n */\n hostTheme: 'hostTheme',\n /**\n * Inside the theme editor's Typography card (AGL-3656): the control that\n * chooses the site's fonts. Props: {@link ConsoleThemeEditorFontsZoneProps}\n * — the site, the editor's draft, and `updateDraft`, which changes the\n * draft as any of the editor's own controls does.\n *\n * A widget here edits the draft and never writes: the editor's Save keeps\n * the change and Discard drops it, with the preview beside it following\n * every step. With no widget the editor offers its own short list of\n * fonts, so a workspace without a fonts plugin can still choose one.\n */\n themeEditorFonts: 'themeEditorFonts',\n /**\n * The staff overview, among its platform-wide cards (AGL-3080). No props:\n * the overview is about the platform, not one org, so a widget here reads\n * what its plugin holds across every workspace through its own staff\n * route. A staff zone — see {@link CONSOLE_STAFF_WIDGET_SLOTS}.\n */\n staffOverview: 'staffOverview',\n /**\n * Staff admin org detail (staff-only surfaces). Props: orgId. A staff\n * zone — see {@link CONSOLE_STAFF_WIDGET_SLOTS}.\n */\n adminOrgDetail: 'adminOrgDetail',\n /**\n * Billing → Usage, below the meters (AGL-2940). Props: `orgId`, `org` (the\n * billing-merged org doc), `canManage` (the reader holds\n * `billing.manage`). A card here explains or controls consumption the\n * meters above it show.\n */\n orgBillingUsage: 'orgBillingUsage',\n /**\n * Billing → Overview, among the plan and add-on cards (AGL-2940). Props:\n * `orgId`, `org`, `plan` (the page's own defaulted plan), `canManage`.\n */\n orgBillingOverview: 'orgBillingOverview',\n /**\n * The staff org page, among its cards (AGL-2940). Props: `orgId`. Staff\n * only — the page is behind `StaffOnly`, and a widget here may read the\n * staff-only routes. A staff zone — see {@link CONSOLE_STAFF_WIDGET_SLOTS}.\n */\n staffOrg: 'staffOrg',\n /**\n * The staff user page, below the account's activity. Props: `uid`. A\n * staff zone — see {@link CONSOLE_STAFF_WIDGET_SLOTS}.\n */\n staffUser: 'staffUser',\n /**\n * The staff site page, below its own cards (AGL-3379). Props: `hostId`,\n * `orgId` (the site's organization, `''` for none) and `host` (the site\n * document as the page read it, or `undefined` while it loads). What a\n * plugin holds for one site — its automations, its sends — is shown here,\n * by the plugin that owns it. A staff zone — see\n * {@link CONSOLE_STAFF_WIDGET_SLOTS}.\n */\n staffSite: 'staffSite',\n /**\n * A COLUMN of the staff Organizations list (AGL-2984) — see\n * {@link ConsoleWidget.column}. The list renders the widget's component\n * once per row with `{ row, orgId, orgIds }`: the row as the list route\n * serves it, that row's org id, and every org id on the page, so a column\n * that reads figures of its own asks once for the page rather than once\n * per row. Its `Header` receives `{ orgIds }` beside the sort props. A\n * staff zone — see {@link CONSOLE_STAFF_WIDGET_SLOTS}.\n */\n staffOrgsListColumn: 'staffOrgsListColumn',\n /**\n * The staff org usage table (AGL-2984): the monthly rollups on the staff\n * org page and in the Organizations list's usage dialog. A widget with a\n * `column` is a column of the table, between Forms and Cost, rendered once\n * per month with `{ month, orgId }` — `month` is the rollup row as\n * `/api/admin/org-usage` serves it. A widget without one renders above\n * the table with `{ orgId, org }`, where `org` is the org document when the\n * surface holds one and `undefined` when it does not. A staff zone — see\n * {@link CONSOLE_STAFF_WIDGET_SLOTS}.\n */\n staffOrgUsageColumn: 'staffOrgUsageColumn',\n /**\n * The org's team member detail page, below the member's activity\n * (AGL-2940). Props: `orgId`, `orgSlug`, `uid`, `member` (the org member\n * document as the page loaded it), `hosts` (the org's sites, for naming\n * them), `canManage` (the reader may manage the org).\n */\n orgMember: 'orgMember',\n /**\n * A COLUMN of the org Team table (AGL-2940) — see\n * {@link ConsoleWidget.column}: the widget declares the header and the\n * shell renders its component once per row with `{ member, orgId,\n * canManage }`. A widget on this slot without a `column` renders nothing.\n */\n orgMembersListColumn: 'orgMembersListColumn',\n /**\n * The site collaborators card (AGL-2940). A widget with a `column` is a\n * column of its table, rendered per row with `{ member, orgId, hostId,\n * canManage }` — the owner's row too, with `member` carrying the owner's\n * `uid` and `role: 'owner'`; a widget without one renders beneath the\n * table with `{ hostId, canManage }`.\n */\n hostMembers: 'hostMembers',\n /**\n * One visitor account's drawer on a site's Users page (AGL-546), under the\n * account's password controls and above its saved addresses: what a plugin\n * holds about the person behind the account — what they bought, what they\n * subscribe to. Props: {@link ConsoleSiteMemberZoneProps}. Each widget is\n * one section of the drawer's own column, which spaces it; the drawer\n * draws the account itself, its suspension and its password help.\n */\n siteMember: 'siteMember',\n /**\n * The console dock (AGL-2940): the one position above every route boundary\n * in both the `(app)` and `(editor)` shells, where a floating panel — an\n * assistant, a helper — survives a navigation. Props:\n * {@link ConsoleDockZoneProps}. Named for the position, not for what a\n * plugin puts there (AGL-3080: it was `assistPanel`).\n */\n consoleDock: 'consoleDock',\n /**\n * The console's top bar, among its own status controls just ahead of the\n * notifications bell (AGL-3593), in both shells: a compact indicator a\n * plugin keeps in view on every page — work in progress, something waiting\n * on the reader — that opens the plugin's own surface when pressed. Props:\n * {@link ConsoleTopBarZoneProps}, the dock's answers, because both sit\n * above every route and answer the same questions. A widget here is one\n * control in a row the bar spaces; it draws nothing at all when it has\n * nothing to say, and never more than one small control.\n */\n consoleTopBar: 'consoleTopBar',\n /**\n * A section at the bottom of the besigner's Attributes panel (AGL-2940),\n * under the selected element's own fields. Props: `hostId` (`null` on an\n * editor that names no site), and `node`, the selected element (AGL-2984)\n * — present wherever the designer draws the panel for a selection.\n */\n besignerInspector: 'besignerInspector',\n /**\n * The besigner's Interactions section, on every editor that offers one\n * (AGL-3080): the section experiments a plugin runs on a page's elements.\n * Props: {@link ConsoleBesignerInteractionsZoneProps}.\n *\n * The section is the designer's, and so are an element's own interactions:\n * they live on its node and ride the document's save. A section experiment\n * is not a node's: it is a record of whichever plugin runs experiments,\n * stored where that plugin keeps them. So a widget here draws nothing. It\n * reads its own records and REPORTS them, and the section badges an element\n * and offers to start one from what was reported. With no widget the\n * section offers none, which is a workspace with no plugin that runs them.\n */\n besignerInteractions: 'besignerInteractions',\n /**\n * Inside a SEARCH LISTING editor (AGL-2910), under its fields. Props:\n * {@link ConsoleSeoFieldsZoneProps} — what the listing describes, the\n * fields the editor edits and what they hold, and `proposeValues`, which\n * stages values in those fields as unsaved edits.\n *\n * Two editors host it: the screen detail page's SEO card, and the commerce\n * product editor's search engine listing, which draws it through\n * `useConsoleWidgetSlot` because a plugin's dialog cannot mount the shell's\n * slot itself. A widget here proposes and never writes: the editor's own\n * Save is the write, with the guards that write carries. A keyword checker,\n * a translation memory and a generator are the same shape of widget.\n */\n seoFields: 'seoFields',\n /**\n * The host setup SEO section, under the site's SEO check and above the site\n * SEO form (AGL-2910). Props: {@link ConsoleHostSeoZoneProps} — the site,\n * its stored SEO settings, the check's last report, and `proposeDraft`,\n * which puts values in the form as unsaved edits. The form's Update stores\n * them; nothing a widget proposes reaches the published site before that.\n */\n hostSeo: 'hostSeo',\n /** {@link ConsoleRecordInsightsZoneProps} */\n recordInsights: 'recordInsights',\n /** {@link ConsoleRecordEmailZoneProps} */\n recordEmail: 'recordEmail',\n /** {@link ConsoleImportMappingZoneProps} */\n importMapping: 'importMapping',\n /**\n * The besigner's secondary toolbar, after the undo and redo controls\n * (AGL-2984), on every editor the designer opens: screens, layouts,\n * components, forms, templates and email designs. A control here acts on\n * the document in the editor. Props: `hostId` (`null` on an editor that\n * names no site).\n */\n besignerToolbar: 'besignerToolbar',\n /**\n * A section at the foot of the besigner's Page Properties drawer\n * (AGL-3475), under the page's publishing, layout, SEO and password\n * sections: what a plugin makes of the PAGE itself, such as serving it once\n * per record. Props: {@link ConsoleBesignerPagePropertiesZoneProps}. The\n * drawer's column spaces each widget as one of its sections; a widget saves\n * through its own routes, never through the drawer's buttons.\n */\n besignerPageProperties: 'besignerPageProperties',\n /**\n * Inside one row of a site's Pages list (AGL-3475), beside the page's name:\n * a chip a plugin draws about that page — that it is a record template,\n * and how many pages it serves. Props:\n * {@link ConsoleHostScreenRowZoneProps}. Drawn once per row, so a widget\n * reads what it needs once for the site and answers each row from that.\n */\n hostScreenRow: 'hostScreenRow',\n /**\n * A site's Screens page, beside its Templates and Create New Screen actions\n * (AGL-2907): another way to start a screen. Props:\n * {@link ConsoleHostScreensZoneProps}. A widget here runs its own flow and\n * writes nothing through the page; the screens list shows what it makes once\n * it exists.\n */\n hostScreens: 'hostScreens',\n /**\n * A site's Templates page, beside its Create Template action (AGL-3043):\n * another way to start a template. Props:\n * {@link ConsoleHostTemplatesZoneProps}. The `hostScreens` contract: a\n * widget here runs its own flow and writes nothing through the page, and\n * the template list shows what it makes once it exists.\n */\n hostTemplates: 'hostTemplates',\n /**\n * A site's Layouts page, beside its Templates and Create New Layout actions\n * (AGL-3043): another way to start a layout. Props:\n * {@link ConsoleHostLayoutsZoneProps}, on the `hostScreens` contract.\n */\n hostLayouts: 'hostLayouts',\n /**\n * A site's Components page, beside its Templates and Create Component\n * actions (AGL-3051): another way to start a reusable component. Props:\n * {@link ConsoleHostComponentsZoneProps}, on the `hostScreens` contract.\n */\n hostComponents: 'hostComponents',\n /**\n * The organization's sites page, beside the sites themselves (AGL-2911):\n * an action a member takes across MANY of the org's sites at once, rather\n * than a card totaling them. Props: {@link ConsoleOrgSitesZoneProps}.\n *\n * Distinct from `orgDashboard`, which is on the same page and gated on the\n * org CRM hub's reach verdict because every card there reads across the\n * host boundary. A widget here reads nothing of the kind: the sites it acts\n * on are the ones the page already resolved for this reader, and its own\n * door proves the reader's permission on each of them again. So the zone\n * carries no gate beyond the slot's own — enablement, entitlement, and the\n * widget's declared permission.\n */\n orgSites: 'orgSites',\n /**\n * One side of one item in a site package import's Changes step, beside\n * the other side (AGL-3545): the item drawn the way its owner previews it.\n * Props: {@link ConsoleSitePackageItemPreviewZoneProps}.\n *\n * A package carries items of many kinds, and how one looks belongs to\n * whoever keeps that kind — a form through the form's own preview, a site\n * email through the email preview. So a widget here names the kinds it\n * draws in its `itemKinds`, the import draws it for those and nothing else,\n * and an item of a kind no widget names keeps the console's own rendering\n * or its value list. The widget reads nothing it is not handed beyond what\n * its preview always reads, and never writes: the import writes, after the\n * person decides.\n */\n sitePackageItemPreview: 'sitePackageItemPreview',\n /**\n * The media library, beside its Upload media and New folder actions and\n * again in its empty state (AGL-3602): another way to add a file. Props:\n * {@link ConsoleMediaLibraryZoneProps}. The `hostScreens` contract — a\n * widget runs its own flow and writes nothing through the library — with\n * one door back: `onCreated`, which the widget calls with the assets it\n * added so the library shows them, selected.\n */\n mediaLibrary: 'mediaLibrary',\n} as const\n\nexport type ConsoleWidgetSlot =\n (typeof CONSOLE_WIDGET_SLOTS)[keyof typeof CONSOLE_WIDGET_SLOTS]\n\n/** What the `besignerPageProperties` zone hands each widget (AGL-3475). */\nexport interface ConsoleBesignerPagePropertiesZoneProps {\n hostId: string\n /** The org the page names; `undefined` while it resolves. */\n orgId: string | undefined\n /** The page in the editor. */\n screenId: string\n /** The page's `kind` as stored: `'template'` for a template, absent for a page. */\n screenKind?: string\n}\n\n/** What the `hostScreenRow` zone hands each widget (AGL-3475). */\nexport interface ConsoleHostScreenRowZoneProps {\n hostId: string\n /** The org the page names; `undefined` while it resolves. */\n orgId: string | undefined\n /** The row's page. */\n screenId: string\n /** The page's `kind` as stored. */\n screenKind?: string\n}\n\n/** What the `hostScreens` zone hands each widget (AGL-2907). */\nexport interface ConsoleHostScreensZoneProps {\n hostId: string\n /** The org the page names; `undefined` while it resolves. */\n orgId: string | undefined\n}\n\n/**\n * What the `hostTemplates` and `hostLayouts` zones (AGL-3043) and the\n * `hostComponents` zone (AGL-3051) hand each widget: the site and its org, as\n * `hostScreens` hands them. A plugin that hosts a resource page of its own\n * hands the same contract from a zone it declares (the forms plugin's\n * `hostForms`).\n */\nexport type ConsoleHostTemplatesZoneProps = ConsoleHostScreensZoneProps\n/** See {@link ConsoleHostTemplatesZoneProps}. */\nexport type ConsoleHostLayoutsZoneProps = ConsoleHostScreensZoneProps\n/** See {@link ConsoleHostTemplatesZoneProps}. */\nexport type ConsoleHostComponentsZoneProps = ConsoleHostScreensZoneProps\n\n/** What the `mediaLibrary` zone hands each widget (AGL-3602). */\nexport interface ConsoleMediaLibraryZoneProps {\n /**\n * The site whose library is open; for the organization's library, the site\n * on screen when there is one (a site's Media tab, a picker opened for a\n * site), else `null`.\n */\n hostId: string | null\n /** The org the library belongs to; `undefined` while it resolves. */\n orgId: string | undefined\n /** Which library is open: a site's own, or the organization's. */\n library: 'host' | 'org'\n /** The folder open in the library, where new files land; `null` for none. */\n folderId: string | null\n /** Hands the library the assets a widget added, to show and select them. */\n onCreated: (mediaIds: readonly string[]) => void\n}\n\n/** What the `orgSites` zone hands each widget (AGL-2911). */\nexport interface ConsoleOrgSitesZoneProps {\n /** Always `null`: the zone belongs to the organization, not to one site. */\n hostId: null\n /** The org and the sites the page resolved for this reader. */\n orgMount: ConsolePluginOrgMount\n /** The sites page's own path, for the links a widget builds. */\n basePath: string\n}\n\n\n/** What the `hostFirstRun` zone hands each widget (AGL-2918). */\nexport interface ConsoleHostFirstRunZoneProps {\n hostId: string\n /** The org the page names; `undefined` while it resolves. */\n orgId: string | undefined\n /** Path slug for building `/[orgSlug]/…` links. */\n orgSlug: string\n /** The site's subdomain, which is what a console URL names a site by. */\n host: string | null\n /**\n * Leaves the guided start for a blank site: this same page, with nothing\n * begun. The shell records the choice for this site, draws the zone no more,\n * and gives a site born for the guided start its starter — the published\n * Home page and layout every other new site is born with (AGL-3594).\n *\n * Required of every widget on this zone, drawn where its questions start\n * rather than after them, and — for a widget that takes the screen — what\n * every way of dismissing it does BEFORE it has started anything. See\n * `hostFirstRun` in {@link CONSOLE_WIDGET_SLOTS}.\n */\n startBlank: () => void\n /**\n * Closes the zone after its widget STARTED the site some other way — a\n * guided start whose job is running (AGL-3594). The shell records that the\n * site was asked and draws the zone no more, and writes no starter: the\n * site's pages are the job's to build. A widget that started nothing calls\n * `startBlank` instead. Optional, so a shell that predates it still closes\n * the zone through `startBlank`.\n */\n leave?: () => void\n}\n\n/**\n * Something a console page holds that somebody may want to publish\n * (AGL-3080).\n *\n * The console's own vocabulary and nothing else: a `kind` naming what the\n * thing IS, the scope that holds it, and the document. Where it can be\n * published to, what a listing of it is called and what it may cost are the\n * publishing plugin's, which is the whole point of handing this over rather\n * than building a request.\n *\n * `kind` is an open string for the reason a plugin's zone ids are: core\n * listing every publishable noun would be core holding the catalog again. A\n * widget that does not publish a kind draws nothing for it.\n */\nexport interface ConsolePublishableArtifact {\n /** What the thing is: `layout`, `component`, `theme`, `site`, … */\n kind: string\n /** The site it belongs to, where the kind belongs to one. */\n hostId?: string | null\n /** The organization, for a kind held by the org rather than a site. */\n orgId?: string | null\n /**\n * The document, in the holding surface's terms. Absent for a kind that IS\n * the site or the org — a theme, a whole site template — where the scope\n * above already names it.\n */\n artifactId?: string | null\n /** Seeds the listing's name; the person may change it. */\n displayName?: string\n description?: string\n}\n\n/** One site of the workspace, as the console names it. */\nexport interface ConsoleZoneSite {\n id: string\n label: string\n}\n\n/** What the `orgPluginInstalls` zone hands each widget (AGL-3080). */\nexport interface ConsoleOrgPluginInstallsZoneProps {\n /** The organization whose inventory the page is. */\n orgId: string\n /** Path slug for building `/[orgSlug]/…` links. */\n orgSlug: string\n /**\n * The sites of the organization the reader can see. An installation may be\n * pinned to some of them rather than to the whole organization, so a widget\n * reads each site's pins through this list rather than listing sites\n * itself.\n */\n hosts: ReadonlyArray<ConsoleZoneSite>\n}\n\n/** What the `pluginInstallStatus` zone hands each widget (AGL-3080). */\nexport interface ConsolePluginInstallStatusZoneProps {\n /** Path slug for building `/[orgSlug]/…` links. */\n orgSlug: string\n /** The installation's id: the segment of the page, which is the pin's key. */\n pluginRef: string\n /**\n * One pin of the installation — org-wide if there is one, else any site's.\n * Every pin carries the same version and manifest, which is what a status\n * is about.\n */\n pin: Readonly<Record<string, unknown>>\n}\n\n/** A release flag's verdict as the console applies it: the staff bypass included. */\nexport interface ConsoleReleaseVerdict {\n /** Released, or the reader is staff. */\n visible: boolean\n /** Visible ONLY because the reader is staff. */\n staffPreview: boolean\n}\n\n/** What the `consoleDock` zone hands each widget (AGL-2940, AGL-3080). */\nexport interface ConsoleDockZoneProps {\n orgId?: string\n org?: unknown\n orgReady: boolean\n /**\n * The org a widget may speak for, act as, and be METERED against — or\n * `undefined` where the page named none, or where the membership positively\n * contradicts the URL (AGL-1130, AGL-1916, AGL-1934).\n */\n scopedOrgId?: string\n /** Path slug for building `/[orgSlug]/…` links. */\n orgSlug: string\n /** The site in view, or null off a host route. */\n hostId: string | null\n /** The product's name as this org reads it (AGL-2319). */\n productName: string\n /**\n * The verdict for any release flag the widget names, staff bypass applied.\n * The shell names no plugin's flag; a widget asks for its own.\n */\n releaseVerdict: (key: string) => ConsoleReleaseVerdict\n isStaff: boolean\n /** The reader's verdict for every declared permission key on the site in view. */\n permissionsOnHost?: { loaded: boolean; granted: Readonly<Record<string, boolean>> }\n}\n\n/**\n * What the `consoleTopBar` zone hands each widget (AGL-3593): the same\n * answers the console dock gets, from the same shell resolution.\n */\nexport type ConsoleTopBarZoneProps = ConsoleDockZoneProps\n\n/** How one plugin's shelf of the template gallery stands (AGL-3080). */\nexport type ConsoleTemplateGalleryShelfState = 'loading' | 'empty' | 'shown'\n\n/** What the `templateGallery` zone hands each widget (AGL-3080). */\nexport interface ConsoleTemplateGalleryZoneProps {\n /** The site a template installs into. */\n hostId: string\n /**\n * The kind of template the gallery picks: `page` on the Screens page,\n * `layout` and `component` on theirs. A shelf offering only one kind\n * draws nothing for the others.\n */\n kind: 'page' | 'component' | 'layout'\n /** The word typed in the gallery's search box, `''` for none. */\n search: string\n /** Closes the gallery once an install has landed in the library. */\n onInstalled: () => void\n /**\n * Says how this shelf stands, keyed by a name the widget chooses and keeps,\n * so the gallery's \"nothing matches\" line counts it. Report `empty` rather\n * than nothing once a read answers with nothing.\n */\n reportShelf: (shelfId: string, state: ConsoleTemplateGalleryShelfState) => void\n}\n\n/** What the `siteMember` zone hands each widget (AGL-3080). */\nexport interface ConsoleSiteMemberZoneProps {\n /** The site whose visitor account it is. */\n hostId: string\n /**\n * The account's `siteMembers` document as the drawer holds it, `$id`\n * included. Its `email` is how a plugin finds what the person did on the\n * site.\n */\n member: Readonly<Record<string, unknown>> & { $id: string }\n}\n\n/** One section experiment, as the besigner's Interactions section lists it (AGL-3080). */\nexport interface ConsoleBesignerSectionExperiment {\n id: string\n name?: string\n /** The element the experiment varies. */\n nodeId: string\n status?: string\n}\n\n/** What one plugin reports to the besigner's Interactions section (AGL-3080). */\nexport interface ConsoleBesignerSectionExperiments {\n experiments: ConsoleBesignerSectionExperiment[]\n /**\n * Starts a draft experiment on an element. Reported only where the editor's\n * document is a page (`screenId` is set): a layout or a component is no\n * page for an experiment to run on.\n */\n create?: (options: { nodeId: string }) => void\n}\n\n/** What the `besignerInteractions` zone hands each widget (AGL-3080). */\nexport interface ConsoleBesignerInteractionsZoneProps {\n /** The site whose editor this is. */\n hostId: string\n /** The page under edit, or `null` on a layout or a component. */\n screenId: string | null\n /**\n * Hands the section what this plugin runs, keyed by a name the widget\n * chooses and keeps; `null` withdraws it. Call it from an effect.\n */\n reportSectionExperiments: (\n reporterId: string,\n report: ConsoleBesignerSectionExperiments | null,\n ) => void\n}\n\n/** What the `templateInstallStatus` zone hands each widget (AGL-3080). */\nexport interface ConsoleTemplateInstallStatusZoneProps {\n /** The site whose library the row is in. */\n hostId: string\n /**\n * The row's template document as the library read it, `$id` included:\n * its server-managed `source` and `installedFrom` stamps say who installed\n * it, and `editedAt` whether the site has edited its copy since.\n */\n template: Readonly<Record<string, unknown>>\n}\n\n/** What the `sitePackageItemPreview` zone hands each widget (AGL-3545). */\nexport interface ConsoleSitePackageItemPreviewZoneProps {\n /** The site the package is being imported into. */\n hostId: string\n /** Which side this is: the site's copy, or the file's. */\n side: 'site' | 'file'\n /** The item's package key, `<kind>/<id>`. */\n itemKey: string\n /** One of the kinds the widget names in `itemKinds`. */\n kind: string\n /** The document the side is: the site's item, or the id the file's would land under. */\n itemId: string\n /** The item as an import would write it: the document, without `$id`. */\n content: unknown\n /** What the import calls the item. */\n title: string\n}\n\n/** What the `hostArtifactPublish` zone hands each widget (AGL-3080). */\nexport interface ConsoleArtifactPublishZoneProps {\n /**\n * What the page asked to publish, or `null` while nothing is open. The\n * page owns the open/closed state because the control that opens this is\n * the page's own.\n */\n artifact: ConsolePublishableArtifact | null\n /** Closes it, however it was closed — cancelled, published, or refused. */\n onClose: () => void\n}\n\n/** What the `hostTheme` zone hands each widget (AGL-2938). */\nexport interface ConsoleHostThemeZoneProps {\n hostId: string\n /** The org the page names; `undefined` while it resolves. */\n orgId: string | undefined\n /** Path slug for building `/[orgSlug]/…` links. */\n orgSlug: string\n /** The site's subdomain, which is what a console URL names a site by. */\n host: string | null\n /** The theme the editor shows: the site's theme with its overrides resolved. */\n theme: HostTheme | undefined\n /** Where that theme came from, which decides how an edit to it is stored. */\n themeSource: HostThemeSource\n /** The editor's own preview, which renders a theme over the brand base. */\n ThemePreview: ComponentType<{ theme: HostTheme; scheme: HostThemeScheme }>\n /**\n * Puts `theme` in the editor as unsaved changes under `key`. A new key\n * replaces the previous draft; the same key again changes nothing until\n * the editor has saved or discarded it.\n */\n proposeDraft: (theme: HostTheme, key: string) => void\n}\n\n/** What the `themeEditorFonts` zone hands each widget (AGL-3656). */\nexport interface ConsoleThemeEditorFontsZoneProps {\n /** The site whose theme is being edited; `null` on an editor that names none. */\n hostId: string | null\n /** The editor's draft: the saved theme with every unsaved edit on it. */\n draft: HostTheme\n /**\n * Changes the draft. The updater gets the draft as it is when the change\n * applies, so two edits in one tick both land.\n */\n updateDraft: (updater: (draft: HostTheme) => HostTheme) => void\n}\n\n/**\n * The zones on the STAFF pages (AGL-2939): the staff overview, the staff org\n * page, its detail zone, and the staff user page.\n *\n * No workspace names the plugin set there. A staff page is ABOUT an org or\n * an account, and the reader's own memberships have nothing to do with what\n * it shows, so the console reads these zones from the plugins it loads for\n * the staff area — every plugin that declares a `staff` register surface —\n * and consults neither a widget's entitlement nor its permission: both are\n * answers about a workspace, and the staff area's guard is what admits the\n * reader.\n */\nexport const CONSOLE_STAFF_WIDGET_SLOTS: readonly ConsoleWidgetSlot[] = [\n CONSOLE_WIDGET_SLOTS.staffOverview,\n CONSOLE_WIDGET_SLOTS.adminOrgDetail,\n CONSOLE_WIDGET_SLOTS.staffOrg,\n CONSOLE_WIDGET_SLOTS.staffUser,\n CONSOLE_WIDGET_SLOTS.staffSite,\n CONSOLE_WIDGET_SLOTS.staffOrgsListColumn,\n CONSOLE_WIDGET_SLOTS.staffOrgUsageColumn,\n]\n\n/** Whether a slot is one of the {@link CONSOLE_STAFF_WIDGET_SLOTS}. */\nexport function isConsoleStaffWidgetSlot(slot: string): boolean {\n return (CONSOLE_STAFF_WIDGET_SLOTS as readonly string[]).includes(slot)\n}\n\n/**\n * The zones whose HOST decides who reads them (AGL-3554): each widget draws\n * only the props the host hands it, and the host page is already gated on\n * what its own route requires — a site package import's side-by-side diff,\n * opened by whoever may import a package into the site. So the console asks\n * the widget's own `permission` there and not its extension's: an\n * extension's permission guards the extension's own surfaces and reads (the\n * Email plugin's `data.manage`, for the audiences its page lists), and\n * would otherwise hide a preview from the very person importing the item.\n * The extension's plan feature is still asked.\n */\nexport const CONSOLE_HOST_GATED_WIDGET_SLOTS: readonly ConsoleWidgetSlot[] = [\n CONSOLE_WIDGET_SLOTS.sitePackageItemPreview,\n]\n\n/** Whether a slot is one of the {@link CONSOLE_HOST_GATED_WIDGET_SLOTS}. */\nexport function isConsoleHostGatedWidgetSlot(slot: string): boolean {\n return (CONSOLE_HOST_GATED_WIDGET_SLOTS as readonly string[]).includes(slot)\n}\n\n/** Search listing values by field; a field the editor does not hold is absent. */\nexport type ConsoleSeoFieldValues = Partial<Record<SeoListingFieldKey, string>>\n\n/**\n * What a search listing describes (AGL-2910). A screen is read from its own\n * documents by whoever needs more than its name; a product travels with its\n * name and description, because the product document is the commerce\n * plugin's and nothing else reads it.\n */\nexport type ConsoleSeoFieldsSubject =\n | {\n kind: 'screen'\n /** The screen document id. */\n id: string\n /** The version the page is showing, whose content the listing is about. */\n versionId: string | null\n name: string\n }\n | {\n kind: 'product'\n /** `null` for a product that has not been saved yet. */\n id: string | null\n name: string\n description: string\n }\n\n/** What the `seoFields` zone hands each widget (AGL-2910). */\nexport interface ConsoleSeoFieldsZoneProps {\n hostId: string\n /** The org the page names; `undefined` while it resolves. */\n orgId: string | undefined\n /** Path slug for building `/[orgSlug]/…` links. */\n orgSlug: string\n subject: ConsoleSeoFieldsSubject\n /**\n * The fields this editor edits, in its order — the only ones a widget may\n * propose. Keys of the `seo-listing-fields` catalog, which carries each\n * field's label and length.\n */\n fields: readonly SeoListingFieldKey[]\n /** What each field holds as the editor shows it: saved, or staged and unsaved. */\n values: ConsoleSeoFieldValues\n /** Whether the listing has a social image — an image description needs one. */\n hasImage: boolean\n /**\n * Stages `values` in the editor as unsaved edits under `key`. Fields the\n * editor does not edit are ignored. The editor's own Save is what stores\n * them; a widget never writes the listing itself.\n */\n proposeValues: (values: ConsoleSeoFieldValues, key: string) => void\n}\n\n/**\n * The site's SEO check as the SEO section last ran it: the platform's\n * findings (`app-utils/seo-audit`), which the section draws for every site\n * owner, and the target keyword lines the check was run with.\n */\nexport interface ConsoleSeoCheck {\n report: SeoAuditReport\n /** The keyword lines as typed, one page a line: `/pricing: plans, pricing`. */\n keywords: string\n}\n\n/** What the `hostSeo` zone hands each widget (AGL-2910). */\nexport interface ConsoleHostSeoZoneProps {\n hostId: string\n /** The org the page names; `undefined` while it resolves. */\n orgId: string | undefined\n /** Path slug for building `/[orgSlug]/…` links. */\n orgSlug: string\n /** The site's subdomain, which is what a console URL names a site by. */\n host: string | null\n /** The site's stored `seo` settings, as the form was seeded with them. */\n seo: Record<string, unknown> | undefined\n /**\n * Puts `values` in the site SEO form as unsaved edits, keyed by the form's\n * field names (`seo.entity.description`, `seo.agent.whenToUse`). Proposals\n * land in the form's draft beside what was typed, the same `key` twice\n * applies once, and the form's Update is what stores them.\n */\n proposeDraft: (values: Record<string, string>, key: string) => void\n /**\n * The SEO check the section drew above the zone, once somebody has run it\n * this visit; `null` before. The section lists the findings: a widget adds\n * what it has for them — a proposed fix, say — and never lists them again.\n */\n check: ConsoleSeoCheck | null\n}\n\n/**\n * A column a widget contributes to a shell-owned table (AGL-2940) — the org\n * Team table and the site collaborators table read these. The widget's\n * `Component` is the CELL renderer, mounted once per row with the row as a\n * prop beside the slot's own props; the header and the sort key are the\n * table's to draw.\n */\nexport interface ConsoleWidgetColumn {\n /** The header cell's text, and the column's name wherever it is listed. */\n header: string\n /**\n * The row field a table that sorts orders this column by. Carried for\n * every column contract so a sortable table can honor it; the two member\n * tables render in fetch order and do not read it today.\n */\n sortKey?: string\n align?: 'left' | 'right' | 'center'\n /**\n * The header cell's content when a plain `header` is not enough\n * (AGL-2939): a hint, or a sort over values only the plugin can read.\n * Mounted once per table with the slot's props beside\n * {@link ConsoleWidgetColumnHeaderProps}; without it the table draws\n * `header` as text.\n */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n Header?: ComponentType<any>\n}\n\n/**\n * What a column's own header receives beside the slot's props (AGL-2939).\n * The table keeps one sort at a time, so a column that sorts replaces\n * another's order.\n */\nexport interface ConsoleWidgetColumnHeaderProps {\n /**\n * Hands the table a row comparator, or `null` to put the rows back in the\n * table's own order. Stable for the life of the table.\n */\n onSort: (compare: ((a: never, b: never) => number) | null) => void\n /** Whether the rows are in this column's order. */\n sorted: boolean\n}\n\n/**\n * A component a plugin renders into a NAMED console slot (AGL-419/433) —\n * see {@link CONSOLE_WIDGET_SLOTS} for the guaranteed zones and their\n * props. The shell owns placement; the plugin owns the UI.\n */\nexport interface ConsoleWidget {\n slot: string\n /**\n * Present when the widget is a table COLUMN rather than a card (AGL-2940)\n * — see {@link ConsoleWidgetColumn}. Only the slots documented as column\n * slots read it; elsewhere it is ignored.\n */\n column?: ConsoleWidgetColumn\n /**\n * The kinds of item the widget draws, for a zone that draws one item of\n * many kinds (`sitePackageItemPreview`, AGL-3545). Only the slots\n * documented as reading it do; elsewhere it is ignored.\n */\n itemKinds?: readonly string[]\n /**\n * Stable identity for this widget, unique within the plugin per slot.\n * The id names the CARD, not its placement: the same card registered on\n * a second slot — the CRM's glance on the host dashboard and again on the\n * org's sites page — carries one id on both.\n *\n * A PERSISTED IDENTIFIER wherever the shell lets someone arrange the\n * surface it lands on — the console stores dashboard cards a reader has\n * switched off by this string, and reads it back sessions later. Giving a\n * retired id to a different card therefore shows that reader an\n * arrangement they never chose. Retire an id by leaving it reserved and\n * minting a new one, never by reusing it.\n */\n widgetId: string\n /**\n * What to call this widget where it is LISTED rather than rendered — the\n * console's dashboard customize dialog is the one such place today.\n *\n * Match the card's own heading: the two names sit a click apart, and a\n * switch labeled differently from the card it controls reads as a switch\n * for something else. Omitting it falls back to the extension's\n * `displayName`, which is right for a plugin contributing one card and\n * ambiguous for one contributing several.\n */\n title?: string\n /**\n * The permission a reader must hold for this card, when it is narrower\n * than its extension's own {@link ConsoleExtension.permission}.\n *\n * Composes by AND with the extension's, like a nav item's does: a widget\n * cannot escape its extension's gate by naming a key its reader happens to\n * hold, and declaring nothing here inherits the extension's requirement\n * rather than clearing it.\n *\n * A card is a surface the reader never asked for — the shell drops it onto\n * a page they opened for something else — so there is nowhere in it to put\n * an upsell or a refusal, and a widget its reader may not have is simply\n * absent. That is the same treatment the entitlement gate gives a widget,\n * and for the same reason.\n */\n permission?: string\n /**\n * The entitlement THIS card needs, when it is narrower than its\n * extension's {@link ConsoleExtension.featureFlag} (AGL-2611).\n *\n * Composes by AND with the extension's, exactly as `permission` above\n * does: a card cannot escape its extension's gate by declaring nothing,\n * and declaring one here can only narrow. The case is an extension whose\n * surface ships on every plan while one of its cards belongs to a paid\n * part of it. Absent without an upsell, for the reason `permission` gives.\n */\n featureFlag?: keyof OrgFeatureFlags\n /**\n * Mount this widget as its own upsell when the ONLY thing missing is the\n * plan entitlement (AGL-3601).\n *\n * Without it a widget whose `featureFlag` the plan lacks is absent, as\n * above. With it, the shell still mounts it — with `entitled={false}` and\n * an `upgrade` prop ({@link ConsoleWidgetUpgrade}) — but only when every\n * other gate passes (the reader's permission, the plugin being on for this\n * workspace and this site) and the missing flag is one an add-on this\n * workspace can buy switches on. Where nothing can be bought the widget\n * stays absent, so the widget never has to decide that itself.\n *\n * The widget owns what it draws in that state, and must not open the\n * feature: the shell has decided the plan does not include it.\n */\n showWhenNotEntitled?: boolean\n /**\n * The release flag this widget is behind, which the shell resolves from the\n * flags it already loads for every page (AGL-3601) — staff bypass applied,\n * as the server's own doors apply it. The widget is absent while the flag\n * is off for this workspace, and while the flags have not settled, so a\n * control is never drawn and then taken away.\n *\n * For a widget that would otherwise have to ask a server door whether its\n * feature exists before it draws anything.\n */\n releaseFlag?: ReleaseFlagKey\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n Component: ComponentType<any>\n}\n\n/**\n * Where a widget mounted by {@link ConsoleWidget.showWhenNotEntitled} sends a\n * reader to buy what it lacks. The shell builds it, so no extension supplies a\n * URL the console's own chrome then renders.\n */\nexport interface ConsoleWidgetUpgrade {\n /** The workspace's Billing page, at the section that sells add-ons. */\n billingHref: string\n /** Whether the reader may buy it (`billing.manage`). */\n canManageBilling: boolean\n}\n\n/**\n * The props the shell adds to a widget that declared\n * {@link ConsoleWidget.showWhenNotEntitled}, beside its zone's own.\n * `entitled` is `true` when the plan includes the feature, and `upgrade`\n * is present only when it is `false`.\n */\nexport interface ConsoleWidgetEntitlementProps {\n entitled?: boolean\n upgrade?: ConsoleWidgetUpgrade\n}\n\n/**\n * Console half of the pattern: everything a feature contributes to the\n * console shell. Declarative — the shell owns rendering and applies the\n * feature-flag gate, so extensions cannot bypass entitlements.\n *\n * That sentence describes `apps/console/utils/extension-entitlement.ts`,\n * which the plugin route and `PluginWidgetSlot` both call before they mount\n * anything an extension registered (AGL-2484). It was an aspiration until\n * then: the route resolved the entitlement and handed it to the extension as\n * the `entitled` PROP, and the widget slot did not resolve it at all, so\n * enforcement rested on each extension policing itself — which one\n * first-party page did not do.\n *\n * What the gate covers, exactly: a `featureFlag` refuses to RENDER the\n * extension's page body and its widgets. Nav items stay visible on purpose.\n * Hiding the tab would hide the only route most workspaces have to the page\n * that sells the feature, and a nav entry leading to the shell's own upgrade\n * notice bypasses nothing.\n *\n * TWO QUESTIONS, BOTH ANSWERED BY THE SHELL. `featureFlag` is about the\n * organization's plan; `permission` is about the person reading, and it is\n * enforced in the same place and the same way — resolved from the member's\n * own permission map, never from anything the extension supplies, and read\n * before the surface is constructed. An extension declares what it requires\n * and the shell decides whether the requirement is met, so the sentence\n * above holds for authorization as well as for entitlements.\n */\n/**\n * What a blocked org is told about a feature it does not hold, in the\n * extension's own words.\n *\n * PRESENTATION ONLY. The shell decides entitlement from the org billing doc\n * and this extension's `featureFlag`, and reads this object solely to render\n * a refusal it has ALREADY decided on. Nothing here is an input to that\n * decision, and an extension supplying it gains no access — the surface it\n * describes stays unmounted either way.\n *\n * It exists because the shell's own copy can only speak in generalities. An\n * entitlement that no plan grants — one sold as a per-organization add-on —\n * is described exactly wrong by \"not included in your current plan\", which\n * sends the reader to compare plan tiers that would not have helped.\n */\nexport interface ConsoleUpgradeNotice {\n /**\n * The sentence a blocked org reads. Plain text: the shell renders it as a\n * string, never as markup, and a third-party extension writes this.\n */\n message: string\n /**\n * Which card on the billing page sells it, as a bare fragment id\n * ('addons'). The console validates it against the anchors that page\n * actually has and drops it otherwise — the `docsTopic` treatment, for the\n * same reason: an extension can name any string, and an unrecognized one\n * must degrade to the plain Billing link rather than build a dead URL.\n *\n * Deliberately not an href. A full URL from an extension would be an\n * open redirect rendered by the console's own chrome; the shell keeps\n * ownership of the route and accepts only which part of it to scroll to.\n */\n billingAnchor?: string\n}\n\n/** What the console's generic staff route hands a staff page. */\nexport interface ConsoleStaffPageProps {\n /** The page's own console path, `/admin/{id}`. */\n basePath: string\n /**\n * Path segments beneath {@link basePath}, `[]` on the page's own URL\n * (AGL-3080). Only ever non-empty for a page that declared\n * {@link ConsoleStaffPage.ownsSubtree}.\n *\n * The staff twin of {@link ConsolePluginPageProps.segments}, and for the\n * same case: a queue is a list, and a row of it is a page. A staff page\n * without this could only ever BE the list.\n */\n segments?: readonly string[]\n /**\n * The viewer's staff ROLE, or `null` while the claim is still resolving\n * (AGL-3080).\n *\n * Not every staff act is open to every staff role — six are `super`-only\n * on the server — and a page that cannot tell renders live controls to a\n * `support` engineer who clicks them and gets a raw 403. Pass it to\n * `resolveStaffRoleGate` and render the verdict with\n * `BlockedControl`; `null` must never be treated as a refusal, or every\n * page flashes a disabled button at the people who may use it.\n *\n * ⚠️ NOT the boundary. The routes verify the decoded token per request and\n * refuse regardless of what rendered. This exists so the console stops\n * promising what the server will refuse.\n */\n staffRole?: string | null\n /**\n * Console destinations a staff page may link ACROSS to, built by the shell\n * (AGL-3080) — the staff twin of {@link ConsolePluginOrgMount}'s paths,\n * and for the same reason: a plugin cannot import the console's route\n * table, and a plugin that rebuilt one of these from a string would break\n * silently the day the console moved it.\n */\n staffPaths?: ConsoleStaffPagePaths\n}\n\nexport interface ConsoleStaffPagePaths {\n /**\n * The staff console's page for one workspace. `undefined` on a deployment\n * that has no such page, which a caller renders as no link rather than a\n * dead one.\n */\n orgDetail(orgId: string): string | undefined\n}\n\n/**\n * A page a plugin adds to the STAFF area (AGL-2939): a tab in the staff\n * strip and a page at `/admin/{id}`, rendered by the console's generic staff\n * route. The shell owns the layout, the header, the breadcrumbs, the staff\n * guard and the tab; the plugin owns the body.\n *\n * Staff pages load with the staff area's plugins — those declaring a `staff`\n * register surface — not with a workspace's, because no org names the plugin\n * set on `/admin`. They are admitted by the staff claim alone, so the\n * extension's `featureFlag` and `permission` do not apply, and every read a\n * staff page makes is refused server-side to a caller without the claim.\n */\nexport interface ConsoleStaffPage {\n /**\n * The URL segment under `/admin`, and the page's identity. The console's\n * own staff routes win a segment they use, so pick one they do not. It is\n * in links staff keep — treat it as persisted.\n */\n id: string\n /** The tab's label in the staff strip. */\n label: string\n /**\n * The page header (title and icon), and the docs topic its help `?`\n * explains — a plain string for the reason {@link ConsoleNavItem.header}\n * gives, validated by the console.\n */\n header?: { title: string; icon?: MdiIconProps; docsTopic?: string }\n /**\n * Whether this page claims every path beneath `/admin/{id}` too\n * (AGL-3080) — the staff twin of {@link ConsoleNavItem.ownsSubtree}, with\n * the same trade and the same duty.\n *\n * The case is a staff QUEUE: the list is the page, and each row opens one\n * submission. The set of ids is a property of the data, so no static list\n * could enumerate them, and without this every row's URL is a 404.\n *\n * A page that claims its subtree can no longer tell a typo from an id, so\n * it takes on saying \"no such thing\" itself — which a queue has to be able\n * to do anyway for a submission withdrawn while a link to it was still in\n * a reviewer's inbox.\n *\n * The console's own staff routes keep winning their segments either way:\n * a static path beats a dynamic one segment by segment, so `/admin/orgs/1`\n * is still the orgs route and never a staff page's subtree.\n */\n ownsSubtree?: boolean\n Component: ComponentType<ConsoleStaffPageProps>\n}\n\n/** What the console's generic public route hands a public page. */\nexport interface ConsolePublicPageProps {\n /** The plugin that registered the page, the URL's first segment after `/kiosk`. */\n pluginId: PluginId\n /** The page's own path, as it registered it (`/display`). */\n path: string\n}\n\n/**\n * A full-screen console page that needs NO staff session (AGL-3608): served\n * at `/kiosk/{pluginId}{path}` by the console's generic public route, outside\n * the workspace shell, with only the console theme around it.\n *\n * The case is a device a business sets down in front of the public — a\n * screen facing a customer, a sign-in tablet at a front desk — which is\n * nobody's console session and must never become one. The shell draws no\n * nav, no workspace, no org switcher and no plugin providers here, and the\n * route loads this one plugin's console bundle and nothing else.\n *\n * Neither the extension's `featureFlag` nor its `permission` applies: there is\n * no member to ask. The page proves itself to its own API routes — a pairing\n * code exchanged for a device token, say — and every read it makes must be\n * refused server-side without that proof. Treat everything the page renders\n * as visible to whoever is standing in front of the device.\n *\n * The plugin also declares each path in its manifest as\n * `contributes.console.publicRoutes`, which is what lets the route load the\n * bundle only for a path that exists. First-party plugins only: the route\n * reads the console's generated manifest, never a marketplace install.\n */\nexport interface ConsolePublicPage {\n /**\n * The path beneath `/kiosk/{pluginId}`, with its leading slash\n * (`/display`). Matched exactly. It is in URLs saved on devices — treat it\n * as persisted.\n */\n path: string\n /** The browser tab's title, which is also what a home-screen shortcut shows. */\n title: string\n Component: ComponentType<ConsolePublicPageProps>\n}\n\nexport interface ConsoleExtension {\n pluginId: PluginId\n displayName: string\n /** Entitlement flag gating every surface this extension registers. */\n featureFlag?: keyof OrgFeatureFlags\n /**\n * The permission a reader must hold for every surface this extension\n * registers — the AUTHORIZATION half of the sentence above `featureFlag`.\n *\n * `featureFlag` answers what the ORGANIZATION bought; this answers what\n * the PERSON reading may open, and the two are independent: an org can\n * hold a feature that most of its members have no business using.\n *\n * A key in the console's permission vocabulary, which is two spaces and\n * they are not interchangeable. Either a dotted {@link OrgPermission} from\n * the built-in catalog ('data.manage'), which the shell answers from the\n * member's resolved granular map; or a key some plugin declared through\n * `registerPluginPermissions` ('managePos'), which the shell answers from\n * the resolved permission map that carries those keys. A key belonging to\n * NEITHER space refuses the surface rather than passing it — a requirement\n * nothing can answer is not a requirement that has been met.\n *\n * Declared here rather than checked inside the page for the reason the\n * entitlement gate moved out of the pages: a check the extension performs\n * on itself is enforcement only for as long as every extension remembers\n * to perform it, and the surface has already mounted and opened its\n * listeners by the time it runs.\n *\n * Omit for a surface every member of the workspace may open.\n */\n permission?: string\n /**\n * Refusal copy for an org that does not hold `featureFlag`, rendered by\n * the shell in place of the surface. Omit to get the shell's generic\n * plan-tier sentence.\n */\n upgradeNotice?: ConsoleUpgradeNotice\n navItems?: ConsoleNavItem[]\n /**\n * Surfaces mounted at the ORGANIZATION level rather than under a site\n * (AGL-2974): each is served at `/[orgSlug]{href}` by the console's generic\n * org route and listed on the organization's tab strip.\n *\n * A separate list rather than a flag on {@link ConsoleNavItem}, because the\n * two levels are read by different consumers. The site strip, the site\n * route and every title and section lookup iterate `navItems`, and a scope\n * field on one of those entries would reach each of them as a site surface\n * until every one of them learned to skip it. Nothing that reads `navItems`\n * sees these.\n *\n * The same contract a site nav item has, with the site taken away: the\n * page receives `hostId: null` and an `orgMount` naming the organization\n * and its sites, sections resolve and gate the same way, and the\n * extension's `featureFlag` and `permission` apply unchanged. The shell\n * admits only a reader whose reach is the whole organization, because a\n * surface with no site has no scope to narrow a site collaborator to.\n *\n * The href must not name one of the console's own organization routes\n * (`/hosts`, `/team`, `/settings`, `/crm` and the rest): a named route\n * always wins over the generic one, so such a surface would never render.\n */\n orgNavItems?: ConsoleNavItem[]\n dashboardCards?: ConsoleDashboardCard[]\n settingsSections?: ConsoleSettingsSection[]\n /** Slot-addressed components the shell renders in place (AGL-419). */\n widgets?: ConsoleWidget[]\n /**\n * Built-in themes this plugin adds to every site's theme picker\n * (AGL-3404) — see {@link ConsoleThemePreset}. Loaded with the plugin at\n * the {@link THEME_PRESETS_LOAD_POINT}, which the plugin declares among its\n * `console.slots`.\n */\n themePresets?: readonly ConsoleThemePreset[]\n /**\n * The kinds of record this plugin lets the console's search find — see\n * {@link ConsoleSearchSource}. Loaded with the plugin at the\n * {@link CONSOLE_SEARCH_LOAD_POINT}, which the plugin declares among its\n * `console.slots`.\n */\n searchSources?: readonly ConsoleSearchSource[]\n /**\n * Pages in the STAFF area (AGL-2939) — see {@link ConsoleStaffPage}.\n * Neither `featureFlag` nor `permission` applies to them.\n */\n staffPages?: readonly ConsoleStaffPage[]\n /**\n * Full-screen pages that need no staff session (AGL-3608) — see\n * {@link ConsolePublicPage}. Neither `featureFlag` nor `permission` applies\n * to them.\n */\n publicPages?: readonly ConsolePublicPage[]\n /**\n * App-level providers the shell mounts around every console page\n * (AGL-419) — e.g. the marketplace plugin's AI-assist provider.\n */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n providers?: Array<ComponentType<any>>\n}\n\nconst consoleExtensions = new Map<PluginId, ConsoleExtension>()\n\n/** Idempotent by pluginId — re-registration replaces the previous entry. */\nexport function registerConsoleExtension(extension: ConsoleExtension): void {\n consoleExtensions.set(extension.pluginId, extension)\n}\n\nexport function unregisterConsoleExtension(pluginId: PluginId): void {\n consoleExtensions.delete(pluginId)\n}\n\n/**\n * Registration-ordered extensions; the console shell filters by flag.\n *\n * AGL-758: the registry is a module-global that only ever grows — nothing\n * outside tests unregisters, and loaded chunks can't unload — so after\n * visiting two workspaces it holds the UNION of both plugin sets. Every\n * read is therefore scoped by the caller's effective enabled set; pass the\n * current org's plugin ids so one workspace never serves another's nav\n * items, widgets, pages or providers. Omitting the argument keeps the\n * unfiltered union (tests, and non-org surfaces that have no such set).\n */\nexport function listConsoleExtensions(\n enabledPluginIds?: readonly PluginId[],\n): ConsoleExtension[] {\n const all = Array.from(consoleExtensions.values())\n if (!enabledPluginIds) return all\n const enabled = new Set(enabledPluginIds)\n return all.filter((extension) => enabled.has(extension.pluginId))\n}\n\n/** A nav item flattened with its owning extension's id + entitlement flag. */\nexport interface ConsoleNavEntry extends ConsoleNavItem {\n pluginId: PluginId\n featureFlag?: keyof OrgFeatureFlags\n}\n\n/**\n * Every registered nav item, flattened for the shell's nav strip. The\n * shell appends these to its static tabs, so a plugin adds a menu item\n * by registering here — no edit to the console's nav constants.\n */\nexport function listConsoleNavItems(\n enabledPluginIds?: readonly PluginId[],\n): ConsoleNavEntry[] {\n return inTabOrder(\n listConsoleExtensions(enabledPluginIds).flatMap((extension) =>\n (extension.navItems ?? []).filter((navItem) => !navItem.unlisted).map((navItem) => ({\n ...navItem,\n pluginId: extension.pluginId,\n featureFlag: extension.featureFlag,\n })),\n ),\n (entry) => entry.tabOrder,\n )\n}\n\n/**\n * A strip's plugin entries by {@link ConsoleNavItem.tabOrder}, lower first,\n * a tie in the order they arrived — which is registration order.\n */\nfunction inTabOrder<T>(\n entries: readonly T[],\n tabOrderOf: (entry: T) => number | undefined,\n): T[] {\n return entries\n .map((entry, index) => ({ entry, index, order: tabOrderOf(entry) ?? 0 }))\n .sort((a, b) => a.order - b.order || a.index - b.index)\n .map(({ entry }) => entry)\n}\n\n/** What {@link resolveConsolePluginPage} answers for a matched href. */\nexport interface ResolvedConsolePluginPage {\n extension: ConsoleExtension\n navItem: ConsoleNavItem\n /**\n * The section the href names, when the nav item declares sections and the\n * href reaches past its own. Undefined on the nav item's own href.\n */\n section?: ConsoleNavSection\n /** Path segments beneath `navItem.href`; `[]` on the nav item's own href. */\n segments: readonly string[]\n /**\n * The href was one of the nav item's `legacyHrefs`, not its current one.\n * The shell redirects to `navItem.href` plus the same section and segments.\n */\n legacy?: boolean\n}\n\n/**\n * One nav item against one href: exact, a declared section beneath it, or —\n * for a nav item that asks — anything beneath it.\n *\n * A nav item that declares neither `sections` nor `ownsSubtree` matches its\n * own href and nothing else. That is what keeps every plugin written before\n * AGL-2501 behaving as it did: without it, prefix matching would quietly hand\n * `/products/anything` to the Products page, which is the \"it opened the wrong\n * page\" report rather than a 404.\n */\nfunction matchNavItem(\n navItem: ConsoleNavItem,\n href: string,\n): { section?: ConsoleNavSection; segments: readonly string[]; legacy?: boolean } | undefined {\n const current = matchNavItemHref(navItem, navItem.href, href)\n if (current) return current\n for (const legacyHref of navItem.legacyHrefs ?? []) {\n const match = matchNavItemHref(navItem, legacyHref, href)\n if (match) return { ...match, legacy: true }\n }\n return undefined\n}\n\nfunction matchNavItemHref(\n navItem: ConsoleNavItem,\n itemHref: string,\n href: string,\n): { section?: ConsoleNavSection; segments: readonly string[] } | undefined {\n if (itemHref === href) return { segments: [] }\n if (!navItem.sections?.length && !navItem.ownsSubtree) return undefined\n // On a separator boundary, so `/products` cannot claim `/products-archive`.\n if (!href.startsWith(`${itemHref}/`)) return undefined\n const segments = href.slice(itemHref.length + 1).split('/').filter(Boolean)\n const section = navItem.sections?.find((item) => item.id === segments[0])\n if (section) return { section, segments }\n // An id the nav item never declared is NOT this page — unless the surface\n // claimed the subtree, in which case the deeper segments are its own\n // entity ids and it answers for them. Returning the nav item otherwise\n // would render the surface's default section under a URL naming a\n // different one, which reads to the person who typed it as the wrong page\n // opening rather than as a typo.\n return navItem.ownsSubtree ? { segments } : undefined\n}\n\n/**\n * Resolves a host-relative href (e.g. '/events', '/products/orders') to the\n * extension + nav item that owns a renderable page for it, and the section\n * within it. The shell's generic host route uses this to render plugin pages\n * without a per-plugin page file.\n *\n * ## Which registration wins (AGL-2501)\n *\n * LONGEST declared `href` wins, and an exact match therefore always beats a\n * section match — an exact `href` spans the whole path, so nothing matching a\n * prefix of it can be longer. `/products/orders` goes to a plugin that\n * declares that path over one that declares `/products` with an `orders`\n * section, and a prefix only matches on a SEGMENT boundary, so `/products`\n * never claims `/products-archive`.\n *\n * A TIE REFUSES. Two enabled plugins matching the same path at the same length\n * resolve to nothing, and the console 404s. Registry insertion order is an\n * accident of which chunk loaded first, so picking from it means one\n * workspace serves plugin A's page at a URL where another serves plugin B's —\n * silently, and differently per session. Nobody can debug that from the\n * symptom, so it is refused and logged instead. Two nav items of the SAME\n * extension are not a tie: that order is authored, and the first wins as it\n * always has.\n *\n * This is a rule rather than an accident because the registry is a\n * session-wide UNION across plugins from different authors (AGL-758) — one\n * plugin registering `/products` and another `/products/orders` is two\n * workspaces' code meeting in one module-global, not one author's tidiness\n * problem. Scoping is unchanged and load-bearing: every candidate still comes\n * from `listConsoleExtensions(enabledPluginIds)`, so a plugin the current org\n * has not enabled cannot win a path — or collide with one.\n */\nexport function resolveConsolePluginPage(\n href: string,\n enabledPluginIds?: readonly PluginId[],\n): ResolvedConsolePluginPage | undefined {\n return resolvePluginPageAmong(\n href,\n enabledPluginIds,\n (extension) => extension.navItems,\n )\n}\n\n/** One organization-level nav item with the extension that declared it. */\nexport interface ConsoleOrgNavEntry {\n extension: ConsoleExtension\n navItem: ConsoleNavItem\n}\n\n/**\n * Every registered {@link ConsoleExtension.orgNavItems} entry, in\n * {@link ConsoleNavItem.tabOrder} and then registration order, for the\n * organization's tab strip (AGL-2974).\n *\n * Carries the extension whole rather than a flattened copy of two of its\n * fields: a tab for a surface the reader cannot open is hidden, and deciding\n * that takes the extension's `permission`, `featureFlag` and\n * `upgradeNotice`, which is the same set the org route reads.\n */\nexport function listConsoleOrgNavItems(\n enabledPluginIds?: readonly PluginId[],\n): ConsoleOrgNavEntry[] {\n return inTabOrder(\n listConsoleExtensions(enabledPluginIds).flatMap((extension) =>\n (extension.orgNavItems ?? []).map((navItem) => ({ extension, navItem })),\n ),\n (entry) => entry.navItem.tabOrder,\n )\n}\n\n/**\n * {@link resolveConsolePluginPage} for the ORGANIZATION level (AGL-2974): an\n * org-relative href (`/outreach/sequences`) against every enabled\n * extension's `orgNavItems`, with the same matching, the same longest-href\n * rule and the same refusal of a tie between two plugins. Site nav items are\n * never candidates, so a surface registered under a site cannot be opened\n * without one.\n */\nexport function resolveConsoleOrgPluginPage(\n href: string,\n enabledPluginIds?: readonly PluginId[],\n): ResolvedConsolePluginPage | undefined {\n return resolvePluginPageAmong(\n href,\n enabledPluginIds,\n (extension) => extension.orgNavItems,\n )\n}\n\n/** The resolver both levels share; `navItemsOf` picks which list is read. */\nfunction resolvePluginPageAmong(\n href: string,\n enabledPluginIds: readonly PluginId[] | undefined,\n navItemsOf: (extension: ConsoleExtension) => ConsoleNavItem[] | undefined,\n): ResolvedConsolePluginPage | undefined {\n let best: ResolvedConsolePluginPage | undefined\n /** Extensions matching at `best`'s length — more than one is the tie. */\n let contenders: PluginId[] = []\n for (const extension of listConsoleExtensions(enabledPluginIds)) {\n for (const navItem of navItemsOf(extension) ?? []) {\n if (!navItem.Component) continue\n const match = matchNavItem(navItem, href)\n if (!match) continue\n const bestLength = best?.navItem.href.length ?? -1\n if (navItem.href.length > bestLength) {\n best = { extension, navItem, ...match }\n contenders = [extension.pluginId]\n continue\n }\n // Same length, different plugin: ambiguous. Same plugin: authored order,\n // and the first nav item keeps the path.\n if (\n navItem.href.length === bestLength &&\n !contenders.includes(extension.pluginId)\n ) {\n contenders.push(extension.pluginId)\n }\n }\n }\n if (contenders.length > 1) {\n // Loud, because the symptom — a 404 on a page that is plainly installed —\n // names neither plugin. This line is the only place the collision is\n // visible, so it carries both ids and the path they are fighting over.\n console.error(\n `[aglyn] console page path \"${href}\" is claimed by more than one ` +\n `enabled plugin (${contenders.join(', ')}); refusing to guess which ` +\n 'one owns it. Change one plugin\\'s nav item href.',\n )\n return undefined\n }\n return best\n}\n\n/** A staff page flattened with its owning extension's id. */\nexport interface ConsoleStaffPageEntry extends ConsoleStaffPage {\n pluginId: PluginId\n}\n\n/**\n * Every registered staff page, in registration order — the staff strip's\n * plugin tabs, after the console's own.\n */\nexport function listConsoleStaffPages(\n enabledPluginIds?: readonly PluginId[],\n): ConsoleStaffPageEntry[] {\n return listConsoleExtensions(enabledPluginIds).flatMap((extension) =>\n (extension.staffPages ?? []).map((page) => ({\n ...page,\n pluginId: extension.pluginId,\n })),\n )\n}\n\n/**\n * The staff page at `/admin/{id}` (AGL-2939), or `undefined`.\n *\n * Two plugins claiming one id resolve to nothing, and say so: registry order\n * is an accident of which chunk loaded first, and a staff page that is one\n * plugin's on one load and another's on the next cannot be debugged from the\n * symptom — the same rule {@link resolveConsolePluginPage} applies to paths.\n */\nexport function resolveConsoleStaffPage(\n id: string,\n enabledPluginIds?: readonly PluginId[],\n): ConsoleStaffPageEntry | undefined {\n const matches = listConsoleStaffPages(enabledPluginIds).filter(\n (page) => page.id === id,\n )\n const owners = [...new Set(matches.map((page) => page.pluginId))]\n if (owners.length > 1) {\n console.error(\n `[aglyn] staff page \"/admin/${id}\" is claimed by more than one plugin ` +\n `(${owners.join(', ')}); refusing to guess which one owns it. ` +\n \"Change one plugin's staff page id.\",\n )\n return undefined\n }\n return matches[0]\n}\n\n/** A public page flattened with its owning extension's id. */\nexport interface ConsolePublicPageEntry extends ConsolePublicPage {\n pluginId: PluginId\n}\n\n/** `pos-display`, `/pos-display/` and `/pos-display` all name `/pos-display`. */\nexport function normalizeConsolePublicPath(path: string): string {\n return `/${String(path ?? '').split('/').filter(Boolean).join('/')}`\n}\n\n/** Every registered public page, in registration order. */\nexport function listConsolePublicPages(\n enabledPluginIds?: readonly PluginId[],\n): ConsolePublicPageEntry[] {\n return listConsoleExtensions(enabledPluginIds).flatMap((extension) =>\n (extension.publicPages ?? []).map((page) => ({\n ...page,\n path: normalizeConsolePublicPath(page.path),\n pluginId: extension.pluginId,\n })),\n )\n}\n\n/**\n * The public page at `/kiosk/{pluginId}{path}` (AGL-3608), or `undefined`.\n *\n * Scoped to the ONE plugin the URL names, because the registry is a\n * session-wide union (AGL-758): a device that once loaded another plugin\n * must not have that plugin answer a path in this one's namespace. Two\n * extensions of the same plugin claiming one path resolve to nothing and\n * say so, the rule {@link resolveConsoleStaffPage} applies to ids.\n */\nexport function resolveConsolePublicPage(\n pluginId: PluginId,\n path: string,\n): ConsolePublicPageEntry | undefined {\n if (!pluginId) return undefined\n const wanted = normalizeConsolePublicPath(path)\n if (wanted === '/') return undefined\n const matches = listConsolePublicPages([pluginId]).filter(\n (page) => page.path === wanted,\n )\n if (matches.length > 1) {\n console.error(\n `[aglyn] public page \"/kiosk/${pluginId}${wanted}\" is registered more ` +\n 'than once; refusing to guess which one to serve.',\n )\n return undefined\n }\n return matches[0]\n}\n\n/** Widgets registered for a slot, across every extension (AGL-419). */\nexport function listConsoleWidgets(\n slot: string,\n enabledPluginIds?: readonly PluginId[],\n): Array<{ extension: ConsoleExtension; widget: ConsoleWidget }> {\n const out: Array<{ extension: ConsoleExtension; widget: ConsoleWidget }> = []\n for (const extension of listConsoleExtensions(enabledPluginIds)) {\n for (const widget of extension.widgets ?? []) {\n if (widget.slot === slot) out.push({ extension, widget })\n }\n }\n return out\n}\n\n/**\n * A built-in theme a plugin contributes (AGL-3404): a complete, JSON\n * {@link HostTheme} a site can pick on Setup → Theme.\n *\n * Picking one COPIES it onto the site, and the site's edits are an override\n * on top, so a preset is never modified by a site and a later version of the\n * plugin never repaints a site that did not pick it again. That is also why a\n * preset needs no server surface: the console sends the picked theme with the\n * request.\n */\nexport interface ConsoleThemePreset {\n /**\n * Unique across every plugin, and persisted in the site's selection — so\n * namespace it with the plugin (`themes.bootstrap`) and never rename it.\n */\n id: string\n /** The name in the picker. */\n name: string\n /** One line under the name: what the theme looks like. */\n description?: string\n theme: HostTheme\n}\n\n/**\n * The load point a plugin contributing theme presets declares in its\n * `console.slots`, and the theme page loads before it lists them — so the\n * presets' code is fetched there and nowhere else.\n */\nexport const THEME_PRESETS_LOAD_POINT = 'hostThemePresets'\n\n/**\n * Every built-in theme the enabled plugins contribute, in registration order,\n * with the plugin each came from. A second preset with an id already listed\n * is dropped rather than shown twice, so two plugins cannot make one entry of\n * the picker ambiguous.\n */\nexport function listConsoleThemePresets(\n enabledPluginIds?: readonly PluginId[],\n): Array<ConsoleThemePreset & { pluginId: PluginId }> {\n const seen = new Set<string>()\n const out: Array<ConsoleThemePreset & { pluginId: PluginId }> = []\n for (const extension of listConsoleExtensions(enabledPluginIds)) {\n for (const preset of extension.themePresets ?? []) {\n if (!preset?.id || seen.has(preset.id)) continue\n seen.add(preset.id)\n out.push({ ...preset, pluginId: extension.pluginId })\n }\n }\n return out\n}\n\n/**\n * A kind of record a plugin lets the console's search find (AGL-3080): the\n * collection its rows are read from, the fields a row is named and matched\n * by, and where a row opens.\n *\n * The palette reads a source the way it reads the console's own groups — one\n * capped window per collection, ordered by document id and matched in the\n * browser — so a source describes a read the Firestore rules already admit\n * and needs no index of its own. `host` reads `hosts/{hostId}/{collection}`\n * under the site that is open; `orgData` reads `orgs/{orgId}/{collection}`,\n * the organization's shared data, through the reader's `visibleTo` tokens\n * under a site (the predicate the rules evaluate) and unfiltered at the\n * organization level, where only an org-wide member is offered it.\n *\n * Gated as every surface the extension registers is: the plugin must be on\n * for the workspace and the site, and the extension's `featureFlag` and\n * `permission` compose with the source's own. A source the reader may not\n * open is never read, because a row that links to a page the reader is\n * refused is a dead row.\n */\nexport interface ConsoleSearchSource {\n /**\n * The group's id, unique across the palette: its rows, its cache and its\n * key in the rendered list are held under it. Name it by the plugin's own\n * words (`contacts`, `products`) and keep it stable.\n */\n id: string\n /** The heading above the group's rows. */\n group: string\n /** The kind in a sentence: \"Only the first 30 {noun} were searched.\" */\n noun: string\n /** Where the collection hangs, which decides the path and who may read it. */\n scope: 'host' | 'orgData'\n /** The collection's name under the scope's root. */\n collection: string\n /** The field holding a row's human-readable name. */\n nameField: string\n /** The field a row is labeled by when `nameField` is empty. */\n fallbackNameField?: string\n /** Further fields a reader may find a row by (an address, a slug). */\n extraFields?: readonly string[]\n /**\n * A plan quota that must be non-zero for the group to be read at all: a\n * collection the organization cannot hold costs a read to render nothing.\n */\n entitlementKey?: string\n /** A plan flag this group needs beyond the extension's `featureFlag`. */\n featureFlag?: keyof OrgFeatureFlags\n /** A permission this group needs beyond the extension's `permission`. */\n permission?: string\n /**\n * Where the group is listed, ascending. The console's own groups hold 10\n * (sites), 20 (pages), 30 (emails) and 100 to 140 (components, layouts,\n * templates, content, authors); equal numbers keep registration order.\n */\n order: number\n /**\n * Where one row opens, or `null` when it cannot be addressed from here — a\n * row with nowhere to go is dropped rather than drawn dead. `host` is the\n * open site's subdomain, or `null` at the organization level.\n */\n href(\n row: Readonly<Record<string, unknown>>,\n context: ConsoleSearchLinkContext,\n ): string | null\n}\n\n/** The two route params a search row is linked from. */\nexport interface ConsoleSearchLinkContext {\n orgSlug: string\n host: string | null\n}\n\n/**\n * The load point a plugin contributing search sources declares in its\n * `console.slots`, and the palette loads before it lists them.\n */\nexport const CONSOLE_SEARCH_LOAD_POINT = 'consoleSearch'\n\n/**\n * Every search source the enabled plugins contribute, with the extension\n * each came from so the caller can apply its gates. A second source with an\n * id already listed is dropped, so two plugins cannot share one group.\n */\nexport function listConsoleSearchSources(\n enabledPluginIds?: readonly PluginId[],\n): Array<{ extension: ConsoleExtension; source: ConsoleSearchSource }> {\n const seen = new Set<string>()\n const out: Array<{ extension: ConsoleExtension; source: ConsoleSearchSource }> = []\n for (const extension of listConsoleExtensions(enabledPluginIds)) {\n for (const source of extension.searchSources ?? []) {\n if (!source?.id || seen.has(source.id)) continue\n seen.add(source.id)\n out.push({ extension, source })\n }\n }\n return out\n}\n\n/** Providers registered by every extension, in registration order. */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function listConsoleProviders(\n enabledPluginIds?: readonly PluginId[],\n): Array<ComponentType<any>> {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const out: Array<ComponentType<any>> = []\n for (const extension of listConsoleExtensions(enabledPluginIds)) {\n out.push(...(extension.providers ?? []))\n }\n return out\n}\n"],"names":["runInAction","MUI_BUNDLE_ID","defineUiFeatureBundle","options","registrar","dependencies","id","dependsOn","$id","bundleId","displayName","title","description","icon","load","entry","components","registerComponent","component","schema","presets","length","registerPreset","destroy","unregisterPreset","map","preset","unregisterComponent","CONSOLE_WIDGET_SLOTS","hostActivity","hostDashboard","orgDashboard","commerceGlance","hostAnalytics","orgData","besignerFunctions","hostArtifactPublish","orgPluginInstalls","pluginInstallStatus","templateGallery","templateInstallStatus","dashboardFooter","orgSettings","hostSettings","hostFirstRun","hostTheme","themeEditorFonts","staffOverview","adminOrgDetail","orgBillingUsage","orgBillingOverview","staffOrg","staffUser","staffSite","staffOrgsListColumn","staffOrgUsageColumn","orgMember","orgMembersListColumn","hostMembers","siteMember","consoleDock","consoleTopBar","besignerInspector","besignerInteractions","seoFields","hostSeo","recordInsights","recordEmail","importMapping","besignerToolbar","besignerPageProperties","hostScreenRow","hostScreens","hostTemplates","hostLayouts","hostComponents","orgSites","sitePackageItemPreview","mediaLibrary","CONSOLE_STAFF_WIDGET_SLOTS","isConsoleStaffWidgetSlot","slot","includes","CONSOLE_HOST_GATED_WIDGET_SLOTS","isConsoleHostGatedWidgetSlot","consoleExtensions","Map","registerConsoleExtension","extension","set","pluginId","unregisterConsoleExtension","delete","listConsoleExtensions","enabledPluginIds","all","Array","from","values","enabled","Set","filter","has","listConsoleNavItems","inTabOrder","flatMap","navItems","navItem","unlisted","featureFlag","tabOrder","entries","tabOrderOf","index","order","sort","a","b","matchNavItem","href","current","matchNavItemHref","legacyHref","legacyHrefs","match","legacy","undefined","itemHref","segments","sections","ownsSubtree","startsWith","slice","split","Boolean","section","find","item","resolveConsolePluginPage","resolvePluginPageAmong","listConsoleOrgNavItems","orgNavItems","resolveConsoleOrgPluginPage","navItemsOf","best","contenders","Component","bestLength","push","console","error","join","listConsoleStaffPages","staffPages","page","resolveConsoleStaffPage","matches","owners","normalizeConsolePublicPath","path","String","listConsolePublicPages","publicPages","resolveConsolePublicPage","wanted","listConsoleWidgets","out","widget","widgets","THEME_PRESETS_LOAD_POINT","listConsoleThemePresets","seen","themePresets","add","CONSOLE_SEARCH_LOAD_POINT","listConsoleSearchSources","source","searchSources","listConsoleProviders","providers"],"mappings":"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"}