@aglyn/aglyn 1.0.0-beta.145 → 1.0.0-beta.146

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aglyn/aglyn",
3
- "version": "1.0.0-beta.145",
3
+ "version": "1.0.0-beta.146",
4
4
  "license": "Apache-2.0",
5
5
  "homepage": "https://aglyn.com",
6
6
  "repository": {
@@ -33,15 +33,15 @@
33
33
  "./package.json": "./package.json"
34
34
  },
35
35
  "dependencies": {
36
- "@aglyn/shared-data-enums": "1.0.0-beta.145",
37
- "@aglyn/shared-data-mdi": "1.0.0-beta.145",
38
- "@aglyn/shared-data-types": "1.0.0-beta.145",
39
- "@aglyn/shared-util-email": "1.0.0-beta.145",
40
- "@aglyn/shared-util-http": "1.0.0-beta.145",
41
- "@aglyn/shared-util-logger": "1.0.0-beta.145",
42
- "@aglyn/shared-util-timestamp": "1.0.0-beta.145",
43
- "@aglyn/shared-util-tools": "1.0.0-beta.145",
44
- "@aglyn/shared-util-vendor": "1.0.0-beta.145",
36
+ "@aglyn/shared-data-enums": "1.0.0-beta.146",
37
+ "@aglyn/shared-data-mdi": "1.0.0-beta.146",
38
+ "@aglyn/shared-data-types": "1.0.0-beta.146",
39
+ "@aglyn/shared-util-email": "1.0.0-beta.146",
40
+ "@aglyn/shared-util-http": "1.0.0-beta.146",
41
+ "@aglyn/shared-util-logger": "1.0.0-beta.146",
42
+ "@aglyn/shared-util-timestamp": "1.0.0-beta.146",
43
+ "@aglyn/shared-util-tools": "1.0.0-beta.146",
44
+ "@aglyn/shared-util-vendor": "1.0.0-beta.146",
45
45
  "@data-driven-forms/react-form-renderer": "^4.2.0",
46
46
  "@msgpack/msgpack": "^3.1.3",
47
47
  "@types/unist": "^3.0.3",
@@ -24,6 +24,7 @@
24
24
  * (client) — never `@aglyn/aglyn/server` (AGL-405).
25
25
  */
26
26
  export * from './console-widget-slot-context';
27
+ export * from './deployment-capabilities-context';
27
28
  export * from './enabled-plugins-context';
28
29
  export * from './entity-picker-context';
29
30
  export * from './link-target-search-context';
@@ -22,6 +22,7 @@
22
22
  * `./server` barrel and only reachable via the full `@aglyn/aglyn` barrel
23
23
  * (client) — never `@aglyn/aglyn/server` (AGL-405).
24
24
  */ export * from "./console-widget-slot-context.js";
25
+ export * from "./deployment-capabilities-context.js";
25
26
  export * from "./enabled-plugins-context.js";
26
27
  export * from "./entity-picker-context.js";
27
28
  export * from "./link-target-search-context.js";
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/contexts.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2022 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 * Client-only React contexts that live in @aglyn/aglyn (not a UI lib) so\n * every canvas-rendering surface — console, plugins, tenant render — shares\n * ONE context module instance (a second instance renders the site blank).\n * They call `createContext` at module scope, which App Router forbids in a\n * Server Component's module graph, so they are split out of the server-safe\n * `./server` barrel and only reachable via the full `@aglyn/aglyn` barrel\n * (client) — never `@aglyn/aglyn/server` (AGL-405).\n */\nexport * from './console-widget-slot-context'\nexport * from './enabled-plugins-context'\nexport * from './entity-picker-context'\nexport * from './link-target-search-context'\nexport * from './media-picker-context'\nexport * from './node-identity'\nexport * from './page-header-actions-context'\nexport * from './page-header-record-context'\nexport * from './screen-link-context'\nexport * from './site-context'\n"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;CAQC,GACD,cAAc,mCAA+B;AAC7C,cAAc,+BAA2B;AACzC,cAAc,6BAAyB;AACvC,cAAc,kCAA8B;AAC5C,cAAc,4BAAwB;AACtC,cAAc,qBAAiB;AAC/B,cAAc,mCAA+B;AAC7C,cAAc,kCAA8B;AAC5C,cAAc,2BAAuB;AACrC,cAAc,oBAAgB"}
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/contexts.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2022 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 * Client-only React contexts that live in @aglyn/aglyn (not a UI lib) so\n * every canvas-rendering surface — console, plugins, tenant render — shares\n * ONE context module instance (a second instance renders the site blank).\n * They call `createContext` at module scope, which App Router forbids in a\n * Server Component's module graph, so they are split out of the server-safe\n * `./server` barrel and only reachable via the full `@aglyn/aglyn` barrel\n * (client) — never `@aglyn/aglyn/server` (AGL-405).\n */\nexport * from './console-widget-slot-context'\nexport * from './deployment-capabilities-context'\nexport * from './enabled-plugins-context'\nexport * from './entity-picker-context'\nexport * from './link-target-search-context'\nexport * from './media-picker-context'\nexport * from './node-identity'\nexport * from './page-header-actions-context'\nexport * from './page-header-record-context'\nexport * from './screen-link-context'\nexport * from './site-context'\n"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;CAQC,GACD,cAAc,mCAA+B;AAC7C,cAAc,uCAAmC;AACjD,cAAc,+BAA2B;AACzC,cAAc,6BAAyB;AACvC,cAAc,kCAA8B;AAC5C,cAAc,4BAAwB;AACtC,cAAc,qBAAiB;AAC/B,cAAc,mCAA+B;AAC7C,cAAc,kCAA8B;AAC5C,cAAc,2BAAuB;AACrC,cAAc,oBAAgB"}
@@ -0,0 +1,65 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ /**
18
+ * What THIS DEPLOYMENT can do — facts about the install, not about the
19
+ * organization reading it (AGL-3080).
20
+ *
21
+ * The third kind of answer a plugin surface needs and cannot reach for
22
+ * itself. `entitled` says what the workspace bought and `permissions` says
23
+ * who is reading; neither says whether the software in front of them is
24
+ * wired up to a provider at all. A self-host install is the case: the
25
+ * marketplace's release flag defaults ON, so a fresh install draws the whole
26
+ * thing — browse, listing pages, a Buy button, a payout panel — all of it
27
+ * backed by Aglyn's Stripe Connect platform, which the operator does not have
28
+ * and cannot get.
29
+ *
30
+ * WHY A CONTEXT, and why the value comes from a SERVER layout. The facts are
31
+ * server-only secrets: `STRIPE_SECRET_KEY` carries no `NEXT_PUBLIC_` prefix,
32
+ * so a client component reading `process.env` for it gets `undefined` and
33
+ * concludes "not configured" on every deployment, our own included. The
34
+ * console's server layout knows the answer before any JavaScript ships and
35
+ * hands it down, which is also why this is not a fetch: a notice that says
36
+ * what a deployment cannot do must not arrive after the click it exists to
37
+ * precede.
38
+ *
39
+ * WHAT IT IS NOT. Not a release flag and not an entitlement — the three
40
+ * answer different questions and a surface usually needs all three. The flag
41
+ * asks whether this deployment should show the feature; the entitlement asks
42
+ * whether this organization bought it; this asks whether the deployment can
43
+ * carry it out. Nothing here is a gate: browsing and free installs genuinely
44
+ * work without Stripe, so a surface reads this to EXPLAIN itself, and hiding
45
+ * on it would remove working functionality to avoid describing one that is
46
+ * off.
47
+ */
48
+ export interface DeploymentCapabilities {
49
+ /**
50
+ * True when this deployment has a Stripe platform at all (AGL-2019) —
51
+ * Aglyn as the biller, which is infrastructure, not a tenant's own
52
+ * checkout.
53
+ *
54
+ * ⚠️ Read it the safe way round. The default below is `true`, because the
55
+ * absence of a provider is not evidence of an absent platform: every
56
+ * surface rendered outside one — a test harness, a widget mounted in a
57
+ * zone — would otherwise draw a permanent "not configured" banner on
58
+ * Aglyn's own console.
59
+ */
60
+ payments: boolean;
61
+ }
62
+ export declare const DEPLOYMENT_CAPABILITIES_DEFAULT: DeploymentCapabilities;
63
+ export declare const DeploymentCapabilitiesContext: import("react").Context<DeploymentCapabilities>;
64
+ /** Hook form of {@link DeploymentCapabilitiesContext}. */
65
+ export declare function useDeploymentCapabilities(): DeploymentCapabilities;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ // Same placement rationale as page-header-record-context.ts: lives in
17
+ // @aglyn/aglyn without a 'use client' banner so both the console app and the
18
+ // feature plugins share one context module.
19
+ import { createContext, useContext } from "react";
20
+ export const DEPLOYMENT_CAPABILITIES_DEFAULT = {
21
+ payments: true
22
+ };
23
+ export const DeploymentCapabilitiesContext = createContext(DEPLOYMENT_CAPABILITIES_DEFAULT);
24
+ DeploymentCapabilitiesContext.displayName = 'DeploymentCapabilitiesContext';
25
+ /** Hook form of {@link DeploymentCapabilitiesContext}. */ export function useDeploymentCapabilities() {
26
+ return useContext(DeploymentCapabilitiesContext);
27
+ }
28
+
29
+ //# sourceMappingURL=deployment-capabilities-context.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/deployment-capabilities-context.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// Same placement rationale as page-header-record-context.ts: lives in\n// @aglyn/aglyn without a 'use client' banner so both the console app and the\n// feature plugins share one context module.\nimport { createContext, useContext } from 'react'\n\n/**\n * What THIS DEPLOYMENT can do — facts about the install, not about the\n * organization reading it (AGL-3080).\n *\n * The third kind of answer a plugin surface needs and cannot reach for\n * itself. `entitled` says what the workspace bought and `permissions` says\n * who is reading; neither says whether the software in front of them is\n * wired up to a provider at all. A self-host install is the case: the\n * marketplace's release flag defaults ON, so a fresh install draws the whole\n * thing — browse, listing pages, a Buy button, a payout panel — all of it\n * backed by Aglyn's Stripe Connect platform, which the operator does not have\n * and cannot get.\n *\n * WHY A CONTEXT, and why the value comes from a SERVER layout. The facts are\n * server-only secrets: `STRIPE_SECRET_KEY` carries no `NEXT_PUBLIC_` prefix,\n * so a client component reading `process.env` for it gets `undefined` and\n * concludes \"not configured\" on every deployment, our own included. The\n * console's server layout knows the answer before any JavaScript ships and\n * hands it down, which is also why this is not a fetch: a notice that says\n * what a deployment cannot do must not arrive after the click it exists to\n * precede.\n *\n * WHAT IT IS NOT. Not a release flag and not an entitlement — the three\n * answer different questions and a surface usually needs all three. The flag\n * asks whether this deployment should show the feature; the entitlement asks\n * whether this organization bought it; this asks whether the deployment can\n * carry it out. Nothing here is a gate: browsing and free installs genuinely\n * work without Stripe, so a surface reads this to EXPLAIN itself, and hiding\n * on it would remove working functionality to avoid describing one that is\n * off.\n */\nexport interface DeploymentCapabilities {\n /**\n * True when this deployment has a Stripe platform at all (AGL-2019) —\n * Aglyn as the biller, which is infrastructure, not a tenant's own\n * checkout.\n *\n * ⚠️ Read it the safe way round. The default below is `true`, because the\n * absence of a provider is not evidence of an absent platform: every\n * surface rendered outside one — a test harness, a widget mounted in a\n * zone — would otherwise draw a permanent \"not configured\" banner on\n * Aglyn's own console.\n */\n payments: boolean\n}\n\nexport const DEPLOYMENT_CAPABILITIES_DEFAULT: DeploymentCapabilities = {\n payments: true,\n}\n\nexport const DeploymentCapabilitiesContext =\n createContext<DeploymentCapabilities>(DEPLOYMENT_CAPABILITIES_DEFAULT)\nDeploymentCapabilitiesContext.displayName = 'DeploymentCapabilitiesContext'\n\n/** Hook form of {@link DeploymentCapabilitiesContext}. */\nexport function useDeploymentCapabilities(): DeploymentCapabilities {\n return useContext(DeploymentCapabilitiesContext)\n}\n"],"names":["createContext","useContext","DEPLOYMENT_CAPABILITIES_DEFAULT","payments","DeploymentCapabilitiesContext","displayName","useDeploymentCapabilities"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,sEAAsE;AACtE,6EAA6E;AAC7E,4CAA4C;AAC5C,SAASA,aAAa,EAAEC,UAAU,QAAQ,QAAO;AAgDjD,OAAO,MAAMC,kCAA0D;IACrEC,UAAU;AACZ,EAAC;AAED,OAAO,MAAMC,gCACXJ,cAAsCE,iCAAgC;AACxEE,8BAA8BC,WAAW,GAAG;AAE5C,wDAAwD,GACxD,OAAO,SAASC;IACd,OAAOL,WAAWG;AACpB"}
@@ -27,6 +27,35 @@
27
27
  * generated from; this module holds the row's shape and the resolvers.
28
28
  */
29
29
  import { FIRST_PARTY_PLUGINS, PUBLISHED_SITE_IMPACT } from './first-party-plugins.generated';
30
+ /**
31
+ * One quick link on a live site's admin edit bar, declared by the plugin whose
32
+ * console page it opens (AGL-3080).
33
+ *
34
+ * The bar is drawn by the TENANT server, which never loads a plugin's console
35
+ * code, so this is DATA compiled from `plugins.config.json` rather than a
36
+ * runtime registration — a registry that process had not filled would drop the
37
+ * link silently instead of failing, which is the AGL-3025 shape. It is also
38
+ * why the label is written here rather than taken from the plugin's nav item:
39
+ * the nav item lives in the console bundle, and "Orders" is a section of
40
+ * Commerce rather than the plugin's own name.
41
+ *
42
+ * The link is drawn only where the SITE runs the plugin. The bar asks the same
43
+ * resolved plugin set the rest of the edit context is built from, so a site
44
+ * with Commerce switched off is not offered its Orders page.
45
+ */
46
+ export interface PluginEditBarLink {
47
+ /** The plugin that owns the page — also the id the site's set is checked against. */
48
+ pluginId: string;
49
+ /** Where it sits in the bar; every declared order is distinct. */
50
+ order: number;
51
+ /** What the bar says. A SECTION's name where that is the honest one. */
52
+ label: string;
53
+ /**
54
+ * Console path beneath the site, beginning with `/` and carrying no query —
55
+ * `/inbox`, `/products/orders`. Joined onto `/{orgSlug}/hosts/{host}`.
56
+ */
57
+ path: string;
58
+ }
30
59
  export interface FirstPartyPlugin {
31
60
  /**
32
61
  * Stable plugin id — persisted in `org.enabledPlugins`,
@@ -197,6 +226,21 @@ export declare const FORMS_PLUGIN_ID = "forms";
197
226
  * those bundles had not filled would read as every plugin switched OFF.
198
227
  */
199
228
  export { FIRST_PARTY_PLUGINS };
229
+ /**
230
+ * The admin edit bar's quick links that THIS SITE runs, in the order the bar
231
+ * draws them (AGL-3080).
232
+ *
233
+ * `enabledPluginIds` is the site's resolved plugin set — the same one the rest
234
+ * of the edit context is built from, already narrowed by the org's switchboard,
235
+ * the site's own deny-list and the release flags. A link whose plugin is not
236
+ * in it is not offered, which is what stops the bar linking a site with
237
+ * Commerce switched off to its Orders page.
238
+ *
239
+ * Compiled in rather than registered, for the reason the catalog above is:
240
+ * the tenant server draws this bar and never loads a plugin's console code,
241
+ * so a registry it had not filled would drop every link with nothing red.
242
+ */
243
+ export declare function pluginEditBarLinks(enabledPluginIds: readonly string[] | undefined): readonly PluginEditBarLink[];
200
244
  /** Ids loaded for orgs that have never touched the switchboard. */
201
245
  export declare const DEFAULT_ENABLED_PLUGINS: readonly string[];
202
246
  /**
@@ -24,7 +24,7 @@
24
24
  * The catalog's rows are declared by the plugins themselves, in
25
25
  * `plugins.config.json`, beside the package names the loader manifests are
26
26
  * generated from; this module holds the row's shape and the resolvers.
27
- */ import { FIRST_PARTY_PLUGINS, PUBLISHED_SITE_IMPACT } from "./first-party-plugins.generated.js";
27
+ */ import { FIRST_PARTY_PLUGINS, PLUGIN_EDIT_BAR_LINKS, PUBLISHED_SITE_IMPACT } from "./first-party-plugins.generated.js";
28
28
  /**
29
29
  * Every catalog id classified, from the `publishedSiteImpact` each plugin
30
30
  * declares on its own row. The generator refuses a row whose verdict
@@ -61,6 +61,23 @@
61
61
  * is synchronous and runs in each bundle of both apps; a registry that one of
62
62
  * those bundles had not filled would read as every plugin switched OFF.
63
63
  */ export { FIRST_PARTY_PLUGINS };
64
+ /**
65
+ * The admin edit bar's quick links that THIS SITE runs, in the order the bar
66
+ * draws them (AGL-3080).
67
+ *
68
+ * `enabledPluginIds` is the site's resolved plugin set — the same one the rest
69
+ * of the edit context is built from, already narrowed by the org's switchboard,
70
+ * the site's own deny-list and the release flags. A link whose plugin is not
71
+ * in it is not offered, which is what stops the bar linking a site with
72
+ * Commerce switched off to its Orders page.
73
+ *
74
+ * Compiled in rather than registered, for the reason the catalog above is:
75
+ * the tenant server draws this bar and never loads a plugin's console code,
76
+ * so a registry it had not filled would drop every link with nothing red.
77
+ */ export function pluginEditBarLinks(enabledPluginIds) {
78
+ const enabled = new Set(enabledPluginIds != null ? enabledPluginIds : []);
79
+ return PLUGIN_EDIT_BAR_LINKS.filter((link)=>enabled.has(link.pluginId));
80
+ }
64
81
  /** Ids loaded for orgs that have never touched the switchboard. */ export const DEFAULT_ENABLED_PLUGINS = FIRST_PARTY_PLUGINS.map((plugin)=>plugin.id);
65
82
  /**
66
83
  * The id a stored plugin id means today — the seam a rename re-arms.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/enabled-plugins.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Per-org plugin enablement (AGL-416): `org.enabledPlugins` is the\n * switchboard that decides which plugins LOAD for a workspace — the loader\n * (AGL-417) dynamically imports only these. It composes with (not replaces)\n * the existing gates: a surface renders when its plugin is enabled AND its\n * `featureFlag` entitlement resolves; marketplace/marketplace listings keep\n * their per-host/org `installs` docs on top.\n *\n * The catalog's rows are declared by the plugins themselves, in\n * `plugins.config.json`, beside the package names the loader manifests are\n * generated from; this module holds the row's shape and the resolvers.\n */\n\nimport {\n FIRST_PARTY_PLUGINS,\n PUBLISHED_SITE_IMPACT,\n} from './first-party-plugins.generated'\n\nexport interface FirstPartyPlugin {\n /**\n * Stable plugin id — persisted in `org.enabledPlugins`,\n * `host.disabledPlugins` and `host.enabledPlugins`, and the document id\n * under every `pluginSettings` collection.\n *\n * Renaming one is therefore a data change, not a code change, and it is\n * done in two halves that must both ship: the old id is read through\n * {@link canonicalPluginId} so every stored list still resolves, and a\n * backfill rewrites the stored documents so the alias can be retired once\n * it reads nothing.\n */\n id: string\n /** Console-facing display name. */\n label: string\n /**\n * On for every workspace AND every site, with no switch anywhere: the base\n * component library the canvas cannot render without.\n */\n alwaysOn?: boolean\n /**\n * On for every WORKSPACE, and switchable for one SITE (AGL-3028, AGL-3029).\n *\n * The workspace half is what `alwaysOn` gives: the id is unioned into the\n * org's resolved set whatever `org.enabledPlugins` stores, so a list saved\n * before the plugin existed still runs it, and no workspace switch is\n * offered. The site half is ordinary: the id is subtracted by a site's\n * `disabledPlugins` deny-list like any other, so an absent host field means\n * ON and nothing needs migrating.\n *\n * It exists for capabilities whose workspace-level half must never stop.\n * AI carries the add-on, credits, allotments and overage billing, none of\n * which belongs to a site; Forms carries the catalog and the submissions\n * already stored. A workspace switch would take those down with the\n * site-facing half, so the only switch is the site's.\n */\n alwaysOnForWorkspace?: boolean\n /**\n * What switching this plugin off for ONE site stops, and what it leaves\n * running (AGL-3028, AGL-3029) — the copy the site's Admin › Plugins page\n * states beside the switch, so the decision is made knowing both halves.\n *\n * `confirm` makes the site switch ask before it applies, naming what it is\n * about to break on published pages.\n */\n siteOff?: {\n /** What stops on the site, stated as the consequence. */\n stops: string\n /** What keeps running, because it carries no site. */\n keeps: string\n /** Ask before applying. */\n confirm?: boolean\n /**\n * How the confirmation names the site's published pages the switch\n * reaches (AGL-3029): the sentence above the list, and the one that\n * stands in for an empty list when the scan read everything.\n */\n pages?: { heading: string; none: string }\n }\n /** One-line description for the org-settings toggle list. */\n description?: string\n /**\n * Release flag gating this plugin platform-wide (AGL-422). A flagged-off\n * plugin is subtracted from every workspace's effective set — console\n * loader, published sites, and API dispatch — unless the subject is\n * staff. Always-on plugins carry no flag.\n */\n releaseFlag?: string\n /**\n * OFF for a site until that site turns it on (AGL-2486) — the inversion of\n * this switchboard's default, and the only field here that changes what an\n * absent host doc means.\n *\n * The per-host field is a DENY-list: a site records what it switches off,\n * which makes \"absent means on\" the default for all twelve other bundles.\n * That is right for a capability whose worst case is an unused nav tab, and\n * wrong for one whose worst case is a PAGE — `/signin`, `/signup`,\n * `/recover` were served by every published site on the platform,\n * including marketing sites whose real sign-in is somewhere else entirely.\n * A sign-in-shaped page on a brand's own domain that is not that brand's\n * sign-in is a credential-confusion hazard, so this one defaults closed.\n *\n * A site un-defaults it by listing the id in `host.enabledPlugins`. That\n * list is scoped to default-off ids and cannot widen anything else, so the\n * AGL-1014 invariant survives intact: a site still can never reach past\n * what its org enables.\n */\n defaultOffPerSite?: boolean\n /**\n * Plugin ids this one cannot function without (AGL-2486) — DECLARED, never\n * inferred.\n *\n * The switchboard has always let a workspace turn off a plugin another one\n * is built on, and said nothing. `accounts` is the case that proves it: the\n * Members blocks and every `membership/*` API handler ship inside the\n * COMMERCE bundle, so switching Commerce off leaves a site advertising\n * `/signin` while nothing can answer the login POST.\n *\n * Declared rather than derived on purpose. The couplings that matter are\n * exactly the ones no static read can see — which bundle happens to\n * register whose components — so a graph inferred from imports or from the\n * registry would miss the real edges while looking authoritative. An\n * incomplete warning that presents itself as complete is worse than none,\n * so this list is the contract, and {@link resolveDisableCascade} is honest\n * about covering only what is on it.\n */\n requires?: readonly string[]\n}\n\n/**\n * What a DISABLE does to a site that is already published (AGL-2486).\n *\n * The cascade dialog has to state a consequence, and there are two very\n * different ones. \"These blocks will no longer be offered\" is a different\n * decision from \"parts of your live pages go blank\", and one generic sentence\n * for both would be a lie in one direction or the other.\n *\n * - `elements` — the plugin registers site components. The tenant loads only\n * the site's enabled bundles, so elements ALREADY PLACED on published pages\n * stop rendering. Pre-existing AGL-1014 behaviour.\n * - `routes` — the plugin registers no components, but a published site\n * stops serving something: `accounts` gates `/signin`, `/signup`,\n * `/recover`; `redirects` stops applying its rules; `workflows` stops\n * answering its hooks.\n * - `console-only` — nothing a visitor can reach changes; the plugin leaves\n * navigation and the editor.\n */\nexport type PublishedSiteImpact = 'elements' | 'routes' | 'console-only'\n\n/**\n * Every catalog id classified, from the `publishedSiteImpact` each plugin\n * declares on its own row. The generator refuses a row whose verdict\n * disagrees with its `register.site`, so a site-registering bundle cannot be\n * added without declaring its consequence.\n */\nexport { PUBLISHED_SITE_IMPACT }\n\n/**\n * The user-accounts capability (AGL-2486): visitor sign-in, sign-up and\n * password recovery on a published site.\n *\n * It carries no loader manifest entry, and that is deliberate rather than an\n * oversight. The member blocks and the `membership/*` API handlers already\n * ship inside the commerce bundle, so there is no separate package to load;\n * what this id contributes is the SWITCH — the thing the tenant route gate,\n * the console card and the sitemap all ask. Several catalog ids already have\n * no tenant bundle (`contacts`, `data`, `logic`), so a manifest-less entry is\n * the existing shape, not a new one.\n */\nexport const ACCOUNTS_PLUGIN_ID = 'accounts'\n\n/**\n * The Forms capability's id, named in core (AGL-3029).\n *\n * A form's server half is core — `/api/forms/submit`, the publish-time\n * contract check, the published page's render — and core may not import a\n * plugin, so each asks the site's plugin set about this id rather than asking\n * the Forms plugin anything.\n */\nexport const FORMS_PLUGIN_ID = 'forms'\n\n/**\n * The switchboard catalog. The core holds no row of it: each plugin declares\n * its own in `plugins.config.json` (`catalog`, and one per `capabilities`\n * entry), and the generator compiles them here in their declared order, so\n * adding a plugin edits no core file.\n *\n * Compiled in rather than registered at runtime, because every resolver below\n * is synchronous and runs in each bundle of both apps; a registry that one of\n * those bundles had not filled would read as every plugin switched OFF.\n */\nexport { FIRST_PARTY_PLUGINS }\n\n/** Ids loaded for orgs that have never touched the switchboard. */\nexport const DEFAULT_ENABLED_PLUGINS: readonly string[] =\n FIRST_PARTY_PLUGINS.map((plugin) => plugin.id)\n\n/**\n * The id a stored plugin id means today — the seam a rename re-arms.\n *\n * A first-party id is written into every org's `enabledPlugins` and every\n * site's `disabledPlugins` the moment somebody touches the switchboard, so a\n * rename in this file alone would silently turn the plugin OFF for every\n * workspace that had listed it under the old name — the stored id would match\n * nothing in the catalog and fall through as a marketplace listing id. Every\n * reader of a stored list runs it through here first, so a document written\n * before a rename keeps meaning what it meant while the backfill runs.\n *\n * Nothing is aliased today. `contacts` read as `crm` from the CRM's rename\n * (AGL-2595) until its backfill reported zero documents carrying the old id,\n * and the alias was retired (AGL-2614) — an alias that outlives its backfill\n * is a second name for the plugin that nothing writes and every reader must\n * keep honoring. The next rename adds its pair here, ships a backfill over\n * `org.enabledPlugins`, `host.disabledPlugins`, `host.enabledPlugins` and the\n * `pluginSettings` document ids, and removes the pair the same way.\n */\nexport function canonicalPluginId(pluginId: string): string {\n return pluginId\n}\n\n/** A stored list, read through the alias seam and de-duplicated. */\nfunction canonicalPluginIds(pluginIds: readonly unknown[]): string[] {\n return Array.from(\n new Set(pluginIds.map((id) => canonicalPluginId(String(id)))),\n )\n}\n\n/** On everywhere, a site's deny-list included: the base component library. */\nconst ALWAYS_ON: readonly string[] = FIRST_PARTY_PLUGINS.filter(\n (plugin) => plugin.alwaysOn,\n).map((plugin) => plugin.id)\n\n/**\n * Unioned into every workspace's set whatever the org stored: {@link ALWAYS_ON}\n * plus the ids that are on for every workspace and switchable per site.\n */\nconst ALWAYS_ON_FOR_WORKSPACE: readonly string[] = FIRST_PARTY_PLUGINS.filter(\n (plugin) => plugin.alwaysOn || plugin.alwaysOnForWorkspace,\n).map((plugin) => plugin.id)\n\n/**\n * Whether the WORKSPACE switch for this plugin is locked on (AGL-3028): the\n * base library, and every plugin that is on for every workspace and\n * switchable only per site. The org switchboard renders these on and inert.\n */\nexport function isLockedOnForWorkspace(pluginId: string): boolean {\n return ALWAYS_ON_FOR_WORKSPACE.includes(pluginId)\n}\n\n/**\n * Whether the SITE switch for this plugin is locked on: the base library\n * alone. Every other plugin a workspace runs can be switched off for one site.\n */\nexport function isLockedOnForSite(pluginId: string): boolean {\n return ALWAYS_ON.includes(pluginId)\n}\n\nconst FIRST_PARTY_IDS: ReadonlySet<string> = new Set(\n FIRST_PARTY_PLUGINS.map((plugin) => plugin.id),\n)\n\n/**\n * Ids a SITE does not get until it asks (AGL-2486). See\n * {@link FirstPartyPlugin.defaultOffPerSite} for why this inversion exists.\n */\nexport const DEFAULT_OFF_PER_SITE_PLUGIN_IDS: ReadonlySet<string> = new Set(\n FIRST_PARTY_PLUGINS.filter((plugin) => plugin.defaultOffPerSite).map(\n (plugin) => plugin.id,\n ),\n)\n\n/** Whether a plugin id is off for a site until that site opts in. */\nexport function isDefaultOffPerSite(pluginId: string): boolean {\n return DEFAULT_OFF_PER_SITE_PLUGIN_IDS.has(pluginId)\n}\n\n/**\n * Applies a host's `enabledPlugins` OPT-IN list (AGL-2486): subtracts every\n * default-off id the host has not explicitly asked for.\n *\n * Narrow-only, like its deny-list sibling. The list can only ever REMOVE the\n * default-off subtraction for an id the org already enables — listing an\n * ordinary id buys nothing, and listing one the org switched off buys\n * nothing either, because this runs against the org's resolved set.\n */\nexport function applyDefaultOffOptIn(\n pluginIds: readonly string[],\n optedIn?: readonly string[] | null,\n): string[] {\n if (!DEFAULT_OFF_PER_SITE_PLUGIN_IDS.size) return [...pluginIds]\n const asked = new Set(\n Array.isArray(optedIn) ? canonicalPluginIds(optedIn) : [],\n )\n return pluginIds.filter(\n (id) => !DEFAULT_OFF_PER_SITE_PLUGIN_IDS.has(id) || asked.has(id),\n )\n}\n\n/**\n * Whether an `enabledPlugins` id is a first-party BUNDLE (vs a marketplace\n * listing id) — AGL-777. `enabledPlugins` is a flat mix of the two: bundle\n * ids are the short, stable names in {@link FIRST_PARTY_PLUGINS}; marketplace\n * installs ride the same field under their Firestore listing doc id. This is\n * the single classifier both writers and readers use so the two kinds never\n * get confused — e.g. an install sync must never add/remove a bundle id.\n */\nexport function isFirstPartyPlugin(pluginId: string): boolean {\n return FIRST_PARTY_IDS.has(pluginId)\n}\n\n/**\n * Splits a mixed `enabledPlugins` array into first-party bundle ids and\n * marketplace listing ids (AGL-777). The field stays a single flat list;\n * this just names the two kinds for callers that need to treat them apart.\n */\nexport function classifyEnabledPlugins(pluginIds: readonly string[]): {\n bundles: string[]\n listings: string[]\n} {\n const bundles: string[] = []\n const listings: string[] = []\n for (const id of pluginIds) {\n if (isFirstPartyPlugin(id)) bundles.push(id)\n else listings.push(id)\n }\n return { bundles, listings }\n}\n\n/**\n * The org's effective enabled-plugin set. Absent field → every first-party\n * plugin (existing orgs keep working untouched); always-on ids — and the ids\n * that are on for every workspace and switchable only per site — are unioned\n * in, so no stored list can switch them off for a workspace, including one\n * saved before the plugin existed. Unknown ids are kept — realm-trusted\n * marketplace plugins (AGL-420) ride the same field.\n */\nexport function resolveEnabledPlugins(\n org?: { enabledPlugins?: string[] } | null,\n): string[] {\n const configured = org?.enabledPlugins\n const base = Array.isArray(configured)\n ? canonicalPluginIds(configured)\n : [...DEFAULT_ENABLED_PLUGINS]\n return Array.from(new Set([...ALWAYS_ON_FOR_WORKSPACE, ...base]))\n}\n\n/**\n * Subtracts a host's per-site deny-list from an enabled set (AGL-1014).\n * Always-on ids survive — the base component library cannot be switched\n * off per site any more than per org. A plugin that is on for every\n * workspace (`alwaysOnForWorkspace`) does NOT survive: its site switch is\n * the one switch it has. Order of the surviving ids is kept.\n */\nexport function subtractDisabledPlugins(\n pluginIds: readonly string[],\n disabledPlugins?: readonly string[] | null,\n): string[] {\n if (!Array.isArray(disabledPlugins) || !disabledPlugins.length)\n return [...pluginIds]\n const disabled = new Set(canonicalPluginIds(disabledPlugins))\n return pluginIds.filter(\n (id) => ALWAYS_ON.includes(id) || !disabled.has(id),\n )\n}\n\n/**\n * A HOST's effective enabled-plugin set (AGL-1014): the org's resolved set\n * minus the host's `disabledPlugins` deny-list. This is the single source\n * of truth for per-site enablement — console navigation, the editor,\n * published sites, and API dispatch must all read it, or a \"disabled\"\n * plugin is merely hidden, not off.\n *\n * Semantics are narrow-only by construction: a host stores what it turns\n * OFF, so it can never widen beyond what the org enables, and an absent\n * field means every org-enabled plugin runs (newly installed plugins\n * default to enabled per site until a host admin disables them).\n *\n * ONE class of id reads the other way (AGL-2486): a `defaultOffPerSite`\n * plugin is subtracted unless the host names it in `enabledPlugins`. That\n * list un-defaults; it does not grant. Both fields are still bounded by the\n * org's set, and an explicit deny still beats an explicit opt-in — the two\n * are applied in that order below, so the safe reading wins whenever a\n * hand-edited or stale doc sets both.\n */\nexport function resolveHostEnabledPlugins(\n org?: { enabledPlugins?: string[] } | null,\n host?: { disabledPlugins?: string[]; enabledPlugins?: string[] } | null,\n): string[] {\n return subtractDisabledPlugins(\n applyDefaultOffOptIn(resolveEnabledPlugins(org), host?.enabledPlugins),\n host?.disabledPlugins,\n )\n}\n\n/**\n * Whether ONE plugin runs on this site — the host-aware counterpart of\n * {@link isPluginEnabled}, and the form every route gate wants.\n */\nexport function isHostPluginEnabled(\n org: { enabledPlugins?: string[] } | null | undefined,\n host: { disabledPlugins?: string[]; enabledPlugins?: string[] } | null | undefined,\n pluginId: string,\n): boolean {\n return resolveHostEnabledPlugins(org, host).includes(pluginId)\n}\n\nexport function isPluginEnabled(\n org: { enabledPlugins?: string[] } | null | undefined,\n pluginId: string,\n): boolean {\n return resolveEnabledPlugins(org).includes(pluginId)\n}\n\n/**\n * Where one plugin stands ON ONE SITE (AGL-1014, AGL-2486).\n *\n * Three surfaces need this answer and they must not each derive it: the site\n * plugin page's \"Where it runs\" card, that page's dependency list — where the\n * same question is asked about a NEIGHBOR, and where the answer is the whole\n * point, because a dependency the workspace enables and this site has switched\n * off is a broken plugin behind a healthy-looking workspace page — and the\n * per-site switchboard. A second derivation is a second chance to disagree\n * with `resolveHostEnabledPlugins`, which is what actually runs.\n *\n * - `always-on` — the base library; it cannot be switched off anywhere.\n * A plugin that is on for every workspace but\n * switchable per site is NOT this: it reads\n * `runs-here` or `off-for-site` like any other.\n * - `off-for-workspace` — the org has it off, so it runs on none of its sites\n * and this site cannot turn it on.\n * - `runs-here` — the effective host set contains it.\n * - `awaiting-opt-in` — a `defaultOffPerSite` plugin the org enables and\n * this site has not asked for. Off, but off by\n * DEFAULT rather than by a decision, which is a\n * different thing to tell an operator.\n * - `off-for-site` — the org enables it and this site switched it off.\n */\nexport type PluginSiteState =\n | 'always-on'\n | 'off-for-workspace'\n | 'runs-here'\n | 'awaiting-opt-in'\n | 'off-for-site'\n\nexport function resolvePluginSiteState(\n org: { enabledPlugins?: string[] } | null | undefined,\n host: { disabledPlugins?: string[]; enabledPlugins?: string[] } | null | undefined,\n pluginId: string,\n): PluginSiteState {\n if (ALWAYS_ON.includes(pluginId)) return 'always-on'\n if (!resolveEnabledPlugins(org).includes(pluginId)) return 'off-for-workspace'\n if (resolveHostEnabledPlugins(org, host).includes(pluginId)) return 'runs-here'\n // A default-off plugin this site has not named is off because nobody asked\n // for it; naming it in the deny-list is a decision, and an explicit deny\n // beats an opt-in, so the deny-list is checked first.\n const denied = Array.isArray(host?.disabledPlugins)\n ? host.disabledPlugins.map(String).includes(pluginId)\n : false\n return !denied && isDefaultOffPerSite(pluginId)\n ? 'awaiting-opt-in'\n : 'off-for-site'\n}\n\n/**\n * The declared dependency graph, both directions, built once (AGL-2486).\n *\n * `requires` on the catalog is the forward edge — what a plugin cannot run\n * without — and every consumer so far wanted the reverse one. Building both\n * here keeps the cascade, the console's dependency card and the server-side\n * refusal reading the SAME edges: a graph assembled a second time is a graph\n * that can disagree with itself about which plugins are coupled, and the\n * disagreement would show up as a warning shown on one surface and not on the\n * one that writes.\n *\n * `extraRequirements` extends the catalog and cannot shrink it, exactly as it\n * does for {@link resolveDisableCascade} — a third-party id can add edges of\n * its own but can never declare away a first-party one.\n */\nfunction dependencyEdges(\n extraRequirements?: Readonly<Record<string, readonly string[]>>,\n): {\n /** Keyed by a required id; holds the ids that depend on it. */\n dependents: Map<string, string[]>\n /** Keyed by a dependent id; holds the ids it requires. */\n requires: Map<string, string[]>\n} {\n const dependents = new Map<string, string[]>()\n const requires = new Map<string, string[]>()\n const addEdge = (dependent: string, required: string) => {\n const reverse = dependents.get(required)\n if (reverse) reverse.push(dependent)\n else dependents.set(required, [dependent])\n const forward = requires.get(dependent)\n if (forward) forward.push(required)\n else requires.set(dependent, [required])\n }\n for (const plugin of FIRST_PARTY_PLUGINS)\n for (const required of plugin.requires ?? []) addEdge(plugin.id, required)\n for (const [dependent, required] of Object.entries(extraRequirements ?? {}))\n for (const one of required ?? []) addEdge(dependent, String(one))\n return { dependents, requires }\n}\n\n/** What `pluginId` declares it cannot run without — DIRECT edges only. */\nexport function pluginRequirements(\n pluginId: string,\n extraRequirements?: Readonly<Record<string, readonly string[]>>,\n): string[] {\n return [...(dependencyEdges(extraRequirements).requires.get(pluginId) ?? [])]\n}\n\n/** What declares it cannot run without `pluginId` — DIRECT edges only. */\nexport function pluginDependents(\n pluginId: string,\n extraRequirements?: Readonly<Record<string, readonly string[]>>,\n): string[] {\n return [...(dependencyEdges(extraRequirements).dependents.get(pluginId) ?? [])]\n}\n\n/**\n * Which of `enabledIds` would be left running with a requirement switched off\n * (AGL-2486) — the same question the cascade dialog asks, phrased so a WRITER\n * can refuse instead of a reader warning.\n *\n * The dialog is a courtesy: it stands in front of the console's own switches\n * and nothing else. A direct API call, a stale tab that posts an older set, or\n * a second console surface that forgot to ask can still store a set where\n * User Accounts is on and the Commerce bundle that answers its sign-in POST is\n * not. This is what a write path calls to reject that set outright, so the\n * boundary does not live in a dialog anyone can skip.\n *\n * Only DECLARED requirements are checked, with the same limit\n * {@link PLUGIN_CASCADE_IS_DECLARED_ONLY} states: an undeclared coupling is\n * invisible here too. A requirement naming an id outside the catalog is\n * ignored rather than treated as unmet — a stale edge must not lock a\n * workspace out of its own switchboard.\n */\nexport function strandedDependents(\n enabledIds: readonly string[],\n extraRequirements?: Readonly<Record<string, readonly string[]>>,\n): Array<{ pluginId: string; missing: string[] }> {\n const enabled = new Set(enabledIds.map(String))\n const known = new Set<string>([\n ...FIRST_PARTY_PLUGINS.map((plugin) => plugin.id),\n ...enabled,\n ])\n const { requires } = dependencyEdges(extraRequirements)\n const stranded: Array<{ pluginId: string; missing: string[] }> = []\n for (const pluginId of enabled) {\n const missing = (requires.get(pluginId) ?? []).filter(\n (required) => known.has(required) && !enabled.has(required),\n )\n if (missing.length) stranded.push({ pluginId, missing })\n }\n return stranded\n}\n\n/**\n * Everything that must ALSO be switched off when `pluginId` is (AGL-2486) —\n * the transitive closure over reverse `requires` edges, restricted to what is\n * currently on.\n *\n * Pure and surface-agnostic: the org switchboard and the per-site card both\n * call it with their own \"currently enabled\" set, so the same graph answers\n * both, and the org's wider blast radius comes from the set it passes, not\n * from a second implementation.\n *\n * `extraRequirements` is how a marketplace listing joins the graph. Third-party\n * ids ride the same `enabledPlugins` field, so a listing whose manifest\n * declares `requires` is cascaded exactly like a bundle. It EXTENDS the\n * catalog graph and cannot shrink it — a listing cannot declare away a\n * first-party edge.\n *\n * ⚠️ The result is only ever as complete as what has been DECLARED. A\n * marketplace plugin that uses first-party components without saying so in its\n * manifest will not appear here, and callers must not present the list as\n * exhaustive. See `PLUGIN_CASCADE_IS_DECLARED_ONLY`.\n */\nexport function resolveDisableCascade(\n pluginId: string,\n enabledIds: readonly string[],\n extraRequirements?: Readonly<Record<string, readonly string[]>>,\n): string[] {\n const enabled = new Set(enabledIds.map(String))\n const dependents = dependencyEdges(extraRequirements).dependents\n\n // Breadth-first over reverse edges. `seen` is seeded with the origin so a\n // cycle back to it terminates and the plugin being disabled is never listed\n // among its own dependents.\n const seen = new Set<string>([pluginId])\n const cascade: string[] = []\n const queue: string[] = [pluginId]\n while (queue.length) {\n const current = queue.shift() as string\n for (const dependent of dependents.get(current) ?? []) {\n if (seen.has(dependent)) continue\n seen.add(dependent)\n // Walk THROUGH an already-off dependent — something may depend on it in\n // turn — but do not claim it is being turned off.\n if (enabled.has(dependent)) cascade.push(dependent)\n queue.push(dependent)\n }\n }\n return cascade\n}\n\n/**\n * Why the cascade list must never be presented as exhaustive (AGL-2486).\n *\n * Exported as copy rather than left to each caller to paraphrase: the whole\n * value of the warning rests on it being honest about its own limits, and two\n * surfaces wording that differently is how one of them ends up overclaiming.\n *\n * It says built-in ONLY, and that is the current truth rather than a hedge.\n * `PluginManifest` carries no `requires` field, so a publisher cannot declare\n * a dependency even if they want to, and `extraRequirements` above — the seam\n * that would carry them — is supplied by nothing outside tests. An earlier\n * draft said \"a plugin that uses this one WITHOUT declaring it cannot be\n * detected\", which quietly implied declaring was possible and would have made\n * this the exact overclaiming warning it exists to avoid. Adding the manifest\n * field is a separate change: it needs a publish form, validation and a line\n * on the install screen, or it is a field written by nobody and read by\n * nothing. Update this sentence in the same change, not before.\n */\nexport const PLUGIN_CASCADE_IS_DECLARED_ONLY =\n 'This covers built-in plugins only. A marketplace plugin has no way to ' +\n 'declare that it depends on another one yet, so none are listed here — ' +\n 'check any third-party plugins you rely on before continuing.'\n\n/** Reverse lookup: which first-party plugin a release flag gates, if any. */\nexport function pluginForReleaseFlag(\n flagKey: string,\n): FirstPartyPlugin | undefined {\n return FIRST_PARTY_PLUGINS.find((plugin) => plugin.releaseFlag === flagKey)\n}\n\n/**\n * Subtracts release-flagged-off plugins from an effective set (AGL-422).\n * Pure — the caller supplies the verdict source (client: the activated\n * Remote Config hook state; server: the cached admin-SDK template read),\n * so the same policy runs identically on every surface:\n *\n * - unknown ids (marketplace/realm installs) and always-on ids pass;\n * - a first-party id with a `releaseFlag` passes only when the flag is on\n * for the subject, or `staffBypass` is set (staff preview keeps working\n * while a feature is dark).\n */\nexport function filterPluginsByReleaseFlags(\n pluginIds: readonly string[],\n isFlagOn: (flagKey: string) => boolean,\n options?: { staffBypass?: boolean },\n): string[] {\n if (options?.staffBypass) return [...pluginIds]\n const catalog = new Map(\n FIRST_PARTY_PLUGINS.map((plugin) => [plugin.id, plugin]),\n )\n return pluginIds.filter((pluginId) => {\n const plugin = catalog.get(pluginId)\n if (!plugin?.releaseFlag || plugin.alwaysOn) return true\n return isFlagOn(plugin.releaseFlag)\n })\n}\n"],"names":["FIRST_PARTY_PLUGINS","PUBLISHED_SITE_IMPACT","ACCOUNTS_PLUGIN_ID","FORMS_PLUGIN_ID","DEFAULT_ENABLED_PLUGINS","map","plugin","id","canonicalPluginId","pluginId","canonicalPluginIds","pluginIds","Array","from","Set","String","ALWAYS_ON","filter","alwaysOn","ALWAYS_ON_FOR_WORKSPACE","alwaysOnForWorkspace","isLockedOnForWorkspace","includes","isLockedOnForSite","FIRST_PARTY_IDS","DEFAULT_OFF_PER_SITE_PLUGIN_IDS","defaultOffPerSite","isDefaultOffPerSite","has","applyDefaultOffOptIn","optedIn","size","asked","isArray","isFirstPartyPlugin","classifyEnabledPlugins","bundles","listings","push","resolveEnabledPlugins","org","configured","enabledPlugins","base","subtractDisabledPlugins","disabledPlugins","length","disabled","resolveHostEnabledPlugins","host","isHostPluginEnabled","isPluginEnabled","resolvePluginSiteState","denied","dependencyEdges","extraRequirements","dependents","Map","requires","addEdge","dependent","required","reverse","get","set","forward","Object","entries","one","pluginRequirements","pluginDependents","strandedDependents","enabledIds","enabled","known","stranded","missing","resolveDisableCascade","seen","cascade","queue","current","shift","add","PLUGIN_CASCADE_IS_DECLARED_ONLY","pluginForReleaseFlag","flagKey","find","releaseFlag","filterPluginsByReleaseFlags","isFlagOn","options","staffBypass","catalog"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/enabled-plugins.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Per-org plugin enablement (AGL-416): `org.enabledPlugins` is the\n * switchboard that decides which plugins LOAD for a workspace — the loader\n * (AGL-417) dynamically imports only these. It composes with (not replaces)\n * the existing gates: a surface renders when its plugin is enabled AND its\n * `featureFlag` entitlement resolves; marketplace/marketplace listings keep\n * their per-host/org `installs` docs on top.\n *\n * The catalog's rows are declared by the plugins themselves, in\n * `plugins.config.json`, beside the package names the loader manifests are\n * generated from; this module holds the row's shape and the resolvers.\n */\n\nimport {\n FIRST_PARTY_PLUGINS,\n PLUGIN_EDIT_BAR_LINKS,\n PUBLISHED_SITE_IMPACT,\n} from './first-party-plugins.generated'\n\n/**\n * One quick link on a live site's admin edit bar, declared by the plugin whose\n * console page it opens (AGL-3080).\n *\n * The bar is drawn by the TENANT server, which never loads a plugin's console\n * code, so this is DATA compiled from `plugins.config.json` rather than a\n * runtime registration — a registry that process had not filled would drop the\n * link silently instead of failing, which is the AGL-3025 shape. It is also\n * why the label is written here rather than taken from the plugin's nav item:\n * the nav item lives in the console bundle, and \"Orders\" is a section of\n * Commerce rather than the plugin's own name.\n *\n * The link is drawn only where the SITE runs the plugin. The bar asks the same\n * resolved plugin set the rest of the edit context is built from, so a site\n * with Commerce switched off is not offered its Orders page.\n */\nexport interface PluginEditBarLink {\n /** The plugin that owns the page — also the id the site's set is checked against. */\n pluginId: string\n /** Where it sits in the bar; every declared order is distinct. */\n order: number\n /** What the bar says. A SECTION's name where that is the honest one. */\n label: string\n /**\n * Console path beneath the site, beginning with `/` and carrying no query —\n * `/inbox`, `/products/orders`. Joined onto `/{orgSlug}/hosts/{host}`.\n */\n path: string\n}\n\nexport interface FirstPartyPlugin {\n /**\n * Stable plugin id — persisted in `org.enabledPlugins`,\n * `host.disabledPlugins` and `host.enabledPlugins`, and the document id\n * under every `pluginSettings` collection.\n *\n * Renaming one is therefore a data change, not a code change, and it is\n * done in two halves that must both ship: the old id is read through\n * {@link canonicalPluginId} so every stored list still resolves, and a\n * backfill rewrites the stored documents so the alias can be retired once\n * it reads nothing.\n */\n id: string\n /** Console-facing display name. */\n label: string\n /**\n * On for every workspace AND every site, with no switch anywhere: the base\n * component library the canvas cannot render without.\n */\n alwaysOn?: boolean\n /**\n * On for every WORKSPACE, and switchable for one SITE (AGL-3028, AGL-3029).\n *\n * The workspace half is what `alwaysOn` gives: the id is unioned into the\n * org's resolved set whatever `org.enabledPlugins` stores, so a list saved\n * before the plugin existed still runs it, and no workspace switch is\n * offered. The site half is ordinary: the id is subtracted by a site's\n * `disabledPlugins` deny-list like any other, so an absent host field means\n * ON and nothing needs migrating.\n *\n * It exists for capabilities whose workspace-level half must never stop.\n * AI carries the add-on, credits, allotments and overage billing, none of\n * which belongs to a site; Forms carries the catalog and the submissions\n * already stored. A workspace switch would take those down with the\n * site-facing half, so the only switch is the site's.\n */\n alwaysOnForWorkspace?: boolean\n /**\n * What switching this plugin off for ONE site stops, and what it leaves\n * running (AGL-3028, AGL-3029) — the copy the site's Admin › Plugins page\n * states beside the switch, so the decision is made knowing both halves.\n *\n * `confirm` makes the site switch ask before it applies, naming what it is\n * about to break on published pages.\n */\n siteOff?: {\n /** What stops on the site, stated as the consequence. */\n stops: string\n /** What keeps running, because it carries no site. */\n keeps: string\n /** Ask before applying. */\n confirm?: boolean\n /**\n * How the confirmation names the site's published pages the switch\n * reaches (AGL-3029): the sentence above the list, and the one that\n * stands in for an empty list when the scan read everything.\n */\n pages?: { heading: string; none: string }\n }\n /** One-line description for the org-settings toggle list. */\n description?: string\n /**\n * Release flag gating this plugin platform-wide (AGL-422). A flagged-off\n * plugin is subtracted from every workspace's effective set — console\n * loader, published sites, and API dispatch — unless the subject is\n * staff. Always-on plugins carry no flag.\n */\n releaseFlag?: string\n /**\n * OFF for a site until that site turns it on (AGL-2486) — the inversion of\n * this switchboard's default, and the only field here that changes what an\n * absent host doc means.\n *\n * The per-host field is a DENY-list: a site records what it switches off,\n * which makes \"absent means on\" the default for all twelve other bundles.\n * That is right for a capability whose worst case is an unused nav tab, and\n * wrong for one whose worst case is a PAGE — `/signin`, `/signup`,\n * `/recover` were served by every published site on the platform,\n * including marketing sites whose real sign-in is somewhere else entirely.\n * A sign-in-shaped page on a brand's own domain that is not that brand's\n * sign-in is a credential-confusion hazard, so this one defaults closed.\n *\n * A site un-defaults it by listing the id in `host.enabledPlugins`. That\n * list is scoped to default-off ids and cannot widen anything else, so the\n * AGL-1014 invariant survives intact: a site still can never reach past\n * what its org enables.\n */\n defaultOffPerSite?: boolean\n /**\n * Plugin ids this one cannot function without (AGL-2486) — DECLARED, never\n * inferred.\n *\n * The switchboard has always let a workspace turn off a plugin another one\n * is built on, and said nothing. `accounts` is the case that proves it: the\n * Members blocks and every `membership/*` API handler ship inside the\n * COMMERCE bundle, so switching Commerce off leaves a site advertising\n * `/signin` while nothing can answer the login POST.\n *\n * Declared rather than derived on purpose. The couplings that matter are\n * exactly the ones no static read can see — which bundle happens to\n * register whose components — so a graph inferred from imports or from the\n * registry would miss the real edges while looking authoritative. An\n * incomplete warning that presents itself as complete is worse than none,\n * so this list is the contract, and {@link resolveDisableCascade} is honest\n * about covering only what is on it.\n */\n requires?: readonly string[]\n}\n\n/**\n * What a DISABLE does to a site that is already published (AGL-2486).\n *\n * The cascade dialog has to state a consequence, and there are two very\n * different ones. \"These blocks will no longer be offered\" is a different\n * decision from \"parts of your live pages go blank\", and one generic sentence\n * for both would be a lie in one direction or the other.\n *\n * - `elements` — the plugin registers site components. The tenant loads only\n * the site's enabled bundles, so elements ALREADY PLACED on published pages\n * stop rendering. Pre-existing AGL-1014 behaviour.\n * - `routes` — the plugin registers no components, but a published site\n * stops serving something: `accounts` gates `/signin`, `/signup`,\n * `/recover`; `redirects` stops applying its rules; `workflows` stops\n * answering its hooks.\n * - `console-only` — nothing a visitor can reach changes; the plugin leaves\n * navigation and the editor.\n */\nexport type PublishedSiteImpact = 'elements' | 'routes' | 'console-only'\n\n/**\n * Every catalog id classified, from the `publishedSiteImpact` each plugin\n * declares on its own row. The generator refuses a row whose verdict\n * disagrees with its `register.site`, so a site-registering bundle cannot be\n * added without declaring its consequence.\n */\nexport { PUBLISHED_SITE_IMPACT }\n\n/**\n * The user-accounts capability (AGL-2486): visitor sign-in, sign-up and\n * password recovery on a published site.\n *\n * It carries no loader manifest entry, and that is deliberate rather than an\n * oversight. The member blocks and the `membership/*` API handlers already\n * ship inside the commerce bundle, so there is no separate package to load;\n * what this id contributes is the SWITCH — the thing the tenant route gate,\n * the console card and the sitemap all ask. Several catalog ids already have\n * no tenant bundle (`contacts`, `data`, `logic`), so a manifest-less entry is\n * the existing shape, not a new one.\n */\nexport const ACCOUNTS_PLUGIN_ID = 'accounts'\n\n/**\n * The Forms capability's id, named in core (AGL-3029).\n *\n * A form's server half is core — `/api/forms/submit`, the publish-time\n * contract check, the published page's render — and core may not import a\n * plugin, so each asks the site's plugin set about this id rather than asking\n * the Forms plugin anything.\n */\nexport const FORMS_PLUGIN_ID = 'forms'\n\n/**\n * The switchboard catalog. The core holds no row of it: each plugin declares\n * its own in `plugins.config.json` (`catalog`, and one per `capabilities`\n * entry), and the generator compiles them here in their declared order, so\n * adding a plugin edits no core file.\n *\n * Compiled in rather than registered at runtime, because every resolver below\n * is synchronous and runs in each bundle of both apps; a registry that one of\n * those bundles had not filled would read as every plugin switched OFF.\n */\nexport { FIRST_PARTY_PLUGINS }\n\n/**\n * The admin edit bar's quick links that THIS SITE runs, in the order the bar\n * draws them (AGL-3080).\n *\n * `enabledPluginIds` is the site's resolved plugin set — the same one the rest\n * of the edit context is built from, already narrowed by the org's switchboard,\n * the site's own deny-list and the release flags. A link whose plugin is not\n * in it is not offered, which is what stops the bar linking a site with\n * Commerce switched off to its Orders page.\n *\n * Compiled in rather than registered, for the reason the catalog above is:\n * the tenant server draws this bar and never loads a plugin's console code,\n * so a registry it had not filled would drop every link with nothing red.\n */\nexport function pluginEditBarLinks(\n enabledPluginIds: readonly string[] | undefined,\n): readonly PluginEditBarLink[] {\n const enabled = new Set(enabledPluginIds ?? [])\n return PLUGIN_EDIT_BAR_LINKS.filter((link) => enabled.has(link.pluginId))\n}\n\n/** Ids loaded for orgs that have never touched the switchboard. */\nexport const DEFAULT_ENABLED_PLUGINS: readonly string[] =\n FIRST_PARTY_PLUGINS.map((plugin) => plugin.id)\n\n/**\n * The id a stored plugin id means today — the seam a rename re-arms.\n *\n * A first-party id is written into every org's `enabledPlugins` and every\n * site's `disabledPlugins` the moment somebody touches the switchboard, so a\n * rename in this file alone would silently turn the plugin OFF for every\n * workspace that had listed it under the old name — the stored id would match\n * nothing in the catalog and fall through as a marketplace listing id. Every\n * reader of a stored list runs it through here first, so a document written\n * before a rename keeps meaning what it meant while the backfill runs.\n *\n * Nothing is aliased today. `contacts` read as `crm` from the CRM's rename\n * (AGL-2595) until its backfill reported zero documents carrying the old id,\n * and the alias was retired (AGL-2614) — an alias that outlives its backfill\n * is a second name for the plugin that nothing writes and every reader must\n * keep honoring. The next rename adds its pair here, ships a backfill over\n * `org.enabledPlugins`, `host.disabledPlugins`, `host.enabledPlugins` and the\n * `pluginSettings` document ids, and removes the pair the same way.\n */\nexport function canonicalPluginId(pluginId: string): string {\n return pluginId\n}\n\n/** A stored list, read through the alias seam and de-duplicated. */\nfunction canonicalPluginIds(pluginIds: readonly unknown[]): string[] {\n return Array.from(\n new Set(pluginIds.map((id) => canonicalPluginId(String(id)))),\n )\n}\n\n/** On everywhere, a site's deny-list included: the base component library. */\nconst ALWAYS_ON: readonly string[] = FIRST_PARTY_PLUGINS.filter(\n (plugin) => plugin.alwaysOn,\n).map((plugin) => plugin.id)\n\n/**\n * Unioned into every workspace's set whatever the org stored: {@link ALWAYS_ON}\n * plus the ids that are on for every workspace and switchable per site.\n */\nconst ALWAYS_ON_FOR_WORKSPACE: readonly string[] = FIRST_PARTY_PLUGINS.filter(\n (plugin) => plugin.alwaysOn || plugin.alwaysOnForWorkspace,\n).map((plugin) => plugin.id)\n\n/**\n * Whether the WORKSPACE switch for this plugin is locked on (AGL-3028): the\n * base library, and every plugin that is on for every workspace and\n * switchable only per site. The org switchboard renders these on and inert.\n */\nexport function isLockedOnForWorkspace(pluginId: string): boolean {\n return ALWAYS_ON_FOR_WORKSPACE.includes(pluginId)\n}\n\n/**\n * Whether the SITE switch for this plugin is locked on: the base library\n * alone. Every other plugin a workspace runs can be switched off for one site.\n */\nexport function isLockedOnForSite(pluginId: string): boolean {\n return ALWAYS_ON.includes(pluginId)\n}\n\nconst FIRST_PARTY_IDS: ReadonlySet<string> = new Set(\n FIRST_PARTY_PLUGINS.map((plugin) => plugin.id),\n)\n\n/**\n * Ids a SITE does not get until it asks (AGL-2486). See\n * {@link FirstPartyPlugin.defaultOffPerSite} for why this inversion exists.\n */\nexport const DEFAULT_OFF_PER_SITE_PLUGIN_IDS: ReadonlySet<string> = new Set(\n FIRST_PARTY_PLUGINS.filter((plugin) => plugin.defaultOffPerSite).map(\n (plugin) => plugin.id,\n ),\n)\n\n/** Whether a plugin id is off for a site until that site opts in. */\nexport function isDefaultOffPerSite(pluginId: string): boolean {\n return DEFAULT_OFF_PER_SITE_PLUGIN_IDS.has(pluginId)\n}\n\n/**\n * Applies a host's `enabledPlugins` OPT-IN list (AGL-2486): subtracts every\n * default-off id the host has not explicitly asked for.\n *\n * Narrow-only, like its deny-list sibling. The list can only ever REMOVE the\n * default-off subtraction for an id the org already enables — listing an\n * ordinary id buys nothing, and listing one the org switched off buys\n * nothing either, because this runs against the org's resolved set.\n */\nexport function applyDefaultOffOptIn(\n pluginIds: readonly string[],\n optedIn?: readonly string[] | null,\n): string[] {\n if (!DEFAULT_OFF_PER_SITE_PLUGIN_IDS.size) return [...pluginIds]\n const asked = new Set(\n Array.isArray(optedIn) ? canonicalPluginIds(optedIn) : [],\n )\n return pluginIds.filter(\n (id) => !DEFAULT_OFF_PER_SITE_PLUGIN_IDS.has(id) || asked.has(id),\n )\n}\n\n/**\n * Whether an `enabledPlugins` id is a first-party BUNDLE (vs a marketplace\n * listing id) — AGL-777. `enabledPlugins` is a flat mix of the two: bundle\n * ids are the short, stable names in {@link FIRST_PARTY_PLUGINS}; marketplace\n * installs ride the same field under their Firestore listing doc id. This is\n * the single classifier both writers and readers use so the two kinds never\n * get confused — e.g. an install sync must never add/remove a bundle id.\n */\nexport function isFirstPartyPlugin(pluginId: string): boolean {\n return FIRST_PARTY_IDS.has(pluginId)\n}\n\n/**\n * Splits a mixed `enabledPlugins` array into first-party bundle ids and\n * marketplace listing ids (AGL-777). The field stays a single flat list;\n * this just names the two kinds for callers that need to treat them apart.\n */\nexport function classifyEnabledPlugins(pluginIds: readonly string[]): {\n bundles: string[]\n listings: string[]\n} {\n const bundles: string[] = []\n const listings: string[] = []\n for (const id of pluginIds) {\n if (isFirstPartyPlugin(id)) bundles.push(id)\n else listings.push(id)\n }\n return { bundles, listings }\n}\n\n/**\n * The org's effective enabled-plugin set. Absent field → every first-party\n * plugin (existing orgs keep working untouched); always-on ids — and the ids\n * that are on for every workspace and switchable only per site — are unioned\n * in, so no stored list can switch them off for a workspace, including one\n * saved before the plugin existed. Unknown ids are kept — realm-trusted\n * marketplace plugins (AGL-420) ride the same field.\n */\nexport function resolveEnabledPlugins(\n org?: { enabledPlugins?: string[] } | null,\n): string[] {\n const configured = org?.enabledPlugins\n const base = Array.isArray(configured)\n ? canonicalPluginIds(configured)\n : [...DEFAULT_ENABLED_PLUGINS]\n return Array.from(new Set([...ALWAYS_ON_FOR_WORKSPACE, ...base]))\n}\n\n/**\n * Subtracts a host's per-site deny-list from an enabled set (AGL-1014).\n * Always-on ids survive — the base component library cannot be switched\n * off per site any more than per org. A plugin that is on for every\n * workspace (`alwaysOnForWorkspace`) does NOT survive: its site switch is\n * the one switch it has. Order of the surviving ids is kept.\n */\nexport function subtractDisabledPlugins(\n pluginIds: readonly string[],\n disabledPlugins?: readonly string[] | null,\n): string[] {\n if (!Array.isArray(disabledPlugins) || !disabledPlugins.length)\n return [...pluginIds]\n const disabled = new Set(canonicalPluginIds(disabledPlugins))\n return pluginIds.filter(\n (id) => ALWAYS_ON.includes(id) || !disabled.has(id),\n )\n}\n\n/**\n * A HOST's effective enabled-plugin set (AGL-1014): the org's resolved set\n * minus the host's `disabledPlugins` deny-list. This is the single source\n * of truth for per-site enablement — console navigation, the editor,\n * published sites, and API dispatch must all read it, or a \"disabled\"\n * plugin is merely hidden, not off.\n *\n * Semantics are narrow-only by construction: a host stores what it turns\n * OFF, so it can never widen beyond what the org enables, and an absent\n * field means every org-enabled plugin runs (newly installed plugins\n * default to enabled per site until a host admin disables them).\n *\n * ONE class of id reads the other way (AGL-2486): a `defaultOffPerSite`\n * plugin is subtracted unless the host names it in `enabledPlugins`. That\n * list un-defaults; it does not grant. Both fields are still bounded by the\n * org's set, and an explicit deny still beats an explicit opt-in — the two\n * are applied in that order below, so the safe reading wins whenever a\n * hand-edited or stale doc sets both.\n */\nexport function resolveHostEnabledPlugins(\n org?: { enabledPlugins?: string[] } | null,\n host?: { disabledPlugins?: string[]; enabledPlugins?: string[] } | null,\n): string[] {\n return subtractDisabledPlugins(\n applyDefaultOffOptIn(resolveEnabledPlugins(org), host?.enabledPlugins),\n host?.disabledPlugins,\n )\n}\n\n/**\n * Whether ONE plugin runs on this site — the host-aware counterpart of\n * {@link isPluginEnabled}, and the form every route gate wants.\n */\nexport function isHostPluginEnabled(\n org: { enabledPlugins?: string[] } | null | undefined,\n host: { disabledPlugins?: string[]; enabledPlugins?: string[] } | null | undefined,\n pluginId: string,\n): boolean {\n return resolveHostEnabledPlugins(org, host).includes(pluginId)\n}\n\nexport function isPluginEnabled(\n org: { enabledPlugins?: string[] } | null | undefined,\n pluginId: string,\n): boolean {\n return resolveEnabledPlugins(org).includes(pluginId)\n}\n\n/**\n * Where one plugin stands ON ONE SITE (AGL-1014, AGL-2486).\n *\n * Three surfaces need this answer and they must not each derive it: the site\n * plugin page's \"Where it runs\" card, that page's dependency list — where the\n * same question is asked about a NEIGHBOR, and where the answer is the whole\n * point, because a dependency the workspace enables and this site has switched\n * off is a broken plugin behind a healthy-looking workspace page — and the\n * per-site switchboard. A second derivation is a second chance to disagree\n * with `resolveHostEnabledPlugins`, which is what actually runs.\n *\n * - `always-on` — the base library; it cannot be switched off anywhere.\n * A plugin that is on for every workspace but\n * switchable per site is NOT this: it reads\n * `runs-here` or `off-for-site` like any other.\n * - `off-for-workspace` — the org has it off, so it runs on none of its sites\n * and this site cannot turn it on.\n * - `runs-here` — the effective host set contains it.\n * - `awaiting-opt-in` — a `defaultOffPerSite` plugin the org enables and\n * this site has not asked for. Off, but off by\n * DEFAULT rather than by a decision, which is a\n * different thing to tell an operator.\n * - `off-for-site` — the org enables it and this site switched it off.\n */\nexport type PluginSiteState =\n | 'always-on'\n | 'off-for-workspace'\n | 'runs-here'\n | 'awaiting-opt-in'\n | 'off-for-site'\n\nexport function resolvePluginSiteState(\n org: { enabledPlugins?: string[] } | null | undefined,\n host: { disabledPlugins?: string[]; enabledPlugins?: string[] } | null | undefined,\n pluginId: string,\n): PluginSiteState {\n if (ALWAYS_ON.includes(pluginId)) return 'always-on'\n if (!resolveEnabledPlugins(org).includes(pluginId)) return 'off-for-workspace'\n if (resolveHostEnabledPlugins(org, host).includes(pluginId)) return 'runs-here'\n // A default-off plugin this site has not named is off because nobody asked\n // for it; naming it in the deny-list is a decision, and an explicit deny\n // beats an opt-in, so the deny-list is checked first.\n const denied = Array.isArray(host?.disabledPlugins)\n ? host.disabledPlugins.map(String).includes(pluginId)\n : false\n return !denied && isDefaultOffPerSite(pluginId)\n ? 'awaiting-opt-in'\n : 'off-for-site'\n}\n\n/**\n * The declared dependency graph, both directions, built once (AGL-2486).\n *\n * `requires` on the catalog is the forward edge — what a plugin cannot run\n * without — and every consumer so far wanted the reverse one. Building both\n * here keeps the cascade, the console's dependency card and the server-side\n * refusal reading the SAME edges: a graph assembled a second time is a graph\n * that can disagree with itself about which plugins are coupled, and the\n * disagreement would show up as a warning shown on one surface and not on the\n * one that writes.\n *\n * `extraRequirements` extends the catalog and cannot shrink it, exactly as it\n * does for {@link resolveDisableCascade} — a third-party id can add edges of\n * its own but can never declare away a first-party one.\n */\nfunction dependencyEdges(\n extraRequirements?: Readonly<Record<string, readonly string[]>>,\n): {\n /** Keyed by a required id; holds the ids that depend on it. */\n dependents: Map<string, string[]>\n /** Keyed by a dependent id; holds the ids it requires. */\n requires: Map<string, string[]>\n} {\n const dependents = new Map<string, string[]>()\n const requires = new Map<string, string[]>()\n const addEdge = (dependent: string, required: string) => {\n const reverse = dependents.get(required)\n if (reverse) reverse.push(dependent)\n else dependents.set(required, [dependent])\n const forward = requires.get(dependent)\n if (forward) forward.push(required)\n else requires.set(dependent, [required])\n }\n for (const plugin of FIRST_PARTY_PLUGINS)\n for (const required of plugin.requires ?? []) addEdge(plugin.id, required)\n for (const [dependent, required] of Object.entries(extraRequirements ?? {}))\n for (const one of required ?? []) addEdge(dependent, String(one))\n return { dependents, requires }\n}\n\n/** What `pluginId` declares it cannot run without — DIRECT edges only. */\nexport function pluginRequirements(\n pluginId: string,\n extraRequirements?: Readonly<Record<string, readonly string[]>>,\n): string[] {\n return [...(dependencyEdges(extraRequirements).requires.get(pluginId) ?? [])]\n}\n\n/** What declares it cannot run without `pluginId` — DIRECT edges only. */\nexport function pluginDependents(\n pluginId: string,\n extraRequirements?: Readonly<Record<string, readonly string[]>>,\n): string[] {\n return [...(dependencyEdges(extraRequirements).dependents.get(pluginId) ?? [])]\n}\n\n/**\n * Which of `enabledIds` would be left running with a requirement switched off\n * (AGL-2486) — the same question the cascade dialog asks, phrased so a WRITER\n * can refuse instead of a reader warning.\n *\n * The dialog is a courtesy: it stands in front of the console's own switches\n * and nothing else. A direct API call, a stale tab that posts an older set, or\n * a second console surface that forgot to ask can still store a set where\n * User Accounts is on and the Commerce bundle that answers its sign-in POST is\n * not. This is what a write path calls to reject that set outright, so the\n * boundary does not live in a dialog anyone can skip.\n *\n * Only DECLARED requirements are checked, with the same limit\n * {@link PLUGIN_CASCADE_IS_DECLARED_ONLY} states: an undeclared coupling is\n * invisible here too. A requirement naming an id outside the catalog is\n * ignored rather than treated as unmet — a stale edge must not lock a\n * workspace out of its own switchboard.\n */\nexport function strandedDependents(\n enabledIds: readonly string[],\n extraRequirements?: Readonly<Record<string, readonly string[]>>,\n): Array<{ pluginId: string; missing: string[] }> {\n const enabled = new Set(enabledIds.map(String))\n const known = new Set<string>([\n ...FIRST_PARTY_PLUGINS.map((plugin) => plugin.id),\n ...enabled,\n ])\n const { requires } = dependencyEdges(extraRequirements)\n const stranded: Array<{ pluginId: string; missing: string[] }> = []\n for (const pluginId of enabled) {\n const missing = (requires.get(pluginId) ?? []).filter(\n (required) => known.has(required) && !enabled.has(required),\n )\n if (missing.length) stranded.push({ pluginId, missing })\n }\n return stranded\n}\n\n/**\n * Everything that must ALSO be switched off when `pluginId` is (AGL-2486) —\n * the transitive closure over reverse `requires` edges, restricted to what is\n * currently on.\n *\n * Pure and surface-agnostic: the org switchboard and the per-site card both\n * call it with their own \"currently enabled\" set, so the same graph answers\n * both, and the org's wider blast radius comes from the set it passes, not\n * from a second implementation.\n *\n * `extraRequirements` is how a marketplace listing joins the graph. Third-party\n * ids ride the same `enabledPlugins` field, so a listing whose manifest\n * declares `requires` is cascaded exactly like a bundle. It EXTENDS the\n * catalog graph and cannot shrink it — a listing cannot declare away a\n * first-party edge.\n *\n * ⚠️ The result is only ever as complete as what has been DECLARED. A\n * marketplace plugin that uses first-party components without saying so in its\n * manifest will not appear here, and callers must not present the list as\n * exhaustive. See `PLUGIN_CASCADE_IS_DECLARED_ONLY`.\n */\nexport function resolveDisableCascade(\n pluginId: string,\n enabledIds: readonly string[],\n extraRequirements?: Readonly<Record<string, readonly string[]>>,\n): string[] {\n const enabled = new Set(enabledIds.map(String))\n const dependents = dependencyEdges(extraRequirements).dependents\n\n // Breadth-first over reverse edges. `seen` is seeded with the origin so a\n // cycle back to it terminates and the plugin being disabled is never listed\n // among its own dependents.\n const seen = new Set<string>([pluginId])\n const cascade: string[] = []\n const queue: string[] = [pluginId]\n while (queue.length) {\n const current = queue.shift() as string\n for (const dependent of dependents.get(current) ?? []) {\n if (seen.has(dependent)) continue\n seen.add(dependent)\n // Walk THROUGH an already-off dependent — something may depend on it in\n // turn — but do not claim it is being turned off.\n if (enabled.has(dependent)) cascade.push(dependent)\n queue.push(dependent)\n }\n }\n return cascade\n}\n\n/**\n * Why the cascade list must never be presented as exhaustive (AGL-2486).\n *\n * Exported as copy rather than left to each caller to paraphrase: the whole\n * value of the warning rests on it being honest about its own limits, and two\n * surfaces wording that differently is how one of them ends up overclaiming.\n *\n * It says built-in ONLY, and that is the current truth rather than a hedge.\n * `PluginManifest` carries no `requires` field, so a publisher cannot declare\n * a dependency even if they want to, and `extraRequirements` above — the seam\n * that would carry them — is supplied by nothing outside tests. An earlier\n * draft said \"a plugin that uses this one WITHOUT declaring it cannot be\n * detected\", which quietly implied declaring was possible and would have made\n * this the exact overclaiming warning it exists to avoid. Adding the manifest\n * field is a separate change: it needs a publish form, validation and a line\n * on the install screen, or it is a field written by nobody and read by\n * nothing. Update this sentence in the same change, not before.\n */\nexport const PLUGIN_CASCADE_IS_DECLARED_ONLY =\n 'This covers built-in plugins only. A marketplace plugin has no way to ' +\n 'declare that it depends on another one yet, so none are listed here — ' +\n 'check any third-party plugins you rely on before continuing.'\n\n/** Reverse lookup: which first-party plugin a release flag gates, if any. */\nexport function pluginForReleaseFlag(\n flagKey: string,\n): FirstPartyPlugin | undefined {\n return FIRST_PARTY_PLUGINS.find((plugin) => plugin.releaseFlag === flagKey)\n}\n\n/**\n * Subtracts release-flagged-off plugins from an effective set (AGL-422).\n * Pure — the caller supplies the verdict source (client: the activated\n * Remote Config hook state; server: the cached admin-SDK template read),\n * so the same policy runs identically on every surface:\n *\n * - unknown ids (marketplace/realm installs) and always-on ids pass;\n * - a first-party id with a `releaseFlag` passes only when the flag is on\n * for the subject, or `staffBypass` is set (staff preview keeps working\n * while a feature is dark).\n */\nexport function filterPluginsByReleaseFlags(\n pluginIds: readonly string[],\n isFlagOn: (flagKey: string) => boolean,\n options?: { staffBypass?: boolean },\n): string[] {\n if (options?.staffBypass) return [...pluginIds]\n const catalog = new Map(\n FIRST_PARTY_PLUGINS.map((plugin) => [plugin.id, plugin]),\n )\n return pluginIds.filter((pluginId) => {\n const plugin = catalog.get(pluginId)\n if (!plugin?.releaseFlag || plugin.alwaysOn) return true\n return isFlagOn(plugin.releaseFlag)\n })\n}\n"],"names":["FIRST_PARTY_PLUGINS","PLUGIN_EDIT_BAR_LINKS","PUBLISHED_SITE_IMPACT","ACCOUNTS_PLUGIN_ID","FORMS_PLUGIN_ID","pluginEditBarLinks","enabledPluginIds","enabled","Set","filter","link","has","pluginId","DEFAULT_ENABLED_PLUGINS","map","plugin","id","canonicalPluginId","canonicalPluginIds","pluginIds","Array","from","String","ALWAYS_ON","alwaysOn","ALWAYS_ON_FOR_WORKSPACE","alwaysOnForWorkspace","isLockedOnForWorkspace","includes","isLockedOnForSite","FIRST_PARTY_IDS","DEFAULT_OFF_PER_SITE_PLUGIN_IDS","defaultOffPerSite","isDefaultOffPerSite","applyDefaultOffOptIn","optedIn","size","asked","isArray","isFirstPartyPlugin","classifyEnabledPlugins","bundles","listings","push","resolveEnabledPlugins","org","configured","enabledPlugins","base","subtractDisabledPlugins","disabledPlugins","length","disabled","resolveHostEnabledPlugins","host","isHostPluginEnabled","isPluginEnabled","resolvePluginSiteState","denied","dependencyEdges","extraRequirements","dependents","Map","requires","addEdge","dependent","required","reverse","get","set","forward","Object","entries","one","pluginRequirements","pluginDependents","strandedDependents","enabledIds","known","stranded","missing","resolveDisableCascade","seen","cascade","queue","current","shift","add","PLUGIN_CASCADE_IS_DECLARED_ONLY","pluginForReleaseFlag","flagKey","find","releaseFlag","filterPluginsByReleaseFlags","isFlagOn","options","staffBypass","catalog"],"mappings":"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"}
@@ -497,6 +497,19 @@ export declare const CONSOLE_WIDGET_SLOTS: {
497
497
  readonly besignerFunctions: "besignerFunctions";
498
498
  /** Marketplace listing detail body. Props: hostId, listingId, permissions. */
499
499
  readonly marketplaceListing: "marketplaceListing";
500
+ /**
501
+ * Above the whole Marketplace subtree: what THIS DEPLOYMENT cannot do
502
+ * (AGL-2019, moved out of the console app by AGL-3080). No props — the
503
+ * widget reads {@link DeploymentCapabilities} from the context the org's
504
+ * server layout provides, and draws nothing when the deployment is
505
+ * configured.
506
+ *
507
+ * A zone rather than a component the app holds, because the sentence is
508
+ * the marketplace's: it names what still works without a Stripe platform
509
+ * (browsing, free installs) and that is the plugin's knowledge, not the
510
+ * shell's. The app supplies only the fact.
511
+ */
512
+ readonly marketplaceCapability: "marketplaceCapability";
500
513
  /**
501
514
  * Org marketplace browse body (AGL-772). Props: hostId (acting site),
502
515
  * permissions, orgScoped. The single org-scope place to browse + install,
@@ -121,6 +121,18 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
121
121
  /** Besigner functions (ƒx) panel. Props: hostId. */ besignerFunctions: 'besignerFunctions',
122
122
  /** Marketplace listing detail body. Props: hostId, listingId, permissions. */ marketplaceListing: 'marketplaceListing',
123
123
  /**
124
+ * Above the whole Marketplace subtree: what THIS DEPLOYMENT cannot do
125
+ * (AGL-2019, moved out of the console app by AGL-3080). No props — the
126
+ * widget reads {@link DeploymentCapabilities} from the context the org's
127
+ * server layout provides, and draws nothing when the deployment is
128
+ * configured.
129
+ *
130
+ * A zone rather than a component the app holds, because the sentence is
131
+ * the marketplace's: it names what still works without a Stripe platform
132
+ * (browsing, free installs) and that is the plugin's knowledge, not the
133
+ * shell's. The app supplies only the fact.
134
+ */ marketplaceCapability: 'marketplaceCapability',
135
+ /**
124
136
  * Org marketplace browse body (AGL-772). Props: hostId (acting site),
125
137
  * permissions, orgScoped. The single org-scope place to browse + install,
126
138
  * replacing the per-site marketplace tab.
@@ -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 { 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/marketplace-theme'\nimport type { HostTheme, HostThemeScheme } from '@aglyn/shared-data-types'\nimport type { ComponentType } from 'react'\n// The commerce zones' props live in a type-only module (AGL-2916).\nexport type {\n ConsoleProductCopyValues,\n ConsoleProductDraft,\n ConsoleProductEditorZoneProps,\n ConsoleProductImportZoneProps,\n ConsoleProductOption,\n ConsoleProductSummary,\n ConsoleProductType,\n ConsoleProductsHubZoneProps,\n ConsoleProposedDiscount,\n ConsoleProposedProduct,\n} from './commerce-zone-props'\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 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\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 /** 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 * 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 * 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 header?: { title: string; icon?: MdiIconProps; docsTopic?: string }\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 /** Marketplace listing detail body. Props: hostId, listingId, permissions. */\n marketplaceListing: 'marketplaceListing',\n /**\n * Org marketplace browse body (AGL-772). Props: hostId (acting site),\n * permissions, orgScoped. The single org-scope place to browse + install,\n * replacing the per-site marketplace tab.\n */\n orgMarketplace: 'orgMarketplace',\n /** Plugins & add-ons hub installs section. Props: hostId. */\n orgAddons: 'orgAddons',\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 \"What you have changed\" card\n * and the editor (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 * 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 * 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 * The console shell's assistant dock (AGL-2940): the one position above\n * every route boundary in both the `(app)` and `(editor)` shells, where a\n * floating helper survives a navigation. Props: none — a widget here\n * resolves its own scope from the URL, as the shell's own chrome does.\n */\n assistPanel: 'assistPanel',\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 * 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, above the site SEO form (AGL-2910). Props:\n * {@link ConsoleHostSeoZoneProps} — the site, its stored SEO settings, and\n * `proposeDraft`, which puts values in the form as unsaved edits. The\n * form's Update stores them; nothing a widget proposes reaches the\n * 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 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 * The Forms page of a site, beside its Create Form action (AGL-3043):\n * another way to start a form. Props: {@link ConsoleHostFormsZoneProps}, on\n * the `hostScreens` contract.\n *\n * The Forms page is the forms plugin's own surface, so the plugin HOSTS\n * this zone: it draws the renderer `useConsoleWidgetSlot` hands down, as\n * the product editor draws `seoFields`, and every gate a console page's\n * slot applies applies here too.\n */\n hostForms: 'hostForms',\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 * The Automation page's Actions, beside Add action and Recipes (AGL-2919):\n * another way to start an automation. Props:\n * {@link ConsoleHostAutomationsZoneProps}.\n *\n * The Automation page is the workflows plugin's own surface, so the plugin\n * HOSTS this zone through the renderer `useConsoleWidgetSlot` hands down, as\n * the product editor hosts `seoFields`, and every gate a console page's slot\n * applies applies here too. A widget here writes nothing through the page:\n * the Actions list shows what it makes once it exists, and `openAction`\n * opens a listed action in the Actions editor.\n */\n hostAutomations: 'hostAutomations',\n /**\n * The editor of one SAVED automation — an action or a workflow — on the\n * Automation page (AGL-2919). Props: {@link ConsoleAutomationEditorZoneProps}.\n * Hosted by the workflows plugin, as `hostAutomations` is. A widget here\n * reads the automation as it is stored and changes nothing in the editor.\n */\n automationEditor: 'automationEditor',\n /**\n * One FAILED run in an automation's run history (AGL-2919), drawn once per\n * failed row. Props: {@link ConsoleAutomationRunZoneProps}. Hosted by the\n * workflows plugin. A widget here reads the run as it was recorded and\n * changes nothing.\n */\n automationRun: 'automationRun',\n /** The commerce product editor's fields (AGL-2916). Props: {@link ConsoleProductEditorZoneProps}. */\n productEditor: 'productEditor',\n /** Above the commerce products hub's catalog table (AGL-2916). Props: {@link ConsoleProductsHubZoneProps}. */\n productsHub: 'productsHub',\n /** Inside the commerce CSV import dialog (AGL-2916). Props: {@link ConsoleProductImportZoneProps}. */\n productImport: 'productImport',\n} as const\n\nexport type ConsoleWidgetSlot =\n (typeof CONSOLE_WIDGET_SLOTS)[keyof typeof CONSOLE_WIDGET_SLOTS]\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`, `hostLayouts` and `hostForms` zones (AGL-3043) and\n * the `hostComponents` zone (AGL-3051) hand each widget: the site and its org,\n * as `hostScreens` hands them.\n */\nexport type ConsoleHostTemplatesZoneProps = ConsoleHostScreensZoneProps\n/** See {@link ConsoleHostTemplatesZoneProps}. */\nexport type ConsoleHostLayoutsZoneProps = ConsoleHostScreensZoneProps\n/** See {@link ConsoleHostTemplatesZoneProps}. */\nexport type ConsoleHostFormsZoneProps = ConsoleHostScreensZoneProps\n/** See {@link ConsoleHostTemplatesZoneProps}. */\nexport type ConsoleHostComponentsZoneProps = ConsoleHostScreensZoneProps\n\n/** What the `hostAutomations` zone hands each widget (AGL-2919). */\nexport interface ConsoleHostAutomationsZoneProps {\n hostId: string\n /** The org the page names; `undefined` while it resolves. */\n orgId: string | undefined\n /**\n * Opens the Actions editor on a listed action. `false` when the id names no\n * action the list has read yet, which is how a widget whose draft was just\n * written tells a list that has not caught up from one that opened it.\n */\n openAction: (actionId: string) => boolean\n}\n\n/** One automation on the Automation page: an action, or a workflow (AGL-2919). */\nexport interface ConsoleAutomationTarget {\n type: 'action' | 'workflow'\n id: string\n name: string\n}\n\n/** What the `automationEditor` zone hands each widget (AGL-2919). */\nexport interface ConsoleAutomationEditorZoneProps {\n hostId: string\n /** The org the page names; `undefined` while it resolves. */\n orgId: string | undefined\n /** The automation the editor has open, as it is stored. */\n target: ConsoleAutomationTarget\n}\n\n/** What the `automationRun` zone hands each widget (AGL-2919). */\nexport interface ConsoleAutomationRunZoneProps {\n hostId: string\n /** The org the page names; `undefined` while it resolves. */\n orgId: string | undefined\n /** The automation the run belongs to. */\n target: ConsoleAutomationTarget\n /** The run's entry in the site's activity log. */\n runId: string\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 the blank site the person already has: this\n * same page, with nothing created and nothing begun. The shell records the\n * choice for this site and draws the zone no more.\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. See `hostFirstRun` in\n * {@link CONSOLE_WIDGET_SLOTS}.\n */\n startBlank: () => 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 org page, its detail\n * 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.adminOrgDetail,\n CONSOLE_WIDGET_SLOTS.staffOrg,\n CONSOLE_WIDGET_SLOTS.staffUser,\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/** 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/** 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\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 * 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\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 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 * 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 listConsoleExtensions(enabledPluginIds).flatMap((extension) =>\n (extension.navItems ?? []).map((navItem) => ({\n ...navItem,\n pluginId: extension.pluginId,\n featureFlag: extension.featureFlag,\n })),\n )\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 * registration order, for the 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 listConsoleExtensions(enabledPluginIds).flatMap((extension) =>\n (extension.orgNavItems ?? []).map((navItem) => ({ extension, navItem })),\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/** 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","marketplaceListing","orgMarketplace","orgAddons","dashboardFooter","orgSettings","hostSettings","hostFirstRun","hostTheme","adminOrgDetail","orgBillingUsage","orgBillingOverview","staffOrg","staffUser","staffOrgsListColumn","staffOrgUsageColumn","orgMember","orgMembersListColumn","hostMembers","assistPanel","besignerInspector","seoFields","hostSeo","recordInsights","recordEmail","importMapping","besignerToolbar","hostScreens","hostTemplates","hostLayouts","hostForms","hostComponents","orgSites","hostAutomations","automationEditor","automationRun","productEditor","productsHub","productImport","CONSOLE_STAFF_WIDGET_SLOTS","isConsoleStaffWidgetSlot","slot","includes","consoleExtensions","Map","registerConsoleExtension","extension","set","pluginId","unregisterConsoleExtension","delete","listConsoleExtensions","enabledPluginIds","all","Array","from","values","enabled","Set","filter","has","listConsoleNavItems","flatMap","navItems","navItem","featureFlag","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","listConsoleProviders","providers"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;CAqBC,GAED,SAASA,WAAW,QAAQ,OAAM;AAgClC,0DAA0D,GAC1D,OAAO,MAAMC,gBAA0B,MAAK;AA2B5C;;;;;CAKC,GACD,OAAO,SAASC,sBACdC,OAA+B,EAC/BC,SAA6B;QAGZD;IADjB,MAAME,eAAuC;QAAE,CAACJ,cAAc,EAAE;IAAK;IACrE,KAAK,MAAMK,OAAMH,qBAAAA,QAAQI,SAAS,YAAjBJ,qBAAqB,EAAE,CAAEE,YAAY,CAACC,GAAG,GAAG;IAC7D,OAAO;QACLE,KAAKL,QAAQM,QAAQ;QACrBC,aAAaP,QAAQO,WAAW;QAChCC,OAAOR,QAAQO,WAAW;QAC1BE,aAAaT,QAAQS,WAAW;QAChCC,MAAMV,QAAQU,IAAI;QAClBR;QACAS;YACE,kEAAkE;YAClE,mEAAmE;YACnEd,YAAY;gBACV,KAAK,MAAMe,SAASZ,QAAQa,UAAU,CAAE;oBACtCZ,UAAUa,iBAAiB,CAACF,MAAMG,SAAS,EAAEH,MAAMI,MAAM;gBAC3D;gBACA,KAAK,MAAMJ,SAASZ,QAAQa,UAAU,CAAE;wBAClCD;oBAAJ,KAAIA,iBAAAA,MAAMK,OAAO,qBAAbL,eAAeM,MAAM,EAAEjB,UAAUkB,cAAc,CAACP,MAAMK,OAAO;gBACnE;YACF;QACF;QACAG;YACEvB,YAAY;gBACV,KAAK,MAAMe,SAASZ,QAAQa,UAAU,CAAE;wBAClCD;oBAAJ,KAAIA,iBAAAA,MAAMK,OAAO,qBAAbL,eAAeM,MAAM,EAAE;wBACzBjB,UAAUoB,gBAAgB,CACxBT,MAAMK,OAAO,CAACK,GAAG,CAAC,CAACC,SAAWA,OAAOlB,GAAG;oBAE5C;gBACF;gBACA,KAAK,MAAMO,SAASZ,QAAQa,UAAU,CAAE;oBACtCZ,UAAUuB,mBAAmB,CAACZ,MAAMI,MAAM,CAACX,GAAG;gBAChD;YACF;QACF;IACF;AACF;AAoZA;;;;;CAKC,GACD,OAAO,MAAMoB,uBAAuB;IAClC,iEAAiE,GACjEC,cAAc;IACd;;;;;;;;;;GAUC,GACDC,eAAe;IACf;;;;;;;;;;;;;;GAcC,GACDC,cAAc;IACd,yDAAyD,GACzDC,gBAAgB;IAChB,2CAA2C,GAC3CC,SAAS;IACT,kDAAkD,GAClDC,mBAAmB;IACnB,4EAA4E,GAC5EC,oBAAoB;IACpB;;;;GAIC,GACDC,gBAAgB;IAChB,2DAA2D,GAC3DC,WAAW;IACX,gEAAgE,GAChEC,iBAAiB;IACjB,kEAAkE,GAClEC,aAAa;IACb,mEAAmE,GACnEC,cAAc;IACd;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCC,GACDC,cAAc;IACd;;;;;;;;;;;;GAYC,GACDC,WAAW;IACX;;;GAGC,GACDC,gBAAgB;IAChB;;;;;GAKC,GACDC,iBAAiB;IACjB;;;GAGC,GACDC,oBAAoB;IACpB;;;;GAIC,GACDC,UAAU;IACV;;;GAGC,GACDC,WAAW;IACX;;;;;;;;GAQC,GACDC,qBAAqB;IACrB;;;;;;;;;GASC,GACDC,qBAAqB;IACrB;;;;;GAKC,GACDC,WAAW;IACX;;;;;GAKC,GACDC,sBAAsB;IACtB;;;;;;GAMC,GACDC,aAAa;IACb;;;;;GAKC,GACDC,aAAa;IACb;;;;;GAKC,GACDC,mBAAmB;IACnB;;;;;;;;;;;;GAYC,GACDC,WAAW;IACX;;;;;;GAMC,GACDC,SAAS;IACT,2CAA2C,GAC3CC,gBAAgB;IAChB,wCAAwC,GACxCC,aAAa;IACb,0CAA0C,GAC1CC,eAAe;IACf;;;;;;GAMC,GACDC,iBAAiB;IACjB;;;;;;GAMC,GACDC,aAAa;IACb;;;;;;GAMC,GACDC,eAAe;IACf;;;;GAIC,GACDC,aAAa;IACb;;;;;;;;;GASC,GACDC,WAAW;IACX;;;;GAIC,GACDC,gBAAgB;IAChB;;;;;;;;;;;;GAYC,GACDC,UAAU;IACV;;;;;;;;;;;GAWC,GACDC,iBAAiB;IACjB;;;;;GAKC,GACDC,kBAAkB;IAClB;;;;;GAKC,GACDC,eAAe;IACf,mGAAmG,GACnGC,eAAe;IACf,4GAA4G,GAC5GC,aAAa;IACb,oGAAoG,GACpGC,eAAe;AACjB,EAAU;AAyHV;;;;;;;;;;;CAWC,GACD,OAAO,MAAMC,6BAA2D;IACtE7C,qBAAqBe,cAAc;IACnCf,qBAAqBkB,QAAQ;IAC7BlB,qBAAqBmB,SAAS;IAC9BnB,qBAAqBoB,mBAAmB;IACxCpB,qBAAqBqB,mBAAmB;CACzC,CAAA;AAED,qEAAqE,GACrE,OAAO,SAASyB,yBAAyBC,IAAY;IACnD,OAAO,AAACF,2BAAiDG,QAAQ,CAACD;AACpE;AA2WA,MAAME,oBAAoB,IAAIC;AAE9B,0EAA0E,GAC1E,OAAO,SAASC,yBAAyBC,SAA2B;IAClEH,kBAAkBI,GAAG,CAACD,UAAUE,QAAQ,EAAEF;AAC5C;AAEA,OAAO,SAASG,2BAA2BD,QAAkB;IAC3DL,kBAAkBO,MAAM,CAACF;AAC3B;AAEA;;;;;;;;;;CAUC,GACD,OAAO,SAASG,sBACdC,gBAAsC;IAEtC,MAAMC,MAAMC,MAAMC,IAAI,CAACZ,kBAAkBa,MAAM;IAC/C,IAAI,CAACJ,kBAAkB,OAAOC;IAC9B,MAAMI,UAAU,IAAIC,IAAIN;IACxB,OAAOC,IAAIM,MAAM,CAAC,CAACb,YAAcW,QAAQG,GAAG,CAACd,UAAUE,QAAQ;AACjE;AAQA;;;;CAIC,GACD,OAAO,SAASa,oBACdT,gBAAsC;IAEtC,OAAOD,sBAAsBC,kBAAkBU,OAAO,CAAC,CAAChB;YACrDA;eAAD,EAACA,sBAAAA,UAAUiB,QAAQ,YAAlBjB,sBAAsB,EAAE,EAAEvD,GAAG,CAAC,CAACyE,UAAa,aACxCA;gBACHhB,UAAUF,UAAUE,QAAQ;gBAC5BiB,aAAanB,UAAUmB,WAAW;;;AAGxC;AAoBA;;;;;;;;;CASC,GACD,SAASC,aACPF,OAAuB,EACvBG,IAAY;QAIaH;IAFzB,MAAMI,UAAUC,iBAAiBL,SAASA,QAAQG,IAAI,EAAEA;IACxD,IAAIC,SAAS,OAAOA;IACpB,KAAK,MAAME,eAAcN,uBAAAA,QAAQO,WAAW,YAAnBP,uBAAuB,EAAE,CAAE;QAClD,MAAMQ,QAAQH,iBAAiBL,SAASM,YAAYH;QACpD,IAAIK,OAAO,OAAO,aAAKA;YAAOC,QAAQ;;IACxC;IACA,OAAOC;AACT;AAEA,SAASL,iBACPL,OAAuB,EACvBW,QAAgB,EAChBR,IAAY;QAGPH,mBAIWA;IALhB,IAAIW,aAAaR,MAAM,OAAO;QAAES,UAAU,EAAE;IAAC;IAC7C,IAAI,GAACZ,oBAAAA,QAAQa,QAAQ,qBAAhBb,kBAAkB7E,MAAM,KAAI,CAAC6E,QAAQc,WAAW,EAAE,OAAOJ;IAC9D,4EAA4E;IAC5E,IAAI,CAACP,KAAKY,UAAU,CAAC,GAAGJ,SAAS,CAAC,CAAC,GAAG,OAAOD;IAC7C,MAAME,WAAWT,KAAKa,KAAK,CAACL,SAASxF,MAAM,GAAG,GAAG8F,KAAK,CAAC,KAAKtB,MAAM,CAACuB;IACnE,MAAMC,WAAUnB,qBAAAA,QAAQa,QAAQ,qBAAhBb,mBAAkBoB,IAAI,CAAC,CAACC,OAASA,KAAKjH,EAAE,KAAKwG,QAAQ,CAAC,EAAE;IACxE,IAAIO,SAAS,OAAO;QAAEA;QAASP;IAAS;IACxC,0EAA0E;IAC1E,qEAAqE;IACrE,uEAAuE;IACvE,kEAAkE;IAClE,0EAA0E;IAC1E,iCAAiC;IACjC,OAAOZ,QAAQc,WAAW,GAAG;QAAEF;IAAS,IAAIF;AAC9C;AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+BC,GACD,OAAO,SAASY,yBACdnB,IAAY,EACZf,gBAAsC;IAEtC,OAAOmC,uBACLpB,MACAf,kBACA,CAACN,YAAcA,UAAUiB,QAAQ;AAErC;AAQA;;;;;;;;CAQC,GACD,OAAO,SAASyB,uBACdpC,gBAAsC;IAEtC,OAAOD,sBAAsBC,kBAAkBU,OAAO,CAAC,CAAChB;YACrDA;eAAD,EAACA,yBAAAA,UAAU2C,WAAW,YAArB3C,yBAAyB,EAAE,EAAEvD,GAAG,CAAC,CAACyE,UAAa,CAAA;gBAAElB;gBAAWkB;YAAQ,CAAA;;AAEzE;AAEA;;;;;;;CAOC,GACD,OAAO,SAAS0B,4BACdvB,IAAY,EACZf,gBAAsC;IAEtC,OAAOmC,uBACLpB,MACAf,kBACA,CAACN,YAAcA,UAAU2C,WAAW;AAExC;AAEA,2EAA2E,GAC3E,SAASF,uBACPpB,IAAY,EACZf,gBAAiD,EACjDuC,UAAyE;IAEzE,IAAIC;IACJ,uEAAuE,GACvE,IAAIC,aAAyB,EAAE;IAC/B,KAAK,MAAM/C,aAAaK,sBAAsBC,kBAAmB;YACzCuC;QAAtB,KAAK,MAAM3B,YAAW2B,cAAAA,WAAW7C,sBAAX6C,cAAyB,EAAE,CAAE;;YACjD,IAAI,CAAC3B,QAAQ8B,SAAS,EAAE;YACxB,MAAMtB,QAAQN,aAAaF,SAASG;YACpC,IAAI,CAACK,OAAO;YACZ,MAAMuB,qBAAaH,wBAAAA,KAAM5B,OAAO,CAACG,IAAI,CAAChF,MAAM,mBAAI,CAAC;YACjD,IAAI6E,QAAQG,IAAI,CAAChF,MAAM,GAAG4G,YAAY;gBACpCH,OAAO;oBAAE9C;oBAAWkB;mBAAYQ;gBAChCqB,aAAa;oBAAC/C,UAAUE,QAAQ;iBAAC;gBACjC;YACF;YACA,yEAAyE;YACzE,yCAAyC;YACzC,IACEgB,QAAQG,IAAI,CAAChF,MAAM,KAAK4G,cACxB,CAACF,WAAWnD,QAAQ,CAACI,UAAUE,QAAQ,GACvC;gBACA6C,WAAWG,IAAI,CAAClD,UAAUE,QAAQ;YACpC;QACF;IACF;IACA,IAAI6C,WAAW1G,MAAM,GAAG,GAAG;QACzB,0EAA0E;QAC1E,qEAAqE;QACrE,uEAAuE;QACvE8G,QAAQC,KAAK,CACX,CAAC,2BAA2B,EAAE/B,KAAK,8BAA8B,CAAC,GAChE,CAAC,gBAAgB,EAAE0B,WAAWM,IAAI,CAAC,MAAM,2BAA2B,CAAC,GACrE;QAEJ,OAAOzB;IACT;IACA,OAAOkB;AACT;AAOA;;;CAGC,GACD,OAAO,SAASQ,sBACdhD,gBAAsC;IAEtC,OAAOD,sBAAsBC,kBAAkBU,OAAO,CAAC,CAAChB;YACrDA;eAAD,EAACA,wBAAAA,UAAUuD,UAAU,YAApBvD,wBAAwB,EAAE,EAAEvD,GAAG,CAAC,CAAC+G,OAAU,aACvCA;gBACHtD,UAAUF,UAAUE,QAAQ;;;AAGlC;AAEA;;;;;;;CAOC,GACD,OAAO,SAASuD,wBACdnI,EAAU,EACVgF,gBAAsC;IAEtC,MAAMoD,UAAUJ,sBAAsBhD,kBAAkBO,MAAM,CAC5D,CAAC2C,OAASA,KAAKlI,EAAE,KAAKA;IAExB,MAAMqI,SAAS;WAAI,IAAI/C,IAAI8C,QAAQjH,GAAG,CAAC,CAAC+G,OAASA,KAAKtD,QAAQ;KAAG;IACjE,IAAIyD,OAAOtH,MAAM,GAAG,GAAG;QACrB8G,QAAQC,KAAK,CACX,CAAC,2BAA2B,EAAE9H,GAAG,qCAAqC,CAAC,GACrE,CAAC,CAAC,EAAEqI,OAAON,IAAI,CAAC,MAAM,wCAAwC,CAAC,GAC/D;QAEJ,OAAOzB;IACT;IACA,OAAO8B,OAAO,CAAC,EAAE;AACnB;AAEA,qEAAqE,GACrE,OAAO,SAASE,mBACdjE,IAAY,EACZW,gBAAsC;IAEtC,MAAMuD,MAAqE,EAAE;IAC7E,KAAK,MAAM7D,aAAaK,sBAAsBC,kBAAmB;YAC1CN;QAArB,KAAK,MAAM8D,WAAU9D,qBAAAA,UAAU+D,OAAO,YAAjB/D,qBAAqB,EAAE,CAAE;YAC5C,IAAI8D,OAAOnE,IAAI,KAAKA,MAAMkE,IAAIX,IAAI,CAAC;gBAAElD;gBAAW8D;YAAO;QACzD;IACF;IACA,OAAOD;AACT;AAEA,oEAAoE,GACpE,8DAA8D;AAC9D,OAAO,SAASG,qBACd1D,gBAAsC;IAEtC,8DAA8D;IAC9D,MAAMuD,MAAiC,EAAE;IACzC,KAAK,MAAM7D,aAAaK,sBAAsBC,kBAAmB;YAClDN;QAAb6D,IAAIX,IAAI,KAAKlD,uBAAAA,UAAUiE,SAAS,YAAnBjE,uBAAuB,EAAE;IACxC;IACA,OAAO6D;AACT"}
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 { 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/marketplace-theme'\nimport type { HostTheme, HostThemeScheme } from '@aglyn/shared-data-types'\nimport type { ComponentType } from 'react'\n// The commerce zones' props live in a type-only module (AGL-2916).\nexport type {\n ConsoleProductCopyValues,\n ConsoleProductDraft,\n ConsoleProductEditorZoneProps,\n ConsoleProductImportZoneProps,\n ConsoleProductOption,\n ConsoleProductSummary,\n ConsoleProductType,\n ConsoleProductsHubZoneProps,\n ConsoleProposedDiscount,\n ConsoleProposedProduct,\n} from './commerce-zone-props'\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 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\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 /** 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 * 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 * 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 header?: { title: string; icon?: MdiIconProps; docsTopic?: string }\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 /** Marketplace listing detail body. Props: hostId, listingId, permissions. */\n marketplaceListing: 'marketplaceListing',\n /**\n * Above the whole Marketplace subtree: what THIS DEPLOYMENT cannot do\n * (AGL-2019, moved out of the console app by AGL-3080). No props — the\n * widget reads {@link DeploymentCapabilities} from the context the org's\n * server layout provides, and draws nothing when the deployment is\n * configured.\n *\n * A zone rather than a component the app holds, because the sentence is\n * the marketplace's: it names what still works without a Stripe platform\n * (browsing, free installs) and that is the plugin's knowledge, not the\n * shell's. The app supplies only the fact.\n */\n marketplaceCapability: 'marketplaceCapability',\n /**\n * Org marketplace browse body (AGL-772). Props: hostId (acting site),\n * permissions, orgScoped. The single org-scope place to browse + install,\n * replacing the per-site marketplace tab.\n */\n orgMarketplace: 'orgMarketplace',\n /** Plugins & add-ons hub installs section. Props: hostId. */\n orgAddons: 'orgAddons',\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 \"What you have changed\" card\n * and the editor (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 * 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 * 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 * The console shell's assistant dock (AGL-2940): the one position above\n * every route boundary in both the `(app)` and `(editor)` shells, where a\n * floating helper survives a navigation. Props: none — a widget here\n * resolves its own scope from the URL, as the shell's own chrome does.\n */\n assistPanel: 'assistPanel',\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 * 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, above the site SEO form (AGL-2910). Props:\n * {@link ConsoleHostSeoZoneProps} — the site, its stored SEO settings, and\n * `proposeDraft`, which puts values in the form as unsaved edits. The\n * form's Update stores them; nothing a widget proposes reaches the\n * 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 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 * The Forms page of a site, beside its Create Form action (AGL-3043):\n * another way to start a form. Props: {@link ConsoleHostFormsZoneProps}, on\n * the `hostScreens` contract.\n *\n * The Forms page is the forms plugin's own surface, so the plugin HOSTS\n * this zone: it draws the renderer `useConsoleWidgetSlot` hands down, as\n * the product editor draws `seoFields`, and every gate a console page's\n * slot applies applies here too.\n */\n hostForms: 'hostForms',\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 * The Automation page's Actions, beside Add action and Recipes (AGL-2919):\n * another way to start an automation. Props:\n * {@link ConsoleHostAutomationsZoneProps}.\n *\n * The Automation page is the workflows plugin's own surface, so the plugin\n * HOSTS this zone through the renderer `useConsoleWidgetSlot` hands down, as\n * the product editor hosts `seoFields`, and every gate a console page's slot\n * applies applies here too. A widget here writes nothing through the page:\n * the Actions list shows what it makes once it exists, and `openAction`\n * opens a listed action in the Actions editor.\n */\n hostAutomations: 'hostAutomations',\n /**\n * The editor of one SAVED automation — an action or a workflow — on the\n * Automation page (AGL-2919). Props: {@link ConsoleAutomationEditorZoneProps}.\n * Hosted by the workflows plugin, as `hostAutomations` is. A widget here\n * reads the automation as it is stored and changes nothing in the editor.\n */\n automationEditor: 'automationEditor',\n /**\n * One FAILED run in an automation's run history (AGL-2919), drawn once per\n * failed row. Props: {@link ConsoleAutomationRunZoneProps}. Hosted by the\n * workflows plugin. A widget here reads the run as it was recorded and\n * changes nothing.\n */\n automationRun: 'automationRun',\n /** The commerce product editor's fields (AGL-2916). Props: {@link ConsoleProductEditorZoneProps}. */\n productEditor: 'productEditor',\n /** Above the commerce products hub's catalog table (AGL-2916). Props: {@link ConsoleProductsHubZoneProps}. */\n productsHub: 'productsHub',\n /** Inside the commerce CSV import dialog (AGL-2916). Props: {@link ConsoleProductImportZoneProps}. */\n productImport: 'productImport',\n} as const\n\nexport type ConsoleWidgetSlot =\n (typeof CONSOLE_WIDGET_SLOTS)[keyof typeof CONSOLE_WIDGET_SLOTS]\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`, `hostLayouts` and `hostForms` zones (AGL-3043) and\n * the `hostComponents` zone (AGL-3051) hand each widget: the site and its org,\n * as `hostScreens` hands them.\n */\nexport type ConsoleHostTemplatesZoneProps = ConsoleHostScreensZoneProps\n/** See {@link ConsoleHostTemplatesZoneProps}. */\nexport type ConsoleHostLayoutsZoneProps = ConsoleHostScreensZoneProps\n/** See {@link ConsoleHostTemplatesZoneProps}. */\nexport type ConsoleHostFormsZoneProps = ConsoleHostScreensZoneProps\n/** See {@link ConsoleHostTemplatesZoneProps}. */\nexport type ConsoleHostComponentsZoneProps = ConsoleHostScreensZoneProps\n\n/** What the `hostAutomations` zone hands each widget (AGL-2919). */\nexport interface ConsoleHostAutomationsZoneProps {\n hostId: string\n /** The org the page names; `undefined` while it resolves. */\n orgId: string | undefined\n /**\n * Opens the Actions editor on a listed action. `false` when the id names no\n * action the list has read yet, which is how a widget whose draft was just\n * written tells a list that has not caught up from one that opened it.\n */\n openAction: (actionId: string) => boolean\n}\n\n/** One automation on the Automation page: an action, or a workflow (AGL-2919). */\nexport interface ConsoleAutomationTarget {\n type: 'action' | 'workflow'\n id: string\n name: string\n}\n\n/** What the `automationEditor` zone hands each widget (AGL-2919). */\nexport interface ConsoleAutomationEditorZoneProps {\n hostId: string\n /** The org the page names; `undefined` while it resolves. */\n orgId: string | undefined\n /** The automation the editor has open, as it is stored. */\n target: ConsoleAutomationTarget\n}\n\n/** What the `automationRun` zone hands each widget (AGL-2919). */\nexport interface ConsoleAutomationRunZoneProps {\n hostId: string\n /** The org the page names; `undefined` while it resolves. */\n orgId: string | undefined\n /** The automation the run belongs to. */\n target: ConsoleAutomationTarget\n /** The run's entry in the site's activity log. */\n runId: string\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 the blank site the person already has: this\n * same page, with nothing created and nothing begun. The shell records the\n * choice for this site and draws the zone no more.\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. See `hostFirstRun` in\n * {@link CONSOLE_WIDGET_SLOTS}.\n */\n startBlank: () => 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 org page, its detail\n * 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.adminOrgDetail,\n CONSOLE_WIDGET_SLOTS.staffOrg,\n CONSOLE_WIDGET_SLOTS.staffUser,\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/** 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/** 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\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 * 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\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 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 * 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 listConsoleExtensions(enabledPluginIds).flatMap((extension) =>\n (extension.navItems ?? []).map((navItem) => ({\n ...navItem,\n pluginId: extension.pluginId,\n featureFlag: extension.featureFlag,\n })),\n )\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 * registration order, for the 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 listConsoleExtensions(enabledPluginIds).flatMap((extension) =>\n (extension.orgNavItems ?? []).map((navItem) => ({ extension, navItem })),\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/** 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","marketplaceListing","marketplaceCapability","orgMarketplace","orgAddons","dashboardFooter","orgSettings","hostSettings","hostFirstRun","hostTheme","adminOrgDetail","orgBillingUsage","orgBillingOverview","staffOrg","staffUser","staffOrgsListColumn","staffOrgUsageColumn","orgMember","orgMembersListColumn","hostMembers","assistPanel","besignerInspector","seoFields","hostSeo","recordInsights","recordEmail","importMapping","besignerToolbar","hostScreens","hostTemplates","hostLayouts","hostForms","hostComponents","orgSites","hostAutomations","automationEditor","automationRun","productEditor","productsHub","productImport","CONSOLE_STAFF_WIDGET_SLOTS","isConsoleStaffWidgetSlot","slot","includes","consoleExtensions","Map","registerConsoleExtension","extension","set","pluginId","unregisterConsoleExtension","delete","listConsoleExtensions","enabledPluginIds","all","Array","from","values","enabled","Set","filter","has","listConsoleNavItems","flatMap","navItems","navItem","featureFlag","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","listConsoleProviders","providers"],"mappings":"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"}
@@ -6,6 +6,11 @@
6
6
  * declared by its own `catalog` block in plugins.config.json. The core holds
7
7
  * the types and the resolvers in `enabled-plugins.ts`; it holds no row.
8
8
  */
9
- import type { FirstPartyPlugin, PublishedSiteImpact } from './enabled-plugins';
9
+ import type { FirstPartyPlugin, PluginEditBarLink, PublishedSiteImpact } from './enabled-plugins';
10
10
  export declare const FIRST_PARTY_PLUGINS: readonly FirstPartyPlugin[];
11
11
  export declare const PUBLISHED_SITE_IMPACT: Readonly<Record<string, PublishedSiteImpact>>;
12
+ /**
13
+ * The admin edit bar's quick links, in the order they are drawn — each
14
+ * declared by the plugin whose console page it opens (AGL-3080).
15
+ */
16
+ export declare const PLUGIN_EDIT_BAR_LINKS: readonly PluginEditBarLink[];
@@ -145,5 +145,22 @@ export const PUBLISHED_SITE_IMPACT = {
145
145
  "redirects": "routes",
146
146
  "workflows": "routes"
147
147
  };
148
+ /**
149
+ * The admin edit bar's quick links, in the order they are drawn — each
150
+ * declared by the plugin whose console page it opens (AGL-3080).
151
+ */ export const PLUGIN_EDIT_BAR_LINKS = [
152
+ {
153
+ "pluginId": "inbox",
154
+ "order": 20,
155
+ "label": "Inbox",
156
+ "path": "/inbox"
157
+ },
158
+ {
159
+ "pluginId": "commerce",
160
+ "order": 30,
161
+ "label": "Orders",
162
+ "path": "/products/orders"
163
+ }
164
+ ];
148
165
 
149
166
  //# sourceMappingURL=first-party-plugins.generated.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/first-party-plugins.generated.ts"],"sourcesContent":["/**\n * GENERATED FILE — do not edit. Regenerate with:\n * node tools/scripts/generate-plugin-manifests.mjs\n *\n * The switchboard catalog (AGL-3080): one row per plugin and capability, each\n * declared by its own `catalog` block in plugins.config.json. The core holds\n * the types and the resolvers in `enabled-plugins.ts`; it holds no row.\n */\n\nimport type { FirstPartyPlugin, PublishedSiteImpact } from './enabled-plugins'\n\nexport const FIRST_PARTY_PLUGINS: readonly FirstPartyPlugin[] = [\n {\n \"id\": \"mui\",\n \"label\": \"Components\",\n \"alwaysOn\": true,\n \"description\": \"The base component and theme library every site builds on.\"\n },\n {\n \"id\": \"forms\",\n \"label\": \"Forms\",\n \"alwaysOnForWorkspace\": true,\n \"description\": \"Forms on the site and the catalog that owns them.\",\n \"siteOff\": {\n \"stops\": \"Switching Forms off for this site stops forms rendering on its published pages, refuses every submission sent to it, and blocks publishing a form, or a page that carries one, until Forms is back on.\",\n \"keeps\": \"Submissions already received, the workspace’s form catalog and the CRM leads its forms created are kept, and forms keep working on the workspace’s other sites.\",\n \"confirm\": true,\n \"pages\": {\n \"heading\": \"These published pages carry a form. Their forms stop rendering and stop accepting submissions:\",\n \"none\": \"No published page on this site carries a form.\"\n }\n }\n },\n {\n \"id\": \"ai\",\n \"label\": \"AI\",\n \"alwaysOnForWorkspace\": true,\n \"description\": \"The assistant, generative building and automation, and the AI add-on.\",\n \"siteOff\": {\n \"stops\": \"Switching AI off for this site hides the assistant, Describe it, the AI cards and the editor’s AI controls on this site, refuses every AI request made for it, and stops its queued AI jobs without spending credits.\",\n \"keeps\": \"It does not stop the workspace’s AI add-on, credits, allotments or overage billing, and AI keeps working on the workspace’s other sites.\"\n }\n },\n {\n \"id\": \"accounts\",\n \"label\": \"User Accounts\",\n \"description\": \"Visitor accounts on the site: the /signin, /signup and /recover pages, and the Members blocks. Off for a site until you turn it on.\",\n \"releaseFlag\": \"release_member_accounts\",\n \"defaultOffPerSite\": true,\n \"requires\": [\n \"commerce\"\n ]\n },\n {\n \"id\": \"bookings\",\n \"label\": \"Bookings\",\n \"description\": \"Services, open slots, and paid bookings.\",\n \"releaseFlag\": \"release_bookings\"\n },\n {\n \"id\": \"commerce\",\n \"label\": \"Commerce\",\n \"description\": \"Products, carts, checkout, orders, POS.\",\n \"releaseFlag\": \"release_commerce_v2\"\n },\n {\n \"id\": \"marketplace\",\n \"label\": \"Marketplace\",\n \"description\": \"Marketplace listings, templates, and installs.\",\n \"releaseFlag\": \"release_marketplace\"\n },\n {\n \"id\": \"crm\",\n \"label\": \"CRM\",\n \"description\": \"Leads, contacts, companies, deals, tasks and reports.\",\n \"releaseFlag\": \"release_crm\"\n },\n {\n \"id\": \"outreach\",\n \"label\": \"Sequences\",\n \"description\": \"One-to-one email sequences sent from connected mailboxes.\",\n \"releaseFlag\": \"release_outreach\"\n },\n {\n \"id\": \"data\",\n \"label\": \"Data\",\n \"description\": \"Datasets, records, and CSV import/export.\",\n \"releaseFlag\": \"release_data_store\"\n },\n {\n \"id\": \"email\",\n \"label\": \"Email\",\n \"description\": \"Designed emails and campaign sending.\",\n \"releaseFlag\": \"release_email\"\n },\n {\n \"id\": \"events-calendar\",\n \"label\": \"Events Calendar\",\n \"description\": \"Event lists and calendars.\",\n \"releaseFlag\": \"release_events\"\n },\n {\n \"id\": \"inbox\",\n \"label\": \"Inbox\",\n \"description\": \"Form submissions and lead inbox.\",\n \"releaseFlag\": \"release_inbox\"\n },\n {\n \"id\": \"logic\",\n \"label\": \"Logic\",\n \"description\": \"Variables, functions, and reference health.\",\n \"releaseFlag\": \"release_logic\"\n },\n {\n \"id\": \"marketing\",\n \"label\": \"Marketing\",\n \"description\": \"Overlays, campaigns, and experiments.\",\n \"releaseFlag\": \"release_marketing\"\n },\n {\n \"id\": \"redirects\",\n \"label\": \"Redirects\",\n \"description\": \"URL redirect rules.\",\n \"releaseFlag\": \"release_redirects\"\n },\n {\n \"id\": \"workflows\",\n \"label\": \"Automation\",\n \"description\": \"Workflows, actions, webhooks, and run logs.\",\n \"releaseFlag\": \"release_workflows\"\n },\n]\n\nexport const PUBLISHED_SITE_IMPACT: Readonly<Record<string, PublishedSiteImpact>> = {\n \"mui\": \"elements\",\n \"forms\": \"elements\",\n \"ai\": \"console-only\",\n \"accounts\": \"routes\",\n \"bookings\": \"elements\",\n \"commerce\": \"elements\",\n \"marketplace\": \"console-only\",\n \"crm\": \"console-only\",\n \"outreach\": \"console-only\",\n \"data\": \"console-only\",\n \"email\": \"elements\",\n \"events-calendar\": \"elements\",\n \"inbox\": \"console-only\",\n \"logic\": \"console-only\",\n \"marketing\": \"elements\",\n \"redirects\": \"routes\",\n \"workflows\": \"routes\",\n}\n"],"names":["FIRST_PARTY_PLUGINS","PUBLISHED_SITE_IMPACT"],"mappings":"AAAA;;;;;;;CAOC,GAID,OAAO,MAAMA,sBAAmD;IAC9D;QACE,MAAM;QACN,SAAS;QACT,YAAY;QACZ,eAAe;IACjB;IACA;QACE,MAAM;QACN,SAAS;QACT,wBAAwB;QACxB,eAAe;QACf,WAAW;YACT,SAAS;YACT,SAAS;YACT,WAAW;YACX,SAAS;gBACP,WAAW;gBACX,QAAQ;YACV;QACF;IACF;IACA;QACE,MAAM;QACN,SAAS;QACT,wBAAwB;QACxB,eAAe;QACf,WAAW;YACT,SAAS;YACT,SAAS;QACX;IACF;IACA;QACE,MAAM;QACN,SAAS;QACT,eAAe;QACf,eAAe;QACf,qBAAqB;QACrB,YAAY;YACV;SACD;IACH;IACA;QACE,MAAM;QACN,SAAS;QACT,eAAe;QACf,eAAe;IACjB;IACA;QACE,MAAM;QACN,SAAS;QACT,eAAe;QACf,eAAe;IACjB;IACA;QACE,MAAM;QACN,SAAS;QACT,eAAe;QACf,eAAe;IACjB;IACA;QACE,MAAM;QACN,SAAS;QACT,eAAe;QACf,eAAe;IACjB;IACA;QACE,MAAM;QACN,SAAS;QACT,eAAe;QACf,eAAe;IACjB;IACA;QACE,MAAM;QACN,SAAS;QACT,eAAe;QACf,eAAe;IACjB;IACA;QACE,MAAM;QACN,SAAS;QACT,eAAe;QACf,eAAe;IACjB;IACA;QACE,MAAM;QACN,SAAS;QACT,eAAe;QACf,eAAe;IACjB;IACA;QACE,MAAM;QACN,SAAS;QACT,eAAe;QACf,eAAe;IACjB;IACA;QACE,MAAM;QACN,SAAS;QACT,eAAe;QACf,eAAe;IACjB;IACA;QACE,MAAM;QACN,SAAS;QACT,eAAe;QACf,eAAe;IACjB;IACA;QACE,MAAM;QACN,SAAS;QACT,eAAe;QACf,eAAe;IACjB;IACA;QACE,MAAM;QACN,SAAS;QACT,eAAe;QACf,eAAe;IACjB;CACD,CAAA;AAED,OAAO,MAAMC,wBAAuE;IAClF,OAAO;IACP,SAAS;IACT,MAAM;IACN,YAAY;IACZ,YAAY;IACZ,YAAY;IACZ,eAAe;IACf,OAAO;IACP,YAAY;IACZ,QAAQ;IACR,SAAS;IACT,mBAAmB;IACnB,SAAS;IACT,SAAS;IACT,aAAa;IACb,aAAa;IACb,aAAa;AACf,EAAC"}
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/first-party-plugins.generated.ts"],"sourcesContent":["/**\n * GENERATED FILE — do not edit. Regenerate with:\n * node tools/scripts/generate-plugin-manifests.mjs\n *\n * The switchboard catalog (AGL-3080): one row per plugin and capability, each\n * declared by its own `catalog` block in plugins.config.json. The core holds\n * the types and the resolvers in `enabled-plugins.ts`; it holds no row.\n */\n\nimport type { FirstPartyPlugin, PluginEditBarLink, PublishedSiteImpact } from './enabled-plugins'\n\nexport const FIRST_PARTY_PLUGINS: readonly FirstPartyPlugin[] = [\n {\n \"id\": \"mui\",\n \"label\": \"Components\",\n \"alwaysOn\": true,\n \"description\": \"The base component and theme library every site builds on.\"\n },\n {\n \"id\": \"forms\",\n \"label\": \"Forms\",\n \"alwaysOnForWorkspace\": true,\n \"description\": \"Forms on the site and the catalog that owns them.\",\n \"siteOff\": {\n \"stops\": \"Switching Forms off for this site stops forms rendering on its published pages, refuses every submission sent to it, and blocks publishing a form, or a page that carries one, until Forms is back on.\",\n \"keeps\": \"Submissions already received, the workspace’s form catalog and the CRM leads its forms created are kept, and forms keep working on the workspace’s other sites.\",\n \"confirm\": true,\n \"pages\": {\n \"heading\": \"These published pages carry a form. Their forms stop rendering and stop accepting submissions:\",\n \"none\": \"No published page on this site carries a form.\"\n }\n }\n },\n {\n \"id\": \"ai\",\n \"label\": \"AI\",\n \"alwaysOnForWorkspace\": true,\n \"description\": \"The assistant, generative building and automation, and the AI add-on.\",\n \"siteOff\": {\n \"stops\": \"Switching AI off for this site hides the assistant, Describe it, the AI cards and the editor’s AI controls on this site, refuses every AI request made for it, and stops its queued AI jobs without spending credits.\",\n \"keeps\": \"It does not stop the workspace’s AI add-on, credits, allotments or overage billing, and AI keeps working on the workspace’s other sites.\"\n }\n },\n {\n \"id\": \"accounts\",\n \"label\": \"User Accounts\",\n \"description\": \"Visitor accounts on the site: the /signin, /signup and /recover pages, and the Members blocks. Off for a site until you turn it on.\",\n \"releaseFlag\": \"release_member_accounts\",\n \"defaultOffPerSite\": true,\n \"requires\": [\n \"commerce\"\n ]\n },\n {\n \"id\": \"bookings\",\n \"label\": \"Bookings\",\n \"description\": \"Services, open slots, and paid bookings.\",\n \"releaseFlag\": \"release_bookings\"\n },\n {\n \"id\": \"commerce\",\n \"label\": \"Commerce\",\n \"description\": \"Products, carts, checkout, orders, POS.\",\n \"releaseFlag\": \"release_commerce_v2\"\n },\n {\n \"id\": \"marketplace\",\n \"label\": \"Marketplace\",\n \"description\": \"Marketplace listings, templates, and installs.\",\n \"releaseFlag\": \"release_marketplace\"\n },\n {\n \"id\": \"crm\",\n \"label\": \"CRM\",\n \"description\": \"Leads, contacts, companies, deals, tasks and reports.\",\n \"releaseFlag\": \"release_crm\"\n },\n {\n \"id\": \"outreach\",\n \"label\": \"Sequences\",\n \"description\": \"One-to-one email sequences sent from connected mailboxes.\",\n \"releaseFlag\": \"release_outreach\"\n },\n {\n \"id\": \"data\",\n \"label\": \"Data\",\n \"description\": \"Datasets, records, and CSV import/export.\",\n \"releaseFlag\": \"release_data_store\"\n },\n {\n \"id\": \"email\",\n \"label\": \"Email\",\n \"description\": \"Designed emails and campaign sending.\",\n \"releaseFlag\": \"release_email\"\n },\n {\n \"id\": \"events-calendar\",\n \"label\": \"Events Calendar\",\n \"description\": \"Event lists and calendars.\",\n \"releaseFlag\": \"release_events\"\n },\n {\n \"id\": \"inbox\",\n \"label\": \"Inbox\",\n \"description\": \"Form submissions and lead inbox.\",\n \"releaseFlag\": \"release_inbox\"\n },\n {\n \"id\": \"logic\",\n \"label\": \"Logic\",\n \"description\": \"Variables, functions, and reference health.\",\n \"releaseFlag\": \"release_logic\"\n },\n {\n \"id\": \"marketing\",\n \"label\": \"Marketing\",\n \"description\": \"Overlays, campaigns, and experiments.\",\n \"releaseFlag\": \"release_marketing\"\n },\n {\n \"id\": \"redirects\",\n \"label\": \"Redirects\",\n \"description\": \"URL redirect rules.\",\n \"releaseFlag\": \"release_redirects\"\n },\n {\n \"id\": \"workflows\",\n \"label\": \"Automation\",\n \"description\": \"Workflows, actions, webhooks, and run logs.\",\n \"releaseFlag\": \"release_workflows\"\n },\n]\n\nexport const PUBLISHED_SITE_IMPACT: Readonly<Record<string, PublishedSiteImpact>> = {\n \"mui\": \"elements\",\n \"forms\": \"elements\",\n \"ai\": \"console-only\",\n \"accounts\": \"routes\",\n \"bookings\": \"elements\",\n \"commerce\": \"elements\",\n \"marketplace\": \"console-only\",\n \"crm\": \"console-only\",\n \"outreach\": \"console-only\",\n \"data\": \"console-only\",\n \"email\": \"elements\",\n \"events-calendar\": \"elements\",\n \"inbox\": \"console-only\",\n \"logic\": \"console-only\",\n \"marketing\": \"elements\",\n \"redirects\": \"routes\",\n \"workflows\": \"routes\",\n}\n\n/**\n * The admin edit bar's quick links, in the order they are drawn — each\n * declared by the plugin whose console page it opens (AGL-3080).\n */\nexport const PLUGIN_EDIT_BAR_LINKS: readonly PluginEditBarLink[] = [\n {\n \"pluginId\": \"inbox\",\n \"order\": 20,\n \"label\": \"Inbox\",\n \"path\": \"/inbox\"\n },\n {\n \"pluginId\": \"commerce\",\n \"order\": 30,\n \"label\": \"Orders\",\n \"path\": \"/products/orders\"\n },\n]\n"],"names":["FIRST_PARTY_PLUGINS","PUBLISHED_SITE_IMPACT","PLUGIN_EDIT_BAR_LINKS"],"mappings":"AAAA;;;;;;;CAOC,GAID,OAAO,MAAMA,sBAAmD;IAC9D;QACE,MAAM;QACN,SAAS;QACT,YAAY;QACZ,eAAe;IACjB;IACA;QACE,MAAM;QACN,SAAS;QACT,wBAAwB;QACxB,eAAe;QACf,WAAW;YACT,SAAS;YACT,SAAS;YACT,WAAW;YACX,SAAS;gBACP,WAAW;gBACX,QAAQ;YACV;QACF;IACF;IACA;QACE,MAAM;QACN,SAAS;QACT,wBAAwB;QACxB,eAAe;QACf,WAAW;YACT,SAAS;YACT,SAAS;QACX;IACF;IACA;QACE,MAAM;QACN,SAAS;QACT,eAAe;QACf,eAAe;QACf,qBAAqB;QACrB,YAAY;YACV;SACD;IACH;IACA;QACE,MAAM;QACN,SAAS;QACT,eAAe;QACf,eAAe;IACjB;IACA;QACE,MAAM;QACN,SAAS;QACT,eAAe;QACf,eAAe;IACjB;IACA;QACE,MAAM;QACN,SAAS;QACT,eAAe;QACf,eAAe;IACjB;IACA;QACE,MAAM;QACN,SAAS;QACT,eAAe;QACf,eAAe;IACjB;IACA;QACE,MAAM;QACN,SAAS;QACT,eAAe;QACf,eAAe;IACjB;IACA;QACE,MAAM;QACN,SAAS;QACT,eAAe;QACf,eAAe;IACjB;IACA;QACE,MAAM;QACN,SAAS;QACT,eAAe;QACf,eAAe;IACjB;IACA;QACE,MAAM;QACN,SAAS;QACT,eAAe;QACf,eAAe;IACjB;IACA;QACE,MAAM;QACN,SAAS;QACT,eAAe;QACf,eAAe;IACjB;IACA;QACE,MAAM;QACN,SAAS;QACT,eAAe;QACf,eAAe;IACjB;IACA;QACE,MAAM;QACN,SAAS;QACT,eAAe;QACf,eAAe;IACjB;IACA;QACE,MAAM;QACN,SAAS;QACT,eAAe;QACf,eAAe;IACjB;IACA;QACE,MAAM;QACN,SAAS;QACT,eAAe;QACf,eAAe;IACjB;CACD,CAAA;AAED,OAAO,MAAMC,wBAAuE;IAClF,OAAO;IACP,SAAS;IACT,MAAM;IACN,YAAY;IACZ,YAAY;IACZ,YAAY;IACZ,eAAe;IACf,OAAO;IACP,YAAY;IACZ,QAAQ;IACR,SAAS;IACT,mBAAmB;IACnB,SAAS;IACT,SAAS;IACT,aAAa;IACb,aAAa;IACb,aAAa;AACf,EAAC;AAED;;;CAGC,GACD,OAAO,MAAMC,wBAAsD;IACjE;QACE,YAAY;QACZ,SAAS;QACT,SAAS;QACT,QAAQ;IACV;IACA;QACE,YAAY;QACZ,SAAS;QACT,SAAS;QACT,QAAQ;IACV;CACD,CAAA"}