@aglyn/aglyn 1.0.0-beta.277 → 1.0.0-beta.282

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/package.json +11 -11
  2. package/src/lib/app-utils/account-preferences.d.ts +146 -11
  3. package/src/lib/app-utils/account-preferences.js +266 -18
  4. package/src/lib/app-utils/account-preferences.js.map +1 -1
  5. package/src/lib/app-utils/activity-presenter.js +2 -26
  6. package/src/lib/app-utils/activity-presenter.js.map +1 -1
  7. package/src/lib/app-utils/activity-type-labels.d.ts +18 -0
  8. package/src/lib/app-utils/activity-type-labels.js +46 -0
  9. package/src/lib/app-utils/activity-type-labels.js.map +1 -0
  10. package/src/lib/app-utils/artifact-provenance.d.ts +3 -1
  11. package/src/lib/app-utils/artifact-provenance.js.map +1 -1
  12. package/src/lib/app-utils/bundle-intent.d.ts +37 -0
  13. package/src/lib/app-utils/bundle-intent.js +47 -3
  14. package/src/lib/app-utils/bundle-intent.js.map +1 -1
  15. package/src/lib/app-utils/compose-reusable-components.d.ts +17 -1
  16. package/src/lib/app-utils/compose-reusable-components.js +80 -5
  17. package/src/lib/app-utils/compose-reusable-components.js.map +1 -1
  18. package/src/lib/app-utils/docs-help-sections.generated.js +13 -1
  19. package/src/lib/app-utils/docs-help-sections.generated.js.map +1 -1
  20. package/src/lib/app-utils/docs-help.generated.d.ts +23 -11
  21. package/src/lib/app-utils/docs-help.generated.js +64 -7
  22. package/src/lib/app-utils/docs-help.generated.js.map +1 -1
  23. package/src/lib/app-utils/docs-index.generated.js +357 -49
  24. package/src/lib/app-utils/docs-index.generated.js.map +1 -1
  25. package/src/lib/app-utils/health-report.d.ts +3 -2
  26. package/src/lib/app-utils/health-report.js +22 -2
  27. package/src/lib/app-utils/health-report.js.map +1 -1
  28. package/src/lib/app-utils/native-nav.d.ts +39 -4
  29. package/src/lib/app-utils/native-nav.js +69 -3
  30. package/src/lib/app-utils/native-nav.js.map +1 -1
  31. package/src/lib/app-utils/native-themes.d.ts +28 -17
  32. package/src/lib/app-utils/native-themes.js +123 -45
  33. package/src/lib/app-utils/native-themes.js.map +1 -1
  34. package/src/lib/app-utils/operator-alerts.js +4 -2
  35. package/src/lib/app-utils/operator-alerts.js.map +1 -1
  36. package/src/lib/app-utils/org-permission-editor.d.ts +75 -0
  37. package/src/lib/app-utils/org-permission-editor.js +124 -0
  38. package/src/lib/app-utils/org-permission-editor.js.map +1 -0
  39. package/src/lib/app-utils/org-permissions.d.ts +36 -0
  40. package/src/lib/app-utils/org-permissions.js +78 -26
  41. package/src/lib/app-utils/org-permissions.js.map +1 -1
  42. package/src/lib/app-utils/org-role-grants.d.ts +59 -9
  43. package/src/lib/app-utils/org-role-grants.js +97 -10
  44. package/src/lib/app-utils/org-role-grants.js.map +1 -1
  45. package/src/lib/app-utils/plan-entitlements.d.ts +13 -0
  46. package/src/lib/app-utils/plan-entitlements.js +22 -0
  47. package/src/lib/app-utils/plan-entitlements.js.map +1 -1
  48. package/src/lib/app-utils/release-flags.d.ts +52 -1
  49. package/src/lib/app-utils/release-flags.js +68 -2
  50. package/src/lib/app-utils/release-flags.js.map +1 -1
  51. package/src/lib/app-utils/reusable-component-keys.d.ts +13 -0
  52. package/src/lib/app-utils/reusable-component-keys.js +12 -0
  53. package/src/lib/app-utils/reusable-component-keys.js.map +1 -1
  54. package/src/lib/app-utils/screen-analytics-aggregate.d.ts +34 -0
  55. package/src/lib/app-utils/screen-analytics-aggregate.js +37 -0
  56. package/src/lib/app-utils/screen-analytics-aggregate.js.map +1 -1
  57. package/src/lib/app-utils/site-care.d.ts +35 -0
  58. package/src/lib/app-utils/site-care.js +48 -0
  59. package/src/lib/app-utils/site-care.js.map +1 -1
  60. package/src/lib/app-utils/site-font-pairings.d.ts +59 -0
  61. package/src/lib/app-utils/site-font-pairings.js +514 -0
  62. package/src/lib/app-utils/site-font-pairings.js.map +1 -0
  63. package/src/lib/app-utils/site-starter.d.ts +136 -0
  64. package/src/lib/app-utils/site-starter.js +116 -0
  65. package/src/lib/app-utils/site-starter.js.map +1 -0
  66. package/src/lib/plugin-manager/feature-plugins.d.ts +32 -0
  67. package/src/lib/plugin-manager/feature-plugins.js +12 -0
  68. package/src/lib/plugin-manager/feature-plugins.js.map +1 -1
  69. package/src/lib/plugin-manager/first-party-plugins.generated.js +27 -1
  70. package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
  71. package/src/lib/plugin-manager/plugin-entitlements.d.ts +37 -0
  72. package/src/lib/plugin-manager/plugin-entitlements.js +84 -2
  73. package/src/lib/plugin-manager/plugin-entitlements.js.map +1 -1
  74. package/src/lib/plugin-manager/plugin-permissions.d.ts +7 -0
  75. package/src/lib/plugin-manager/plugin-permissions.js.map +1 -1
  76. package/src/lib/plugin-manager/plugin-product-catalog.d.ts +12 -0
  77. package/src/lib/plugin-manager/plugin-product-catalog.js.map +1 -1
  78. package/src/lib/plugin-manager/plugin-site-starters.d.ts +73 -0
  79. package/src/lib/plugin-manager/plugin-site-starters.js +30 -0
  80. package/src/lib/plugin-manager/plugin-site-starters.js.map +1 -0
  81. package/src/lib/plugin-manager/plugin-sitemap-sections.d.ts +8 -1
  82. package/src/lib/plugin-manager/plugin-sitemap-sections.js.map +1 -1
@@ -162,6 +162,31 @@ export interface PluginOrgPermissionDeclaration {
162
162
  * without it is never held by a collaborator.
163
163
  */
164
164
  hostRoleDefaults?: Readonly<Record<HostAccessRole, boolean>>;
165
+ /**
166
+ * The role editor's group for the key (AGL-4047): a core group id
167
+ * (`CORE_ORG_PERMISSION_GROUPS`) or, absent, a group of the plugin's own.
168
+ */
169
+ group?: string;
170
+ /** The heading of a group of the plugin's own; absent: the plugin's catalog label. */
171
+ groupLabel?: string;
172
+ }
173
+ /**
174
+ * One key of the ONE permission catalog that a plugin contributes (AGL-4047),
175
+ * whichever of the two declaration APIs it came through:
176
+ * `registerPluginEntitlements({ orgPermissions })` (`source: 'catalog'`) or
177
+ * `registerPluginPermissions` (`source: 'tier'`, its per-tier defaults read
178
+ * as org-role defaults, the owner on the admin tier).
179
+ */
180
+ export interface PluginCatalogPermission {
181
+ key: string;
182
+ label: string;
183
+ description: string;
184
+ pluginId: string;
185
+ roleDefaults: Readonly<Record<OrgRole, boolean>>;
186
+ hostRoleDefaults?: Readonly<Record<HostAccessRole, boolean>>;
187
+ group: string;
188
+ groupLabel: string;
189
+ source: 'catalog' | 'tier';
165
190
  }
