@aglyn/aglyn 1.0.0-beta.227 → 1.0.0-beta.229
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +11 -11
- package/src/lib/app-utils/docs-help.generated.d.ts +1 -1
- package/src/lib/app-utils/docs-help.generated.js +1 -0
- package/src/lib/app-utils/docs-help.generated.js.map +1 -1
- package/src/lib/app-utils/docs-index.generated.js +18 -18
- package/src/lib/app-utils/docs-index.generated.js.map +1 -1
- package/src/lib/app-utils/notifications.d.ts +14 -0
- package/src/lib/app-utils/notifications.js +30 -2
- package/src/lib/app-utils/notifications.js.map +1 -1
- package/src/lib/app-utils/plan-entitlements.d.ts +25 -28
- package/src/lib/app-utils/plan-entitlements.js +39 -44
- package/src/lib/app-utils/plan-entitlements.js.map +1 -1
- package/src/lib/plugin-manager/feature-plugins.d.ts +13 -0
- package/src/lib/plugin-manager/feature-plugins.js.map +1 -1
- package/src/lib/plugin-manager/first-party-plugins.generated.js +9 -1
- package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
- package/src/lib/plugin-manager/plugin-entitlements.js +21 -6
- package/src/lib/plugin-manager/plugin-entitlements.js.map +1 -1
|
@@ -44,15 +44,30 @@
|
|
|
44
44
|
* gave, and a list that followed it would put the staff lockdown checklist
|
|
45
45
|
* in a different order per session.
|
|
46
46
|
*/ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
47
|
+
var _globalScope, _REGISTRY_KEY, _;
|
|
47
48
|
import { FIRST_PARTY_PLUGINS } from "./enabled-plugins.js";
|
|
48
49
|
import { registerPluginPermissions } from "./plugin-permissions.js";
|
|
49
|
-
const registrations = new Map();
|
|
50
50
|
/**
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
|
|
51
|
+
* The registrations and their listeners, held on `globalThis` rather than in
|
|
52
|
+
* module `const`s (AGL-3596), as the custom field registry and the
|
|
53
|
+
* declarations repair hook are (AGL-3412).
|
|
54
|
+
*
|
|
55
|
+
* An app registers the plugins' declarations from `instrumentation.ts`, which
|
|
56
|
+
* Next compiles into a different module graph from the routes. A module-scoped
|
|
57
|
+
* map was filled in the instrumentation's copy and read empty in a route's, so
|
|
58
|
+
* a core route that never loaded a plugin knew none of its keys: the site
|
|
59
|
+
* create door asked for `ai.generate`, read `undefined` from a catalog that
|
|
60
|
+
* had never heard of it, and every guided AI start was born with the starter.
|
|
61
|
+
* One map per process, under a `Symbol.for` key, is the same map in every
|
|
62
|
+
* copy.
|
|
63
|
+
*/ const REGISTRY_KEY = Symbol.for('@aglyn/aglyn:plugin-entitlements');
|
|
64
|
+
const globalScope = globalThis;
|
|
65
|
+
const registry = (_ = (_globalScope = globalScope)[_REGISTRY_KEY = REGISTRY_KEY]) != null ? _ : _globalScope[_REGISTRY_KEY] = {
|
|
66
|
+
registrations: new Map(),
|
|
67
|
+
listeners: new Set()
|
|
68
|
+
};
|
|
69
|
+
const registrations = registry.registrations;
|
|
70
|
+
const listeners = registry.listeners;
|
|
56
71
|
/** Calls `listener` after every registration and reset; answers the unsubscribe. */ export function subscribePluginEntitlements(listener) {
|
|
57
72
|
listeners.add(listener);
|
|
58
73
|
return ()=>{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-entitlements.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 * Plugin-declared billing and access keys (AGL-2940).\n *\n * A plugin that sells something, gates something, or can be switched off in\n * an incident declares the KEYS here — a seat add-on, a feature flag, a\n * lockdown lever, a permission — and the core catalogs that used to be\n * edited for each one read the declarations instead:\n *\n * - `plan-entitlements` folds every registered seat add-on into\n * `resolveOrgEntitlements` (a quota raised per unit, features switched on)\n * and answers a registered feature's plan defaults;\n * - `lockdown` lists a registered lever on the staff page, carries its label,\n * its staff-bypass rule and its visitor notice, and maps the API paths it\n * names onto it for the plugin dispatcher;\n * - `plugin-permissions` receives the permissions with the owner filled in;\n * - `org-permissions` folds every declared catalog key into the org\n * permission catalog — the role editor, custom roles, per-member overrides\n * and the rules stamp — and `host-permissions` decides the keys that\n * declare host-role defaults for a site collaborator, per site.\n *\n * The NUMBERS stay where they are checked. A seat add-on's price is a row in\n * `PLAN_PRICING`, read by the Stripe wiring and reconciled by\n * `check-pricing-drift`; a declaration names the quota it widens and by how\n * much per plan, and nothing here restates a price.\n *\n * Registration order is deterministic: the catalog order of\n * `FIRST_PARTY_PLUGINS`, then any other id alphabetically. Plugin modules\n * load in parallel, so the order they REGISTER in is whatever the network\n * gave, and a list that followed it would put the staff lockdown checklist\n * in a different order per session.\n */\n\nimport type { HostAccessRole, OrgPlan, OrgRole } from '../foundation'\nimport { FIRST_PARTY_PLUGINS } from './enabled-plugins'\nimport {\n registerPluginPermissions,\n type PluginPermission,\n} from './plugin-permissions'\n\nexport interface PluginSeatAddonDeclaration {\n /** The `org.seatAddons` key the purchase is stored under (`aiAddon`). */\n key: string\n label: string\n /**\n * How many purchased units count. `1` for an org-wide add-on that is\n * bought once, where a doubled webhook item or a hand edit above one is\n * still one purchase; absent to count the stored quantity.\n */\n maxUnits?: number\n /**\n * The quota each unit widens, per plan: the resolved entitlement named by\n * `key` gains `perUnitByPlan[plan] × units`. A plan the add-on is not sold\n * on declares 0.\n */\n quota?: {\n key: string\n perUnitByPlan: Readonly<Record<OrgPlan, number>>\n }\n /** Feature flags a purchased unit switches on. */\n features?: readonly string[]\n}\n\nexport interface PluginFeatureDeclaration {\n /** The `OrgFeatureFlags` key. */\n key: string\n label: string\n /**\n * The plan defaults, when the key is not one the plan tables already\n * carry. Absent means the plan tables decide, which is the case for every\n * key that predates this registry.\n */\n defaultByPlan?: Readonly<Partial<Record<OrgPlan, boolean>>>\n}\n\nexport interface PluginLockdownFeatureDeclaration {\n /** The lever's wire identity (`ai-generate`). */\n key: string\n /** The staff checklist's label. */\n label: string\n /**\n * What the lever is called in the customer's mail when staff pull it for\n * their workspace (`media uploads`, `AI generation`). A noun phrase that\n * reads right mid-sentence (\"our team paused media uploads\") and after a\n * colon, so lowercase unless it starts with a name or an acronym. The\n * staff `label` never reaches a customer.\n */\n customerName: string\n /**\n * Whether a verified staff claim passes the lock. Granted where a staff\n * action aids incident response — one real call proves a provider is back\n * — and withheld where the staff action would BE the incident.\n */\n staffBypass: boolean\n /**\n * What a visitor reads while the lever is pulled: what is paused AND what\n * still works. The expected-back window is appended by the notice builder.\n */\n notice: { title: string; body: string }\n /**\n * Plugin API paths the lever gates at the dispatcher: exact paths, and\n * prefixes matched on a segment boundary (`ai/generate` also gates\n * `ai/generate/section`, never `ai/generated-report`).\n */\n apiPaths?: { exact?: readonly string[]; prefixes?: readonly string[] }\n}\n\n/**\n * A key a plugin adds to the org permission CATALOG (AGL-2984): the granular\n * map custom roles and per-member overrides are stored in, that\n * `resolveOrgPermissions` layers and the rules stamp carries, and that the\n * role editor lists. A declared key is stored, layered and resolved exactly\n * like a core key; the declaration supplies what a core key's catalog entry\n * and role table supply.\n */\nexport interface PluginOrgPermissionDeclaration {\n /** The dotted key a custom role and a per-member override store (`reviews.reply`). */\n key: string\n /** The role editor's line for the key, and the word a refusal names. */\n label: string\n /** What the key opens, under the label in the role editor. */\n description: string\n /** Each org role's verdict when no custom role or per-member override sets the key. */\n roleDefaults: Readonly<Record<OrgRole, boolean>>\n /**\n * Each HOST role's verdict, for a key a site collaborator holds per site.\n *\n * A collaborator's membership carries no org standing to refine — it\n * exists only to carry access to named sites — so a key with this field is\n * decided for them on the site a request names: the host role they hold\n * there, refined by the per-site toggle on their member document. A key\n * without it is never held by a collaborator.\n */\n hostRoleDefaults?: Readonly<Record<HostAccessRole, boolean>>\n}\n\nexport interface PluginEntitlementRegistration {\n pluginId: string\n seatAddons?: readonly PluginSeatAddonDeclaration[]\n features?: readonly PluginFeatureDeclaration[]\n lockdownFeatures?: readonly PluginLockdownFeatureDeclaration[]\n /** Per-tier keys a plugin's own pages read (`registerPluginPermissions`). */\n permissions?: readonly Omit<PluginPermission, 'pluginId'>[]\n /** Keys added to the org permission catalog — see {@link PluginOrgPermissionDeclaration}. */\n orgPermissions?: readonly PluginOrgPermissionDeclaration[]\n}\n\nconst registrations = new Map<string, PluginEntitlementRegistration>()\n\n/**\n * Readers of the registrations that keep a derived view in step with them —\n * the org permission catalog rebuilds itself on every registration and\n * reset, because a plugin's declarations can register after a module that\n * imported the catalog was evaluated.\n */\nconst listeners = new Set<() => void>()\n\n/** Calls `listener` after every registration and reset; answers the unsubscribe. */\nexport function subscribePluginEntitlements(listener: () => void): () => void {\n listeners.add(listener)\n return () => {\n listeners.delete(listener)\n }\n}\n\nfunction notifyListeners(): void {\n for (const listener of [...listeners]) listener()\n}\n\nconst CATALOG_ORDER = new Map(\n FIRST_PARTY_PLUGINS.map((plugin, index) => [plugin.id, index]),\n)\n\nfunction catalogRank(pluginId: string): number {\n return CATALOG_ORDER.get(pluginId) ?? Number.MAX_SAFE_INTEGER\n}\n\nfunction orderedRegistrations(): PluginEntitlementRegistration[] {\n return [...registrations.values()].sort(\n (a, b) =>\n catalogRank(a.pluginId) - catalogRank(b.pluginId) ||\n a.pluginId.localeCompare(b.pluginId),\n )\n}\n\nfunction refuseStolenKeys(\n pluginId: string,\n kind: 'seat add-on' | 'lockdown feature' | 'org permission',\n keys: readonly string[],\n ownerOf: (key: string) => string | undefined,\n): void {\n for (const key of keys) {\n const owner = ownerOf(key)\n if (owner && owner !== pluginId) {\n throw new Error(\n `${kind} \"${key}\" is already declared by \"${owner}\"; refused \"${pluginId}\"`,\n )\n }\n }\n}\n\n/**\n * Idempotent per plugin — re-registration replaces that plugin's\n * declarations. A seat add-on, lockdown or catalog permission key another\n * plugin already owns refuses the whole registration: one key, one owner,\n * or the two declarations disagree about what the key means.\n */\nexport function registerPluginEntitlements(\n registration: PluginEntitlementRegistration,\n): void {\n const pluginId = registration.pluginId.trim()\n if (!pluginId) throw new Error('plugin entitlements need a pluginId')\n refuseStolenKeys(\n pluginId,\n 'seat add-on',\n (registration.seatAddons ?? []).map((entry) => entry.key),\n (key) => pluginSeatAddonOwner(key),\n )\n refuseStolenKeys(\n pluginId,\n 'lockdown feature',\n (registration.lockdownFeatures ?? []).map((entry) => entry.key),\n (key) => pluginLockdownFeatureOwner(key),\n )\n refuseStolenKeys(\n pluginId,\n 'org permission',\n (registration.orgPermissions ?? []).map((entry) => entry.key),\n (key) => pluginOrgPermissionOwner(key),\n )\n registrations.set(pluginId, { ...registration, pluginId })\n if (registration.permissions?.length) {\n registerPluginPermissions(\n registration.permissions.map((permission) => ({ ...permission, pluginId })),\n )\n }\n notifyListeners()\n}\n\nfunction pluginSeatAddonOwner(key: string): string | undefined {\n for (const entry of registrations.values()) {\n if (entry.seatAddons?.some((addon) => addon.key === key)) return entry.pluginId\n }\n return undefined\n}\n\nfunction pluginLockdownFeatureOwner(key: string): string | undefined {\n for (const entry of registrations.values()) {\n if (entry.lockdownFeatures?.some((feature) => feature.key === key)) {\n return entry.pluginId\n }\n }\n return undefined\n}\n\nfunction pluginOrgPermissionOwner(key: string): string | undefined {\n for (const entry of registrations.values()) {\n if (entry.orgPermissions?.some((permission) => permission.key === key)) {\n return entry.pluginId\n }\n }\n return undefined\n}\n\n/** Every declared catalog permission, in catalog order, with the plugin that declared it. */\nexport function listPluginOrgPermissions(): Array<\n PluginOrgPermissionDeclaration & { pluginId: string }\n> {\n return orderedRegistrations().flatMap((entry) =>\n (entry.orgPermissions ?? []).map((permission) => ({\n ...permission,\n pluginId: entry.pluginId,\n })),\n )\n}\n\n/** Every declared seat add-on, in catalog order. */\nexport function listPluginSeatAddons(): PluginSeatAddonDeclaration[] {\n return orderedRegistrations().flatMap((entry) => [...(entry.seatAddons ?? [])])\n}\n\n/** One declared seat add-on by its `seatAddons` key. */\nexport function pluginSeatAddon(key: string): PluginSeatAddonDeclaration | undefined {\n return listPluginSeatAddons().find((entry) => entry.key === key)\n}\n\n/** Every declared feature, in catalog order. */\nexport function listPluginFeatures(): PluginFeatureDeclaration[] {\n return orderedRegistrations().flatMap((entry) => [...(entry.features ?? [])])\n}\n\n/** Every declared lockdown lever, in catalog order. */\nexport function listPluginLockdownFeatures(): PluginLockdownFeatureDeclaration[] {\n return orderedRegistrations().flatMap((entry) => [\n ...(entry.lockdownFeatures ?? []),\n ])\n}\n\n/** One declared lockdown lever by key. */\nexport function pluginLockdownFeature(\n key: string,\n): PluginLockdownFeatureDeclaration | undefined {\n return listPluginLockdownFeatures().find((entry) => entry.key === key)\n}\n\n/** The plugin that declared a lockdown lever, for attribution. */\nexport function pluginIdForLockdownFeature(key: string): string | undefined {\n return pluginLockdownFeatureOwner(key)\n}\n\n/** Every registration, in catalog order. */\nexport function listPluginEntitlementRegistrations(): PluginEntitlementRegistration[] {\n return orderedRegistrations()\n}\n\n/**\n * Test seam: forget every registration. The per-tier permissions stay\n * registered; the catalog permissions go with their registrations.\n */\nexport function resetPluginEntitlementsForTests(): void {\n registrations.clear()\n notifyListeners()\n}\n"],"names":["FIRST_PARTY_PLUGINS","registerPluginPermissions","registrations","Map","listeners","Set","subscribePluginEntitlements","listener","add","delete","notifyListeners","CATALOG_ORDER","map","plugin","index","id","catalogRank","pluginId","get","Number","MAX_SAFE_INTEGER","orderedRegistrations","values","sort","a","b","localeCompare","refuseStolenKeys","kind","keys","ownerOf","key","owner","Error","registerPluginEntitlements","registration","trim","seatAddons","entry","pluginSeatAddonOwner","lockdownFeatures","pluginLockdownFeatureOwner","orgPermissions","pluginOrgPermissionOwner","set","permissions","length","permission","some","addon","undefined","feature","listPluginOrgPermissions","flatMap","listPluginSeatAddons","pluginSeatAddon","find","listPluginFeatures","features","listPluginLockdownFeatures","pluginLockdownFeature","pluginIdForLockdownFeature","listPluginEntitlementRegistrations","resetPluginEntitlementsForTests","clear"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-entitlements.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 * Plugin-declared billing and access keys (AGL-2940).\n *\n * A plugin that sells something, gates something, or can be switched off in\n * an incident declares the KEYS here — a seat add-on, a feature flag, a\n * lockdown lever, a permission — and the core catalogs that used to be\n * edited for each one read the declarations instead:\n *\n * - `plan-entitlements` folds every registered seat add-on into\n * `resolveOrgEntitlements` (a quota raised per unit, features switched on)\n * and answers a registered feature's plan defaults;\n * - `lockdown` lists a registered lever on the staff page, carries its label,\n * its staff-bypass rule and its visitor notice, and maps the API paths it\n * names onto it for the plugin dispatcher;\n * - `plugin-permissions` receives the permissions with the owner filled in;\n * - `org-permissions` folds every declared catalog key into the org\n * permission catalog — the role editor, custom roles, per-member overrides\n * and the rules stamp — and `host-permissions` decides the keys that\n * declare host-role defaults for a site collaborator, per site.\n *\n * The NUMBERS stay where they are checked. A seat add-on's price is a row in\n * `PLAN_PRICING`, read by the Stripe wiring and reconciled by\n * `check-pricing-drift`; a declaration names the quota it widens and by how\n * much per plan, and nothing here restates a price.\n *\n * Registration order is deterministic: the catalog order of\n * `FIRST_PARTY_PLUGINS`, then any other id alphabetically. Plugin modules\n * load in parallel, so the order they REGISTER in is whatever the network\n * gave, and a list that followed it would put the staff lockdown checklist\n * in a different order per session.\n */\n\nimport type { HostAccessRole, OrgPlan, OrgRole } from '../foundation'\nimport { FIRST_PARTY_PLUGINS } from './enabled-plugins'\nimport {\n registerPluginPermissions,\n type PluginPermission,\n} from './plugin-permissions'\n\nexport interface PluginSeatAddonDeclaration {\n /** The `org.seatAddons` key the purchase is stored under (`aiAddon`). */\n key: string\n label: string\n /**\n * How many purchased units count. `1` for an org-wide add-on that is\n * bought once, where a doubled webhook item or a hand edit above one is\n * still one purchase; absent to count the stored quantity.\n */\n maxUnits?: number\n /**\n * The quota each unit widens, per plan: the resolved entitlement named by\n * `key` gains `perUnitByPlan[plan] × units`. A plan the add-on is not sold\n * on declares 0.\n */\n quota?: {\n key: string\n perUnitByPlan: Readonly<Record<OrgPlan, number>>\n }\n /** Feature flags a purchased unit switches on. */\n features?: readonly string[]\n}\n\nexport interface PluginFeatureDeclaration {\n /** The `OrgFeatureFlags` key. */\n key: string\n label: string\n /**\n * The plan defaults, when the key is not one the plan tables already\n * carry. Absent means the plan tables decide, which is the case for every\n * key that predates this registry.\n */\n defaultByPlan?: Readonly<Partial<Record<OrgPlan, boolean>>>\n}\n\nexport interface PluginLockdownFeatureDeclaration {\n /** The lever's wire identity (`ai-generate`). */\n key: string\n /** The staff checklist's label. */\n label: string\n /**\n * What the lever is called in the customer's mail when staff pull it for\n * their workspace (`media uploads`, `AI generation`). A noun phrase that\n * reads right mid-sentence (\"our team paused media uploads\") and after a\n * colon, so lowercase unless it starts with a name or an acronym. The\n * staff `label` never reaches a customer.\n */\n customerName: string\n /**\n * Whether a verified staff claim passes the lock. Granted where a staff\n * action aids incident response — one real call proves a provider is back\n * — and withheld where the staff action would BE the incident.\n */\n staffBypass: boolean\n /**\n * What a visitor reads while the lever is pulled: what is paused AND what\n * still works. The expected-back window is appended by the notice builder.\n */\n notice: { title: string; body: string }\n /**\n * Plugin API paths the lever gates at the dispatcher: exact paths, and\n * prefixes matched on a segment boundary (`ai/generate` also gates\n * `ai/generate/section`, never `ai/generated-report`).\n */\n apiPaths?: { exact?: readonly string[]; prefixes?: readonly string[] }\n}\n\n/**\n * A key a plugin adds to the org permission CATALOG (AGL-2984): the granular\n * map custom roles and per-member overrides are stored in, that\n * `resolveOrgPermissions` layers and the rules stamp carries, and that the\n * role editor lists. A declared key is stored, layered and resolved exactly\n * like a core key; the declaration supplies what a core key's catalog entry\n * and role table supply.\n */\nexport interface PluginOrgPermissionDeclaration {\n /** The dotted key a custom role and a per-member override store (`reviews.reply`). */\n key: string\n /** The role editor's line for the key, and the word a refusal names. */\n label: string\n /** What the key opens, under the label in the role editor. */\n description: string\n /** Each org role's verdict when no custom role or per-member override sets the key. */\n roleDefaults: Readonly<Record<OrgRole, boolean>>\n /**\n * Each HOST role's verdict, for a key a site collaborator holds per site.\n *\n * A collaborator's membership carries no org standing to refine — it\n * exists only to carry access to named sites — so a key with this field is\n * decided for them on the site a request names: the host role they hold\n * there, refined by the per-site toggle on their member document. A key\n * without it is never held by a collaborator.\n */\n hostRoleDefaults?: Readonly<Record<HostAccessRole, boolean>>\n}\n\nexport interface PluginEntitlementRegistration {\n pluginId: string\n seatAddons?: readonly PluginSeatAddonDeclaration[]\n features?: readonly PluginFeatureDeclaration[]\n lockdownFeatures?: readonly PluginLockdownFeatureDeclaration[]\n /** Per-tier keys a plugin's own pages read (`registerPluginPermissions`). */\n permissions?: readonly Omit<PluginPermission, 'pluginId'>[]\n /** Keys added to the org permission catalog — see {@link PluginOrgPermissionDeclaration}. */\n orgPermissions?: readonly PluginOrgPermissionDeclaration[]\n}\n\n/**\n * The registrations and their listeners, held on `globalThis` rather than in\n * module `const`s (AGL-3596), as the custom field registry and the\n * declarations repair hook are (AGL-3412).\n *\n * An app registers the plugins' declarations from `instrumentation.ts`, which\n * Next compiles into a different module graph from the routes. A module-scoped\n * map was filled in the instrumentation's copy and read empty in a route's, so\n * a core route that never loaded a plugin knew none of its keys: the site\n * create door asked for `ai.generate`, read `undefined` from a catalog that\n * had never heard of it, and every guided AI start was born with the starter.\n * One map per process, under a `Symbol.for` key, is the same map in every\n * copy.\n */\nconst REGISTRY_KEY = Symbol.for('@aglyn/aglyn:plugin-entitlements')\n\ninterface PluginEntitlementsRegistry {\n registrations: Map<string, PluginEntitlementRegistration>\n /**\n * Readers of the registrations that keep a derived view in step with them —\n * every copy of the org permission catalog rebuilds itself on every\n * registration and reset, because a plugin's declarations can register\n * after a module that imported the catalog was evaluated, and in another\n * module graph's copy of this file.\n */\n listeners: Set<() => void>\n}\n\nconst globalScope = globalThis as typeof globalThis & {\n [REGISTRY_KEY]?: PluginEntitlementsRegistry\n}\n\nconst registry: PluginEntitlementsRegistry = (globalScope[REGISTRY_KEY] ??= {\n registrations: new Map(),\n listeners: new Set(),\n})\n\nconst registrations = registry.registrations\nconst listeners = registry.listeners\n\n/** Calls `listener` after every registration and reset; answers the unsubscribe. */\nexport function subscribePluginEntitlements(listener: () => void): () => void {\n listeners.add(listener)\n return () => {\n listeners.delete(listener)\n }\n}\n\nfunction notifyListeners(): void {\n for (const listener of [...listeners]) listener()\n}\n\nconst CATALOG_ORDER = new Map(\n FIRST_PARTY_PLUGINS.map((plugin, index) => [plugin.id, index]),\n)\n\nfunction catalogRank(pluginId: string): number {\n return CATALOG_ORDER.get(pluginId) ?? Number.MAX_SAFE_INTEGER\n}\n\nfunction orderedRegistrations(): PluginEntitlementRegistration[] {\n return [...registrations.values()].sort(\n (a, b) =>\n catalogRank(a.pluginId) - catalogRank(b.pluginId) ||\n a.pluginId.localeCompare(b.pluginId),\n )\n}\n\nfunction refuseStolenKeys(\n pluginId: string,\n kind: 'seat add-on' | 'lockdown feature' | 'org permission',\n keys: readonly string[],\n ownerOf: (key: string) => string | undefined,\n): void {\n for (const key of keys) {\n const owner = ownerOf(key)\n if (owner && owner !== pluginId) {\n throw new Error(\n `${kind} \"${key}\" is already declared by \"${owner}\"; refused \"${pluginId}\"`,\n )\n }\n }\n}\n\n/**\n * Idempotent per plugin — re-registration replaces that plugin's\n * declarations. A seat add-on, lockdown or catalog permission key another\n * plugin already owns refuses the whole registration: one key, one owner,\n * or the two declarations disagree about what the key means.\n */\nexport function registerPluginEntitlements(\n registration: PluginEntitlementRegistration,\n): void {\n const pluginId = registration.pluginId.trim()\n if (!pluginId) throw new Error('plugin entitlements need a pluginId')\n refuseStolenKeys(\n pluginId,\n 'seat add-on',\n (registration.seatAddons ?? []).map((entry) => entry.key),\n (key) => pluginSeatAddonOwner(key),\n )\n refuseStolenKeys(\n pluginId,\n 'lockdown feature',\n (registration.lockdownFeatures ?? []).map((entry) => entry.key),\n (key) => pluginLockdownFeatureOwner(key),\n )\n refuseStolenKeys(\n pluginId,\n 'org permission',\n (registration.orgPermissions ?? []).map((entry) => entry.key),\n (key) => pluginOrgPermissionOwner(key),\n )\n registrations.set(pluginId, { ...registration, pluginId })\n if (registration.permissions?.length) {\n registerPluginPermissions(\n registration.permissions.map((permission) => ({ ...permission, pluginId })),\n )\n }\n notifyListeners()\n}\n\nfunction pluginSeatAddonOwner(key: string): string | undefined {\n for (const entry of registrations.values()) {\n if (entry.seatAddons?.some((addon) => addon.key === key)) return entry.pluginId\n }\n return undefined\n}\n\nfunction pluginLockdownFeatureOwner(key: string): string | undefined {\n for (const entry of registrations.values()) {\n if (entry.lockdownFeatures?.some((feature) => feature.key === key)) {\n return entry.pluginId\n }\n }\n return undefined\n}\n\nfunction pluginOrgPermissionOwner(key: string): string | undefined {\n for (const entry of registrations.values()) {\n if (entry.orgPermissions?.some((permission) => permission.key === key)) {\n return entry.pluginId\n }\n }\n return undefined\n}\n\n/** Every declared catalog permission, in catalog order, with the plugin that declared it. */\nexport function listPluginOrgPermissions(): Array<\n PluginOrgPermissionDeclaration & { pluginId: string }\n> {\n return orderedRegistrations().flatMap((entry) =>\n (entry.orgPermissions ?? []).map((permission) => ({\n ...permission,\n pluginId: entry.pluginId,\n })),\n )\n}\n\n/** Every declared seat add-on, in catalog order. */\nexport function listPluginSeatAddons(): PluginSeatAddonDeclaration[] {\n return orderedRegistrations().flatMap((entry) => [...(entry.seatAddons ?? [])])\n}\n\n/** One declared seat add-on by its `seatAddons` key. */\nexport function pluginSeatAddon(key: string): PluginSeatAddonDeclaration | undefined {\n return listPluginSeatAddons().find((entry) => entry.key === key)\n}\n\n/** Every declared feature, in catalog order. */\nexport function listPluginFeatures(): PluginFeatureDeclaration[] {\n return orderedRegistrations().flatMap((entry) => [...(entry.features ?? [])])\n}\n\n/** Every declared lockdown lever, in catalog order. */\nexport function listPluginLockdownFeatures(): PluginLockdownFeatureDeclaration[] {\n return orderedRegistrations().flatMap((entry) => [\n ...(entry.lockdownFeatures ?? []),\n ])\n}\n\n/** One declared lockdown lever by key. */\nexport function pluginLockdownFeature(\n key: string,\n): PluginLockdownFeatureDeclaration | undefined {\n return listPluginLockdownFeatures().find((entry) => entry.key === key)\n}\n\n/** The plugin that declared a lockdown lever, for attribution. */\nexport function pluginIdForLockdownFeature(key: string): string | undefined {\n return pluginLockdownFeatureOwner(key)\n}\n\n/** Every registration, in catalog order. */\nexport function listPluginEntitlementRegistrations(): PluginEntitlementRegistration[] {\n return orderedRegistrations()\n}\n\n/**\n * Test seam: forget every registration. The per-tier permissions stay\n * registered; the catalog permissions go with their registrations.\n */\nexport function resetPluginEntitlementsForTests(): void {\n registrations.clear()\n notifyListeners()\n}\n"],"names":["globalScope","REGISTRY_KEY","FIRST_PARTY_PLUGINS","registerPluginPermissions","Symbol","for","globalThis","registry","registrations","Map","listeners","Set","subscribePluginEntitlements","listener","add","delete","notifyListeners","CATALOG_ORDER","map","plugin","index","id","catalogRank","pluginId","get","Number","MAX_SAFE_INTEGER","orderedRegistrations","values","sort","a","b","localeCompare","refuseStolenKeys","kind","keys","ownerOf","key","owner","Error","registerPluginEntitlements","registration","trim","seatAddons","entry","pluginSeatAddonOwner","lockdownFeatures","pluginLockdownFeatureOwner","orgPermissions","pluginOrgPermissionOwner","set","permissions","length","permission","some","addon","undefined","feature","listPluginOrgPermissions","flatMap","listPluginSeatAddons","pluginSeatAddon","find","listPluginFeatures","features","listPluginLockdownFeatures","pluginLockdownFeature","pluginIdForLockdownFeature","listPluginEntitlementRegistrations","resetPluginEntitlementsForTests","clear"],"mappings":"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"}
|