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

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.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/enabled-plugins.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Per-org plugin enablement (AGL-416): `org.enabledPlugins` is the\n * switchboard that decides which plugins LOAD for a workspace — the loader\n * (AGL-417) dynamically imports only these. It composes with (not replaces)\n * the existing gates: a surface renders when its plugin is enabled AND its\n * `featureFlag` entitlement resolves; marketplace/marketplace listings keep\n * their per-host/org `installs` docs on top.\n *\n * The catalog's rows are declared by the plugins themselves, in\n * `plugins.config.json`, beside the package names the loader manifests are\n * generated from; this module holds the row's shape and the resolvers.\n */\n\nimport {\n FIRST_PARTY_PLUGINS,\n PLUGIN_EDIT_BAR_LINKS,\n PUBLISHED_SITE_IMPACT,\n} from './first-party-plugins.generated'\n\n/**\n * One quick link on a live site's admin edit bar, declared by the plugin whose\n * console page it opens (AGL-3080).\n *\n * The bar is drawn by the TENANT server, which never loads a plugin's console\n * code, so this is DATA compiled from `plugins.config.json` rather than a\n * runtime registration — a registry that process had not filled would drop the\n * link silently instead of failing, which is the AGL-3025 shape. It is also\n * why the label is written here rather than taken from the plugin's nav item:\n * the nav item lives in the console bundle, and \"Orders\" is a section of\n * Commerce rather than the plugin's own name.\n *\n * The link is drawn only where the SITE runs the plugin. The bar asks the same\n * resolved plugin set the rest of the edit context is built from, so a site\n * with Commerce switched off is not offered its Orders page.\n */\nexport interface PluginEditBarLink {\n /** The plugin that owns the page — also the id the site's set is checked against. */\n pluginId: string\n /** Where it sits in the bar; every declared order is distinct. */\n order: number\n /** What the bar says. A SECTION's name where that is the honest one. */\n label: string\n /**\n * Console path beneath the site, beginning with `/` and carrying no query —\n * `/inbox`, `/products/orders`. Joined onto `/{orgSlug}/hosts/{host}`.\n */\n path: string\n}\n\nexport interface FirstPartyPlugin {\n /**\n * Stable plugin id — persisted in `org.enabledPlugins`,\n * `host.disabledPlugins` and `host.enabledPlugins`, and the document id\n * under every `pluginSettings` collection.\n *\n * Renaming one is therefore a data change, not a code change, and it is\n * done in two halves that must both ship: the old id is read through\n * {@link canonicalPluginId} so every stored list still resolves, and a\n * backfill rewrites the stored documents so the alias can be retired once\n * it reads nothing.\n */\n id: string\n /** Console-facing display name. */\n label: string\n /**\n * On for every workspace AND every site, with no switch anywhere: the base\n * component library the canvas cannot render without.\n */\n alwaysOn?: boolean\n /**\n * On for every WORKSPACE, and switchable for one SITE (AGL-3028, AGL-3029).\n *\n * The workspace half is what `alwaysOn` gives: the id is unioned into the\n * org's resolved set whatever `org.enabledPlugins` stores, so a list saved\n * before the plugin existed still runs it, and no workspace switch is\n * offered. The site half is ordinary: the id is subtracted by a site's\n * `disabledPlugins` deny-list like any other, so an absent host field means\n * ON and nothing needs migrating.\n *\n * It exists for capabilities whose workspace-level half must never stop.\n * AI carries the add-on, credits, allotments and overage billing, none of\n * which belongs to a site; Forms carries the catalog and the submissions\n * already stored. A workspace switch would take those down with the\n * site-facing half, so the only switch is the site's.\n */\n alwaysOnForWorkspace?: boolean\n /**\n * What switching this plugin off for ONE site stops, and what it leaves\n * running (AGL-3028, AGL-3029) — the copy the site's Admin › Plugins page\n * states beside the switch, so the decision is made knowing both halves.\n *\n * `confirm` makes the site switch ask before it applies, naming what it is\n * about to break on published pages.\n */\n siteOff?: {\n /** What stops on the site, stated as the consequence. */\n stops: string\n /** What keeps running, because it carries no site. */\n keeps: string\n /** Ask before applying. */\n confirm?: boolean\n /**\n * How the confirmation names the site's published pages the switch\n * reaches (AGL-3029): the sentence above the list, and the one that\n * stands in for an empty list when the scan read everything.\n */\n pages?: { heading: string; none: string }\n }\n /** One-line description for the org-settings toggle list. */\n description?: string\n /**\n * Release flag gating this plugin platform-wide (AGL-422). A flagged-off\n * plugin is subtracted from every workspace's effective set — console\n * loader, published sites, and API dispatch — unless the subject is\n * staff. Always-on plugins carry no flag.\n */\n releaseFlag?: string\n /**\n * OFF for a site until that site turns it on (AGL-2486) — the inversion of\n * this switchboard's default, and the only field here that changes what an\n * absent host doc means.\n *\n * The per-host field is a DENY-list: a site records what it switches off,\n * which makes \"absent means on\" the default for all twelve other bundles.\n * That is right for a capability whose worst case is an unused nav tab, and\n * wrong for one whose worst case is a PAGE — `/signin`, `/signup`,\n * `/recover` were served by every published site on the platform,\n * including marketing sites whose real sign-in is somewhere else entirely.\n * A sign-in-shaped page on a brand's own domain that is not that brand's\n * sign-in is a credential-confusion hazard, so this one defaults closed.\n *\n * A site un-defaults it by listing the id in `host.enabledPlugins`. That\n * list is scoped to default-off ids and cannot widen anything else, so the\n * AGL-1014 invariant survives intact: a site still can never reach past\n * what its org enables.\n */\n defaultOffPerSite?: boolean\n /**\n * Plugin ids this one cannot function without (AGL-2486) — DECLARED, never\n * inferred.\n *\n * The switchboard has always let a workspace turn off a plugin another one\n * is built on, and said nothing. `accounts` is the case that proves it: the\n * Members blocks and every `membership/*` API handler ship inside the\n * COMMERCE bundle, so switching Commerce off leaves a site advertising\n * `/signin` while nothing can answer the login POST.\n *\n * Declared rather than derived on purpose. The couplings that matter are\n * exactly the ones no static read can see — which bundle happens to\n * register whose components — so a graph inferred from imports or from the\n * registry would miss the real edges while looking authoritative. An\n * incomplete warning that presents itself as complete is worse than none,\n * so this list is the contract, and {@link resolveDisableCascade} is honest\n * about covering only what is on it.\n */\n requires?: readonly string[]\n}\n\n/**\n * What a DISABLE does to a site that is already published (AGL-2486).\n *\n * The cascade dialog has to state a consequence, and there are two very\n * different ones. \"These blocks will no longer be offered\" is a different\n * decision from \"parts of your live pages go blank\", and one generic sentence\n * for both would be a lie in one direction or the other.\n *\n * - `elements` — the plugin registers site components. The tenant loads only\n * the site's enabled bundles, so elements ALREADY PLACED on published pages\n * stop rendering. Pre-existing AGL-1014 behaviour.\n * - `routes` — the plugin registers no components, but a published site\n * stops serving something: `accounts` gates `/signin`, `/signup`,\n * `/recover`; `redirects` stops applying its rules; `workflows` stops\n * answering its hooks.\n * - `console-only` — nothing a visitor can reach changes; the plugin leaves\n * navigation and the editor.\n */\nexport type PublishedSiteImpact = 'elements' | 'routes' | 'console-only'\n\n/**\n * Every catalog id classified, from the `publishedSiteImpact` each plugin\n * declares on its own row. The generator refuses a row whose verdict\n * disagrees with its `register.site`, so a site-registering bundle cannot be\n * added without declaring its consequence.\n */\nexport { PUBLISHED_SITE_IMPACT }\n\n/**\n * The user-accounts capability (AGL-2486): visitor sign-in, sign-up and\n * password recovery on a published site.\n *\n * It carries no loader manifest entry, and that is deliberate rather than an\n * oversight. The member blocks and the `membership/*` API handlers already\n * ship inside the commerce bundle, so there is no separate package to load;\n * what this id contributes is the SWITCH — the thing the tenant route gate,\n * the console card and the sitemap all ask. Several catalog ids already have\n * no tenant bundle (`contacts`, `data`, `logic`), so a manifest-less entry is\n * the existing shape, not a new one.\n */\nexport const ACCOUNTS_PLUGIN_ID = 'accounts'\n\n/**\n * The Forms capability's id, named in core (AGL-3029).\n *\n * A form's server half is core — `/api/forms/submit`, the publish-time\n * contract check, the published page's render — and core may not import a\n * plugin, so each asks the site's plugin set about this id rather than asking\n * the Forms plugin anything.\n */\nexport const FORMS_PLUGIN_ID = 'forms'\n\n/**\n * The switchboard catalog. The core holds no row of it: each plugin declares\n * its own in `plugins.config.json` (`catalog`, and one per `capabilities`\n * entry), and the generator compiles them here in their declared order, so\n * adding a plugin edits no core file.\n *\n * Compiled in rather than registered at runtime, because every resolver below\n * is synchronous and runs in each bundle of both apps; a registry that one of\n * those bundles had not filled would read as every plugin switched OFF.\n */\nexport { FIRST_PARTY_PLUGINS }\n\n/**\n * The admin edit bar's quick links that THIS SITE runs, in the order the bar\n * draws them (AGL-3080).\n *\n * `enabledPluginIds` is the site's resolved plugin set — the same one the rest\n * of the edit context is built from, already narrowed by the org's switchboard,\n * the site's own deny-list and the release flags. A link whose plugin is not\n * in it is not offered, which is what stops the bar linking a site with\n * Commerce switched off to its Orders page.\n *\n * Compiled in rather than registered, for the reason the catalog above is:\n * the tenant server draws this bar and never loads a plugin's console code,\n * so a registry it had not filled would drop every link with nothing red.\n */\nexport function pluginEditBarLinks(\n enabledPluginIds: readonly string[] | undefined,\n): readonly PluginEditBarLink[] {\n const enabled = new Set(enabledPluginIds ?? [])\n return PLUGIN_EDIT_BAR_LINKS.filter((link) => enabled.has(link.pluginId))\n}\n\n/** Ids loaded for orgs that have never touched the switchboard. */\nexport const DEFAULT_ENABLED_PLUGINS: readonly string[] =\n FIRST_PARTY_PLUGINS.map((plugin) => plugin.id)\n\n/**\n * The id a stored plugin id means today — the seam a rename re-arms.\n *\n * A first-party id is written into every org's `enabledPlugins` and every\n * site's `disabledPlugins` the moment somebody touches the switchboard, so a\n * rename in this file alone would silently turn the plugin OFF for every\n * workspace that had listed it under the old name — the stored id would match\n * nothing in the catalog and fall through as a marketplace listing id. Every\n * reader of a stored list runs it through here first, so a document written\n * before a rename keeps meaning what it meant while the backfill runs.\n *\n * Nothing is aliased today. `contacts` read as `crm` from the CRM's rename\n * (AGL-2595) until its backfill reported zero documents carrying the old id,\n * and the alias was retired (AGL-2614) — an alias that outlives its backfill\n * is a second name for the plugin that nothing writes and every reader must\n * keep honoring. The next rename adds its pair here, ships a backfill over\n * `org.enabledPlugins`, `host.disabledPlugins`, `host.enabledPlugins` and the\n * `pluginSettings` document ids, and removes the pair the same way.\n */\nexport function canonicalPluginId(pluginId: string): string {\n return pluginId\n}\n\n/** A stored list, read through the alias seam and de-duplicated. */\nfunction canonicalPluginIds(pluginIds: readonly unknown[]): string[] {\n return Array.from(\n new Set(pluginIds.map((id) => canonicalPluginId(String(id)))),\n )\n}\n\n/** On everywhere, a site's deny-list included: the base component library. */\nconst ALWAYS_ON: readonly string[] = FIRST_PARTY_PLUGINS.filter(\n (plugin) => plugin.alwaysOn,\n).map((plugin) => plugin.id)\n\n/**\n * Unioned into every workspace's set whatever the org stored: {@link ALWAYS_ON}\n * plus the ids that are on for every workspace and switchable per site.\n */\nconst ALWAYS_ON_FOR_WORKSPACE: readonly string[] = FIRST_PARTY_PLUGINS.filter(\n (plugin) => plugin.alwaysOn || plugin.alwaysOnForWorkspace,\n).map((plugin) => plugin.id)\n\n/**\n * Whether the WORKSPACE switch for this plugin is locked on (AGL-3028): the\n * base library, and every plugin that is on for every workspace and\n * switchable only per site. The org switchboard renders these on and inert.\n */\nexport function isLockedOnForWorkspace(pluginId: string): boolean {\n return ALWAYS_ON_FOR_WORKSPACE.includes(pluginId)\n}\n\n/**\n * Whether the SITE switch for this plugin is locked on: the base library\n * alone. Every other plugin a workspace runs can be switched off for one site.\n */\nexport function isLockedOnForSite(pluginId: string): boolean {\n return ALWAYS_ON.includes(pluginId)\n}\n\nconst FIRST_PARTY_IDS: ReadonlySet<string> = new Set(\n FIRST_PARTY_PLUGINS.map((plugin) => plugin.id),\n)\n\n/**\n * Ids a SITE does not get until it asks (AGL-2486). See\n * {@link FirstPartyPlugin.defaultOffPerSite} for why this inversion exists.\n */\nexport const DEFAULT_OFF_PER_SITE_PLUGIN_IDS: ReadonlySet<string> = new Set(\n FIRST_PARTY_PLUGINS.filter((plugin) => plugin.defaultOffPerSite).map(\n (plugin) => plugin.id,\n ),\n)\n\n/** Whether a plugin id is off for a site until that site opts in. */\nexport function isDefaultOffPerSite(pluginId: string): boolean {\n return DEFAULT_OFF_PER_SITE_PLUGIN_IDS.has(pluginId)\n}\n\n/**\n * Applies a host's `enabledPlugins` OPT-IN list (AGL-2486): subtracts every\n * default-off id the host has not explicitly asked for.\n *\n * Narrow-only, like its deny-list sibling. The list can only ever REMOVE the\n * default-off subtraction for an id the org already enables — listing an\n * ordinary id buys nothing, and listing one the org switched off buys\n * nothing either, because this runs against the org's resolved set.\n */\nexport function applyDefaultOffOptIn(\n pluginIds: readonly string[],\n optedIn?: readonly string[] | null,\n): string[] {\n if (!DEFAULT_OFF_PER_SITE_PLUGIN_IDS.size) return [...pluginIds]\n const asked = new Set(\n Array.isArray(optedIn) ? canonicalPluginIds(optedIn) : [],\n )\n return pluginIds.filter(\n (id) => !DEFAULT_OFF_PER_SITE_PLUGIN_IDS.has(id) || asked.has(id),\n )\n}\n\n/**\n * Whether an `enabledPlugins` id is a first-party BUNDLE (vs a marketplace\n * listing id) — AGL-777. `enabledPlugins` is a flat mix of the two: bundle\n * ids are the short, stable names in {@link FIRST_PARTY_PLUGINS}; marketplace\n * installs ride the same field under their Firestore listing doc id. This is\n * the single classifier both writers and readers use so the two kinds never\n * get confused — e.g. an install sync must never add/remove a bundle id.\n */\nexport function isFirstPartyPlugin(pluginId: string): boolean {\n return FIRST_PARTY_IDS.has(pluginId)\n}\n\n/**\n * Splits a mixed `enabledPlugins` array into first-party bundle ids and\n * marketplace listing ids (AGL-777). The field stays a single flat list;\n * this just names the two kinds for callers that need to treat them apart.\n */\nexport function classifyEnabledPlugins(pluginIds: readonly string[]): {\n bundles: string[]\n listings: string[]\n} {\n const bundles: string[] = []\n const listings: string[] = []\n for (const id of pluginIds) {\n if (isFirstPartyPlugin(id)) bundles.push(id)\n else listings.push(id)\n }\n return { bundles, listings }\n}\n\n/**\n * The org's effective enabled-plugin set. Absent field → every first-party\n * plugin (existing orgs keep working untouched); always-on ids — and the ids\n * that are on for every workspace and switchable only per site — are unioned\n * in, so no stored list can switch them off for a workspace, including one\n * saved before the plugin existed. Unknown ids are kept — realm-trusted\n * marketplace plugins (AGL-420) ride the same field.\n */\nexport function resolveEnabledPlugins(\n org?: { enabledPlugins?: string[] } | null,\n): string[] {\n const configured = org?.enabledPlugins\n const base = Array.isArray(configured)\n ? canonicalPluginIds(configured)\n : [...DEFAULT_ENABLED_PLUGINS]\n return Array.from(new Set([...ALWAYS_ON_FOR_WORKSPACE, ...base]))\n}\n\n/**\n * Subtracts a host's per-site deny-list from an enabled set (AGL-1014).\n * Always-on ids survive — the base component library cannot be switched\n * off per site any more than per org. A plugin that is on for every\n * workspace (`alwaysOnForWorkspace`) does NOT survive: its site switch is\n * the one switch it has. Order of the surviving ids is kept.\n */\nexport function subtractDisabledPlugins(\n pluginIds: readonly string[],\n disabledPlugins?: readonly string[] | null,\n): string[] {\n if (!Array.isArray(disabledPlugins) || !disabledPlugins.length)\n return [...pluginIds]\n const disabled = new Set(canonicalPluginIds(disabledPlugins))\n return pluginIds.filter(\n (id) => ALWAYS_ON.includes(id) || !disabled.has(id),\n )\n}\n\n/**\n * A HOST's effective enabled-plugin set (AGL-1014): the org's resolved set\n * minus the host's `disabledPlugins` deny-list. This is the single source\n * of truth for per-site enablement — console navigation, the editor,\n * published sites, and API dispatch must all read it, or a \"disabled\"\n * plugin is merely hidden, not off.\n *\n * Semantics are narrow-only by construction: a host stores what it turns\n * OFF, so it can never widen beyond what the org enables, and an absent\n * field means every org-enabled plugin runs (newly installed plugins\n * default to enabled per site until a host admin disables them).\n *\n * ONE class of id reads the other way (AGL-2486): a `defaultOffPerSite`\n * plugin is subtracted unless the host names it in `enabledPlugins`. That\n * list un-defaults; it does not grant. Both fields are still bounded by the\n * org's set, and an explicit deny still beats an explicit opt-in — the two\n * are applied in that order below, so the safe reading wins whenever a\n * hand-edited or stale doc sets both.\n */\nexport function resolveHostEnabledPlugins(\n org?: { enabledPlugins?: string[] } | null,\n host?: { disabledPlugins?: string[]; enabledPlugins?: string[] } | null,\n): string[] {\n return subtractDisabledPlugins(\n applyDefaultOffOptIn(resolveEnabledPlugins(org), host?.enabledPlugins),\n host?.disabledPlugins,\n )\n}\n\n/**\n * Whether ONE plugin runs on this site — the host-aware counterpart of\n * {@link isPluginEnabled}, and the form every route gate wants.\n */\nexport function isHostPluginEnabled(\n org: { enabledPlugins?: string[] } | null | undefined,\n host: { disabledPlugins?: string[]; enabledPlugins?: string[] } | null | undefined,\n pluginId: string,\n): boolean {\n return resolveHostEnabledPlugins(org, host).includes(pluginId)\n}\n\nexport function isPluginEnabled(\n org: { enabledPlugins?: string[] } | null | undefined,\n pluginId: string,\n): boolean {\n return resolveEnabledPlugins(org).includes(pluginId)\n}\n\n/**\n * Where one plugin stands ON ONE SITE (AGL-1014, AGL-2486).\n *\n * Three surfaces need this answer and they must not each derive it: the site\n * plugin page's \"Where it runs\" card, that page's dependency list — where the\n * same question is asked about a NEIGHBOR, and where the answer is the whole\n * point, because a dependency the workspace enables and this site has switched\n * off is a broken plugin behind a healthy-looking workspace page — and the\n * per-site switchboard. A second derivation is a second chance to disagree\n * with `resolveHostEnabledPlugins`, which is what actually runs.\n *\n * - `always-on` — the base library; it cannot be switched off anywhere.\n * A plugin that is on for every workspace but\n * switchable per site is NOT this: it reads\n * `runs-here` or `off-for-site` like any other.\n * - `off-for-workspace` — the org has it off, so it runs on none of its sites\n * and this site cannot turn it on.\n * - `runs-here` — the effective host set contains it.\n * - `awaiting-opt-in` — a `defaultOffPerSite` plugin the org enables and\n * this site has not asked for. Off, but off by\n * DEFAULT rather than by a decision, which is a\n * different thing to tell an operator.\n * - `off-for-site` — the org enables it and this site switched it off.\n */\nexport type PluginSiteState =\n | 'always-on'\n | 'off-for-workspace'\n | 'runs-here'\n | 'awaiting-opt-in'\n | 'off-for-site'\n\nexport function resolvePluginSiteState(\n org: { enabledPlugins?: string[] } | null | undefined,\n host: { disabledPlugins?: string[]; enabledPlugins?: string[] } | null | undefined,\n pluginId: string,\n): PluginSiteState {\n if (ALWAYS_ON.includes(pluginId)) return 'always-on'\n if (!resolveEnabledPlugins(org).includes(pluginId)) return 'off-for-workspace'\n if (resolveHostEnabledPlugins(org, host).includes(pluginId)) return 'runs-here'\n // A default-off plugin this site has not named is off because nobody asked\n // for it; naming it in the deny-list is a decision, and an explicit deny\n // beats an opt-in, so the deny-list is checked first.\n const denied = Array.isArray(host?.disabledPlugins)\n ? host.disabledPlugins.map(String).includes(pluginId)\n : false\n return !denied && isDefaultOffPerSite(pluginId)\n ? 'awaiting-opt-in'\n : 'off-for-site'\n}\n\n/**\n * The declared dependency graph, both directions, built once (AGL-2486).\n *\n * `requires` on the catalog is the forward edge — what a plugin cannot run\n * without — and every consumer so far wanted the reverse one. Building both\n * here keeps the cascade, the console's dependency card and the server-side\n * refusal reading the SAME edges: a graph assembled a second time is a graph\n * that can disagree with itself about which plugins are coupled, and the\n * disagreement would show up as a warning shown on one surface and not on the\n * one that writes.\n *\n * `extraRequirements` extends the catalog and cannot shrink it, exactly as it\n * does for {@link resolveDisableCascade} — a third-party id can add edges of\n * its own but can never declare away a first-party one.\n */\nfunction dependencyEdges(\n extraRequirements?: Readonly<Record<string, readonly string[]>>,\n): {\n /** Keyed by a required id; holds the ids that depend on it. */\n dependents: Map<string, string[]>\n /** Keyed by a dependent id; holds the ids it requires. */\n requires: Map<string, string[]>\n} {\n const dependents = new Map<string, string[]>()\n const requires = new Map<string, string[]>()\n const addEdge = (dependent: string, required: string) => {\n const reverse = dependents.get(required)\n if (reverse) reverse.push(dependent)\n else dependents.set(required, [dependent])\n const forward = requires.get(dependent)\n if (forward) forward.push(required)\n else requires.set(dependent, [required])\n }\n for (const plugin of FIRST_PARTY_PLUGINS)\n for (const required of plugin.requires ?? []) addEdge(plugin.id, required)\n for (const [dependent, required] of Object.entries(extraRequirements ?? {}))\n for (const one of required ?? []) addEdge(dependent, String(one))\n return { dependents, requires }\n}\n\n/** What `pluginId` declares it cannot run without — DIRECT edges only. */\nexport function pluginRequirements(\n pluginId: string,\n extraRequirements?: Readonly<Record<string, readonly string[]>>,\n): string[] {\n return [...(dependencyEdges(extraRequirements).requires.get(pluginId) ?? [])]\n}\n\n/** What declares it cannot run without `pluginId` — DIRECT edges only. */\nexport function pluginDependents(\n pluginId: string,\n extraRequirements?: Readonly<Record<string, readonly string[]>>,\n): string[] {\n return [...(dependencyEdges(extraRequirements).dependents.get(pluginId) ?? [])]\n}\n\n/**\n * Which of `enabledIds` would be left running with a requirement switched off\n * (AGL-2486) — the same question the cascade dialog asks, phrased so a WRITER\n * can refuse instead of a reader warning.\n *\n * The dialog is a courtesy: it stands in front of the console's own switches\n * and nothing else. A direct API call, a stale tab that posts an older set, or\n * a second console surface that forgot to ask can still store a set where\n * User Accounts is on and the Commerce bundle that answers its sign-in POST is\n * not. This is what a write path calls to reject that set outright, so the\n * boundary does not live in a dialog anyone can skip.\n *\n * Only DECLARED requirements are checked, with the same limit\n * {@link PLUGIN_CASCADE_IS_DECLARED_ONLY} states: an undeclared coupling is\n * invisible here too. A requirement naming an id outside the catalog is\n * ignored rather than treated as unmet — a stale edge must not lock a\n * workspace out of its own switchboard.\n */\nexport function strandedDependents(\n enabledIds: readonly string[],\n extraRequirements?: Readonly<Record<string, readonly string[]>>,\n): Array<{ pluginId: string; missing: string[] }> {\n const enabled = new Set(enabledIds.map(String))\n const known = new Set<string>([\n ...FIRST_PARTY_PLUGINS.map((plugin) => plugin.id),\n ...enabled,\n ])\n const { requires } = dependencyEdges(extraRequirements)\n const stranded: Array<{ pluginId: string; missing: string[] }> = []\n for (const pluginId of enabled) {\n const missing = (requires.get(pluginId) ?? []).filter(\n (required) => known.has(required) && !enabled.has(required),\n )\n if (missing.length) stranded.push({ pluginId, missing })\n }\n return stranded\n}\n\n/**\n * Everything that must ALSO be switched off when `pluginId` is (AGL-2486) —\n * the transitive closure over reverse `requires` edges, restricted to what is\n * currently on.\n *\n * Pure and surface-agnostic: the org switchboard and the per-site card both\n * call it with their own \"currently enabled\" set, so the same graph answers\n * both, and the org's wider blast radius comes from the set it passes, not\n * from a second implementation.\n *\n * `extraRequirements` is how a marketplace listing joins the graph. Third-party\n * ids ride the same `enabledPlugins` field, so a listing whose manifest\n * declares `requires` is cascaded exactly like a bundle. It EXTENDS the\n * catalog graph and cannot shrink it — a listing cannot declare away a\n * first-party edge.\n *\n * ⚠️ The result is only ever as complete as what has been DECLARED. A\n * marketplace plugin that uses first-party components without saying so in its\n * manifest will not appear here, and callers must not present the list as\n * exhaustive. See `PLUGIN_CASCADE_IS_DECLARED_ONLY`.\n */\nexport function resolveDisableCascade(\n pluginId: string,\n enabledIds: readonly string[],\n extraRequirements?: Readonly<Record<string, readonly string[]>>,\n): string[] {\n const enabled = new Set(enabledIds.map(String))\n const dependents = dependencyEdges(extraRequirements).dependents\n\n // Breadth-first over reverse edges. `seen` is seeded with the origin so a\n // cycle back to it terminates and the plugin being disabled is never listed\n // among its own dependents.\n const seen = new Set<string>([pluginId])\n const cascade: string[] = []\n const queue: string[] = [pluginId]\n while (queue.length) {\n const current = queue.shift() as string\n for (const dependent of dependents.get(current) ?? []) {\n if (seen.has(dependent)) continue\n seen.add(dependent)\n // Walk THROUGH an already-off dependent — something may depend on it in\n // turn — but do not claim it is being turned off.\n if (enabled.has(dependent)) cascade.push(dependent)\n queue.push(dependent)\n }\n }\n return cascade\n}\n\n/**\n * Why the cascade list must never be presented as exhaustive (AGL-2486).\n *\n * Exported as copy rather than left to each caller to paraphrase: the whole\n * value of the warning rests on it being honest about its own limits, and two\n * surfaces wording that differently is how one of them ends up overclaiming.\n *\n * It says built-in ONLY, and that is the current truth rather than a hedge.\n * `PluginManifest` carries no `requires` field, so a publisher cannot declare\n * a dependency even if they want to, and `extraRequirements` above — the seam\n * that would carry them — is supplied by nothing outside tests. An earlier\n * draft said \"a plugin that uses this one WITHOUT declaring it cannot be\n * detected\", which quietly implied declaring was possible and would have made\n * this the exact overclaiming warning it exists to avoid. Adding the manifest\n * field is a separate change: it needs a publish form, validation and a line\n * on the install screen, or it is a field written by nobody and read by\n * nothing. Update this sentence in the same change, not before.\n */\nexport const PLUGIN_CASCADE_IS_DECLARED_ONLY =\n 'This covers built-in plugins only. A marketplace plugin has no way to ' +\n 'declare that it depends on another one yet, so none are listed here — ' +\n 'check any third-party plugins you rely on before continuing.'\n\n/** Reverse lookup: which first-party plugin a release flag gates, if any. */\nexport function pluginForReleaseFlag(\n flagKey: string,\n): FirstPartyPlugin | undefined {\n return FIRST_PARTY_PLUGINS.find((plugin) => plugin.releaseFlag === flagKey)\n}\n\n/**\n * Subtracts release-flagged-off plugins from an effective set (AGL-422).\n * Pure — the caller supplies the verdict source (client: the activated\n * Remote Config hook state; server: the cached admin-SDK template read),\n * so the same policy runs identically on every surface:\n *\n * - unknown ids (marketplace/realm installs) and always-on ids pass;\n * - a first-party id with a `releaseFlag` passes only when the flag is on\n * for the subject, or `staffBypass` is set (staff preview keeps working\n * while a feature is dark).\n */\nexport function filterPluginsByReleaseFlags(\n pluginIds: readonly string[],\n isFlagOn: (flagKey: string) => boolean,\n options?: { staffBypass?: boolean },\n): string[] {\n if (options?.staffBypass) return [...pluginIds]\n const catalog = new Map(\n FIRST_PARTY_PLUGINS.map((plugin) => [plugin.id, plugin]),\n )\n return pluginIds.filter((pluginId) => {\n const plugin = catalog.get(pluginId)\n if (!plugin?.releaseFlag || plugin.alwaysOn) return true\n return isFlagOn(plugin.releaseFlag)\n })\n}\n"],"names":["FIRST_PARTY_PLUGINS","PLUGIN_EDIT_BAR_LINKS","PUBLISHED_SITE_IMPACT","ACCOUNTS_PLUGIN_ID","FORMS_PLUGIN_ID","pluginEditBarLinks","enabledPluginIds","enabled","Set","filter","link","has","pluginId","DEFAULT_ENABLED_PLUGINS","map","plugin","id","canonicalPluginId","canonicalPluginIds","pluginIds","Array","from","String","ALWAYS_ON","alwaysOn","ALWAYS_ON_FOR_WORKSPACE","alwaysOnForWorkspace","isLockedOnForWorkspace","includes","isLockedOnForSite","FIRST_PARTY_IDS","DEFAULT_OFF_PER_SITE_PLUGIN_IDS","defaultOffPerSite","isDefaultOffPerSite","applyDefaultOffOptIn","optedIn","size","asked","isArray","isFirstPartyPlugin","classifyEnabledPlugins","bundles","listings","push","resolveEnabledPlugins","org","configured","enabledPlugins","base","subtractDisabledPlugins","disabledPlugins","length","disabled","resolveHostEnabledPlugins","host","isHostPluginEnabled","isPluginEnabled","resolvePluginSiteState","denied","dependencyEdges","extraRequirements","dependents","Map","requires","addEdge","dependent","required","reverse","get","set","forward","Object","entries","one","pluginRequirements","pluginDependents","strandedDependents","enabledIds","known","stranded","missing","resolveDisableCascade","seen","cascade","queue","current","shift","add","PLUGIN_CASCADE_IS_DECLARED_ONLY","pluginForReleaseFlag","flagKey","find","releaseFlag","filterPluginsByReleaseFlags","isFlagOn","options","staffBypass","catalog"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;CAWC,GAED,SACEA,mBAAmB,EACnBC,qBAAqB,EACrBC,qBAAqB,QAChB,qCAAiC;AAiKxC;;;;;CAKC,GACD,SAASA,qBAAqB,GAAE;AAEhC;;;;;;;;;;;CAWC,GACD,OAAO,MAAMC,qBAAqB,WAAU;AAE5C;;;;;;;CAOC,GACD,OAAO,MAAMC,kBAAkB,QAAO;AAEtC;;;;;;;;;CASC,GACD,SAASJ,mBAAmB,GAAE;AAE9B;;;;;;;;;;;;;CAaC,GACD,OAAO,SAASK,mBACdC,gBAA+C;IAE/C,MAAMC,UAAU,IAAIC,IAAIF,2BAAAA,mBAAoB,EAAE;IAC9C,OAAOL,sBAAsBQ,MAAM,CAAC,CAACC,OAASH,QAAQI,GAAG,CAACD,KAAKE,QAAQ;AACzE;AAEA,iEAAiE,GACjE,OAAO,MAAMC,0BACXb,oBAAoBc,GAAG,CAAC,CAACC,SAAWA,OAAOC,EAAE,EAAC;AAEhD;;;;;;;;;;;;;;;;;;CAkBC,GACD,OAAO,SAASC,kBAAkBL,QAAgB;IAChD,OAAOA;AACT;AAEA,kEAAkE,GAClE,SAASM,mBAAmBC,SAA6B;IACvD,OAAOC,MAAMC,IAAI,CACf,IAAIb,IAAIW,UAAUL,GAAG,CAAC,CAACE,KAAOC,kBAAkBK,OAAON;AAE3D;AAEA,4EAA4E,GAC5E,MAAMO,YAA+BvB,oBAAoBS,MAAM,CAC7D,CAACM,SAAWA,OAAOS,QAAQ,EAC3BV,GAAG,CAAC,CAACC,SAAWA,OAAOC,EAAE;AAE3B;;;CAGC,GACD,MAAMS,0BAA6CzB,oBAAoBS,MAAM,CAC3E,CAACM,SAAWA,OAAOS,QAAQ,IAAIT,OAAOW,oBAAoB,EAC1DZ,GAAG,CAAC,CAACC,SAAWA,OAAOC,EAAE;AAE3B;;;;CAIC,GACD,OAAO,SAASW,uBAAuBf,QAAgB;IACrD,OAAOa,wBAAwBG,QAAQ,CAAChB;AAC1C;AAEA;;;CAGC,GACD,OAAO,SAASiB,kBAAkBjB,QAAgB;IAChD,OAAOW,UAAUK,QAAQ,CAAChB;AAC5B;AAEA,MAAMkB,kBAAuC,IAAItB,IAC/CR,oBAAoBc,GAAG,CAAC,CAACC,SAAWA,OAAOC,EAAE;AAG/C;;;CAGC,GACD,OAAO,MAAMe,kCAAuD,IAAIvB,IACtER,oBAAoBS,MAAM,CAAC,CAACM,SAAWA,OAAOiB,iBAAiB,EAAElB,GAAG,CAClE,CAACC,SAAWA,OAAOC,EAAE,GAExB;AAED,mEAAmE,GACnE,OAAO,SAASiB,oBAAoBrB,QAAgB;IAClD,OAAOmB,gCAAgCpB,GAAG,CAACC;AAC7C;AAEA;;;;;;;;CAQC,GACD,OAAO,SAASsB,qBACdf,SAA4B,EAC5BgB,OAAkC;IAElC,IAAI,CAACJ,gCAAgCK,IAAI,EAAE,OAAO;WAAIjB;KAAU;IAChE,MAAMkB,QAAQ,IAAI7B,IAChBY,MAAMkB,OAAO,CAACH,WAAWjB,mBAAmBiB,WAAW,EAAE;IAE3D,OAAOhB,UAAUV,MAAM,CACrB,CAACO,KAAO,CAACe,gCAAgCpB,GAAG,CAACK,OAAOqB,MAAM1B,GAAG,CAACK;AAElE;AAEA;;;;;;;CAOC,GACD,OAAO,SAASuB,mBAAmB3B,QAAgB;IACjD,OAAOkB,gBAAgBnB,GAAG,CAACC;AAC7B;AAEA;;;;CAIC,GACD,OAAO,SAAS4B,uBAAuBrB,SAA4B;IAIjE,MAAMsB,UAAoB,EAAE;IAC5B,MAAMC,WAAqB,EAAE;IAC7B,KAAK,MAAM1B,MAAMG,UAAW;QAC1B,IAAIoB,mBAAmBvB,KAAKyB,QAAQE,IAAI,CAAC3B;aACpC0B,SAASC,IAAI,CAAC3B;IACrB;IACA,OAAO;QAAEyB;QAASC;IAAS;AAC7B;AAEA;;;;;;;CAOC,GACD,OAAO,SAASE,sBACdC,GAA0C;IAE1C,MAAMC,aAAaD,uBAAAA,IAAKE,cAAc;IACtC,MAAMC,OAAO5B,MAAMkB,OAAO,CAACQ,cACvB5B,mBAAmB4B,cACnB;WAAIjC;KAAwB;IAChC,OAAOO,MAAMC,IAAI,CAAC,IAAIb,IAAI;WAAIiB;WAA4BuB;KAAK;AACjE;AAEA;;;;;;CAMC,GACD,OAAO,SAASC,wBACd9B,SAA4B,EAC5B+B,eAA0C;IAE1C,IAAI,CAAC9B,MAAMkB,OAAO,CAACY,oBAAoB,CAACA,gBAAgBC,MAAM,EAC5D,OAAO;WAAIhC;KAAU;IACvB,MAAMiC,WAAW,IAAI5C,IAAIU,mBAAmBgC;IAC5C,OAAO/B,UAAUV,MAAM,CACrB,CAACO,KAAOO,UAAUK,QAAQ,CAACZ,OAAO,CAACoC,SAASzC,GAAG,CAACK;AAEpD;AAEA;;;;;;;;;;;;;;;;;;CAkBC,GACD,OAAO,SAASqC,0BACdR,GAA0C,EAC1CS,IAAuE;IAEvE,OAAOL,wBACLf,qBAAqBU,sBAAsBC,MAAMS,wBAAAA,KAAMP,cAAc,GACrEO,wBAAAA,KAAMJ,eAAe;AAEzB;AAEA;;;CAGC,GACD,OAAO,SAASK,oBACdV,GAAqD,EACrDS,IAAkF,EAClF1C,QAAgB;IAEhB,OAAOyC,0BAA0BR,KAAKS,MAAM1B,QAAQ,CAAChB;AACvD;AAEA,OAAO,SAAS4C,gBACdX,GAAqD,EACrDjC,QAAgB;IAEhB,OAAOgC,sBAAsBC,KAAKjB,QAAQ,CAAChB;AAC7C;AAiCA,OAAO,SAAS6C,uBACdZ,GAAqD,EACrDS,IAAkF,EAClF1C,QAAgB;IAEhB,IAAIW,UAAUK,QAAQ,CAAChB,WAAW,OAAO;IACzC,IAAI,CAACgC,sBAAsBC,KAAKjB,QAAQ,CAAChB,WAAW,OAAO;IAC3D,IAAIyC,0BAA0BR,KAAKS,MAAM1B,QAAQ,CAAChB,WAAW,OAAO;IACpE,2EAA2E;IAC3E,yEAAyE;IACzE,sDAAsD;IACtD,MAAM8C,SAAStC,MAAMkB,OAAO,CAACgB,wBAAAA,KAAMJ,eAAe,IAC9CI,KAAKJ,eAAe,CAACpC,GAAG,CAACQ,QAAQM,QAAQ,CAAChB,YAC1C;IACJ,OAAO,CAAC8C,UAAUzB,oBAAoBrB,YAClC,oBACA;AACN;AAEA;;;;;;;;;;;;;;CAcC,GACD,SAAS+C,gBACPC,iBAA+D;QAkBtC7C;IAXzB,MAAM8C,aAAa,IAAIC;IACvB,MAAMC,WAAW,IAAID;IACrB,MAAME,UAAU,CAACC,WAAmBC;QAClC,MAAMC,UAAUN,WAAWO,GAAG,CAACF;QAC/B,IAAIC,SAASA,QAAQxB,IAAI,CAACsB;aACrBJ,WAAWQ,GAAG,CAACH,UAAU;YAACD;SAAU;QACzC,MAAMK,UAAUP,SAASK,GAAG,CAACH;QAC7B,IAAIK,SAASA,QAAQ3B,IAAI,CAACuB;aACrBH,SAASM,GAAG,CAACJ,WAAW;YAACC;SAAS;IACzC;IACA,KAAK,MAAMnD,UAAUf,oBACnB,KAAK,MAAMkE,aAAYnD,mBAAAA,OAAOgD,QAAQ,YAAfhD,mBAAmB,EAAE,CAAEiD,QAAQjD,OAAOC,EAAE,EAAEkD;IACnE,KAAK,MAAM,CAACD,WAAWC,SAAS,IAAIK,OAAOC,OAAO,CAACZ,4BAAAA,oBAAqB,CAAC,GACvE,KAAK,MAAMa,OAAOP,mBAAAA,WAAY,EAAE,CAAEF,QAAQC,WAAW3C,OAAOmD;IAC9D,OAAO;QAAEZ;QAAYE;IAAS;AAChC;AAEA,wEAAwE,GACxE,OAAO,SAASW,mBACd9D,QAAgB,EAChBgD,iBAA+D;QAEnDD;IAAZ,OAAO;YAAKA,gCAAAA,gBAAgBC,mBAAmBG,QAAQ,CAACK,GAAG,CAACxD,qBAAhD+C,gCAA6D,EAAE;KAAE;AAC/E;AAEA,wEAAwE,GACxE,OAAO,SAASgB,iBACd/D,QAAgB,EAChBgD,iBAA+D;QAEnDD;IAAZ,OAAO;YAAKA,kCAAAA,gBAAgBC,mBAAmBC,UAAU,CAACO,GAAG,CAACxD,qBAAlD+C,kCAA+D,EAAE;KAAE;AACjF;AAEA;;;;;;;;;;;;;;;;;CAiBC,GACD,OAAO,SAASiB,mBACdC,UAA6B,EAC7BjB,iBAA+D;IAE/D,MAAMrD,UAAU,IAAIC,IAAIqE,WAAW/D,GAAG,CAACQ;IACvC,MAAMwD,QAAQ,IAAItE,IAAY;WACzBR,oBAAoBc,GAAG,CAAC,CAACC,SAAWA,OAAOC,EAAE;WAC7CT;KACJ;IACD,MAAM,EAAEwD,QAAQ,EAAE,GAAGJ,gBAAgBC;IACrC,MAAMmB,WAA2D,EAAE;IACnE,KAAK,MAAMnE,YAAYL,QAAS;YACbwD;QAAjB,MAAMiB,UAAU,EAACjB,gBAAAA,SAASK,GAAG,CAACxD,qBAAbmD,gBAA0B,EAAE,EAAEtD,MAAM,CACnD,CAACyD,WAAaY,MAAMnE,GAAG,CAACuD,aAAa,CAAC3D,QAAQI,GAAG,CAACuD;QAEpD,IAAIc,QAAQ7B,MAAM,EAAE4B,SAASpC,IAAI,CAAC;YAAE/B;YAAUoE;QAAQ;IACxD;IACA,OAAOD;AACT;AAEA;;;;;;;;;;;;;;;;;;;;CAoBC,GACD,OAAO,SAASE,sBACdrE,QAAgB,EAChBiE,UAA6B,EAC7BjB,iBAA+D;IAE/D,MAAMrD,UAAU,IAAIC,IAAIqE,WAAW/D,GAAG,CAACQ;IACvC,MAAMuC,aAAaF,gBAAgBC,mBAAmBC,UAAU;IAEhE,0EAA0E;IAC1E,4EAA4E;IAC5E,4BAA4B;IAC5B,MAAMqB,OAAO,IAAI1E,IAAY;QAACI;KAAS;IACvC,MAAMuE,UAAoB,EAAE;IAC5B,MAAMC,QAAkB;QAACxE;KAAS;IAClC,MAAOwE,MAAMjC,MAAM,CAAE;YAEKU;QADxB,MAAMwB,UAAUD,MAAME,KAAK;QAC3B,KAAK,MAAMrB,cAAaJ,kBAAAA,WAAWO,GAAG,CAACiB,oBAAfxB,kBAA2B,EAAE,CAAE;YACrD,IAAIqB,KAAKvE,GAAG,CAACsD,YAAY;YACzBiB,KAAKK,GAAG,CAACtB;YACT,wEAAwE;YACxE,kDAAkD;YAClD,IAAI1D,QAAQI,GAAG,CAACsD,YAAYkB,QAAQxC,IAAI,CAACsB;YACzCmB,MAAMzC,IAAI,CAACsB;QACb;IACF;IACA,OAAOkB;AACT;AAEA;;;;;;;;;;;;;;;;;CAiBC,GACD,OAAO,MAAMK,kCACX,2EACA,2EACA,+DAA8D;AAEhE,2EAA2E,GAC3E,OAAO,SAASC,qBACdC,OAAe;IAEf,OAAO1F,oBAAoB2F,IAAI,CAAC,CAAC5E,SAAWA,OAAO6E,WAAW,KAAKF;AACrE;AAEA;;;;;;;;;;CAUC,GACD,OAAO,SAASG,4BACd1E,SAA4B,EAC5B2E,QAAsC,EACtCC,OAAmC;IAEnC,IAAIA,2BAAAA,QAASC,WAAW,EAAE,OAAO;WAAI7E;KAAU;IAC/C,MAAM8E,UAAU,IAAInC,IAClB9D,oBAAoBc,GAAG,CAAC,CAACC,SAAW;YAACA,OAAOC,EAAE;YAAED;SAAO;IAEzD,OAAOI,UAAUV,MAAM,CAAC,CAACG;QACvB,MAAMG,SAASkF,QAAQ7B,GAAG,CAACxD;QAC3B,IAAI,EAACG,0BAAAA,OAAQ6E,WAAW,KAAI7E,OAAOS,QAAQ,EAAE,OAAO;QACpD,OAAOsE,SAAS/E,OAAO6E,WAAW;IACpC;AACF"}
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/enabled-plugins.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Per-org plugin enablement (AGL-416): `org.enabledPlugins` is the\n * switchboard that decides which plugins LOAD for a workspace — the loader\n * (AGL-417) dynamically imports only these. It composes with (not replaces)\n * the existing gates: a surface renders when its plugin is enabled AND its\n * `featureFlag` entitlement resolves; marketplace/marketplace listings keep\n * their per-host/org `installs` docs on top.\n *\n * The catalog's rows are declared by the plugins themselves, in\n * `plugins.config.json`, beside the package names the loader manifests are\n * generated from; this module holds the row's shape and the resolvers.\n */\n\nimport {\n FIRST_PARTY_PLUGINS,\n PLUGIN_EDIT_BAR_LINKS,\n PUBLISHED_SITE_IMPACT,\n} from './first-party-plugins.generated'\n\n/**\n * The first-party elements that run a site function (AGL-3393), compiled from\n * each plugin's `contributes.site.functionBindings`. Compose merges it with the\n * installed marketplace plugins' declarations via `mergeFunctionBindings`.\n */\nexport { FIRST_PARTY_FUNCTION_BINDINGS } from './first-party-plugins.generated'\n\n/**\n * One quick link on a live site's admin edit bar, declared by the plugin whose\n * console page it opens (AGL-3080).\n *\n * The bar is drawn by the TENANT server, which never loads a plugin's console\n * code, so this is DATA compiled from `plugins.config.json` rather than a\n * runtime registration — a registry that process had not filled would drop the\n * link silently instead of failing, which is the AGL-3025 shape. It is also\n * why the label is written here rather than taken from the plugin's nav item:\n * the nav item lives in the console bundle, and \"Orders\" is a section of\n * Commerce rather than the plugin's own name.\n *\n * The link is drawn only where the SITE runs the plugin. The bar asks the same\n * resolved plugin set the rest of the edit context is built from, so a site\n * with Commerce switched off is not offered its Orders page.\n */\nexport interface PluginEditBarLink {\n /** The plugin that owns the page — also the id the site's set is checked against. */\n pluginId: string\n /** Where it sits in the bar; every declared order is distinct. */\n order: number\n /** What the bar says. A SECTION's name where that is the honest one. */\n label: string\n /**\n * Console path beneath the site, beginning with `/` and carrying no query —\n * `/inbox`, `/products/orders`. Joined onto `/{orgSlug}/hosts/{host}`.\n */\n path: string\n}\n\nexport interface FirstPartyPlugin {\n /**\n * Stable plugin id — persisted in `org.enabledPlugins`,\n * `host.disabledPlugins` and `host.enabledPlugins`, and the document id\n * under every `pluginSettings` collection.\n *\n * Renaming one is therefore a data change, not a code change, and it is\n * done in two halves that must both ship: the old id is read through\n * {@link canonicalPluginId} so every stored list still resolves, and a\n * backfill rewrites the stored documents so the alias can be retired once\n * it reads nothing.\n */\n id: string\n /** Console-facing display name. */\n label: string\n /**\n * On for every workspace AND every site, with no switch anywhere: the base\n * component library the canvas cannot render without.\n */\n alwaysOn?: boolean\n /**\n * On for every WORKSPACE, and switchable for one SITE (AGL-3028, AGL-3029).\n *\n * The workspace half is what `alwaysOn` gives: the id is unioned into the\n * org's resolved set whatever `org.enabledPlugins` stores, so a list saved\n * before the plugin existed still runs it, and no workspace switch is\n * offered. The site half is ordinary: the id is subtracted by a site's\n * `disabledPlugins` deny-list like any other, so an absent host field means\n * ON and nothing needs migrating.\n *\n * It exists for capabilities whose workspace-level half must never stop.\n * AI carries the add-on, credits, allotments and overage billing, none of\n * which belongs to a site; Forms carries the catalog and the submissions\n * already stored. A workspace switch would take those down with the\n * site-facing half, so the only switch is the site's.\n */\n alwaysOnForWorkspace?: boolean\n /**\n * What switching this plugin off for ONE site stops, and what it leaves\n * running (AGL-3028, AGL-3029) — the copy the site's Admin › Plugins page\n * states beside the switch, so the decision is made knowing both halves.\n *\n * `confirm` makes the site switch ask before it applies, naming what it is\n * about to break on published pages.\n */\n siteOff?: {\n /** What stops on the site, stated as the consequence. */\n stops: string\n /** What keeps running, because it carries no site. */\n keeps: string\n /** Ask before applying. */\n confirm?: boolean\n /**\n * How the confirmation names the site's published pages the switch\n * reaches (AGL-3029): the sentence above the list, and the one that\n * stands in for an empty list when the scan read everything.\n */\n pages?: { heading: string; none: string }\n }\n /** One-line description for the org-settings toggle list. */\n description?: string\n /**\n * Release flag gating this plugin platform-wide (AGL-422). A flagged-off\n * plugin is subtracted from every workspace's effective set — console\n * loader, published sites, and API dispatch — unless the subject is\n * staff. Always-on plugins carry no flag.\n */\n releaseFlag?: string\n /**\n * OFF for a site until that site turns it on (AGL-2486) — the inversion of\n * this switchboard's default, and the only field here that changes what an\n * absent host doc means.\n *\n * The per-host field is a DENY-list: a site records what it switches off,\n * which makes \"absent means on\" the default for all twelve other bundles.\n * That is right for a capability whose worst case is an unused nav tab, and\n * wrong for one whose worst case is a PAGE — `/signin`, `/signup`,\n * `/recover` were served by every published site on the platform,\n * including marketing sites whose real sign-in is somewhere else entirely.\n * A sign-in-shaped page on a brand's own domain that is not that brand's\n * sign-in is a credential-confusion hazard, so this one defaults closed.\n *\n * A site un-defaults it by listing the id in `host.enabledPlugins`. That\n * list is scoped to default-off ids and cannot widen anything else, so the\n * AGL-1014 invariant survives intact: a site still can never reach past\n * what its org enables.\n */\n defaultOffPerSite?: boolean\n /**\n * Plugin ids this one cannot function without (AGL-2486) — DECLARED, never\n * inferred.\n *\n * The switchboard has always let a workspace turn off a plugin another one\n * is built on, and said nothing. `accounts` is the case that proves it: the\n * Members blocks and every `membership/*` API handler ship inside the\n * COMMERCE bundle, so switching Commerce off leaves a site advertising\n * `/signin` while nothing can answer the login POST.\n *\n * Declared rather than derived on purpose. The couplings that matter are\n * exactly the ones no static read can see — which bundle happens to\n * register whose components — so a graph inferred from imports or from the\n * registry would miss the real edges while looking authoritative. An\n * incomplete warning that presents itself as complete is worse than none,\n * so this list is the contract, and {@link resolveDisableCascade} is honest\n * about covering only what is on it.\n */\n requires?: readonly string[]\n}\n\n/**\n * What a DISABLE does to a site that is already published (AGL-2486).\n *\n * The cascade dialog has to state a consequence, and there are two very\n * different ones. \"These blocks will no longer be offered\" is a different\n * decision from \"parts of your live pages go blank\", and one generic sentence\n * for both would be a lie in one direction or the other.\n *\n * - `elements` — the plugin registers site components. The tenant loads only\n * the site's enabled bundles, so elements ALREADY PLACED on published pages\n * stop rendering. Pre-existing AGL-1014 behaviour.\n * - `routes` — the plugin registers no components, but a published site\n * stops serving something: `accounts` gates `/signin`, `/signup`,\n * `/recover`; `redirects` stops applying its rules; `workflows` stops\n * answering its hooks.\n * - `console-only` — nothing a visitor can reach changes; the plugin leaves\n * navigation and the editor.\n */\nexport type PublishedSiteImpact = 'elements' | 'routes' | 'console-only'\n\n/**\n * Every catalog id classified, from the `publishedSiteImpact` each plugin\n * declares on its own row. The generator refuses a row whose verdict\n * disagrees with its `register.site`, so a site-registering bundle cannot be\n * added without declaring its consequence.\n */\nexport { PUBLISHED_SITE_IMPACT }\n\n/**\n * The user-accounts capability (AGL-2486): visitor sign-in, sign-up and\n * password recovery on a published site.\n *\n * It carries no loader manifest entry, and that is deliberate rather than an\n * oversight. The member blocks and the `membership/*` API handlers already\n * ship inside the commerce bundle, so there is no separate package to load;\n * what this id contributes is the SWITCH — the thing the tenant route gate,\n * the console card and the sitemap all ask. Several catalog ids already have\n * no tenant bundle (`contacts`, `data`, `logic`), so a manifest-less entry is\n * the existing shape, not a new one.\n */\nexport const ACCOUNTS_PLUGIN_ID = 'accounts'\n\n/**\n * The Forms capability's id, named in core (AGL-3029).\n *\n * A form's server half is core — `/api/forms/submit`, the publish-time\n * contract check, the published page's render — and core may not import a\n * plugin, so each asks the site's plugin set about this id rather than asking\n * the Forms plugin anything.\n */\nexport const FORMS_PLUGIN_ID = 'forms'\n\n/**\n * The switchboard catalog. The core holds no row of it: each plugin declares\n * its own in `plugins.config.json` (`catalog`, and one per `capabilities`\n * entry), and the generator compiles them here in their declared order, so\n * adding a plugin edits no core file.\n *\n * Compiled in rather than registered at runtime, because every resolver below\n * is synchronous and runs in each bundle of both apps; a registry that one of\n * those bundles had not filled would read as every plugin switched OFF.\n */\nexport { FIRST_PARTY_PLUGINS }\n\n/**\n * The admin edit bar's quick links that THIS SITE runs, in the order the bar\n * draws them (AGL-3080).\n *\n * `enabledPluginIds` is the site's resolved plugin set — the same one the rest\n * of the edit context is built from, already narrowed by the org's switchboard,\n * the site's own deny-list and the release flags. A link whose plugin is not\n * in it is not offered, which is what stops the bar linking a site with\n * Commerce switched off to its Orders page.\n *\n * Compiled in rather than registered, for the reason the catalog above is:\n * the tenant server draws this bar and never loads a plugin's console code,\n * so a registry it had not filled would drop every link with nothing red.\n */\nexport function pluginEditBarLinks(\n enabledPluginIds: readonly string[] | undefined,\n): readonly PluginEditBarLink[] {\n const enabled = new Set(enabledPluginIds ?? [])\n return PLUGIN_EDIT_BAR_LINKS.filter((link) => enabled.has(link.pluginId))\n}\n\n/** Ids loaded for orgs that have never touched the switchboard. */\nexport const DEFAULT_ENABLED_PLUGINS: readonly string[] =\n FIRST_PARTY_PLUGINS.map((plugin) => plugin.id)\n\n/**\n * The id a stored plugin id means today — the seam a rename re-arms.\n *\n * A first-party id is written into every org's `enabledPlugins` and every\n * site's `disabledPlugins` the moment somebody touches the switchboard, so a\n * rename in this file alone would silently turn the plugin OFF for every\n * workspace that had listed it under the old name — the stored id would match\n * nothing in the catalog and fall through as a marketplace listing id. Every\n * reader of a stored list runs it through here first, so a document written\n * before a rename keeps meaning what it meant while the backfill runs.\n *\n * Nothing is aliased today. `contacts` read as `crm` from the CRM's rename\n * (AGL-2595) until its backfill reported zero documents carrying the old id,\n * and the alias was retired (AGL-2614) — an alias that outlives its backfill\n * is a second name for the plugin that nothing writes and every reader must\n * keep honoring. The next rename adds its pair here, ships a backfill over\n * `org.enabledPlugins`, `host.disabledPlugins`, `host.enabledPlugins` and the\n * `pluginSettings` document ids, and removes the pair the same way.\n */\nexport function canonicalPluginId(pluginId: string): string {\n return pluginId\n}\n\n/** A stored list, read through the alias seam and de-duplicated. */\nfunction canonicalPluginIds(pluginIds: readonly unknown[]): string[] {\n return Array.from(\n new Set(pluginIds.map((id) => canonicalPluginId(String(id)))),\n )\n}\n\n/** On everywhere, a site's deny-list included: the base component library. */\nconst ALWAYS_ON: readonly string[] = FIRST_PARTY_PLUGINS.filter(\n (plugin) => plugin.alwaysOn,\n).map((plugin) => plugin.id)\n\n/**\n * Unioned into every workspace's set whatever the org stored: {@link ALWAYS_ON}\n * plus the ids that are on for every workspace and switchable per site.\n */\nconst ALWAYS_ON_FOR_WORKSPACE: readonly string[] = FIRST_PARTY_PLUGINS.filter(\n (plugin) => plugin.alwaysOn || plugin.alwaysOnForWorkspace,\n).map((plugin) => plugin.id)\n\n/**\n * Whether the WORKSPACE switch for this plugin is locked on (AGL-3028): the\n * base library, and every plugin that is on for every workspace and\n * switchable only per site. The org switchboard renders these on and inert.\n */\nexport function isLockedOnForWorkspace(pluginId: string): boolean {\n return ALWAYS_ON_FOR_WORKSPACE.includes(pluginId)\n}\n\n/**\n * Whether the SITE switch for this plugin is locked on: the base library\n * alone. Every other plugin a workspace runs can be switched off for one site.\n */\nexport function isLockedOnForSite(pluginId: string): boolean {\n return ALWAYS_ON.includes(pluginId)\n}\n\nconst FIRST_PARTY_IDS: ReadonlySet<string> = new Set(\n FIRST_PARTY_PLUGINS.map((plugin) => plugin.id),\n)\n\n/**\n * Ids a SITE does not get until it asks (AGL-2486). See\n * {@link FirstPartyPlugin.defaultOffPerSite} for why this inversion exists.\n */\nexport const DEFAULT_OFF_PER_SITE_PLUGIN_IDS: ReadonlySet<string> = new Set(\n FIRST_PARTY_PLUGINS.filter((plugin) => plugin.defaultOffPerSite).map(\n (plugin) => plugin.id,\n ),\n)\n\n/** Whether a plugin id is off for a site until that site opts in. */\nexport function isDefaultOffPerSite(pluginId: string): boolean {\n return DEFAULT_OFF_PER_SITE_PLUGIN_IDS.has(pluginId)\n}\n\n/**\n * Applies a host's `enabledPlugins` OPT-IN list (AGL-2486): subtracts every\n * default-off id the host has not explicitly asked for.\n *\n * Narrow-only, like its deny-list sibling. The list can only ever REMOVE the\n * default-off subtraction for an id the org already enables — listing an\n * ordinary id buys nothing, and listing one the org switched off buys\n * nothing either, because this runs against the org's resolved set.\n */\nexport function applyDefaultOffOptIn(\n pluginIds: readonly string[],\n optedIn?: readonly string[] | null,\n): string[] {\n if (!DEFAULT_OFF_PER_SITE_PLUGIN_IDS.size) return [...pluginIds]\n const asked = new Set(\n Array.isArray(optedIn) ? canonicalPluginIds(optedIn) : [],\n )\n return pluginIds.filter(\n (id) => !DEFAULT_OFF_PER_SITE_PLUGIN_IDS.has(id) || asked.has(id),\n )\n}\n\n/**\n * Whether an `enabledPlugins` id is a first-party BUNDLE (vs a marketplace\n * listing id) — AGL-777. `enabledPlugins` is a flat mix of the two: bundle\n * ids are the short, stable names in {@link FIRST_PARTY_PLUGINS}; marketplace\n * installs ride the same field under their Firestore listing doc id. This is\n * the single classifier both writers and readers use so the two kinds never\n * get confused — e.g. an install sync must never add/remove a bundle id.\n */\nexport function isFirstPartyPlugin(pluginId: string): boolean {\n return FIRST_PARTY_IDS.has(pluginId)\n}\n\n/**\n * Splits a mixed `enabledPlugins` array into first-party bundle ids and\n * marketplace listing ids (AGL-777). The field stays a single flat list;\n * this just names the two kinds for callers that need to treat them apart.\n */\nexport function classifyEnabledPlugins(pluginIds: readonly string[]): {\n bundles: string[]\n listings: string[]\n} {\n const bundles: string[] = []\n const listings: string[] = []\n for (const id of pluginIds) {\n if (isFirstPartyPlugin(id)) bundles.push(id)\n else listings.push(id)\n }\n return { bundles, listings }\n}\n\n/**\n * The org's effective enabled-plugin set. Absent field → every first-party\n * plugin (existing orgs keep working untouched); always-on ids — and the ids\n * that are on for every workspace and switchable only per site — are unioned\n * in, so no stored list can switch them off for a workspace, including one\n * saved before the plugin existed. Unknown ids are kept — realm-trusted\n * marketplace plugins (AGL-420) ride the same field.\n */\nexport function resolveEnabledPlugins(\n org?: { enabledPlugins?: string[] } | null,\n): string[] {\n const configured = org?.enabledPlugins\n const base = Array.isArray(configured)\n ? canonicalPluginIds(configured)\n : [...DEFAULT_ENABLED_PLUGINS]\n return Array.from(new Set([...ALWAYS_ON_FOR_WORKSPACE, ...base]))\n}\n\n/**\n * Subtracts a host's per-site deny-list from an enabled set (AGL-1014).\n * Always-on ids survive — the base component library cannot be switched\n * off per site any more than per org. A plugin that is on for every\n * workspace (`alwaysOnForWorkspace`) does NOT survive: its site switch is\n * the one switch it has. Order of the surviving ids is kept.\n */\nexport function subtractDisabledPlugins(\n pluginIds: readonly string[],\n disabledPlugins?: readonly string[] | null,\n): string[] {\n if (!Array.isArray(disabledPlugins) || !disabledPlugins.length)\n return [...pluginIds]\n const disabled = new Set(canonicalPluginIds(disabledPlugins))\n return pluginIds.filter(\n (id) => ALWAYS_ON.includes(id) || !disabled.has(id),\n )\n}\n\n/**\n * A HOST's effective enabled-plugin set (AGL-1014): the org's resolved set\n * minus the host's `disabledPlugins` deny-list. This is the single source\n * of truth for per-site enablement — console navigation, the editor,\n * published sites, and API dispatch must all read it, or a \"disabled\"\n * plugin is merely hidden, not off.\n *\n * Semantics are narrow-only by construction: a host stores what it turns\n * OFF, so it can never widen beyond what the org enables, and an absent\n * field means every org-enabled plugin runs (newly installed plugins\n * default to enabled per site until a host admin disables them).\n *\n * ONE class of id reads the other way (AGL-2486): a `defaultOffPerSite`\n * plugin is subtracted unless the host names it in `enabledPlugins`. That\n * list un-defaults; it does not grant. Both fields are still bounded by the\n * org's set, and an explicit deny still beats an explicit opt-in — the two\n * are applied in that order below, so the safe reading wins whenever a\n * hand-edited or stale doc sets both.\n */\nexport function resolveHostEnabledPlugins(\n org?: { enabledPlugins?: string[] } | null,\n host?: { disabledPlugins?: string[]; enabledPlugins?: string[] } | null,\n): string[] {\n return subtractDisabledPlugins(\n applyDefaultOffOptIn(resolveEnabledPlugins(org), host?.enabledPlugins),\n host?.disabledPlugins,\n )\n}\n\n/**\n * Whether ONE plugin runs on this site — the host-aware counterpart of\n * {@link isPluginEnabled}, and the form every route gate wants.\n */\nexport function isHostPluginEnabled(\n org: { enabledPlugins?: string[] } | null | undefined,\n host: { disabledPlugins?: string[]; enabledPlugins?: string[] } | null | undefined,\n pluginId: string,\n): boolean {\n return resolveHostEnabledPlugins(org, host).includes(pluginId)\n}\n\nexport function isPluginEnabled(\n org: { enabledPlugins?: string[] } | null | undefined,\n pluginId: string,\n): boolean {\n return resolveEnabledPlugins(org).includes(pluginId)\n}\n\n/**\n * Where one plugin stands ON ONE SITE (AGL-1014, AGL-2486).\n *\n * Three surfaces need this answer and they must not each derive it: the site\n * plugin page's \"Where it runs\" card, that page's dependency list — where the\n * same question is asked about a NEIGHBOR, and where the answer is the whole\n * point, because a dependency the workspace enables and this site has switched\n * off is a broken plugin behind a healthy-looking workspace page — and the\n * per-site switchboard. A second derivation is a second chance to disagree\n * with `resolveHostEnabledPlugins`, which is what actually runs.\n *\n * - `always-on` — the base library; it cannot be switched off anywhere.\n * A plugin that is on for every workspace but\n * switchable per site is NOT this: it reads\n * `runs-here` or `off-for-site` like any other.\n * - `off-for-workspace` — the org has it off, so it runs on none of its sites\n * and this site cannot turn it on.\n * - `runs-here` — the effective host set contains it.\n * - `awaiting-opt-in` — a `defaultOffPerSite` plugin the org enables and\n * this site has not asked for. Off, but off by\n * DEFAULT rather than by a decision, which is a\n * different thing to tell an operator.\n * - `off-for-site` — the org enables it and this site switched it off.\n */\nexport type PluginSiteState =\n | 'always-on'\n | 'off-for-workspace'\n | 'runs-here'\n | 'awaiting-opt-in'\n | 'off-for-site'\n\nexport function resolvePluginSiteState(\n org: { enabledPlugins?: string[] } | null | undefined,\n host: { disabledPlugins?: string[]; enabledPlugins?: string[] } | null | undefined,\n pluginId: string,\n): PluginSiteState {\n if (ALWAYS_ON.includes(pluginId)) return 'always-on'\n if (!resolveEnabledPlugins(org).includes(pluginId)) return 'off-for-workspace'\n if (resolveHostEnabledPlugins(org, host).includes(pluginId)) return 'runs-here'\n // A default-off plugin this site has not named is off because nobody asked\n // for it; naming it in the deny-list is a decision, and an explicit deny\n // beats an opt-in, so the deny-list is checked first.\n const denied = Array.isArray(host?.disabledPlugins)\n ? host.disabledPlugins.map(String).includes(pluginId)\n : false\n return !denied && isDefaultOffPerSite(pluginId)\n ? 'awaiting-opt-in'\n : 'off-for-site'\n}\n\n/**\n * The declared dependency graph, both directions, built once (AGL-2486).\n *\n * `requires` on the catalog is the forward edge — what a plugin cannot run\n * without — and every consumer so far wanted the reverse one. Building both\n * here keeps the cascade, the console's dependency card and the server-side\n * refusal reading the SAME edges: a graph assembled a second time is a graph\n * that can disagree with itself about which plugins are coupled, and the\n * disagreement would show up as a warning shown on one surface and not on the\n * one that writes.\n *\n * `extraRequirements` extends the catalog and cannot shrink it, exactly as it\n * does for {@link resolveDisableCascade} — a third-party id can add edges of\n * its own but can never declare away a first-party one.\n */\nfunction dependencyEdges(\n extraRequirements?: Readonly<Record<string, readonly string[]>>,\n): {\n /** Keyed by a required id; holds the ids that depend on it. */\n dependents: Map<string, string[]>\n /** Keyed by a dependent id; holds the ids it requires. */\n requires: Map<string, string[]>\n} {\n const dependents = new Map<string, string[]>()\n const requires = new Map<string, string[]>()\n const addEdge = (dependent: string, required: string) => {\n const reverse = dependents.get(required)\n if (reverse) reverse.push(dependent)\n else dependents.set(required, [dependent])\n const forward = requires.get(dependent)\n if (forward) forward.push(required)\n else requires.set(dependent, [required])\n }\n for (const plugin of FIRST_PARTY_PLUGINS)\n for (const required of plugin.requires ?? []) addEdge(plugin.id, required)\n for (const [dependent, required] of Object.entries(extraRequirements ?? {}))\n for (const one of required ?? []) addEdge(dependent, String(one))\n return { dependents, requires }\n}\n\n/** What `pluginId` declares it cannot run without — DIRECT edges only. */\nexport function pluginRequirements(\n pluginId: string,\n extraRequirements?: Readonly<Record<string, readonly string[]>>,\n): string[] {\n return [...(dependencyEdges(extraRequirements).requires.get(pluginId) ?? [])]\n}\n\n/** What declares it cannot run without `pluginId` — DIRECT edges only. */\nexport function pluginDependents(\n pluginId: string,\n extraRequirements?: Readonly<Record<string, readonly string[]>>,\n): string[] {\n return [...(dependencyEdges(extraRequirements).dependents.get(pluginId) ?? [])]\n}\n\n/**\n * Which of `enabledIds` would be left running with a requirement switched off\n * (AGL-2486) — the same question the cascade dialog asks, phrased so a WRITER\n * can refuse instead of a reader warning.\n *\n * The dialog is a courtesy: it stands in front of the console's own switches\n * and nothing else. A direct API call, a stale tab that posts an older set, or\n * a second console surface that forgot to ask can still store a set where\n * User Accounts is on and the Commerce bundle that answers its sign-in POST is\n * not. This is what a write path calls to reject that set outright, so the\n * boundary does not live in a dialog anyone can skip.\n *\n * Only DECLARED requirements are checked, with the same limit\n * {@link PLUGIN_CASCADE_IS_DECLARED_ONLY} states: an undeclared coupling is\n * invisible here too. A requirement naming an id outside the catalog is\n * ignored rather than treated as unmet — a stale edge must not lock a\n * workspace out of its own switchboard.\n */\nexport function strandedDependents(\n enabledIds: readonly string[],\n extraRequirements?: Readonly<Record<string, readonly string[]>>,\n): Array<{ pluginId: string; missing: string[] }> {\n const enabled = new Set(enabledIds.map(String))\n const known = new Set<string>([\n ...FIRST_PARTY_PLUGINS.map((plugin) => plugin.id),\n ...enabled,\n ])\n const { requires } = dependencyEdges(extraRequirements)\n const stranded: Array<{ pluginId: string; missing: string[] }> = []\n for (const pluginId of enabled) {\n const missing = (requires.get(pluginId) ?? []).filter(\n (required) => known.has(required) && !enabled.has(required),\n )\n if (missing.length) stranded.push({ pluginId, missing })\n }\n return stranded\n}\n\n/**\n * Everything that must ALSO be switched off when `pluginId` is (AGL-2486) —\n * the transitive closure over reverse `requires` edges, restricted to what is\n * currently on.\n *\n * Pure and surface-agnostic: the org switchboard and the per-site card both\n * call it with their own \"currently enabled\" set, so the same graph answers\n * both, and the org's wider blast radius comes from the set it passes, not\n * from a second implementation.\n *\n * `extraRequirements` is how a marketplace listing joins the graph. Third-party\n * ids ride the same `enabledPlugins` field, so a listing whose manifest\n * declares `requires` is cascaded exactly like a bundle. It EXTENDS the\n * catalog graph and cannot shrink it — a listing cannot declare away a\n * first-party edge.\n *\n * ⚠️ The result is only ever as complete as what has been DECLARED. A\n * marketplace plugin that uses first-party components without saying so in its\n * manifest will not appear here, and callers must not present the list as\n * exhaustive. See `PLUGIN_CASCADE_IS_DECLARED_ONLY`.\n */\nexport function resolveDisableCascade(\n pluginId: string,\n enabledIds: readonly string[],\n extraRequirements?: Readonly<Record<string, readonly string[]>>,\n): string[] {\n const enabled = new Set(enabledIds.map(String))\n const dependents = dependencyEdges(extraRequirements).dependents\n\n // Breadth-first over reverse edges. `seen` is seeded with the origin so a\n // cycle back to it terminates and the plugin being disabled is never listed\n // among its own dependents.\n const seen = new Set<string>([pluginId])\n const cascade: string[] = []\n const queue: string[] = [pluginId]\n while (queue.length) {\n const current = queue.shift() as string\n for (const dependent of dependents.get(current) ?? []) {\n if (seen.has(dependent)) continue\n seen.add(dependent)\n // Walk THROUGH an already-off dependent — something may depend on it in\n // turn — but do not claim it is being turned off.\n if (enabled.has(dependent)) cascade.push(dependent)\n queue.push(dependent)\n }\n }\n return cascade\n}\n\n/**\n * Why the cascade list must never be presented as exhaustive (AGL-2486).\n *\n * Exported as copy rather than left to each caller to paraphrase: the whole\n * value of the warning rests on it being honest about its own limits, and two\n * surfaces wording that differently is how one of them ends up overclaiming.\n *\n * It says built-in ONLY, and that is the current truth rather than a hedge.\n * `PluginManifest` carries no `requires` field, so a publisher cannot declare\n * a dependency even if they want to, and `extraRequirements` above — the seam\n * that would carry them — is supplied by nothing outside tests. An earlier\n * draft said \"a plugin that uses this one WITHOUT declaring it cannot be\n * detected\", which quietly implied declaring was possible and would have made\n * this the exact overclaiming warning it exists to avoid. Adding the manifest\n * field is a separate change: it needs a publish form, validation and a line\n * on the install screen, or it is a field written by nobody and read by\n * nothing. Update this sentence in the same change, not before.\n */\nexport const PLUGIN_CASCADE_IS_DECLARED_ONLY =\n 'This covers built-in plugins only. A marketplace plugin has no way to ' +\n 'declare that it depends on another one yet, so none are listed here — ' +\n 'check any third-party plugins you rely on before continuing.'\n\n/** Reverse lookup: which first-party plugin a release flag gates, if any. */\nexport function pluginForReleaseFlag(\n flagKey: string,\n): FirstPartyPlugin | undefined {\n return FIRST_PARTY_PLUGINS.find((plugin) => plugin.releaseFlag === flagKey)\n}\n\n/**\n * Subtracts release-flagged-off plugins from an effective set (AGL-422).\n * Pure — the caller supplies the verdict source (client: the activated\n * Remote Config hook state; server: the cached admin-SDK template read),\n * so the same policy runs identically on every surface:\n *\n * - unknown ids (marketplace/realm installs) and always-on ids pass;\n * - a first-party id with a `releaseFlag` passes only when the flag is on\n * for the subject, or `staffBypass` is set (staff preview keeps working\n * while a feature is dark).\n */\nexport function filterPluginsByReleaseFlags(\n pluginIds: readonly string[],\n isFlagOn: (flagKey: string) => boolean,\n options?: { staffBypass?: boolean },\n): string[] {\n if (options?.staffBypass) return [...pluginIds]\n const catalog = new Map(\n FIRST_PARTY_PLUGINS.map((plugin) => [plugin.id, plugin]),\n )\n return pluginIds.filter((pluginId) => {\n const plugin = catalog.get(pluginId)\n if (!plugin?.releaseFlag || plugin.alwaysOn) return true\n return isFlagOn(plugin.releaseFlag)\n })\n}\n"],"names":["FIRST_PARTY_PLUGINS","PLUGIN_EDIT_BAR_LINKS","PUBLISHED_SITE_IMPACT","FIRST_PARTY_FUNCTION_BINDINGS","ACCOUNTS_PLUGIN_ID","FORMS_PLUGIN_ID","pluginEditBarLinks","enabledPluginIds","enabled","Set","filter","link","has","pluginId","DEFAULT_ENABLED_PLUGINS","map","plugin","id","canonicalPluginId","canonicalPluginIds","pluginIds","Array","from","String","ALWAYS_ON","alwaysOn","ALWAYS_ON_FOR_WORKSPACE","alwaysOnForWorkspace","isLockedOnForWorkspace","includes","isLockedOnForSite","FIRST_PARTY_IDS","DEFAULT_OFF_PER_SITE_PLUGIN_IDS","defaultOffPerSite","isDefaultOffPerSite","applyDefaultOffOptIn","optedIn","size","asked","isArray","isFirstPartyPlugin","classifyEnabledPlugins","bundles","listings","push","resolveEnabledPlugins","org","configured","enabledPlugins","base","subtractDisabledPlugins","disabledPlugins","length","disabled","resolveHostEnabledPlugins","host","isHostPluginEnabled","isPluginEnabled","resolvePluginSiteState","denied","dependencyEdges","extraRequirements","dependents","Map","requires","addEdge","dependent","required","reverse","get","set","forward","Object","entries","one","pluginRequirements","pluginDependents","strandedDependents","enabledIds","known","stranded","missing","resolveDisableCascade","seen","cascade","queue","current","shift","add","PLUGIN_CASCADE_IS_DECLARED_ONLY","pluginForReleaseFlag","flagKey","find","releaseFlag","filterPluginsByReleaseFlags","isFlagOn","options","staffBypass","catalog"],"mappings":"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"}
@@ -9,6 +9,7 @@
9
9
  import type { FirstPartyPlugin, PluginEditBarLink, PublishedSiteImpact } from './enabled-plugins';
10
10
  import type { ResolvedPluginHostCollection, ResolvedPluginOrgCollection } from './plugin-host-collections';
11
11
  import type { ResolvedPluginOrgCapacity } from './plugin-org-capacity';
12
+ import type { FunctionBindings } from './plugin-contributions';
12
13
  export declare const FIRST_PARTY_PLUGINS: readonly FirstPartyPlugin[];
13
14
  export declare const PUBLISHED_SITE_IMPACT: Readonly<Record<string, PublishedSiteImpact>>;
14
15
  /**
@@ -32,3 +33,8 @@ export declare const PLUGIN_ORG_COLLECTIONS_DECLARED: readonly ResolvedPluginOrg
32
33
  * called.
33
34
  */
34
35
  export declare const PLUGIN_ORG_CAPACITIES_DECLARED: readonly ResolvedPluginOrgCapacity[];
36
+ /**
37
+ * Every first-party element that runs a site function, and the prop naming
38
+ * it, declared by that element's plugin (AGL-3393). Core names no element.
39
+ */
40
+ export declare const FIRST_PARTY_FUNCTION_BINDINGS: FunctionBindings;
@@ -40,7 +40,7 @@
40
40
  {
41
41
  "id": "accounts",
42
42
  "label": "User Accounts",
43
- "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.",
43
+ "description": "Visitor accounts on the site: the /signin, /signup and /recover pages, and the Members blocks. On for a new site; a site created before that stays off until you turn it on.",
44
44
  "releaseFlag": "release_member_accounts",
45
45
  "defaultOffPerSite": true,
46
46
  "requires": [
@@ -445,5 +445,12 @@ export const PUBLISHED_SITE_IMPACT = {
445
445
  }
446
446
  }
447
447
  ];
448
+ /**
449
+ * Every first-party element that runs a site function, and the prop naming
450
+ * it, declared by that element's plugin (AGL-3393). Core names no element.
451
+ */ export const FIRST_PARTY_FUNCTION_BINDINGS = {
452
+ "functionScope": "functionName",
453
+ "functionWidget": "functionName"
454
+ };
448
455
 
449
456
  //# 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, PluginEditBarLink, PublishedSiteImpact } from './enabled-plugins'\nimport type { ResolvedPluginHostCollection, ResolvedPluginOrgCollection } from './plugin-host-collections'\nimport type { ResolvedPluginOrgCapacity } from './plugin-org-capacity'\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\n/**\n * Every host subcollection a first-party plugin owns, declared by that plugin\n * (AGL-3080). Core's readers ask this list; core names no collection.\n */\nexport const PLUGIN_HOST_COLLECTIONS_DECLARED: readonly ResolvedPluginHostCollection[] = [\n {\n \"pluginId\": \"forms\",\n \"name\": \"forms\",\n \"routeSlug\": \"forms\"\n },\n {\n \"pluginId\": \"bookings\",\n \"name\": \"services\",\n \"routeSlug\": \"bookings\"\n },\n {\n \"pluginId\": \"bookings\",\n \"name\": \"bookings\",\n \"mediaScan\": \"none\",\n \"mediaScanReason\": \"Booking records — a customer, a time and a service pointer. The service holds the imagery and IS scanned; a booking is the transaction against it.\"\n },\n {\n \"pluginId\": \"commerce\",\n \"name\": \"products\",\n \"routeSlug\": \"products\"\n },\n {\n \"pluginId\": \"commerce\",\n \"name\": \"productCategories\",\n \"routeSlug\": \"products\"\n },\n {\n \"pluginId\": \"commerce\",\n \"name\": \"suppliers\",\n \"routeSlug\": \"products\"\n },\n {\n \"pluginId\": \"commerce\",\n \"name\": \"locations\",\n \"routeSlug\": \"products\"\n },\n {\n \"pluginId\": \"commerce\",\n \"name\": \"coupons\",\n \"routeSlug\": \"products\"\n },\n {\n \"pluginId\": \"commerce\",\n \"name\": \"discounts\",\n \"routeSlug\": \"products\"\n },\n {\n \"pluginId\": \"commerce\",\n \"name\": \"reviews\",\n \"routeSlug\": \"products\"\n },\n {\n \"pluginId\": \"commerce\",\n \"name\": \"memberPosts\",\n \"routeSlug\": \"products\"\n },\n {\n \"pluginId\": \"commerce\",\n \"name\": \"resources\",\n \"routeSlug\": \"products\"\n },\n {\n \"pluginId\": \"commerce\",\n \"name\": \"settings\",\n \"label\": \"Store settings\",\n \"routeSlug\": \"products\"\n },\n {\n \"pluginId\": \"commerce\",\n \"name\": \"siteMembers\"\n },\n {\n \"pluginId\": \"commerce\",\n \"name\": \"siteMemberCredentials\",\n \"mediaScan\": \"none\",\n \"mediaScanReason\": \"A site member's password hash, one document per member, read and written only by the membership routes (AGL-3308). A credential with no content field, and no client may read it.\"\n },\n {\n \"pluginId\": \"commerce\",\n \"name\": \"orders\",\n \"mediaScan\": \"none\",\n \"mediaScanReason\": \"Completed orders. Line items COPY a product's `imageUrl` at purchase time, so these would match — and matching would be the wrong answer: an order is an immutable record of what was sold, deleting the asset changes nothing about it, and no author can edit one. A busy store also holds more of these than the whole read budget.\"\n },\n {\n \"pluginId\": \"commerce\",\n \"name\": \"carts\",\n \"mediaScan\": \"none\",\n \"mediaScanReason\": \"Live and abandoned carts, with the same copied line-item image and the same reasoning, at higher volume — one document per shopper session.\"\n },\n {\n \"pluginId\": \"commerce\",\n \"name\": \"checkouts\",\n \"mediaScan\": \"none\",\n \"mediaScanReason\": \"In-flight checkout sessions. Transient buyer state carrying the same line-item snapshot as `carts`.\"\n },\n {\n \"pluginId\": \"commerce\",\n \"name\": \"reservations\",\n \"mediaScan\": \"none\",\n \"mediaScanReason\": \"POS and booking holds. Short-lived transaction rows pointing at a resource that is itself scanned.\"\n },\n {\n \"pluginId\": \"commerce\",\n \"name\": \"stockHolds\",\n \"mediaScan\": \"none\",\n \"mediaScanReason\": \"Inventory holds taken during checkout. Machine-written, short-lived, and a quantity rather than content.\"\n },\n {\n \"pluginId\": \"commerce\",\n \"name\": \"subscriptions\",\n \"mediaScan\": \"none\",\n \"mediaScanReason\": \"Site membership subscriptions: a plan pointer, a status and Stripe ids. Server-written from the billing webhook.\"\n },\n {\n \"pluginId\": \"commerce\",\n \"name\": \"registers\",\n \"mediaScan\": \"none\",\n \"mediaScanReason\": \"POS register allocations — a count against the register add-on, read by billing (AGL-1775). No content field at all.\"\n },\n {\n \"pluginId\": \"commerce\",\n \"name\": \"giftCards\",\n \"mediaScan\": \"none\",\n \"mediaScanReason\": \"Gift card balances and redemption history. Money, not content.\"\n },\n {\n \"pluginId\": \"commerce\",\n \"name\": \"licenseKeys\",\n \"mediaScan\": \"none\",\n \"mediaScanReason\": \"Digital-product license keys issued at fulfilment: a code, an order pointer and a revocation flag.\"\n },\n {\n \"pluginId\": \"commerce\",\n \"name\": \"inventoryAdjustments\",\n \"mediaScan\": \"none\",\n \"mediaScanReason\": \"The append-only stock adjustment ledger (AGL-2269). One row per manual stock edit and per cancellation release, unbounded over a store's life.\"\n },\n {\n \"pluginId\": \"commerce\",\n \"name\": \"inventoryReconciliation\",\n \"mediaScan\": \"none\",\n \"mediaScanReason\": \"Reconciliation runs comparing counted stock against recorded stock. Machine-written totals.\"\n },\n {\n \"pluginId\": \"commerce\",\n \"name\": \"restockAlerts\",\n \"mediaScan\": \"none\",\n \"mediaScanReason\": \"Back-in-stock requests: an email address and a product pointer, written by visitors.\"\n },\n {\n \"pluginId\": \"commerce\",\n \"name\": \"stripeTaxRates\",\n \"mediaScan\": \"none\",\n \"mediaScanReason\": \"Cached Stripe tax rate ids, written by the tax sync. Vendor ids.\"\n },\n {\n \"pluginId\": \"marketplace\",\n \"name\": \"installs\"\n },\n {\n \"pluginId\": \"crm\",\n \"name\": \"leads\",\n \"mediaScan\": \"none\",\n \"mediaScanReason\": \"Captured leads, on the same footing as `formSubmissions`: visitor-submitted, unbounded, and never a place an author places an asset.\"\n },\n {\n \"pluginId\": \"email\",\n \"name\": \"suppressions\",\n \"mediaScan\": \"none\",\n \"mediaScanReason\": \"Unsubscribes, bounces and spam complaints. Email addresses and a reason.\"\n },\n {\n \"pluginId\": \"events-calendar\",\n \"name\": \"events\",\n \"routeSlug\": \"events\"\n },\n {\n \"pluginId\": \"inbox\",\n \"name\": \"formSubmissions\",\n \"mediaScan\": \"none\",\n \"mediaScanReason\": \"Visitor form submissions. Unbounded, PII-heavy, and a file attached to one is the visitor's upload — not a library asset an author picked, and not something deleting a library asset would break.\"\n },\n {\n \"pluginId\": \"logic\",\n \"name\": \"functions\",\n \"routeSlug\": \"logic\"\n },\n {\n \"pluginId\": \"logic\",\n \"name\": \"variables\",\n \"routeSlug\": \"logic\"\n },\n {\n \"pluginId\": \"marketing\",\n \"name\": \"experiments\",\n \"routeSlug\": \"marketing\"\n },\n {\n \"pluginId\": \"marketing\",\n \"name\": \"overlays\",\n \"routeSlug\": \"marketing\"\n },\n {\n \"pluginId\": \"marketing\",\n \"name\": \"campaignAttributions\",\n \"mediaScan\": \"none\",\n \"mediaScanReason\": \"One row per conversion — a form submission, a lead, a contact or a booking — recording which campaign the visitor arrived from. Written only by the Admin SDK, and the whole record is three sanitized UTM strings capped at a hundred characters: there is no field an asset could occupy and no surface that could pick one into it. Unbounded on the same footing as the conversions it credits.\"\n },\n {\n \"pluginId\": \"redirects\",\n \"name\": \"redirects\",\n \"routeSlug\": \"redirects\"\n },\n {\n \"pluginId\": \"workflows\",\n \"name\": \"workflows\",\n \"routeSlug\": \"automation\"\n },\n {\n \"pluginId\": \"workflows\",\n \"name\": \"webhooks\",\n \"routeSlug\": \"automation\"\n },\n {\n \"pluginId\": \"workflows\",\n \"name\": \"actions\",\n \"routeSlug\": \"automation\"\n },\n]\n\n/**\n * Every org collection a first-party plugin owns whose documents the media\n * scan reads, declared by that plugin (AGL-3273).\n */\nexport const PLUGIN_ORG_COLLECTIONS_DECLARED: readonly ResolvedPluginOrgCollection[] = [\n {\n \"pluginId\": \"marketing\",\n \"name\": \"campaigns\",\n \"routeSlug\": \"marketing\",\n \"siteField\": \"hostId\",\n \"holdsTransferWhile\": {\n \"field\": \"status\",\n \"values\": [\n \"scheduled\",\n \"sending\"\n ]\n }\n },\n {\n \"pluginId\": \"marketing\",\n \"name\": \"emailCampaigns\",\n \"mediaScan\": \"none\",\n \"mediaScanReason\": \"The campaign CONTAINER — a name, a date window, the list ids it is aimed at, a topic and the sites it runs on. Distinct from `campaigns`, which holds the sends and IS scanned because a send carries the copy that went out. A container carries no copy and no asset reference: the design a send renders is a screen, and the screen is where the picker writes.\"\n },\n {\n \"pluginId\": \"workflows\",\n \"name\": \"automations\",\n \"mediaScan\": \"none\",\n \"mediaScanReason\": \"An org automation (AGL-3302) — a trigger, its conditions, the sites it is placed on, and a step list drawn from the server steps only: email subject and body as plain text, list, dataset and campaign ids, CRM fields and waits. No step it may hold carries a media reference, the in-page steps that could (custom HTML, popups) are refused by its save route, and it is written only by that route.\"\n },\n]\n\n/**\n * Every org capacity a first-party plugin backs, declared by that plugin\n * (AGL-3080). Core owns the money; this says what is counted and what it is\n * called.\n */\nexport const PLUGIN_ORG_CAPACITIES_DECLARED: readonly ResolvedPluginOrgCapacity[] = [\n {\n \"pluginId\": \"data\",\n \"kind\": \"datasets\",\n \"order\": 30,\n \"collection\": \"datasets\",\n \"addonKind\": \"datasets\",\n \"includedEntitlement\": \"datasetsPerOrg\",\n \"purchaseCeilingEntitlement\": \"maxDatasetsPerOrg\",\n \"nouns\": {\n \"one\": \"dataset\",\n \"many\": \"datasets\",\n \"addon\": \"extra datasets\"\n }\n },\n]\n"],"names":["FIRST_PARTY_PLUGINS","PUBLISHED_SITE_IMPACT","PLUGIN_EDIT_BAR_LINKS","PLUGIN_HOST_COLLECTIONS_DECLARED","PLUGIN_ORG_COLLECTIONS_DECLARED","PLUGIN_ORG_CAPACITIES_DECLARED"],"mappings":"AAAA;;;;;;;CAOC,GAMD,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,S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"}
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'\nimport type { ResolvedPluginHostCollection, ResolvedPluginOrgCollection } from './plugin-host-collections'\nimport type { ResolvedPluginOrgCapacity } from './plugin-org-capacity'\nimport type { FunctionBindings } from './plugin-contributions'\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. On for a new site; a site created before that stays off 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\n/**\n * Every host subcollection a first-party plugin owns, declared by that plugin\n * (AGL-3080). Core's readers ask this list; core names no collection.\n */\nexport const PLUGIN_HOST_COLLECTIONS_DECLARED: readonly ResolvedPluginHostCollection[] = [\n {\n \"pluginId\": \"forms\",\n \"name\": \"forms\",\n \"routeSlug\": \"forms\"\n },\n {\n \"pluginId\": \"bookings\",\n \"name\": \"services\",\n \"routeSlug\": \"bookings\"\n },\n {\n \"pluginId\": \"bookings\",\n \"name\": \"bookings\",\n \"mediaScan\": \"none\",\n \"mediaScanReason\": \"Booking records — a customer, a time and a service pointer. The service holds the imagery and IS scanned; a booking is the transaction against it.\"\n },\n {\n \"pluginId\": \"commerce\",\n \"name\": \"products\",\n \"routeSlug\": \"products\"\n },\n {\n \"pluginId\": \"commerce\",\n \"name\": \"productCategories\",\n \"routeSlug\": \"products\"\n },\n {\n \"pluginId\": \"commerce\",\n \"name\": \"suppliers\",\n \"routeSlug\": \"products\"\n },\n {\n \"pluginId\": \"commerce\",\n \"name\": \"locations\",\n \"routeSlug\": \"products\"\n },\n {\n \"pluginId\": \"commerce\",\n \"name\": \"coupons\",\n \"routeSlug\": \"products\"\n },\n {\n \"pluginId\": \"commerce\",\n \"name\": \"discounts\",\n \"routeSlug\": \"products\"\n },\n {\n \"pluginId\": \"commerce\",\n \"name\": \"reviews\",\n \"routeSlug\": \"products\"\n },\n {\n \"pluginId\": \"commerce\",\n \"name\": \"memberPosts\",\n \"routeSlug\": \"products\"\n },\n {\n \"pluginId\": \"commerce\",\n \"name\": \"resources\",\n \"routeSlug\": \"products\"\n },\n {\n \"pluginId\": \"commerce\",\n \"name\": \"settings\",\n \"label\": \"Store settings\",\n \"routeSlug\": \"products\"\n },\n {\n \"pluginId\": \"commerce\",\n \"name\": \"siteMembers\"\n },\n {\n \"pluginId\": \"commerce\",\n \"name\": \"siteMemberCredentials\",\n \"mediaScan\": \"none\",\n \"mediaScanReason\": \"A site member's password hash, one document per member, read and written only by the membership routes (AGL-3308). A credential with no content field, and no client may read it.\"\n },\n {\n \"pluginId\": \"commerce\",\n \"name\": \"orders\",\n \"mediaScan\": \"none\",\n \"mediaScanReason\": \"Completed orders. Line items COPY a product's `imageUrl` at purchase time, so these would match — and matching would be the wrong answer: an order is an immutable record of what was sold, deleting the asset changes nothing about it, and no author can edit one. A busy store also holds more of these than the whole read budget.\"\n },\n {\n \"pluginId\": \"commerce\",\n \"name\": \"carts\",\n \"mediaScan\": \"none\",\n \"mediaScanReason\": \"Live and abandoned carts, with the same copied line-item image and the same reasoning, at higher volume — one document per shopper session.\"\n },\n {\n \"pluginId\": \"commerce\",\n \"name\": \"checkouts\",\n \"mediaScan\": \"none\",\n \"mediaScanReason\": \"In-flight checkout sessions. Transient buyer state carrying the same line-item snapshot as `carts`.\"\n },\n {\n \"pluginId\": \"commerce\",\n \"name\": \"reservations\",\n \"mediaScan\": \"none\",\n \"mediaScanReason\": \"POS and booking holds. Short-lived transaction rows pointing at a resource that is itself scanned.\"\n },\n {\n \"pluginId\": \"commerce\",\n \"name\": \"stockHolds\",\n \"mediaScan\": \"none\",\n \"mediaScanReason\": \"Inventory holds taken during checkout. Machine-written, short-lived, and a quantity rather than content.\"\n },\n {\n \"pluginId\": \"commerce\",\n \"name\": \"subscriptions\",\n \"mediaScan\": \"none\",\n \"mediaScanReason\": \"Site membership subscriptions: a plan pointer, a status and Stripe ids. Server-written from the billing webhook.\"\n },\n {\n \"pluginId\": \"commerce\",\n \"name\": \"registers\",\n \"mediaScan\": \"none\",\n \"mediaScanReason\": \"POS register allocations — a count against the register add-on, read by billing (AGL-1775). No content field at all.\"\n },\n {\n \"pluginId\": \"commerce\",\n \"name\": \"giftCards\",\n \"mediaScan\": \"none\",\n \"mediaScanReason\": \"Gift card balances and redemption history. Money, not content.\"\n },\n {\n \"pluginId\": \"commerce\",\n \"name\": \"licenseKeys\",\n \"mediaScan\": \"none\",\n \"mediaScanReason\": \"Digital-product license keys issued at fulfilment: a code, an order pointer and a revocation flag.\"\n },\n {\n \"pluginId\": \"commerce\",\n \"name\": \"inventoryAdjustments\",\n \"mediaScan\": \"none\",\n \"mediaScanReason\": \"The append-only stock adjustment ledger (AGL-2269). One row per manual stock edit and per cancellation release, unbounded over a store's life.\"\n },\n {\n \"pluginId\": \"commerce\",\n \"name\": \"inventoryReconciliation\",\n \"mediaScan\": \"none\",\n \"mediaScanReason\": \"Reconciliation runs comparing counted stock against recorded stock. Machine-written totals.\"\n },\n {\n \"pluginId\": \"commerce\",\n \"name\": \"restockAlerts\",\n \"mediaScan\": \"none\",\n \"mediaScanReason\": \"Back-in-stock requests: an email address and a product pointer, written by visitors.\"\n },\n {\n \"pluginId\": \"commerce\",\n \"name\": \"stripeTaxRates\",\n \"mediaScan\": \"none\",\n \"mediaScanReason\": \"Cached Stripe tax rate ids, written by the tax sync. Vendor ids.\"\n },\n {\n \"pluginId\": \"marketplace\",\n \"name\": \"installs\"\n },\n {\n \"pluginId\": \"crm\",\n \"name\": \"leads\",\n \"mediaScan\": \"none\",\n \"mediaScanReason\": \"Captured leads, on the same footing as `formSubmissions`: visitor-submitted, unbounded, and never a place an author places an asset.\"\n },\n {\n \"pluginId\": \"email\",\n \"name\": \"suppressions\",\n \"mediaScan\": \"none\",\n \"mediaScanReason\": \"Unsubscribes, bounces and spam complaints. Email addresses and a reason.\"\n },\n {\n \"pluginId\": \"events-calendar\",\n \"name\": \"events\",\n \"routeSlug\": \"events\"\n },\n {\n \"pluginId\": \"inbox\",\n \"name\": \"formSubmissions\",\n \"mediaScan\": \"none\",\n \"mediaScanReason\": \"Visitor form submissions. Unbounded, PII-heavy, and a file attached to one is the visitor's upload — not a library asset an author picked, and not something deleting a library asset would break.\"\n },\n {\n \"pluginId\": \"logic\",\n \"name\": \"functions\",\n \"routeSlug\": \"logic\"\n },\n {\n \"pluginId\": \"logic\",\n \"name\": \"variables\",\n \"routeSlug\": \"logic\"\n },\n {\n \"pluginId\": \"marketing\",\n \"name\": \"experiments\",\n \"routeSlug\": \"marketing\"\n },\n {\n \"pluginId\": \"marketing\",\n \"name\": \"overlays\",\n \"routeSlug\": \"marketing\"\n },\n {\n \"pluginId\": \"marketing\",\n \"name\": \"campaignAttributions\",\n \"mediaScan\": \"none\",\n \"mediaScanReason\": \"One row per conversion — a form submission, a lead, a contact or a booking — recording which campaign the visitor arrived from. Written only by the Admin SDK, and the whole record is three sanitized UTM strings capped at a hundred characters: there is no field an asset could occupy and no surface that could pick one into it. Unbounded on the same footing as the conversions it credits.\"\n },\n {\n \"pluginId\": \"redirects\",\n \"name\": \"redirects\",\n \"routeSlug\": \"redirects\"\n },\n {\n \"pluginId\": \"workflows\",\n \"name\": \"workflows\",\n \"routeSlug\": \"automation\"\n },\n {\n \"pluginId\": \"workflows\",\n \"name\": \"webhooks\",\n \"routeSlug\": \"automation\"\n },\n {\n \"pluginId\": \"workflows\",\n \"name\": \"actions\",\n \"routeSlug\": \"automation\"\n },\n]\n\n/**\n * Every org collection a first-party plugin owns whose documents the media\n * scan reads, declared by that plugin (AGL-3273).\n */\nexport const PLUGIN_ORG_COLLECTIONS_DECLARED: readonly ResolvedPluginOrgCollection[] = [\n {\n \"pluginId\": \"marketing\",\n \"name\": \"campaigns\",\n \"routeSlug\": \"marketing\",\n \"siteField\": \"hostId\",\n \"holdsTransferWhile\": {\n \"field\": \"status\",\n \"values\": [\n \"scheduled\",\n \"sending\"\n ]\n }\n },\n {\n \"pluginId\": \"marketing\",\n \"name\": \"emailCampaigns\",\n \"mediaScan\": \"none\",\n \"mediaScanReason\": \"The campaign CONTAINER — a name, a date window, the list ids it is aimed at, a topic and the sites it runs on. Distinct from `campaigns`, which holds the sends and IS scanned because a send carries the copy that went out. A container carries no copy and no asset reference: the design a send renders is a screen, and the screen is where the picker writes.\"\n },\n {\n \"pluginId\": \"workflows\",\n \"name\": \"automations\",\n \"mediaScan\": \"none\",\n \"mediaScanReason\": \"An org automation (AGL-3302) — a trigger, its conditions, the sites it is placed on, and a step list drawn from the server steps only: email subject and body as plain text, list, dataset and campaign ids, CRM fields and waits. No step it may hold carries a media reference, the in-page steps that could (custom HTML, popups) are refused by its save route, and it is written only by that route.\"\n },\n]\n\n/**\n * Every org capacity a first-party plugin backs, declared by that plugin\n * (AGL-3080). Core owns the money; this says what is counted and what it is\n * called.\n */\nexport const PLUGIN_ORG_CAPACITIES_DECLARED: readonly ResolvedPluginOrgCapacity[] = [\n {\n \"pluginId\": \"data\",\n \"kind\": \"datasets\",\n \"order\": 30,\n \"collection\": \"datasets\",\n \"addonKind\": \"datasets\",\n \"includedEntitlement\": \"datasetsPerOrg\",\n \"purchaseCeilingEntitlement\": \"maxDatasetsPerOrg\",\n \"nouns\": {\n \"one\": \"dataset\",\n \"many\": \"datasets\",\n \"addon\": \"extra datasets\"\n }\n },\n]\n\n/**\n * Every first-party element that runs a site function, and the prop naming\n * it, declared by that element's plugin (AGL-3393). Core names no element.\n */\nexport const FIRST_PARTY_FUNCTION_BINDINGS: FunctionBindings = {\n \"functionScope\": \"functionName\",\n \"functionWidget\": \"functionName\",\n}\n"],"names":["FIRST_PARTY_PLUGINS","PUBLISHED_SITE_IMPACT","PLUGIN_EDIT_BAR_LINKS","PLUGIN_HOST_COLLECTIONS_DECLARED","PLUGIN_ORG_COLLECTIONS_DECLARED","PLUGIN_ORG_CAPACITIES_DECLARED","FIRST_PARTY_FUNCTION_BINDINGS"],"mappings":"AAAA;;;;;;;CAOC,GAOD,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,S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"}
@@ -65,6 +65,19 @@ export interface PluginSiteContributions {
65
65
  * such a site, which is what a feature is.
66
66
  */
67
67
  features?: string[];
68
+ /**
69
+ * The elements that run a site function in the visitor's browser (AGL-3393),
70
+ * each keyed by its component id and naming the prop that holds the
71
+ * function's name. Compose hands such a node the function's definition and
72
+ * the site variables it reads; no other node is given either.
73
+ *
74
+ * Declared DATA rather than read off a registered schema: compose runs where
75
+ * no plugin code loads — the tenant's server components — so the only thing
76
+ * it can consult is a declaration. A first-party plugin declares it in
77
+ * `plugins.config.json`, a marketplace plugin in its manifest, and both reach
78
+ * compose as the same map. Every key must also be one of `components`.
79
+ */
80
+ functionBindings?: Record<string, string>;
68
81
  }
69
82
  /** What a plugin adds to the console. */
70
83
  export interface PluginConsoleContributions {
@@ -184,6 +197,31 @@ export interface PresenceSubject {
184
197
  * elements — the default in the module note.
185
198
  */
186
199
  export declare function isPluginUsedOnPage(subject: PresenceSubject, page: PagePresence): boolean;
200
+ /**
201
+ * Function bindings (AGL-3393): component id → the prop naming the site
202
+ * function that element runs.
203
+ */
204
+ export type FunctionBindings = Readonly<Record<string, string>>;
205
+ /**
206
+ * The function bindings a set of plugins declares, merged into one map.
207
+ *
208
+ * Where two plugins bind the same component id the FIRST one listed wins, so a
209
+ * caller that passes first-party declarations ahead of marketplace ones keeps
210
+ * a marketplace manifest from rebinding a first-party element. Marketplace ids
211
+ * are namespaced by their publisher (see {@link isNamespacedComponentId}), so
212
+ * a collision is a mistake, never a feature.
213
+ */
214
+ export declare function mergeFunctionBindings(...sources: ReadonlyArray<FunctionBindings | {
215
+ contributes?: PluginContributions;
216
+ } | null | undefined>): FunctionBindings;
217
+ /**
218
+ * Whether a component id belongs to a marketplace plugin's namespace
219
+ * (AGL-3387): `<publisher handle>.<plugin id>.<role>`. A `.` is reserved for
220
+ * those ids — no first-party component carries one, which the manifest
221
+ * generator enforces — so compose can tell, without loading anything, that a
222
+ * tree places an element only an installed plugin can declare.
223
+ */
224
+ export declare function isNamespacedComponentId(componentId: unknown): boolean;
187
225
  /**
188
226
  * Where a plugin's console code loads, resolved with the default applied: a
189
227
  * plugin that declares nothing loads with the shell, as it always did.
@@ -58,6 +58,7 @@
58
58
  * A console route: one or more path segments, as a nav item's `href` names
59
59
  * them. Never a URL, never `..`.
60
60
  */ const CONTRIBUTION_ROUTE = /^(\/[A-Za-z0-9_-]+){1,8}$/;
61
+ /** A prop name a function binding may name: a plain identifier. */ const BINDING_PROP = /^[A-Za-z_$][A-Za-z0-9_$]{0,63}$/;
61
62
  /**
62
63
  * Validates a declared `contributes` block.
63
64
  *
@@ -95,6 +96,7 @@
95
96
  }
96
97
  const next = {};
97
98
  for (const key of Object.keys(site)){
99
+ if (key === 'functionBindings') continue;
98
100
  if (key !== 'components' && key !== 'features') {
99
101
  return {
100
102
  ok: false,
@@ -108,6 +110,17 @@
108
110
  };
109
111
  if (list.values.length) next[key] = list.values;
110
112
  }
113
+ if (site['functionBindings'] !== undefined) {
114
+ var _next_components;
115
+ const bindings = functionBindingMap(site['functionBindings'], (_next_components = next.components) != null ? _next_components : []);
116
+ if (bindings.ok === false) return {
117
+ ok: false,
118
+ error: bindings.error
119
+ };
120
+ if (Object.keys(bindings.value).length) {
121
+ next.functionBindings = bindings.value;
122
+ }
123
+ }
111
124
  if (Object.keys(next).length) out.site = next;
112
125
  }
113
126
  if (input['console'] !== undefined) {
@@ -179,6 +192,47 @@ function idList(value, where, pattern) {
179
192
  values
180
193
  };
181
194
  }
195
+ /**
196
+ * `contributes.site.functionBindings`, validated against the components the
197
+ * same block declares: a binding on an element the plugin does not register
198
+ * would hand a function's definition, and the variable values it reads, to
199
+ * whichever element happens to own that id.
200
+ */ function functionBindingMap(value, components) {
201
+ const where = 'contributes.site.functionBindings';
202
+ if (!isPlainObject(value)) {
203
+ return {
204
+ ok: false,
205
+ error: `${where} must be an object`
206
+ };
207
+ }
208
+ const entries = Object.entries(value);
209
+ if (entries.length > PLUGIN_MAX_CONTRIBUTIONS) {
210
+ return {
211
+ ok: false,
212
+ error: `${where} lists more than ${PLUGIN_MAX_CONTRIBUTIONS}`
213
+ };
214
+ }
215
+ const out = {};
216
+ for (const [componentId, prop] of entries){
217
+ if (!components.includes(componentId)) {
218
+ return {
219
+ ok: false,
220
+ error: `${where} names "${componentId}", which contributes.site.components does not declare`
221
+ };
222
+ }
223
+ if (typeof prop !== 'string' || !BINDING_PROP.test(prop)) {
224
+ return {
225
+ ok: false,
226
+ error: `${where}.${componentId} must name a prop, not "${String(prop)}"`
227
+ };
228
+ }
229
+ out[componentId] = prop;
230
+ }
231
+ return {
232
+ ok: true,
233
+ value: out
234
+ };
235
+ }
182
236
  /**
183
237
  * A declared `contributes` block as a loader may trust it: the sanitized
184
238
  * block, or `undefined` for one that is absent or does not validate.
@@ -233,6 +287,38 @@ function idList(value, where, pattern) {
233
287
  if ((_site_features = site.features) == null ? void 0 : _site_features.length) return true;
234
288
  return ((_site_components = site.components) != null ? _site_components : []).some((id)=>page.componentIds.has(id));
235
289
  }
290
+ /**
291
+ * The function bindings a set of plugins declares, merged into one map.
292
+ *
293
+ * Where two plugins bind the same component id the FIRST one listed wins, so a
294
+ * caller that passes first-party declarations ahead of marketplace ones keeps
295
+ * a marketplace manifest from rebinding a first-party element. Marketplace ids
296
+ * are namespaced by their publisher (see {@link isNamespacedComponentId}), so
297
+ * a collision is a mistake, never a feature.
298
+ */ export function mergeFunctionBindings(...sources) {
299
+ const merged = {};
300
+ for (const source of sources){
301
+ var _source_contributes_site, _source_contributes;
302
+ if (!source) continue;
303
+ const declared = 'contributes' in source ? (_source_contributes = source.contributes) == null ? void 0 : (_source_contributes_site = _source_contributes.site) == null ? void 0 : _source_contributes_site.functionBindings : source;
304
+ for (const [componentId, prop] of Object.entries(declared != null ? declared : {})){
305
+ if (typeof prop !== 'string') continue;
306
+ if (!Object.prototype.hasOwnProperty.call(merged, componentId)) {
307
+ merged[componentId] = prop;
308
+ }
309
+ }
310
+ }
311
+ return merged;
312
+ }
313
+ /**
314
+ * Whether a component id belongs to a marketplace plugin's namespace
315
+ * (AGL-3387): `<publisher handle>.<plugin id>.<role>`. A `.` is reserved for
316
+ * those ids — no first-party component carries one, which the manifest
317
+ * generator enforces — so compose can tell, without loading anything, that a
318
+ * tree places an element only an installed plugin can declare.
319
+ */ export function isNamespacedComponentId(componentId) {
320
+ return typeof componentId === 'string' && componentId.includes('.');
321
+ }
236
322
  export function consoleLoadPoints(contributes) {
237
323
  var _contributes_console, _declared_slots, _declared_routes, _declared_orgRoutes;
238
324
  if (!contributes) return {