166
191
  export interface PluginEntitlementRegistration {
167
192
  pluginId: string;
@@ -182,6 +207,18 @@ export declare function subscribePluginEntitlements(listener: () => void): () =>
182
207
  * or the two declarations disagree about what the key means.
183
208
  */
184
209
  export declare function registerPluginEntitlements(registration: PluginEntitlementRegistration): void;
210
+ /**
211
+ * Every key plugins contribute to the permission catalog, through EITHER
212
+ * declaration API, in catalog order (AGL-4047): per plugin, its
213
+ * `orgPermissions` declarations and then its `registerPluginPermissions`
214
+ * keys. A key both APIs carry for the same plugin is the catalog
215
+ * declaration's; one a DIFFERENT plugin already declared is refused and
216
+ * logged once, the first declaration keeping its meaning and defaults.
217
+ *
218
+ * Read live: both registries fill at plugin module scope, which can be after
219
+ * any reader of this was evaluated.
220
+ */
221
+ export declare function listPluginCatalogPermissions(): PluginCatalogPermission[];
185
222
  /** Every declared catalog permission, in catalog order, with the plugin that declared it. */
186
223
  export declare function listPluginOrgPermissions(): Array<PluginOrgPermissionDeclaration & {
187
224
  pluginId: string;
@@ -45,8 +45,9 @@
45
45
  * in a different order per session.
46
46
  */ import { _ as _extends } from "@swc/helpers/_/_extends";
47
47
  var _globalScope, _REGISTRY_KEY, _;
48
+ import { declarationRoleDefaults } from "../app-utils/org-role-grants.js";
48
49
  import { FIRST_PARTY_PLUGINS } from "./enabled-plugins.js";
49
- import { registerPluginPermissions } from "./plugin-permissions.js";
50
+ import { listPluginPermissions, registerPluginPermissions } from "./plugin-permissions.js";
50
51
  /**
51
52
  * Whether a plan sells a declared seat add-on: its own `soldOn`, else a
52
53
  * `perUnitByPlan` figure above zero for the plan, else (no quota) yes.
@@ -122,7 +123,10 @@ function refuseStolenKeys(pluginId, kind, keys, ownerOf) {
122
123
  if (!pluginId) throw new Error('plugin entitlements need a pluginId');
123
124
  refuseStolenKeys(pluginId, 'seat add-on', ((_registration_seatAddons = registration.seatAddons) != null ? _registration_seatAddons : []).map((entry)=>entry.key), (key)=>pluginSeatAddonOwner(key));
124
125
  refuseStolenKeys(pluginId, 'lockdown feature', ((_registration_lockdownFeatures = registration.lockdownFeatures) != null ? _registration_lockdownFeatures : []).map((entry)=>entry.key), (key)=>pluginLockdownFeatureOwner(key));
125
- refuseStolenKeys(pluginId, 'org permission', ((_registration_orgPermissions = registration.orgPermissions) != null ? _registration_orgPermissions : []).map((entry)=>entry.key), (key)=>pluginOrgPermissionOwner(key));
126
+ refuseStolenKeys(pluginId, 'org permission', ((_registration_orgPermissions = registration.orgPermissions) != null ? _registration_orgPermissions : []).map((entry)=>entry.key), (key)=>{
127
+ var _pluginOrgPermissionOwner;
128
+ return (_pluginOrgPermissionOwner = pluginOrgPermissionOwner(key)) != null ? _pluginOrgPermissionOwner : pluginTierPermissionOwner(key);
129
+ });
126
130
  registrations.set(pluginId, _extends({}, registration, {
127
131
  pluginId
128
132
  }));
@@ -158,6 +162,84 @@ function pluginOrgPermissionOwner(key) {
158
162
  }
159
163
  return undefined;
160
164
  }
165
+ function pluginTierPermissionOwner(key) {
166
+ var _listPluginPermissions_find;
167
+ return (_listPluginPermissions_find = listPluginPermissions().find((permission)=>permission.key === key)) == null ? void 0 : _listPluginPermissions_find.pluginId;
168
+ }
169
+ /** Refusals already logged by `listPluginCatalogPermissions`, so each is reported once. */ // boot-registry: per copy — it only de-duplicates a warning; a second compiled copy logging the same refusal once more is harmless, and nothing reads it to decide anything.
170
+ const reportedCatalogRefusals = new Set();
171
+ /** A plugin's catalog label, the heading its own permission group carries. */ function pluginLabel(pluginId) {
172
+ var _ref;
173
+ var _FIRST_PARTY_PLUGINS_find;
174
+ return (_ref = (_FIRST_PARTY_PLUGINS_find = FIRST_PARTY_PLUGINS.find((plugin)=>plugin.id === pluginId)) == null ? void 0 : _FIRST_PARTY_PLUGINS_find.label) != null ? _ref : pluginId;
175
+ }
176
+ /**
177
+ * Every key plugins contribute to the permission catalog, through EITHER
178
+ * declaration API, in catalog order (AGL-4047): per plugin, its
179
+ * `orgPermissions` declarations and then its `registerPluginPermissions`
180
+ * keys. A key both APIs carry for the same plugin is the catalog
181
+ * declaration's; one a DIFFERENT plugin already declared is refused and
182
+ * logged once, the first declaration keeping its meaning and defaults.
183
+ *
184
+ * Read live: both registries fill at plugin module scope, which can be after
185
+ * any reader of this was evaluated.
186
+ */ export function listPluginCatalogPermissions() {
187
+ const byPlugin = new Map();
188
+ const owner = new Map();
189
+ const add = (entry)=>{
190
+ var _byPlugin_get;
191
+ const incumbent = owner.get(entry.key);
192
+ if (incumbent !== undefined) {
193
+ if (incumbent !== entry.pluginId) {
194
+ const refusal = `${entry.pluginId}\x00${entry.key}`;
195
+ if (!reportedCatalogRefusals.has(refusal)) {
196
+ reportedCatalogRefusals.add(refusal);
197
+ console.error(`[plugins] refused permission key "${entry.key}" to "${entry.pluginId}": ` + `already declared by "${incumbent}"`);
198
+ }
199
+ }
200
+ return;
201
+ }
202
+ owner.set(entry.key, entry.pluginId);
203
+ const list = (_byPlugin_get = byPlugin.get(entry.pluginId)) != null ? _byPlugin_get : [];
204
+ list.push(entry);
205
+ byPlugin.set(entry.pluginId, list);
206
+ };
207
+ for (const permission of listPluginOrgPermissions()){
208
+ var _permission_group, _permission_groupLabel;
209
+ add(_extends({
210
+ key: permission.key,
211
+ label: permission.label,
212
+ description: permission.description,
213
+ pluginId: permission.pluginId,
214
+ roleDefaults: declarationRoleDefaults(permission)
215
+ }, permission.hostRoleDefaults ? {
216
+ hostRoleDefaults: permission.hostRoleDefaults
217
+ } : {}, {
218
+ group: (_permission_group = permission.group) != null ? _permission_group : permission.pluginId,
219
+ groupLabel: (_permission_groupLabel = permission.groupLabel) != null ? _permission_groupLabel : pluginLabel(permission.pluginId),
220
+ source: 'catalog'
221
+ }));
222
+ }
223
+ for (const permission of listPluginPermissions()){
224
+ var _permission_description, _permission_group1, _permission_groupLabel1;
225
+ add({
226
+ key: permission.key,
227
+ label: permission.label,
228
+ description: (_permission_description = permission.description) != null ? _permission_description : '',
229
+ pluginId: permission.pluginId,
230
+ roleDefaults: declarationRoleDefaults(permission),
231
+ group: (_permission_group1 = permission.group) != null ? _permission_group1 : permission.pluginId,
232
+ groupLabel: (_permission_groupLabel1 = permission.groupLabel) != null ? _permission_groupLabel1 : pluginLabel(permission.pluginId),
233
+ source: 'tier'
234
+ });
235
+ }
236
+ return [
237
+ ...byPlugin.keys()
238
+ ].sort((a, b)=>catalogRank(a) - catalogRank(b) || a.localeCompare(b)).flatMap((pluginId)=>{
239
+ var _byPlugin_get;
240
+ return (_byPlugin_get = byPlugin.get(pluginId)) != null ? _byPlugin_get : [];
241
+ });
242
+ }
161
243
  /** Every declared catalog permission, in catalog order, with the plugin that declared it. */ export function listPluginOrgPermissions() {
162
244
  return orderedRegistrations().flatMap((entry)=>{
163
245
  var _entry_orgPermissions;
@@ -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 { AglynOrgBilling, 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: the resolved entitlement named by `key`\n * gains `perUnit(org, plan) × units` when the figure depends on WHAT was\n * bought (the Aglyn AI tier, AGL-3965), else `perUnitByPlan[plan] × units`.\n * A plan the add-on is not sold on answers 0.\n */\n quota?: {\n key: string\n perUnitByPlan?: Readonly<Record<OrgPlan, number>>\n /**\n * The per-unit figure read off the org itself. `org` is the billing\n * document as `resolveOrgEntitlements` received it and `plan` its\n * effective plan.\n */\n perUnit?: (org: Readonly<Partial<AglynOrgBilling>>, plan: OrgPlan) => number\n }\n /** Feature flags a purchased unit switches on. */\n features?: readonly string[]\n /**\n * Whether a plan sells the add-on, when `perUnitByPlan` cannot say so\n * (a `perUnit` quota, AGL-3965). Absent: sold where `perUnitByPlan` names\n * a figure above zero, or everywhere for an add-on with no quota.\n */\n soldOn?: (plan: OrgPlan) => boolean\n}\n\n/**\n * Whether a plan sells a declared seat add-on: its own `soldOn`, else a\n * `perUnitByPlan` figure above zero for the plan, else (no quota) yes.\n */\nexport function pluginSeatAddonSoldOn(\n addon: PluginSeatAddonDeclaration,\n plan: OrgPlan,\n): boolean {\n if (addon.soldOn) return addon.soldOn(plan)\n if (!addon.quota) return true\n return (addon.quota.perUnitByPlan?.[plan] ?? 0) > 0\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","pluginSeatAddonSoldOn","addon","plan","soldOn","quota","perUnitByPlan","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","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 { AglynOrgBilling, HostAccessRole, OrgPlan, OrgRole } from '../foundation'\nimport { declarationRoleDefaults } from '../app-utils/org-role-grants'\nimport { FIRST_PARTY_PLUGINS } from './enabled-plugins'\nimport {\n listPluginPermissions,\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: the resolved entitlement named by `key`\n * gains `perUnit(org, plan) × units` when the figure depends on WHAT was\n * bought (the Aglyn AI tier, AGL-3965), else `perUnitByPlan[plan] × units`.\n * A plan the add-on is not sold on answers 0.\n */\n quota?: {\n key: string\n perUnitByPlan?: Readonly<Record<OrgPlan, number>>\n /**\n * The per-unit figure read off the org itself. `org` is the billing\n * document as `resolveOrgEntitlements` received it and `plan` its\n * effective plan.\n */\n perUnit?: (org: Readonly<Partial<AglynOrgBilling>>, plan: OrgPlan) => number\n }\n /** Feature flags a purchased unit switches on. */\n features?: readonly string[]\n /**\n * Whether a plan sells the add-on, when `perUnitByPlan` cannot say so\n * (a `perUnit` quota, AGL-3965). Absent: sold where `perUnitByPlan` names\n * a figure above zero, or everywhere for an add-on with no quota.\n */\n soldOn?: (plan: OrgPlan) => boolean\n}\n\n/**\n * Whether a plan sells a declared seat add-on: its own `soldOn`, else a\n * `perUnitByPlan` figure above zero for the plan, else (no quota) yes.\n */\nexport function pluginSeatAddonSoldOn(\n addon: PluginSeatAddonDeclaration,\n plan: OrgPlan,\n): boolean {\n if (addon.soldOn) return addon.soldOn(plan)\n if (!addon.quota) return true\n return (addon.quota.perUnitByPlan?.[plan] ?? 0) > 0\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 * The role editor's group for the key (AGL-4047): a core group id\n * (`CORE_ORG_PERMISSION_GROUPS`) or, absent, a group of the plugin's own.\n */\n group?: string\n /** The heading of a group of the plugin's own; absent: the plugin's catalog label. */\n groupLabel?: string\n}\n\n/**\n * One key of the ONE permission catalog that a plugin contributes (AGL-4047),\n * whichever of the two declaration APIs it came through:\n * `registerPluginEntitlements({ orgPermissions })` (`source: 'catalog'`) or\n * `registerPluginPermissions` (`source: 'tier'`, its per-tier defaults read\n * as org-role defaults, the owner on the admin tier).\n */\nexport interface PluginCatalogPermission {\n key: string\n label: string\n description: string\n pluginId: string\n roleDefaults: Readonly<Record<OrgRole, boolean>>\n hostRoleDefaults?: Readonly<Record<HostAccessRole, boolean>>\n group: string\n groupLabel: string\n source: 'catalog' | 'tier'\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) ?? pluginTierPermissionOwner(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\nfunction pluginTierPermissionOwner(key: string): string | undefined {\n return listPluginPermissions().find((permission) => permission.key === key)?.pluginId\n}\n\n/** Refusals already logged by `listPluginCatalogPermissions`, so each is reported once. */\n// boot-registry: per copy — it only de-duplicates a warning; a second compiled copy logging the same refusal once more is harmless, and nothing reads it to decide anything.\nconst reportedCatalogRefusals = new Set<string>()\n\n/** A plugin's catalog label, the heading its own permission group carries. */\nfunction pluginLabel(pluginId: string): string {\n return FIRST_PARTY_PLUGINS.find((plugin) => plugin.id === pluginId)?.label ?? pluginId\n}\n\n/**\n * Every key plugins contribute to the permission catalog, through EITHER\n * declaration API, in catalog order (AGL-4047): per plugin, its\n * `orgPermissions` declarations and then its `registerPluginPermissions`\n * keys. A key both APIs carry for the same plugin is the catalog\n * declaration's; one a DIFFERENT plugin already declared is refused and\n * logged once, the first declaration keeping its meaning and defaults.\n *\n * Read live: both registries fill at plugin module scope, which can be after\n * any reader of this was evaluated.\n */\nexport function listPluginCatalogPermissions(): PluginCatalogPermission[] {\n const byPlugin = new Map<string, PluginCatalogPermission[]>()\n const owner = new Map<string, string>()\n const add = (entry: PluginCatalogPermission): void => {\n const incumbent = owner.get(entry.key)\n if (incumbent !== undefined) {\n if (incumbent !== entry.pluginId) {\n const refusal = `${entry.pluginId}\\x00${entry.key}`\n if (!reportedCatalogRefusals.has(refusal)) {\n reportedCatalogRefusals.add(refusal)\n console.error(\n `[plugins] refused permission key \"${entry.key}\" to \"${entry.pluginId}\": ` +\n `already declared by \"${incumbent}\"`,\n )\n }\n }\n return\n }\n owner.set(entry.key, entry.pluginId)\n const list = byPlugin.get(entry.pluginId) ?? []\n list.push(entry)\n byPlugin.set(entry.pluginId, list)\n }\n for (const permission of listPluginOrgPermissions()) {\n add({\n key: permission.key,\n label: permission.label,\n description: permission.description,\n pluginId: permission.pluginId,\n roleDefaults: declarationRoleDefaults(permission),\n ...(permission.hostRoleDefaults ? { hostRoleDefaults: permission.hostRoleDefaults } : {}),\n group: permission.group ?? permission.pluginId,\n groupLabel: permission.groupLabel ?? pluginLabel(permission.pluginId),\n source: 'catalog',\n })\n }\n for (const permission of listPluginPermissions()) {\n add({\n key: permission.key,\n label: permission.label,\n description: permission.description ?? '',\n pluginId: permission.pluginId,\n roleDefaults: declarationRoleDefaults(permission),\n group: permission.group ?? permission.pluginId,\n groupLabel: permission.groupLabel ?? pluginLabel(permission.pluginId),\n source: 'tier',\n })\n }\n return [...byPlugin.keys()]\n .sort((a, b) => catalogRank(a) - catalogRank(b) || a.localeCompare(b))\n .flatMap((pluginId) => byPlugin.get(pluginId) ?? [])\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","declarationRoleDefaults","FIRST_PARTY_PLUGINS","listPluginPermissions","registerPluginPermissions","pluginSeatAddonSoldOn","addon","plan","soldOn","quota","perUnitByPlan","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","pluginTierPermissionOwner","set","permissions","length","permission","some","undefined","feature","find","reportedCatalogRefusals","pluginLabel","label","listPluginCatalogPermissions","byPlugin","incumbent","refusal","has","console","error","list","push","listPluginOrgPermissions","description","roleDefaults","hostRoleDefaults","group","groupLabel","source","flatMap","listPluginSeatAddons","pluginSeatAddon","listPluginFeatures","features","listPluginLockdownFeatures","pluginLockdownFeature","pluginIdForLockdownFeature","listPluginEntitlementRegistrations","resetPluginEntitlementsForTests","clear"],"mappings":"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"}
@@ -35,6 +35,13 @@ export interface PluginPermission {
35
35
  editor: boolean;
36
36
  viewer: boolean;
37
37
  };
38
+ /**
39
+ * The role editor's group for the key in the one permission catalog
40
+ * (AGL-4047): a core group id, or absent for a group of the plugin's own.
41
+ */
42
+ group?: string;
43
+ /** The heading of a group of the plugin's own; absent: the plugin's catalog label. */
44
+ groupLabel?: string;
38
45
  }
39
46
  /** Calls `listener` after every registration; answers the unsubscribe. */
40
47
  export declare function subscribePluginPermissions(listener: () => void): () => void;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-permissions.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 permissions (AGL-435, Strapi RBAC-actions parity): a\n * plugin contributes NAMED permission keys with per-role-tier defaults.\n * `resolveRolePermissions` spreads them into every resolved set (custom\n * roles override key-by-key exactly like built-in keys), so\n * `ConsolePluginPageProps.permissions` and API-side resolution carry them\n * with zero app changes. Register at module scope from both the client\n * barrel and the /server entry (the AGL-428 pattern).\n */\n\nexport interface PluginPermission {\n /** Stable key ('managePos') — persisted in custom role docs. */\n key: string\n pluginId: string\n label: string\n description?: string\n /** Verdict per built-in role tier when a role doesn't name the key. */\n defaults: { admin: boolean; editor: boolean; viewer: boolean }\n}\n\n/*\n * One registry per process, keyed on `globalThis` under `Symbol.for`, like\n * `plugin-services.ts`. Next compiles the boot file and the route handlers\n * apart, so each can hold its own copy of this module: with the map at module\n * scope, a plugin's keys registered by one copy were absent from the copy\n * `resolveOrgPermissions` read, an absent key reads as refused, and the\n * register's `managePos` gate answered 403 to the site's own owner.\n */\nconst REGISTRY_KEY = Symbol.for('@aglyn/aglyn:plugin-permissions')\n\ninterface PluginPermissionTables {\n permissions: Map<string, PluginPermission>\n listeners: Set<() => void>\n version: { value: number }\n}\n\nconst globalScope = globalThis as typeof globalThis & {\n [REGISTRY_KEY]?: PluginPermissionTables\n}\n\nconst tables: PluginPermissionTables =\n globalScope[REGISTRY_KEY] ??\n (globalScope[REGISTRY_KEY] = {\n permissions: new Map(),\n listeners: new Set(),\n version: { value: 0 },\n })\nconst pluginPermissions = tables.permissions\n\n/*\n * Whoever resolved a permission map before a plugin registered holds a map\n * without that plugin's keys, and a key that is absent reads as refused\n * (AGL-3228). The console's permission provider resolved its map when the\n * member document arrived; on a cold load of a plugin's deep link the\n * plugin's chunk can register AFTER that, so the owner was refused a surface\n * they hold by default until the next full render. So registration is\n * observable, the way `subscribePluginEntitlements` makes the org catalog's\n * registrations observable, and the provider re-reads on every change.\n */\nconst listeners = tables.listeners\n\n/** Calls `listener` after every registration; answers the unsubscribe. */\nexport function subscribePluginPermissions(listener: () => void): () => void {\n listeners.add(listener)\n return () => {\n listeners.delete(listener)\n }\n}\n\n/**\n * A number that changes whenever the registry does — the snapshot for a\n * `useSyncExternalStore` over `subscribePluginPermissions`.\n */\nexport function pluginPermissionsVersion(): number {\n return tables.version.value\n}\n\nfunction notifyListeners(): void {\n tables.version.value += 1\n for (const listener of [...listeners]) listener()\n}\n\n/**\n * Idempotent per key FOR ITS OWNER — the declaring plugin may re-register\n * (hot reload, a second surface, a repeated init) and its entry is replaced.\n *\n * A key already declared by a DIFFERENT plugin is refused (AGL-2484). This\n * map keyed on `permission.key` alone, so a later registration could redeclare\n * someone else's key with its own tier defaults — and these defaults are not\n * cosmetic: `managePos` is enforced server-side in\n * `libs/plugins/commerce/src/lib/server/pos-order.ts`, so moving\n * `defaults.viewer` to `true` hands a viewer the register. Reaching this\n * needs `PLUGIN_REMOTE_SERVER=enabled` (off by default), realm trust and a\n * signature; the registry closing its own door is the cheap part of that\n * stack.\n *\n * Per KEY, not per batch: a bundle that collides on one key still gets the\n * keys that are genuinely its own.\n */\nexport function registerPluginPermissions(\n permissions: readonly PluginPermission[],\n): void {\n for (const permission of permissions) {\n const incumbent = pluginPermissions.get(permission.key)\n if (incumbent !== undefined && incumbent.pluginId !== permission.pluginId) {\n console.error(\n `[plugins] refused permission key \"${permission.key}\" to ` +\n `\"${permission.pluginId}\": already declared by ` +\n `\"${incumbent.pluginId}\"`,\n )\n continue\n }\n pluginPermissions.set(permission.key, permission)\n }\n notifyListeners()\n}\n\nexport function listPluginPermissions(): PluginPermission[] {\n return [...pluginPermissions.values()]\n}\n\n/**\n * The registered keys alone (AGL-2474). `resolveRolePermissions` needs the\n * grantable key SPACE, not the tier verdicts: it used to iterate\n * `ORG_ROLE_PERMISSION_KEYS` — the legacy six — when applying overrides and\n * custom roles, so a plugin-declared key materialised into the resolved map\n * and could then only ever hold its tier default. Read live rather than\n * snapshotted: registration happens at plugin module scope, which can run\n * after any module-level constant here has already been evaluated.\n */\nexport function listPluginPermissionKeys(): string[] {\n return [...pluginPermissions.keys()]\n}\n\n/** Tier defaults for every registered plugin permission. */\nexport function pluginPermissionDefaults(\n tier: 'admin' | 'editor' | 'viewer',\n): Record<string, boolean> {\n const defaults: Record<string, boolean> = {}\n for (const permission of pluginPermissions.values()) {\n defaults[permission.key] = permission.defaults[tier]\n }\n return defaults\n}\n"],"names":["globalScope","REGISTRY_KEY","Symbol","for","globalThis","tables","permissions","Map","listeners","Set","version","value","pluginPermissions","subscribePluginPermissions","listener","add","delete","pluginPermissionsVersion","notifyListeners","registerPluginPermissions","permission","incumbent","get","key","undefined","pluginId","console","error","set","listPluginPermissions","values","listPluginPermissionKeys","keys","pluginPermissionDefaults","tier","defaults"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;CAQC,OAiCCA;AArBF;;;;;;;CAOC,GACD,MAAMC,eAAeC,OAAOC,GAAG,CAAC;AAQhC,MAAMH,cAAcI;AAIpB,MAAMC,UACJL,4BAAAA,WAAW,CAACC,aAAa,YAAzBD,4BACCA,WAAW,CAACC,aAAa,GAAG;IAC3BK,aAAa,IAAIC;IACjBC,WAAW,IAAIC;IACfC,SAAS;QAAEC,OAAO;IAAE;AACtB;AACF,MAAMC,oBAAoBP,OAAOC,WAAW;AAE5C;;;;;;;;;CASC,GACD,MAAME,YAAYH,OAAOG,SAAS;AAElC,wEAAwE,GACxE,OAAO,SAASK,2BAA2BC,QAAoB;IAC7DN,UAAUO,GAAG,CAACD;IACd,OAAO;QACLN,UAAUQ,MAAM,CAACF;IACnB;AACF;AAEA;;;CAGC,GACD,OAAO,SAASG;IACd,OAAOZ,OAAOK,OAAO,CAACC,KAAK;AAC7B;AAEA,SAASO;IACPb,OAAOK,OAAO,CAACC,KAAK,IAAI;IACxB,KAAK,MAAMG,YAAY;WAAIN;KAAU,CAAEM;AACzC;AAEA;;;;;;;;;;;;;;;;CAgBC,GACD,OAAO,SAASK,0BACdb,WAAwC;IAExC,KAAK,MAAMc,cAAcd,YAAa;QACpC,MAAMe,YAAYT,kBAAkBU,GAAG,CAACF,WAAWG,GAAG;QACtD,IAAIF,cAAcG,aAAaH,UAAUI,QAAQ,KAAKL,WAAWK,QAAQ,EAAE;YACzEC,QAAQC,KAAK,CACX,CAAC,kCAAkC,EAAEP,WAAWG,GAAG,CAAC,KAAK,CAAC,GACxD,CAAC,CAAC,EAAEH,WAAWK,QAAQ,CAAC,uBAAuB,CAAC,GAChD,CAAC,CAAC,EAAEJ,UAAUI,QAAQ,CAAC,CAAC,CAAC;YAE7B;QACF;QACAb,kBAAkBgB,GAAG,CAACR,WAAWG,GAAG,EAAEH;IACxC;IACAF;AACF;AAEA,OAAO,SAASW;IACd,OAAO;WAAIjB,kBAAkBkB,MAAM;KAAG;AACxC;AAEA;;;;;;;;CAQC,GACD,OAAO,SAASC;IACd,OAAO;WAAInB,kBAAkBoB,IAAI;KAAG;AACtC;AAEA,0DAA0D,GAC1D,OAAO,SAASC,yBACdC,IAAmC;IAEnC,MAAMC,WAAoC,CAAC;IAC3C,KAAK,MAAMf,cAAcR,kBAAkBkB,MAAM,GAAI;QACnDK,QAAQ,CAACf,WAAWG,GAAG,CAAC,GAAGH,WAAWe,QAAQ,CAACD,KAAK;IACtD;IACA,OAAOC;AACT"}
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-permissions.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 permissions (AGL-435, Strapi RBAC-actions parity): a\n * plugin contributes NAMED permission keys with per-role-tier defaults.\n * `resolveRolePermissions` spreads them into every resolved set (custom\n * roles override key-by-key exactly like built-in keys), so\n * `ConsolePluginPageProps.permissions` and API-side resolution carry them\n * with zero app changes. Register at module scope from both the client\n * barrel and the /server entry (the AGL-428 pattern).\n */\n\nexport interface PluginPermission {\n /** Stable key ('managePos') — persisted in custom role docs. */\n key: string\n pluginId: string\n label: string\n description?: string\n /** Verdict per built-in role tier when a role doesn't name the key. */\n defaults: { admin: boolean; editor: boolean; viewer: boolean }\n /**\n * The role editor's group for the key in the one permission catalog\n * (AGL-4047): a core group id, or absent for a group of the plugin's own.\n */\n group?: string\n /** The heading of a group of the plugin's own; absent: the plugin's catalog label. */\n groupLabel?: string\n}\n\n/*\n * One registry per process, keyed on `globalThis` under `Symbol.for`, like\n * `plugin-services.ts`. Next compiles the boot file and the route handlers\n * apart, so each can hold its own copy of this module: with the map at module\n * scope, a plugin's keys registered by one copy were absent from the copy\n * `resolveOrgPermissions` read, an absent key reads as refused, and the\n * register's `managePos` gate answered 403 to the site's own owner.\n */\nconst REGISTRY_KEY = Symbol.for('@aglyn/aglyn:plugin-permissions')\n\ninterface PluginPermissionTables {\n permissions: Map<string, PluginPermission>\n listeners: Set<() => void>\n version: { value: number }\n}\n\nconst globalScope = globalThis as typeof globalThis & {\n [REGISTRY_KEY]?: PluginPermissionTables\n}\n\nconst tables: PluginPermissionTables =\n globalScope[REGISTRY_KEY] ??\n (globalScope[REGISTRY_KEY] = {\n permissions: new Map(),\n listeners: new Set(),\n version: { value: 0 },\n })\nconst pluginPermissions = tables.permissions\n\n/*\n * Whoever resolved a permission map before a plugin registered holds a map\n * without that plugin's keys, and a key that is absent reads as refused\n * (AGL-3228). The console's permission provider resolved its map when the\n * member document arrived; on a cold load of a plugin's deep link the\n * plugin's chunk can register AFTER that, so the owner was refused a surface\n * they hold by default until the next full render. So registration is\n * observable, the way `subscribePluginEntitlements` makes the org catalog's\n * registrations observable, and the provider re-reads on every change.\n */\nconst listeners = tables.listeners\n\n/** Calls `listener` after every registration; answers the unsubscribe. */\nexport function subscribePluginPermissions(listener: () => void): () => void {\n listeners.add(listener)\n return () => {\n listeners.delete(listener)\n }\n}\n\n/**\n * A number that changes whenever the registry does — the snapshot for a\n * `useSyncExternalStore` over `subscribePluginPermissions`.\n */\nexport function pluginPermissionsVersion(): number {\n return tables.version.value\n}\n\nfunction notifyListeners(): void {\n tables.version.value += 1\n for (const listener of [...listeners]) listener()\n}\n\n/**\n * Idempotent per key FOR ITS OWNER — the declaring plugin may re-register\n * (hot reload, a second surface, a repeated init) and its entry is replaced.\n *\n * A key already declared by a DIFFERENT plugin is refused (AGL-2484). This\n * map keyed on `permission.key` alone, so a later registration could redeclare\n * someone else's key with its own tier defaults — and these defaults are not\n * cosmetic: `managePos` is enforced server-side in\n * `libs/plugins/commerce/src/lib/server/pos-order.ts`, so moving\n * `defaults.viewer` to `true` hands a viewer the register. Reaching this\n * needs `PLUGIN_REMOTE_SERVER=enabled` (off by default), realm trust and a\n * signature; the registry closing its own door is the cheap part of that\n * stack.\n *\n * Per KEY, not per batch: a bundle that collides on one key still gets the\n * keys that are genuinely its own.\n */\nexport function registerPluginPermissions(\n permissions: readonly PluginPermission[],\n): void {\n for (const permission of permissions) {\n const incumbent = pluginPermissions.get(permission.key)\n if (incumbent !== undefined && incumbent.pluginId !== permission.pluginId) {\n console.error(\n `[plugins] refused permission key \"${permission.key}\" to ` +\n `\"${permission.pluginId}\": already declared by ` +\n `\"${incumbent.pluginId}\"`,\n )\n continue\n }\n pluginPermissions.set(permission.key, permission)\n }\n notifyListeners()\n}\n\nexport function listPluginPermissions(): PluginPermission[] {\n return [...pluginPermissions.values()]\n}\n\n/**\n * The registered keys alone (AGL-2474). `resolveRolePermissions` needs the\n * grantable key SPACE, not the tier verdicts: it used to iterate\n * `ORG_ROLE_PERMISSION_KEYS` — the legacy six — when applying overrides and\n * custom roles, so a plugin-declared key materialised into the resolved map\n * and could then only ever hold its tier default. Read live rather than\n * snapshotted: registration happens at plugin module scope, which can run\n * after any module-level constant here has already been evaluated.\n */\nexport function listPluginPermissionKeys(): string[] {\n return [...pluginPermissions.keys()]\n}\n\n/** Tier defaults for every registered plugin permission. */\nexport function pluginPermissionDefaults(\n tier: 'admin' | 'editor' | 'viewer',\n): Record<string, boolean> {\n const defaults: Record<string, boolean> = {}\n for (const permission of pluginPermissions.values()) {\n defaults[permission.key] = permission.defaults[tier]\n }\n return defaults\n}\n"],"names":["globalScope","REGISTRY_KEY","Symbol","for","globalThis","tables","permissions","Map","listeners","Set","version","value","pluginPermissions","subscribePluginPermissions","listener","add","delete","pluginPermissionsVersion","notifyListeners","registerPluginPermissions","permission","incumbent","get","key","undefined","pluginId","console","error","set","listPluginPermissions","values","listPluginPermissionKeys","keys","pluginPermissionDefaults","tier","defaults"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;CAQC,OAwCCA;AArBF;;;;;;;CAOC,GACD,MAAMC,eAAeC,OAAOC,GAAG,CAAC;AAQhC,MAAMH,cAAcI;AAIpB,MAAMC,UACJL,4BAAAA,WAAW,CAACC,aAAa,YAAzBD,4BACCA,WAAW,CAACC,aAAa,GAAG;IAC3BK,aAAa,IAAIC;IACjBC,WAAW,IAAIC;IACfC,SAAS;QAAEC,OAAO;IAAE;AACtB;AACF,MAAMC,oBAAoBP,OAAOC,WAAW;AAE5C;;;;;;;;;CASC,GACD,MAAME,YAAYH,OAAOG,SAAS;AAElC,wEAAwE,GACxE,OAAO,SAASK,2BAA2BC,QAAoB;IAC7DN,UAAUO,GAAG,CAACD;IACd,OAAO;QACLN,UAAUQ,MAAM,CAACF;IACnB;AACF;AAEA;;;CAGC,GACD,OAAO,SAASG;IACd,OAAOZ,OAAOK,OAAO,CAACC,KAAK;AAC7B;AAEA,SAASO;IACPb,OAAOK,OAAO,CAACC,KAAK,IAAI;IACxB,KAAK,MAAMG,YAAY;WAAIN;KAAU,CAAEM;AACzC;AAEA;;;;;;;;;;;;;;;;CAgBC,GACD,OAAO,SAASK,0BACdb,WAAwC;IAExC,KAAK,MAAMc,cAAcd,YAAa;QACpC,MAAMe,YAAYT,kBAAkBU,GAAG,CAACF,WAAWG,GAAG;QACtD,IAAIF,cAAcG,aAAaH,UAAUI,QAAQ,KAAKL,WAAWK,QAAQ,EAAE;YACzEC,QAAQC,KAAK,CACX,CAAC,kCAAkC,EAAEP,WAAWG,GAAG,CAAC,KAAK,CAAC,GACxD,CAAC,CAAC,EAAEH,WAAWK,QAAQ,CAAC,uBAAuB,CAAC,GAChD,CAAC,CAAC,EAAEJ,UAAUI,QAAQ,CAAC,CAAC,CAAC;YAE7B;QACF;QACAb,kBAAkBgB,GAAG,CAACR,WAAWG,GAAG,EAAEH;IACxC;IACAF;AACF;AAEA,OAAO,SAASW;IACd,OAAO;WAAIjB,kBAAkBkB,MAAM;KAAG;AACxC;AAEA;;;;;;;;CAQC,GACD,OAAO,SAASC;IACd,OAAO;WAAInB,kBAAkBoB,IAAI;KAAG;AACtC;AAEA,0DAA0D,GAC1D,OAAO,SAASC,yBACdC,IAAmC;IAEnC,MAAMC,WAAoC,CAAC;IAC3C,KAAK,MAAMf,cAAcR,kBAAkBkB,MAAM,GAAI;QACnDK,QAAQ,CAACf,WAAWG,GAAG,CAAC,GAAGH,WAAWe,QAAQ,CAACD,KAAK;IACtD;IACA,OAAOC;AACT"}
@@ -153,6 +153,12 @@ export interface CatalogOffer {
153
153
  shipping: CatalogShippingPrice[];
154
154
  /** When the product last changed, epoch ms; absent when unknown. */
155
155
  updatedAtMs?: number;
156
+ /**
157
+ * The store sells this offer in person only — at its register, never online
158
+ * (AGL-4060). Present only on a page asked for with `inPersonOnly`; a reader
159
+ * that lists offers anywhere a shopper buys online never sees one.
160
+ */
161
+ inPersonOnly?: true;
156
162
  }
157
163
  export interface CatalogPage {
158
164
  offers: CatalogOffer[];
@@ -166,11 +172,17 @@ export interface PluginProductCatalog {
166
172
  * One page of the store's live, listed offers: never a draft, archived or
167
173
  * deleted product. About `limit` offers — a product's configurations are
168
174
  * never split across pages, so a page may run over by one product's worth.
175
+ *
176
+ * Offers the store sells in person only (AGL-4060) are left out, since a
177
+ * feed, an ad catalog, a marketplace or a delivery app sells online. A
178
+ * reader that only counts the shelf — a stock sync — asks with
179
+ * `inPersonOnly: true` and gets them too, each marked `inPersonOnly`.
169
180
  */
170
181
  page(request: {
171
182
  hostId: string;
172
183
  cursor?: string | null;
173
184
  limit: number;
185
+ inPersonOnly?: boolean;
174
186
  }): Promise<CatalogPage>;
175
187
  }
176
188
  export declare const PLUGIN_PRODUCT_CATALOG: import("./plugin-services").PluginServiceContract<PluginProductCatalog>;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-product-catalog.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n definePluginServiceContract,\n registerPluginService,\n resolvePluginServices,\n} from './plugin-services'\n\n/**\n * A site's sellable catalog, read whole and in pages by a plugin that does\n * not own it (AGL-3637).\n *\n * A product feed, a marketplace listing sync and an ad catalog all need the\n * same thing: every offer a store sells, with the facts a shopping channel\n * asks for — price in the store's currency, stock, photos, identifiers,\n * shipping. The plugin that keeps the products answers it here, so a reader\n * never learns where products are stored, how variants are modeled or what a\n * deleted product looks like.\n *\n * ## One OFFER per sellable configuration\n *\n * A product with one configuration is one offer whose `id` is the product's\n * own id. A product with several (sizes, colors) is one offer per\n * configuration, each with its own `id` and the product's id as `groupId`,\n * which is what every shopping channel calls the variant group.\n *\n * ## Money is integer minor units in the store's currency\n *\n * `priceMinor` and every other amount are integers (cents for USD) in\n * {@link CatalogStore.currency}. A reader formats them; it never assumes a\n * currency.\n *\n * ## Pages, not a list\n *\n * A store may hold far more products than one read should load, so the\n * catalog is walked in pages: `page()` answers up to about `limit` offers and\n * a `nextCursor` to continue from, `null` on the last page. A cursor is the\n * owner's own opaque string; a reader passes it back untouched.\n *\n * ## One owner\n *\n * A slot, like the tax profile: a site has one catalog, so a second plugin's\n * registration is refused and the incumbent keeps serving. No owner\n * registered is an ordinary answer — {@link pluginProductCatalog} returns\n * `undefined` and a reader serves nothing.\n */\n\n/** The store an offer belongs to, as a channel needs it. */\nexport interface CatalogStore {\n hostId: string\n /** The store's name, as its site calls itself. */\n name: string\n /**\n * The origin the store's pages are served on (`https://shop.example.com`),\n * or `null` when the site has no address yet.\n */\n origin: string | null\n /** ISO 4217, upper case. Every amount in the catalog is in it. */\n currency: string\n /**\n * Whether `/products/{slug}` pages are served. When `false`, every offer's\n * `path` answers 404, and a channel will refuse the offers.\n */\n productPagesServed: boolean\n /**\n * Countries the store's checkout prices shipping to by a live carrier\n * quote rather than its own table (ISO 3166-1 alpha-2, upper case). An\n * offer carries no {@link CatalogShippingPrice} for them, because the price\n * depends on the address; a channel that calculates carrier rates itself\n * prices them from the offer's weight and dimensions.\n */\n carrierPricedCountries: string[]\n}\n\nexport type CatalogAvailability = 'in_stock' | 'out_of_stock' | 'backorder'\nexport type CatalogCondition = 'new' | 'refurbished' | 'used'\nexport type CatalogProductKind = 'physical' | 'digital' | 'service'\n\n/** What one offer costs to ship to one country, cheapest rate first chosen. */\nexport interface CatalogShippingPrice {\n /** ISO 3166-1 alpha-2, upper case. */\n country: string\n /** The rate's name as the shopper sees it at checkout. */\n service: string\n priceMinor: number\n}\n\n/** One sellable configuration of a product. */\nexport interface CatalogOffer {\n /** Stable, unique within the store: the product id, or product and variant. */\n id: string\n /** The product's id: the variant group every configuration shares. */\n groupId: string\n /** Whether the product has more than one configuration. */\n hasVariants: boolean\n productId: string\n variantId: string\n /**\n * The merchant's own SKU for this configuration, as entered; absent when\n * they entered none. What a marketplace matches its listing by (AGL-3638).\n */\n sku?: string\n /** The product's name. */\n productName: string\n /** The product's name with this configuration's choices, e.g. `Tee — Red / M`. */\n title: string\n /** Plain text: no markup. May be empty. */\n description: string\n /** Site-relative path of the product page, e.g. `/products/tee`. */\n path: string\n /** Absolute URL of the photo shown for this configuration. */\n imageUrl?: string\n /** Absolute URLs of the product's other photos, in order. */\n additionalImageUrls: string[]\n /** What the shopper pays, before any sale: the regular price. */\n priceMinor: number\n /** A lower price the offer is on sale for now; absent when not on sale. */\n salePriceMinor?: number\n availability: CatalogAvailability\n /** Units in stock, or `null` when stock is not tracked. */\n quantity: number | null\n kind: CatalogProductKind\n /** Whether the product is sold only as a recurring subscription. */\n subscriptionOnly: boolean\n /** What the merchant entered for shopping channels; absent when they did not. */\n condition?: CatalogCondition\n brand?: string\n /** GTIN (UPC, EAN, ISBN, JAN, ITF-14) as entered, digits only. */\n gtin?: string\n mpn?: string\n /** A Google product taxonomy id or full path, as entered. */\n googleProductCategory?: string\n /** The store's own category path, e.g. `Apparel > Shirts`. */\n productType?: string\n /** The configuration's choices, by option name. */\n options: Readonly<Record<string, string>>\n /** The choice of an option named like a color, when there is one. */\n color?: string\n /** The choice of an option named like a size, when there is one. */\n size?: string\n weightGrams?: number\n /** One unit's packed size, in centimeters, when the merchant entered it. */\n dimensionsCm?: { length: number; width: number; height: number }\n /** What it costs to ship one unit, per country the store ships to. */\n shipping: CatalogShippingPrice[]\n /** When the product last changed, epoch ms; absent when unknown. */\n updatedAtMs?: number\n}\n\nexport interface CatalogPage {\n offers: CatalogOffer[]\n /** Where the next page starts, or `null` when this is the last one. */\n nextCursor: string | null\n}\n\nexport interface PluginProductCatalog {\n /** The store, or `null` for a site that does not sell. */\n store(hostId: string): Promise<CatalogStore | null>\n /**\n * One page of the store's live, listed offers: never a draft, archived or\n * deleted product. About `limit` offers — a product's configurations are\n * never split across pages, so a page may run over by one product's worth.\n */\n page(request: { hostId: string; cursor?: string | null; limit: number }): Promise<CatalogPage>\n}\n\nexport const PLUGIN_PRODUCT_CATALOG = definePluginServiceContract<PluginProductCatalog>(\n 'core.product-catalog',\n { multiple: false },\n)\n\n/** Registers the store catalog. A second plugin's is refused, naming both. */\nexport function registerPluginProductCatalog(\n catalog: PluginProductCatalog,\n options?: { pluginId?: string },\n): void {\n registerPluginService(PLUGIN_PRODUCT_CATALOG, catalog, {\n ...(options?.pluginId ? { pluginId: options.pluginId } : {}),\n })\n}\n\n/** The registered catalog, or `undefined` when no plugin sells. */\nexport function pluginProductCatalog(): PluginProductCatalog | undefined {\n return resolvePluginServices(PLUGIN_PRODUCT_CATALOG)[0]?.impl\n}\n\n/**\n * The plugin that publishes the catalog to shopping channels, as the\n * catalog's owner hands it an address of its own (AGL-3637).\n *\n * Before any plugin published feeds, the plugin that keeps the products\n * served one feed itself, and merchants pasted that address into their\n * channel. The address stays the owner's — a request for it reaches the\n * owner's routes — but the feed is written by whoever publishes the catalog\n * now, so the owner answers it from here. Nobody registered is an ordinary\n * answer: {@link pluginCatalogFeed} returns `undefined` and the owner\n * answers 404.\n */\nexport interface PluginCatalogFeed {\n /**\n * Answers a request for the owner's pre-existing feed address for the site\n * `hostId`: the feed, a 404 when the site's publisher has retired the\n * address or does not publish, or a 503 to try later.\n */\n serveLegacyFeed(request: Request, hostId: string): Promise<Response>\n}\n\nexport const PLUGIN_CATALOG_FEED = definePluginServiceContract<PluginCatalogFeed>(\n 'core.catalog-feed',\n { multiple: false },\n)\n\n/** Registers the catalog's feed publisher. A second plugin's is refused, naming both. */\nexport function registerPluginCatalogFeed(\n feed: PluginCatalogFeed,\n options?: { pluginId?: string },\n): void {\n registerPluginService(PLUGIN_CATALOG_FEED, feed, {\n ...(options?.pluginId ? { pluginId: options.pluginId } : {}),\n })\n}\n\n/** The registered feed publisher, or `undefined` when no plugin publishes feeds. */\nexport function pluginCatalogFeed(): PluginCatalogFeed | undefined {\n return resolvePluginServices(PLUGIN_CATALOG_FEED)[0]?.impl\n}\n"],"names":["definePluginServiceContract","registerPluginService","resolvePluginServices","PLUGIN_PRODUCT_CATALOG","multiple","registerPluginProductCatalog","catalog","options","pluginId","pluginProductCatalog","impl","PLUGIN_CATALOG_FEED","registerPluginCatalogFeed","feed","pluginCatalogFeed"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,2BAA2B,EAC3BC,qBAAqB,EACrBC,qBAAqB,QAChB,uBAAmB;AAgK1B,OAAO,MAAMC,yBAAyBH,4BACpC,wBACA;IAAEI,UAAU;AAAM,GACnB;AAED,4EAA4E,GAC5E,OAAO,SAASC,6BACdC,OAA6B,EAC7BC,OAA+B;IAE/BN,sBAAsBE,wBAAwBG,SAAS,aACjDC,CAAAA,2BAAAA,QAASC,QAAQ,IAAG;QAAEA,UAAUD,QAAQC,QAAQ;IAAC,IAAI,CAAC;AAE9D;AAEA,iEAAiE,GACjE,OAAO,SAASC;QACPP;IAAP,QAAOA,0BAAAA,sBAAsBC,uBAAuB,CAAC,EAAE,qBAAhDD,wBAAkDQ,IAAI;AAC/D;AAuBA,OAAO,MAAMC,sBAAsBX,4BACjC,qBACA;IAAEI,UAAU;AAAM,GACnB;AAED,uFAAuF,GACvF,OAAO,SAASQ,0BACdC,IAAuB,EACvBN,OAA+B;IAE/BN,sBAAsBU,qBAAqBE,MAAM,aAC3CN,CAAAA,2BAAAA,QAASC,QAAQ,IAAG;QAAEA,UAAUD,QAAQC,QAAQ;IAAC,IAAI,CAAC;AAE9D;AAEA,kFAAkF,GAClF,OAAO,SAASM;QACPZ;IAAP,QAAOA,0BAAAA,sBAAsBS,oBAAoB,CAAC,EAAE,qBAA7CT,wBAA+CQ,IAAI;AAC5D"}
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-product-catalog.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n definePluginServiceContract,\n registerPluginService,\n resolvePluginServices,\n} from './plugin-services'\n\n/**\n * A site's sellable catalog, read whole and in pages by a plugin that does\n * not own it (AGL-3637).\n *\n * A product feed, a marketplace listing sync and an ad catalog all need the\n * same thing: every offer a store sells, with the facts a shopping channel\n * asks for — price in the store's currency, stock, photos, identifiers,\n * shipping. The plugin that keeps the products answers it here, so a reader\n * never learns where products are stored, how variants are modeled or what a\n * deleted product looks like.\n *\n * ## One OFFER per sellable configuration\n *\n * A product with one configuration is one offer whose `id` is the product's\n * own id. A product with several (sizes, colors) is one offer per\n * configuration, each with its own `id` and the product's id as `groupId`,\n * which is what every shopping channel calls the variant group.\n *\n * ## Money is integer minor units in the store's currency\n *\n * `priceMinor` and every other amount are integers (cents for USD) in\n * {@link CatalogStore.currency}. A reader formats them; it never assumes a\n * currency.\n *\n * ## Pages, not a list\n *\n * A store may hold far more products than one read should load, so the\n * catalog is walked in pages: `page()` answers up to about `limit` offers and\n * a `nextCursor` to continue from, `null` on the last page. A cursor is the\n * owner's own opaque string; a reader passes it back untouched.\n *\n * ## One owner\n *\n * A slot, like the tax profile: a site has one catalog, so a second plugin's\n * registration is refused and the incumbent keeps serving. No owner\n * registered is an ordinary answer — {@link pluginProductCatalog} returns\n * `undefined` and a reader serves nothing.\n */\n\n/** The store an offer belongs to, as a channel needs it. */\nexport interface CatalogStore {\n hostId: string\n /** The store's name, as its site calls itself. */\n name: string\n /**\n * The origin the store's pages are served on (`https://shop.example.com`),\n * or `null` when the site has no address yet.\n */\n origin: string | null\n /** ISO 4217, upper case. Every amount in the catalog is in it. */\n currency: string\n /**\n * Whether `/products/{slug}` pages are served. When `false`, every offer's\n * `path` answers 404, and a channel will refuse the offers.\n */\n productPagesServed: boolean\n /**\n * Countries the store's checkout prices shipping to by a live carrier\n * quote rather than its own table (ISO 3166-1 alpha-2, upper case). An\n * offer carries no {@link CatalogShippingPrice} for them, because the price\n * depends on the address; a channel that calculates carrier rates itself\n * prices them from the offer's weight and dimensions.\n */\n carrierPricedCountries: string[]\n}\n\nexport type CatalogAvailability = 'in_stock' | 'out_of_stock' | 'backorder'\nexport type CatalogCondition = 'new' | 'refurbished' | 'used'\nexport type CatalogProductKind = 'physical' | 'digital' | 'service'\n\n/** What one offer costs to ship to one country, cheapest rate first chosen. */\nexport interface CatalogShippingPrice {\n /** ISO 3166-1 alpha-2, upper case. */\n country: string\n /** The rate's name as the shopper sees it at checkout. */\n service: string\n priceMinor: number\n}\n\n/** One sellable configuration of a product. */\nexport interface CatalogOffer {\n /** Stable, unique within the store: the product id, or product and variant. */\n id: string\n /** The product's id: the variant group every configuration shares. */\n groupId: string\n /** Whether the product has more than one configuration. */\n hasVariants: boolean\n productId: string\n variantId: string\n /**\n * The merchant's own SKU for this configuration, as entered; absent when\n * they entered none. What a marketplace matches its listing by (AGL-3638).\n */\n sku?: string\n /** The product's name. */\n productName: string\n /** The product's name with this configuration's choices, e.g. `Tee — Red / M`. */\n title: string\n /** Plain text: no markup. May be empty. */\n description: string\n /** Site-relative path of the product page, e.g. `/products/tee`. */\n path: string\n /** Absolute URL of the photo shown for this configuration. */\n imageUrl?: string\n /** Absolute URLs of the product's other photos, in order. */\n additionalImageUrls: string[]\n /** What the shopper pays, before any sale: the regular price. */\n priceMinor: number\n /** A lower price the offer is on sale for now; absent when not on sale. */\n salePriceMinor?: number\n availability: CatalogAvailability\n /** Units in stock, or `null` when stock is not tracked. */\n quantity: number | null\n kind: CatalogProductKind\n /** Whether the product is sold only as a recurring subscription. */\n subscriptionOnly: boolean\n /** What the merchant entered for shopping channels; absent when they did not. */\n condition?: CatalogCondition\n brand?: string\n /** GTIN (UPC, EAN, ISBN, JAN, ITF-14) as entered, digits only. */\n gtin?: string\n mpn?: string\n /** A Google product taxonomy id or full path, as entered. */\n googleProductCategory?: string\n /** The store's own category path, e.g. `Apparel > Shirts`. */\n productType?: string\n /** The configuration's choices, by option name. */\n options: Readonly<Record<string, string>>\n /** The choice of an option named like a color, when there is one. */\n color?: string\n /** The choice of an option named like a size, when there is one. */\n size?: string\n weightGrams?: number\n /** One unit's packed size, in centimeters, when the merchant entered it. */\n dimensionsCm?: { length: number; width: number; height: number }\n /** What it costs to ship one unit, per country the store ships to. */\n shipping: CatalogShippingPrice[]\n /** When the product last changed, epoch ms; absent when unknown. */\n updatedAtMs?: number\n /**\n * The store sells this offer in person only — at its register, never online\n * (AGL-4060). Present only on a page asked for with `inPersonOnly`; a reader\n * that lists offers anywhere a shopper buys online never sees one.\n */\n inPersonOnly?: true\n}\n\nexport interface CatalogPage {\n offers: CatalogOffer[]\n /** Where the next page starts, or `null` when this is the last one. */\n nextCursor: string | null\n}\n\nexport interface PluginProductCatalog {\n /** The store, or `null` for a site that does not sell. */\n store(hostId: string): Promise<CatalogStore | null>\n /**\n * One page of the store's live, listed offers: never a draft, archived or\n * deleted product. About `limit` offers — a product's configurations are\n * never split across pages, so a page may run over by one product's worth.\n *\n * Offers the store sells in person only (AGL-4060) are left out, since a\n * feed, an ad catalog, a marketplace or a delivery app sells online. A\n * reader that only counts the shelf — a stock sync — asks with\n * `inPersonOnly: true` and gets them too, each marked `inPersonOnly`.\n */\n page(request: {\n hostId: string\n cursor?: string | null\n limit: number\n inPersonOnly?: boolean\n }): Promise<CatalogPage>\n}\n\nexport const PLUGIN_PRODUCT_CATALOG = definePluginServiceContract<PluginProductCatalog>(\n 'core.product-catalog',\n { multiple: false },\n)\n\n/** Registers the store catalog. A second plugin's is refused, naming both. */\nexport function registerPluginProductCatalog(\n catalog: PluginProductCatalog,\n options?: { pluginId?: string },\n): void {\n registerPluginService(PLUGIN_PRODUCT_CATALOG, catalog, {\n ...(options?.pluginId ? { pluginId: options.pluginId } : {}),\n })\n}\n\n/** The registered catalog, or `undefined` when no plugin sells. */\nexport function pluginProductCatalog(): PluginProductCatalog | undefined {\n return resolvePluginServices(PLUGIN_PRODUCT_CATALOG)[0]?.impl\n}\n\n/**\n * The plugin that publishes the catalog to shopping channels, as the\n * catalog's owner hands it an address of its own (AGL-3637).\n *\n * Before any plugin published feeds, the plugin that keeps the products\n * served one feed itself, and merchants pasted that address into their\n * channel. The address stays the owner's — a request for it reaches the\n * owner's routes — but the feed is written by whoever publishes the catalog\n * now, so the owner answers it from here. Nobody registered is an ordinary\n * answer: {@link pluginCatalogFeed} returns `undefined` and the owner\n * answers 404.\n */\nexport interface PluginCatalogFeed {\n /**\n * Answers a request for the owner's pre-existing feed address for the site\n * `hostId`: the feed, a 404 when the site's publisher has retired the\n * address or does not publish, or a 503 to try later.\n */\n serveLegacyFeed(request: Request, hostId: string): Promise<Response>\n}\n\nexport const PLUGIN_CATALOG_FEED = definePluginServiceContract<PluginCatalogFeed>(\n 'core.catalog-feed',\n { multiple: false },\n)\n\n/** Registers the catalog's feed publisher. A second plugin's is refused, naming both. */\nexport function registerPluginCatalogFeed(\n feed: PluginCatalogFeed,\n options?: { pluginId?: string },\n): void {\n registerPluginService(PLUGIN_CATALOG_FEED, feed, {\n ...(options?.pluginId ? { pluginId: options.pluginId } : {}),\n })\n}\n\n/** The registered feed publisher, or `undefined` when no plugin publishes feeds. */\nexport function pluginCatalogFeed(): PluginCatalogFeed | undefined {\n return resolvePluginServices(PLUGIN_CATALOG_FEED)[0]?.impl\n}\n"],"names":["definePluginServiceContract","registerPluginService","resolvePluginServices","PLUGIN_PRODUCT_CATALOG","multiple","registerPluginProductCatalog","catalog","options","pluginId","pluginProductCatalog","impl","PLUGIN_CATALOG_FEED","registerPluginCatalogFeed","feed","pluginCatalogFeed"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,2BAA2B,EAC3BC,qBAAqB,EACrBC,qBAAqB,QAChB,uBAAmB;AAgL1B,OAAO,MAAMC,yBAAyBH,4BACpC,wBACA;IAAEI,UAAU;AAAM,GACnB;AAED,4EAA4E,GAC5E,OAAO,SAASC,6BACdC,OAA6B,EAC7BC,OAA+B;IAE/BN,sBAAsBE,wBAAwBG,SAAS,aACjDC,CAAAA,2BAAAA,QAASC,QAAQ,IAAG;QAAEA,UAAUD,QAAQC,QAAQ;IAAC,IAAI,CAAC;AAE9D;AAEA,iEAAiE,GACjE,OAAO,SAASC;QACPP;IAAP,QAAOA,0BAAAA,sBAAsBC,uBAAuB,CAAC,EAAE,qBAAhDD,wBAAkDQ,IAAI;AAC/D;AAuBA,OAAO,MAAMC,sBAAsBX,4BACjC,qBACA;IAAEI,UAAU;AAAM,GACnB;AAED,uFAAuF,GACvF,OAAO,SAASQ,0BACdC,IAAuB,EACvBN,OAA+B;IAE/BN,sBAAsBU,qBAAqBE,MAAM,aAC3CN,CAAAA,2BAAAA,QAASC,QAAQ,IAAG;QAAEA,UAAUD,QAAQC,QAAQ;IAAC,IAAI,CAAC;AAE9D;AAEA,kFAAkF,GAClF,OAAO,SAASM;QACPZ;IAAP,QAAOA,0BAAAA,sBAAsBS,oBAAoB,CAAC,EAAE,qBAA7CT,wBAA+CQ,IAAI;AAC5D"}
@@ -0,0 +1,73 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import type { SiteStarterChoice, SiteStarterFacets, SiteStarterPayload } from '../app-utils/site-starter';
18
+ /**
19
+ * Where site starters come from, as the console's SERVER reads them
20
+ * (AGL-4049).
21
+ *
22
+ * "Create site" offers a starter for every site, chosen before the site
23
+ * exists, and the site is created from the one picked. The starters are
24
+ * listings a plugin keeps — searched, priced and bought there — so the core
25
+ * names the two questions the create path asks and one plugin answers them:
26
+ * which starters match a search, and what a picked one creates. With none
27
+ * registered there are no starters to offer, and the create path keeps its
28
+ * own default site.
29
+ */
30
+ /** One starter as a search lists it. */
31
+ export interface SiteStarterSummary {
32
+ listingId: string;
33
+ displayName: string;
34
+ description: string;
35
+ /** Who made it, as the search shows them. */
36
+ authorName: string;
37
+ /** Published by the platform itself; listed first. */
38
+ official: boolean;
39
+ /** The price in US dollars; 0 for free. */
40
+ priceUsd: number;
41
+ previewImageUrl: string | null;
42
+ facets: SiteStarterFacets;
43
+ }
44
+ /** Who is asking: the workspace the site will belong to, and the person. */
45
+ export interface SiteStarterViewer {
46
+ orgId: string | null;
47
+ uid: string;
48
+ }
49
+ export interface PluginSiteStarterSource {
50
+ /** The starters matching a search, best first, at most `limit` of them. */
51
+ search(input: {
52
+ word: string;
53
+ filter: Partial<SiteStarterFacets>;
54
+ limit: number;
55
+ }, viewer: SiteStarterViewer): Promise<SiteStarterSummary[]>;
56
+ /**
57
+ * What a picked starter creates, or why this viewer cannot have it: 402
58
+ * while a priced starter is unpaid (with its price), 403 when the viewer may
59
+ * not install it, 404 when it is gone.
60
+ */
61
+ resolve(choice: SiteStarterChoice, viewer: SiteStarterViewer): Promise<SiteStarterPayload | {
62
+ error: string;
63
+ status: 402 | 403 | 404;
64
+ priceUsd?: number;
65
+ }>;
66
+ }
67
+ export declare const PLUGIN_SITE_STARTERS: import("./plugin-services").PluginServiceContract<PluginSiteStarterSource>;
68
+ /** Registers the plugin that serves site starters. One per deployment; a second is refused. */
69
+ export declare function registerPluginSiteStarterSource(impl: PluginSiteStarterSource, options: {
70
+ pluginId: string;
71
+ }): void;
72
+ /** The registered starter source, or `null` when no plugin serves starters. */
73
+ export declare function resolveSiteStarterSource(): PluginSiteStarterSource | null;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ import { definePluginServiceContract, registerPluginService, resolvePluginService } from "./plugin-services.js";
17
+ export const PLUGIN_SITE_STARTERS = definePluginServiceContract('core.site-starters', {
18
+ multiple: false
19
+ });
20
+ /** Registers the plugin that serves site starters. One per deployment; a second is refused. */ export function registerPluginSiteStarterSource(impl, options) {
21
+ registerPluginService(PLUGIN_SITE_STARTERS, impl, {
22
+ pluginId: options.pluginId
23
+ });
24
+ }
25
+ /** The registered starter source, or `null` when no plugin serves starters. */ export function resolveSiteStarterSource() {
26
+ var _resolvePluginService;
27
+ return (_resolvePluginService = resolvePluginService(PLUGIN_SITE_STARTERS)) != null ? _resolvePluginService : null;
28
+ }
29
+
30
+ //# sourceMappingURL=plugin-site-starters.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-site-starters.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type {\n SiteStarterChoice,\n SiteStarterFacets,\n SiteStarterPayload,\n} from '../app-utils/site-starter'\nimport {\n definePluginServiceContract,\n registerPluginService,\n resolvePluginService,\n} from './plugin-services'\n\n/**\n * Where site starters come from, as the console's SERVER reads them\n * (AGL-4049).\n *\n * \"Create site\" offers a starter for every site, chosen before the site\n * exists, and the site is created from the one picked. The starters are\n * listings a plugin keeps — searched, priced and bought there — so the core\n * names the two questions the create path asks and one plugin answers them:\n * which starters match a search, and what a picked one creates. With none\n * registered there are no starters to offer, and the create path keeps its\n * own default site.\n */\n\n/** One starter as a search lists it. */\nexport interface SiteStarterSummary {\n listingId: string\n displayName: string\n description: string\n /** Who made it, as the search shows them. */\n authorName: string\n /** Published by the platform itself; listed first. */\n official: boolean\n /** The price in US dollars; 0 for free. */\n priceUsd: number\n previewImageUrl: string | null\n facets: SiteStarterFacets\n}\n\n/** Who is asking: the workspace the site will belong to, and the person. */\nexport interface SiteStarterViewer {\n orgId: string | null\n uid: string\n}\n\nexport interface PluginSiteStarterSource {\n /** The starters matching a search, best first, at most `limit` of them. */\n search(\n input: { word: string; filter: Partial<SiteStarterFacets>; limit: number },\n viewer: SiteStarterViewer,\n ): Promise<SiteStarterSummary[]>\n /**\n * What a picked starter creates, or why this viewer cannot have it: 402\n * while a priced starter is unpaid (with its price), 403 when the viewer may\n * not install it, 404 when it is gone.\n */\n resolve(\n choice: SiteStarterChoice,\n viewer: SiteStarterViewer,\n ): Promise<SiteStarterPayload | { error: string; status: 402 | 403 | 404; priceUsd?: number }>\n}\n\nexport const PLUGIN_SITE_STARTERS = definePluginServiceContract<PluginSiteStarterSource>(\n 'core.site-starters',\n { multiple: false },\n)\n\n/** Registers the plugin that serves site starters. One per deployment; a second is refused. */\nexport function registerPluginSiteStarterSource(\n impl: PluginSiteStarterSource,\n options: { pluginId: string },\n): void {\n registerPluginService(PLUGIN_SITE_STARTERS, impl, { pluginId: options.pluginId })\n}\n\n/** The registered starter source, or `null` when no plugin serves starters. */\nexport function resolveSiteStarterSource(): PluginSiteStarterSource | null {\n return resolvePluginService(PLUGIN_SITE_STARTERS) ?? null\n}\n"],"names":["definePluginServiceContract","registerPluginService","resolvePluginService","PLUGIN_SITE_STARTERS","multiple","registerPluginSiteStarterSource","impl","options","pluginId","resolveSiteStarterSource"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAOD,SACEA,2BAA2B,EAC3BC,qBAAqB,EACrBC,oBAAoB,QACf,uBAAmB;AAqD1B,OAAO,MAAMC,uBAAuBH,4BAClC,sBACA;IAAEI,UAAU;AAAM,GACnB;AAED,6FAA6F,GAC7F,OAAO,SAASC,gCACdC,IAA6B,EAC7BC,OAA6B;IAE7BN,sBAAsBE,sBAAsBG,MAAM;QAAEE,UAAUD,QAAQC,QAAQ;IAAC;AACjF;AAEA,6EAA6E,GAC7E,OAAO,SAASC;QACPP;IAAP,QAAOA,wBAAAA,qBAAqBC,iCAArBD,wBAA8C;AACvD"}
@@ -42,7 +42,8 @@
42
42
  *
43
43
  * Only what a sitemap needs and a query can express: one equality filter, the
44
44
  * setting that must be set for the pages to render at all, the address with
45
- * `{slug}` in it, a field that drops a row (a soft delete), and the date
45
+ * `{slug}` in it, a field that drops a row (a soft delete), a switch whose
46
+ * `false` drops one (a page turned off), and the date
46
47
  * fields to try in order. A row without a slug addresses nothing and is left
47
48
  * out. Anything richer belongs to the plugin's own page, not to its sitemap.
48
49
  *
@@ -73,6 +74,12 @@ export interface PluginSitemapSectionDeclaration {
73
74
  slugField?: string;
74
75
  /** A field whose truthy value leaves the row out, for a soft delete the filter cannot see. */
75
76
  skipWhen?: string;
77
+ /**
78
+ * A field whose `false` leaves the row out: a switch that is on unless it
79
+ * was turned off, so an absent value keeps the row — a product sold in
80
+ * person only (AGL-4060). A query cannot ask for "absent or true".
81
+ */
82
+ skipWhenFalse?: string;
76
83
  /** The fields a row's `lastmod` is read from, first present wins. */
77
84
  lastmod?: readonly string[];
78
85
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-sitemap-sections.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { PLUGIN_SITEMAP_SECTIONS_DECLARED } from './first-party-plugins.generated'\n\n/**\n * A child sitemap a plugin's documents fill, declared by that plugin\n * (AGL-3080).\n *\n * The tenant's `/sitemap.xml` is an index over one child per section (AGL-2520):\n * the site's pages, its content collections and its authors are the platform's\n * and are built by the route itself. A plugin that serves pages of its own — a\n * product at `/products/{slug}` — had its section written into that route by\n * hand: the collection, the filter, the store setting that switches it on, the\n * address and the date field were all core's knowledge of one plugin's model.\n *\n * So the plugin declares the section, and the route builds every declared one\n * the same way: counted for the index, paged by document id for the child.\n *\n * ## Compiled, because the reader is a published page's\n *\n * The sitemap is what a search engine is told the site holds. A section\n * registered at runtime would be missing from any process that had not loaded\n * the plugin, and a crawler reads a missing section as pages that no longer\n * exist — the products drop out of the index silently, on a live site. So\n * sections are compiled from `plugins.config.json` by\n * `tools/scripts/generate-plugin-manifests.mjs`, present the moment this\n * module is imported, and there is no registrar.\n *\n * ## What a declaration can say\n *\n * Only what a sitemap needs and a query can express: one equality filter, the\n * setting that must be set for the pages to render at all, the address with\n * `{slug}` in it, a field that drops a row (a soft delete), and the date\n * fields to try in order. A row without a slug addresses nothing and is left\n * out. Anything richer belongs to the plugin's own page, not to its sitemap.\n *\n * Reached by its own subpath, never the barrel.\n */\nexport interface PluginSitemapSectionDeclaration {\n /** The section's path segment: `/sitemaps/{section}/{page}.xml`. */\n section: string\n /** The subcollection under `hosts/{hostId}` whose documents are the pages. */\n collection: string\n /** The one equality a document must meet to be a page. */\n where?: { field: string; equals: string | number | boolean }\n /**\n * A `collection/doc` under the host and a field on it that must be set for\n * the section to exist: the template the pages render through. Unset, the\n * section is left out rather than listing addresses that 404.\n */\n enabledBy?: { doc: string; field: string }\n /** The page's site path, with `{slug}` standing for the document's slug. */\n path: string\n /** The field holding the slug; `slug` when absent. */\n slugField?: string\n /** A field whose truthy value leaves the row out, for a soft delete the filter cannot see. */\n skipWhen?: string\n /** The fields a row's `lastmod` is read from, first present wins. */\n lastmod?: readonly string[]\n}\n\n/** A declaration with the plugin that made it. */\nexport type ResolvedPluginSitemapSection = PluginSitemapSectionDeclaration & {\n pluginId: string\n}\n\n/** Every declared section, in config order — the order the index lists them. */\nexport function listPluginSitemapSections(): readonly ResolvedPluginSitemapSection[] {\n return PLUGIN_SITEMAP_SECTIONS_DECLARED\n}\n\n/** One declared section by its name, or `null`. */\nexport function pluginSitemapSection(\n section: string,\n): ResolvedPluginSitemapSection | null {\n return PLUGIN_SITEMAP_SECTIONS_DECLARED.find((one) => one.section === section) ?? null\n}\n\n/** The site path of one row, or `null` for a row with no slug. */\nexport function pluginSitemapSectionPath(\n declaration: PluginSitemapSectionDeclaration,\n slug: unknown,\n): string | null {\n if (typeof slug !== 'string' || !slug) return null\n return declaration.path.replace('{slug}', slug)\n}\n"],"names":["PLUGIN_SITEMAP_SECTIONS_DECLARED","listPluginSitemapSections","pluginSitemapSection","section","find","one","pluginSitemapSectionPath","declaration","slug","path","replace"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,gCAAgC,QAAQ,qCAAiC;AAgElF,8EAA8E,GAC9E,OAAO,SAASC;IACd,OAAOD;AACT;AAEA,iDAAiD,GACjD,OAAO,SAASE,qBACdC,OAAe;QAERH;IAAP,QAAOA,yCAAAA,iCAAiCI,IAAI,CAAC,CAACC,MAAQA,IAAIF,OAAO,KAAKA,oBAA/DH,yCAA2E;AACpF;AAEA,gEAAgE,GAChE,OAAO,SAASM,yBACdC,WAA4C,EAC5CC,IAAa;IAEb,IAAI,OAAOA,SAAS,YAAY,CAACA,MAAM,OAAO;IAC9C,OAAOD,YAAYE,IAAI,CAACC,OAAO,CAAC,UAAUF;AAC5C"}
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-sitemap-sections.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { PLUGIN_SITEMAP_SECTIONS_DECLARED } from './first-party-plugins.generated'\n\n/**\n * A child sitemap a plugin's documents fill, declared by that plugin\n * (AGL-3080).\n *\n * The tenant's `/sitemap.xml` is an index over one child per section (AGL-2520):\n * the site's pages, its content collections and its authors are the platform's\n * and are built by the route itself. A plugin that serves pages of its own — a\n * product at `/products/{slug}` — had its section written into that route by\n * hand: the collection, the filter, the store setting that switches it on, the\n * address and the date field were all core's knowledge of one plugin's model.\n *\n * So the plugin declares the section, and the route builds every declared one\n * the same way: counted for the index, paged by document id for the child.\n *\n * ## Compiled, because the reader is a published page's\n *\n * The sitemap is what a search engine is told the site holds. A section\n * registered at runtime would be missing from any process that had not loaded\n * the plugin, and a crawler reads a missing section as pages that no longer\n * exist — the products drop out of the index silently, on a live site. So\n * sections are compiled from `plugins.config.json` by\n * `tools/scripts/generate-plugin-manifests.mjs`, present the moment this\n * module is imported, and there is no registrar.\n *\n * ## What a declaration can say\n *\n * Only what a sitemap needs and a query can express: one equality filter, the\n * setting that must be set for the pages to render at all, the address with\n * `{slug}` in it, a field that drops a row (a soft delete), a switch whose\n * `false` drops one (a page turned off), and the date\n * fields to try in order. A row without a slug addresses nothing and is left\n * out. Anything richer belongs to the plugin's own page, not to its sitemap.\n *\n * Reached by its own subpath, never the barrel.\n */\nexport interface PluginSitemapSectionDeclaration {\n /** The section's path segment: `/sitemaps/{section}/{page}.xml`. */\n section: string\n /** The subcollection under `hosts/{hostId}` whose documents are the pages. */\n collection: string\n /** The one equality a document must meet to be a page. */\n where?: { field: string; equals: string | number | boolean }\n /**\n * A `collection/doc` under the host and a field on it that must be set for\n * the section to exist: the template the pages render through. Unset, the\n * section is left out rather than listing addresses that 404.\n */\n enabledBy?: { doc: string; field: string }\n /** The page's site path, with `{slug}` standing for the document's slug. */\n path: string\n /** The field holding the slug; `slug` when absent. */\n slugField?: string\n /** A field whose truthy value leaves the row out, for a soft delete the filter cannot see. */\n skipWhen?: string\n /**\n * A field whose `false` leaves the row out: a switch that is on unless it\n * was turned off, so an absent value keeps the row — a product sold in\n * person only (AGL-4060). A query cannot ask for \"absent or true\".\n */\n skipWhenFalse?: string\n /** The fields a row's `lastmod` is read from, first present wins. */\n lastmod?: readonly string[]\n}\n\n/** A declaration with the plugin that made it. */\nexport type ResolvedPluginSitemapSection = PluginSitemapSectionDeclaration & {\n pluginId: string\n}\n\n/** Every declared section, in config order — the order the index lists them. */\nexport function listPluginSitemapSections(): readonly ResolvedPluginSitemapSection[] {\n return PLUGIN_SITEMAP_SECTIONS_DECLARED\n}\n\n/** One declared section by its name, or `null`. */\nexport function pluginSitemapSection(\n section: string,\n): ResolvedPluginSitemapSection | null {\n return PLUGIN_SITEMAP_SECTIONS_DECLARED.find((one) => one.section === section) ?? null\n}\n\n/** The site path of one row, or `null` for a row with no slug. */\nexport function pluginSitemapSectionPath(\n declaration: PluginSitemapSectionDeclaration,\n slug: unknown,\n): string | null {\n if (typeof slug !== 'string' || !slug) return null\n return declaration.path.replace('{slug}', slug)\n}\n"],"names":["PLUGIN_SITEMAP_SECTIONS_DECLARED","listPluginSitemapSections","pluginSitemapSection","section","find","one","pluginSitemapSectionPath","declaration","slug","path","replace"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,gCAAgC,QAAQ,qCAAiC;AAuElF,8EAA8E,GAC9E,OAAO,SAASC;IACd,OAAOD;AACT;AAEA,iDAAiD,GACjD,OAAO,SAASE,qBACdC,OAAe;QAERH;IAAP,QAAOA,yCAAAA,iCAAiCI,IAAI,CAAC,CAACC,MAAQA,IAAIF,OAAO,KAAKA,oBAA/DH,yCAA2E;AACpF;AAEA,gEAAgE,GAChE,OAAO,SAASM,yBACdC,WAA4C,EAC5CC,IAAa;IAEb,IAAI,OAAOA,SAAS,YAAY,CAACA,MAAM,OAAO;IAC9C,OAAOD,YAAYE,IAAI,CAACC,OAAO,CAAC,UAAUF;AAC5C"}