@aglyn/aglyn 1.0.0-beta.285 → 1.0.0-beta.288

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 (91) hide show
  1. package/package.json +11 -11
  2. package/src/lib/app-utils/account-acquisition.d.ts +14 -0
  3. package/src/lib/app-utils/account-acquisition.js +19 -0
  4. package/src/lib/app-utils/account-acquisition.js.map +1 -1
  5. package/src/lib/app-utils/api-plugins.d.ts +25 -0
  6. package/src/lib/app-utils/api-plugins.js +13 -0
  7. package/src/lib/app-utils/api-plugins.js.map +1 -1
  8. package/src/lib/app-utils/business-profile-values.d.ts +15 -3
  9. package/src/lib/app-utils/business-profile-values.js +19 -3
  10. package/src/lib/app-utils/business-profile-values.js.map +1 -1
  11. package/src/lib/app-utils/business-profile.d.ts +5 -3
  12. package/src/lib/app-utils/business-profile.js +2 -1
  13. package/src/lib/app-utils/business-profile.js.map +1 -1
  14. package/src/lib/app-utils/contacts.d.ts +11 -0
  15. package/src/lib/app-utils/contacts.js +18 -0
  16. package/src/lib/app-utils/contacts.js.map +1 -1
  17. package/src/lib/app-utils/docs-deflection.d.ts +1 -1
  18. package/src/lib/app-utils/docs-deflection.js +17 -0
  19. package/src/lib/app-utils/docs-deflection.js.map +1 -1
  20. package/src/lib/app-utils/docs-help-sections.generated.js +5 -2
  21. package/src/lib/app-utils/docs-help-sections.generated.js.map +1 -1
  22. package/src/lib/app-utils/docs-help.generated.d.ts +9 -9
  23. package/src/lib/app-utils/docs-help.generated.js +20 -0
  24. package/src/lib/app-utils/docs-help.generated.js.map +1 -1
  25. package/src/lib/app-utils/docs-index.generated.js +168 -63
  26. package/src/lib/app-utils/docs-index.generated.js.map +1 -1
  27. package/src/lib/app-utils/docs-retrieval.d.ts +17 -0
  28. package/src/lib/app-utils/docs-retrieval.js +28 -0
  29. package/src/lib/app-utils/docs-retrieval.js.map +1 -1
  30. package/src/lib/app-utils/host-naming.d.ts +2 -1
  31. package/src/lib/app-utils/host-naming.js +65 -2
  32. package/src/lib/app-utils/host-naming.js.map +1 -1
  33. package/src/lib/app-utils/host-permissions.d.ts +159 -3
  34. package/src/lib/app-utils/host-permissions.js +384 -12
  35. package/src/lib/app-utils/host-permissions.js.map +1 -1
  36. package/src/lib/app-utils/org-permission-editor.d.ts +128 -1
  37. package/src/lib/app-utils/org-permission-editor.js +176 -1
  38. package/src/lib/app-utils/org-permission-editor.js.map +1 -1
  39. package/src/lib/app-utils/org-permissions.d.ts +62 -1
  40. package/src/lib/app-utils/org-permissions.js +55 -5
  41. package/src/lib/app-utils/org-permissions.js.map +1 -1
  42. package/src/lib/app-utils/org-role-grants.d.ts +28 -0
  43. package/src/lib/app-utils/org-role-grants.js +180 -2
  44. package/src/lib/app-utils/org-role-grants.js.map +1 -1
  45. package/src/lib/app-utils/org-roles.js +11 -2
  46. package/src/lib/app-utils/org-roles.js.map +1 -1
  47. package/src/lib/app-utils/redirect-query.d.ts +39 -0
  48. package/src/lib/app-utils/redirect-query.js +57 -0
  49. package/src/lib/app-utils/redirect-query.js.map +1 -0
  50. package/src/lib/app-utils/release-flags.js +2 -2
  51. package/src/lib/app-utils/release-flags.js.map +1 -1
  52. package/src/lib/app-utils/site-starter-links.d.ts +42 -0
  53. package/src/lib/app-utils/site-starter-links.js +79 -0
  54. package/src/lib/app-utils/site-starter-links.js.map +1 -0
  55. package/src/lib/app-utils/site-starter-nodes.d.ts +91 -0
  56. package/src/lib/app-utils/site-starter-nodes.js +210 -0
  57. package/src/lib/app-utils/site-starter-nodes.js.map +1 -0
  58. package/src/lib/app-utils/site-starter-theme.d.ts +90 -0
  59. package/src/lib/app-utils/site-starter-theme.js +241 -0
  60. package/src/lib/app-utils/site-starter-theme.js.map +1 -0
  61. package/src/lib/app-utils/webhook-delivery.js +6 -1
  62. package/src/lib/app-utils/webhook-delivery.js.map +1 -1
  63. package/src/lib/foundation/definitions/organization.types.d.ts +12 -2
  64. package/src/lib/foundation/definitions/organization.types.js.map +1 -1
  65. package/src/lib/plugin-manager/first-party-plugins.generated.js +39 -0
  66. package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
  67. package/src/lib/plugin-manager/plugin-closure-notices.d.ts +88 -0
  68. package/src/lib/plugin-manager/plugin-closure-notices.js +123 -0
  69. package/src/lib/plugin-manager/plugin-closure-notices.js.map +1 -0
  70. package/src/lib/plugin-manager/plugin-contact-capture.d.ts +15 -0
  71. package/src/lib/plugin-manager/plugin-contact-capture.js +24 -1
  72. package/src/lib/plugin-manager/plugin-contact-capture.js.map +1 -1
  73. package/src/lib/plugin-manager/plugin-customer-display-verifier.d.ts +77 -0
  74. package/src/lib/plugin-manager/plugin-customer-display-verifier.js +74 -0
  75. package/src/lib/plugin-manager/plugin-customer-display-verifier.js.map +1 -0
  76. package/src/lib/plugin-manager/plugin-customer-display.d.ts +135 -0
  77. package/src/lib/plugin-manager/plugin-customer-display.js +189 -0
  78. package/src/lib/plugin-manager/plugin-customer-display.js.map +1 -0
  79. package/src/lib/plugin-manager/plugin-entitlements.d.ts +59 -0
  80. package/src/lib/plugin-manager/plugin-entitlements.js +32 -3
  81. package/src/lib/plugin-manager/plugin-entitlements.js.map +1 -1
  82. package/src/lib/plugin-manager/plugin-events.d.ts +45 -0
  83. package/src/lib/plugin-manager/plugin-events.js.map +1 -1
  84. package/src/lib/plugin-manager/plugin-permissions.d.ts +74 -0
  85. package/src/lib/plugin-manager/plugin-permissions.js +91 -1
  86. package/src/lib/plugin-manager/plugin-permissions.js.map +1 -1
  87. package/src/lib/plugin-manager/plugin-site-starters.d.ts +7 -0
  88. package/src/lib/plugin-manager/plugin-site-starters.js.map +1 -1
  89. package/src/lib/plugin-manager/plugin-tax-return-sources.d.ts +87 -4
  90. package/src/lib/plugin-manager/plugin-tax-return-sources.js +155 -35
  91. package/src/lib/plugin-manager/plugin-tax-return-sources.js.map +1 -1
@@ -180,6 +180,51 @@ export interface PluginEventPayloads {
180
180
  domain: string;
181
181
  reason: 'detached' | 'site-deleted';
182
182
  };
183
+ /**
184
+ * A site collaborator's per-site toggles were written through the platform's
185
+ * own door (AGL-4047), with their verdict for every per-site key after it.
186
+ * `memberId` is their row on the site's collaborators roster
187
+ * (`hosts/{hostId}/members/{memberId}`), for a plugin that keeps a display
188
+ * copy of its own keys there.
189
+ */
190
+ 'host.member.permissions.changed': {
191
+ orgId: string;
192
+ hostId: string;
193
+ memberId: string;
194
+ uid: string;
195
+ permissions: Record<string, boolean>;
196
+ };
197
+ /**
198
+ * The workspace's own logo changed (AGL-4048): the logo or dark-mode logo
199
+ * on its organization profile, or the logo of its white-label brand.
200
+ * Raised once the save has answered, never in front of it, so a handler
201
+ * that redraws the logo somewhere of its own — a device's start-up
202
+ * screen, a generated image — does so now rather than on its next pass,
203
+ * and the person saving waits on none of it. `changed` names which logos
204
+ * moved; a handler that draws only some of them may ignore the rest.
205
+ */
206
+ 'org.logo.changed': {
207
+ orgId: string;
208
+ actor: PluginEventActor;
209
+ changed: ReadonlyArray<'logo' | 'logoDark' | 'brandLogo' | 'brandLogoDark'>;
210
+ };
211
+ /**
212
+ * A workspace was created for its owner, by either door that creates one
213
+ * (the new-workspace page, or an account's first site). Raised once the
214
+ * workspace exists and its welcome is sent, so a plugin holding something
215
+ * for that person until they had a workspace — a purchase made before they
216
+ * signed up — hands it over now. `emailVerified` says whether the owner's
217
+ * address is proven; a plugin that matches by address waits for it.
218
+ */
219
+ 'org.created': {
220
+ orgId: string;
221
+ owner: {
222
+ uid: string;
223
+ email: string | null;
224
+ emailVerified: boolean;
225
+ };
226
+ via: 'workspace-page' | 'first-site';
227
+ };
183
228
  /** One permission key moved on a subject of the org. */
184
229
  'org.permissions.changed': {
185
230
  orgId: string;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-events.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 * Platform events a plugin listens to (AGL-2939, AGL-2940).\n *\n * Core raises a small number of facts about the workspace that a plugin\n * may care about — a seat add-on was bought or dropped, a permission key\n * moved on a member, a role or a site — and a plugin subscribes from its\n * declarations rather than core calling the plugin's own writer. The\n * billing-webhook hooks are the precedent for one vendor's events; this is\n * the same shape for the platform's own.\n *\n * Events a PLUGIN raises for other plugins — an order paid, a return\n * refunded — are `plugin-domain-events.ts` (AGL-3611): declared by the plugin,\n * delivered through an outbox, at least once.\n *\n * Handlers run sequentially and are ISOLATED: a plugin that throws is\n * logged and the next one runs, because these are consequences of a write\n * that has already happened — an activity row, a notification — and a\n * failure in one must not undo or hide the write for the customer.\n */\n\nimport type { OrgSeatAddons } from '../foundation'\nimport { getRegisteringPluginId } from '../app-utils/registering-plugin'\n\n/** Who did it, as the activity logs record an actor. */\nexport interface PluginEventActor {\n uid: string | null\n email: string | null\n}\n\n/** The payload each platform event carries. */\nexport interface PluginEventPayloads {\n /** An org's purchased seat add-ons were written (a purchase, a removal, a webhook mirror). */\n 'org.seatAddons.changed': {\n orgId: string\n actor: PluginEventActor\n before: OrgSeatAddons | null | undefined\n after: OrgSeatAddons | null | undefined\n }\n /**\n * The workspace's Stripe invoice was paid, in full, by a real charge.\n *\n * Raised from the platform billing webhook once the invoice has resolved\n * to a workspace, so a plugin that bills its own usage learns the outcome\n * of the invoice it asked for without subscribing to Stripe itself. The\n * invoice's own `metadata` travels with it, unread by core: a plugin that\n * stamped `pluginId` on the invoice it created recognizes its own by it,\n * and every other handler ignores the event.\n *\n * `paidOutOfBand` is Stripe's own flag for an invoice marked paid without\n * a charge. A plugin counting payment history must not count one: it is a\n * staff action, not evidence that money moved.\n */\n 'billing.invoice.paid': {\n orgId: string\n invoiceId: string\n amountPaidCents: number\n currency: string\n paidOutOfBand: boolean\n metadata: Readonly<Record<string, string>>\n }\n /** A charge attempt on the workspace's invoice failed or needs action. */\n 'billing.invoice.failed': {\n orgId: string\n invoiceId: string\n amountDueCents: number\n metadata: Readonly<Record<string, string>>\n }\n /**\n * An invoice was closed without being paid — voided, or written off as\n * uncollectible.\n *\n * ONE event for both Stripe events because they are one fact to a plugin:\n * this invoice will never be paid and nothing more is owed on it. A\n * handler that must tell them apart reads `reason`; one that only needs\n * to stop waiting does not.\n */\n 'billing.invoice.closed': {\n orgId: string\n invoiceId: string\n reason: 'voided' | 'uncollectible'\n metadata: Readonly<Record<string, string>>\n }\n /** A cardholder disputed a charge of the workspace's. */\n 'billing.dispute.opened': {\n orgId: string\n chargeId: string\n invoiceId: string | null\n amountCents: number\n }\n /**\n * The workspace's default payment method changed, or its type did.\n *\n * `defaultType` is Stripe's own type string (`card`, `us_bank_account`,\n * `link`, …), or `null` when the customer has no default. Core states the\n * fact and takes no view: which types a plugin will extend credit against\n * is the plugin's decision, not the platform's.\n */\n 'billing.paymentMethod.changed': {\n orgId: string\n defaultType: string | null\n }\n /**\n * Records were REMOVED from a collection, by a path that is not the\n * owning plugin's own (AGL-3330): a deletion through the public API, a\n * person's erasure.\n *\n * Raised after the delete has landed, with each removed document's data\n * as it stood, so a plugin that keeps a figure over those rows — a counter\n * no increment can take back — can recount what they fed. Core names the\n * collection and takes no view of what it holds.\n *\n * `hostIds` are the sites the records were on: the one site for a site's\n * collection, every site of the organization for an organization row.\n */\n 'host.records.removed': {\n orgId: string | null\n hostIds: readonly string[]\n collection: string\n records: ReadonlyArray<{ id: string; data: Readonly<Record<string, unknown>> }>\n }\n /**\n * A site's email was opened, or a link in it clicked (AGL-3605): the\n * delivery events one webhook delivery recorded for a send tagged with the\n * site, the FIRST of each type per message only — the delivery log decides\n * which those are, so a replay or a reader opening one email six times is\n * one event. Raised by the plugin whose webhook reads the provider, after\n * the log has recorded them; a subscriber that counts engagement of its own\n * reads it here rather than subscribing to the provider.\n */\n 'host.email.engaged': {\n hostId: string\n events: ReadonlyArray<{\n /** The recipient, lowercased. */\n to: string\n /** `opened` or `clicked`. */\n type: string\n /** When it happened, epoch ms. */\n at: number\n firstOfType: boolean\n }>\n }\n /**\n * A custom domain was connected to a site and the hosting provider accepted\n * it (AGL-3629). Raised once the claim is written, so `domain` is the site's\n * `cname` and no other site's. The site may still be awaiting a certificate\n * or a DNS change; a plugin that needs the domain registered somewhere — a\n * payment provider, a search console — does that now rather than waiting\n * for traffic.\n */\n 'host.domain.attached': {\n orgId: string | null\n hostId: string\n domain: string\n }\n /**\n * A domain stopped serving a site (AGL-3629): its custom domain was\n * disconnected, or the site was deleted — which releases its platform\n * subdomain too, raised as a second event with that name. A plugin undoes\n * whatever it registered for the name, and must check the name is not\n * still served by ANOTHER site first (a `www.` twin, a reused subdomain).\n */\n 'host.domain.released': {\n orgId: string | null\n hostId: string\n domain: string\n reason: 'detached' | 'site-deleted'\n }\n /** One permission key moved on a subject of the org. */\n 'org.permissions.changed': {\n orgId: string\n actor: PluginEventActor\n subject: {\n type: 'org' | 'role' | 'member' | 'host'\n id?: string | null\n name?: string | null\n }\n permission: string\n granted: boolean\n }\n}\n\nexport type PluginEventName = keyof PluginEventPayloads\n\nexport type PluginEventHandler<E extends PluginEventName> = (\n payload: PluginEventPayloads[E],\n) => void | Promise<void>\n\ninterface Registration {\n pluginId: string\n handler: PluginEventHandler<PluginEventName>\n}\n\n/**\n * On `globalThis` under `Symbol.for`, never at module scope (AGL-3947).\n * The handlers are registered at boot, from `instrumentation.ts`, and Next\n * compiles that file apart from the routes, so the boot and a route each hold\n * their own copy of this module. At module scope, the boot filled its copy and\n * every route read an empty one (AGL-3412, AGL-3868).\n */\nconst REGISTRY_KEY = Symbol.for('@aglyn/aglyn:plugin-event-handlers')\nconst globalScope = globalThis as typeof globalThis & {\n [REGISTRY_KEY]?: Map<PluginEventName, Registration[]>\n}\nconst handlers: Map<PluginEventName, Registration[]> =\n globalScope[REGISTRY_KEY] ?? (globalScope[REGISTRY_KEY] = new Map())\n\n/**\n * Subscribes to an event. Owner = the loader's marker inside a register fn,\n * else `options.pluginId`; a subscription with neither throws. A plugin\n * re-subscribing replaces its earlier handler for that event, so a module\n * evaluated twice does not write two rows.\n */\nexport function registerPluginEventHandler<E extends PluginEventName>(\n event: E,\n handler: PluginEventHandler<E>,\n options?: { pluginId?: string },\n): void {\n const pluginId = (getRegisteringPluginId() ?? options?.pluginId ?? '').trim()\n if (!pluginId) {\n throw new Error(\n `plugin event \"${event}\" subscribed with no owner: pass { pluginId } ` +\n 'when subscribing outside a plugin register fn',\n )\n }\n const list = (handlers.get(event) ?? []).filter((entry) => entry.pluginId !== pluginId)\n list.push({ pluginId, handler: handler as PluginEventHandler<PluginEventName> })\n handlers.set(event, list)\n}\n\n/**\n * Raises an event. Every handler runs, in subscription order; a throw is\n * logged with the plugin's id and does not stop the others.\n */\nexport async function runPluginEventHandlers<E extends PluginEventName>(\n event: E,\n payload: PluginEventPayloads[E],\n): Promise<{ handled: number; failed: string[] }> {\n let handled = 0\n const failed: string[] = []\n for (const { pluginId, handler } of handlers.get(event) ?? []) {\n try {\n await handler(payload)\n handled += 1\n } catch (error) {\n failed.push(pluginId)\n console.error(`[plugins] ${pluginId} failed on event ${event}`, error)\n }\n }\n return { handled, failed }\n}\n\n/** The plugins subscribed to an event, for diagnostics. */\nexport function listPluginEventHandlers(event: PluginEventName): string[] {\n return (handlers.get(event) ?? []).map((entry) => entry.pluginId)\n}\n\n/** Test seam: forget every subscription. */\nexport function resetPluginEventHandlersForTests(): void {\n handlers.clear()\n}\n"],"names":["globalScope","getRegisteringPluginId","REGISTRY_KEY","Symbol","for","globalThis","handlers","Map","registerPluginEventHandler","event","handler","options","pluginId","trim","Error","list","get","filter","entry","push","set","runPluginEventHandlers","payload","handled","failed","error","console","listPluginEventHandlers","map","resetPluginEventHandlersForTests","clear"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;CAkBC,OA0LCA;AAvLF,SAASC,sBAAsB,QAAQ,qCAAiC;AA2KxE;;;;;;CAMC,GACD,MAAMC,eAAeC,OAAOC,GAAG,CAAC;AAChC,MAAMJ,cAAcK;AAGpB,MAAMC,YACJN,4BAAAA,WAAW,CAACE,aAAa,YAAzBF,4BAA8BA,WAAW,CAACE,aAAa,GAAG,IAAIK;AAEhE;;;;;CAKC,GACD,OAAO,SAASC,2BACdC,KAAQ,EACRC,OAA8B,EAC9BC,OAA+B;QAEbV,MAAAA,yBAOJK;IAPd,MAAMM,WAAW,EAACX,QAAAA,0BAAAA,oCAAAA,0BAA4BU,2BAAAA,QAASC,QAAQ,YAA7CX,OAAiD,IAAIY,IAAI;IAC3E,IAAI,CAACD,UAAU;QACb,MAAM,IAAIE,MACR,CAAC,cAAc,EAAEL,MAAM,8CAA8C,CAAC,GACpE;IAEN;IACA,MAAMM,OAAO,EAACT,gBAAAA,SAASU,GAAG,CAACP,kBAAbH,gBAAuB,EAAE,EAAEW,MAAM,CAAC,CAACC,QAAUA,MAAMN,QAAQ,KAAKA;IAC9EG,KAAKI,IAAI,CAAC;QAAEP;QAAUF,SAASA;IAA+C;IAC9EJ,SAASc,GAAG,CAACX,OAAOM;AACtB;AAEA;;;CAGC,GACD,OAAO,eAAeM,uBACpBZ,KAAQ,EACRa,OAA+B;QAIKhB;IAFpC,IAAIiB,UAAU;IACd,MAAMC,SAAmB,EAAE;IAC3B,KAAK,MAAM,EAAEZ,QAAQ,EAAEF,OAAO,EAAE,KAAIJ,gBAAAA,SAASU,GAAG,CAACP,kBAAbH,gBAAuB,EAAE,CAAE;QAC7D,IAAI;YACF,MAAMI,QAAQY;YACdC,WAAW;QACb,EAAE,OAAOE,OAAO;YACdD,OAAOL,IAAI,CAACP;YACZc,QAAQD,KAAK,CAAC,CAAC,UAAU,EAAEb,SAAS,iBAAiB,EAAEH,OAAO,EAAEgB;QAClE;IACF;IACA,OAAO;QAAEF;QAASC;IAAO;AAC3B;AAEA,yDAAyD,GACzD,OAAO,SAASG,wBAAwBlB,KAAsB;QACpDH;IAAR,OAAO,EAACA,gBAAAA,SAASU,GAAG,CAACP,kBAAbH,gBAAuB,EAAE,EAAEsB,GAAG,CAAC,CAACV,QAAUA,MAAMN,QAAQ;AAClE;AAEA,0CAA0C,GAC1C,OAAO,SAASiB;IACdvB,SAASwB,KAAK;AAChB"}
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-events.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 * Platform events a plugin listens to (AGL-2939, AGL-2940).\n *\n * Core raises a small number of facts about the workspace that a plugin\n * may care about — a seat add-on was bought or dropped, a permission key\n * moved on a member, a role or a site — and a plugin subscribes from its\n * declarations rather than core calling the plugin's own writer. The\n * billing-webhook hooks are the precedent for one vendor's events; this is\n * the same shape for the platform's own.\n *\n * Events a PLUGIN raises for other plugins — an order paid, a return\n * refunded — are `plugin-domain-events.ts` (AGL-3611): declared by the plugin,\n * delivered through an outbox, at least once.\n *\n * Handlers run sequentially and are ISOLATED: a plugin that throws is\n * logged and the next one runs, because these are consequences of a write\n * that has already happened — an activity row, a notification — and a\n * failure in one must not undo or hide the write for the customer.\n */\n\nimport type { OrgSeatAddons } from '../foundation'\nimport { getRegisteringPluginId } from '../app-utils/registering-plugin'\n\n/** Who did it, as the activity logs record an actor. */\nexport interface PluginEventActor {\n uid: string | null\n email: string | null\n}\n\n/** The payload each platform event carries. */\nexport interface PluginEventPayloads {\n /** An org's purchased seat add-ons were written (a purchase, a removal, a webhook mirror). */\n 'org.seatAddons.changed': {\n orgId: string\n actor: PluginEventActor\n before: OrgSeatAddons | null | undefined\n after: OrgSeatAddons | null | undefined\n }\n /**\n * The workspace's Stripe invoice was paid, in full, by a real charge.\n *\n * Raised from the platform billing webhook once the invoice has resolved\n * to a workspace, so a plugin that bills its own usage learns the outcome\n * of the invoice it asked for without subscribing to Stripe itself. The\n * invoice's own `metadata` travels with it, unread by core: a plugin that\n * stamped `pluginId` on the invoice it created recognizes its own by it,\n * and every other handler ignores the event.\n *\n * `paidOutOfBand` is Stripe's own flag for an invoice marked paid without\n * a charge. A plugin counting payment history must not count one: it is a\n * staff action, not evidence that money moved.\n */\n 'billing.invoice.paid': {\n orgId: string\n invoiceId: string\n amountPaidCents: number\n currency: string\n paidOutOfBand: boolean\n metadata: Readonly<Record<string, string>>\n }\n /** A charge attempt on the workspace's invoice failed or needs action. */\n 'billing.invoice.failed': {\n orgId: string\n invoiceId: string\n amountDueCents: number\n metadata: Readonly<Record<string, string>>\n }\n /**\n * An invoice was closed without being paid — voided, or written off as\n * uncollectible.\n *\n * ONE event for both Stripe events because they are one fact to a plugin:\n * this invoice will never be paid and nothing more is owed on it. A\n * handler that must tell them apart reads `reason`; one that only needs\n * to stop waiting does not.\n */\n 'billing.invoice.closed': {\n orgId: string\n invoiceId: string\n reason: 'voided' | 'uncollectible'\n metadata: Readonly<Record<string, string>>\n }\n /** A cardholder disputed a charge of the workspace's. */\n 'billing.dispute.opened': {\n orgId: string\n chargeId: string\n invoiceId: string | null\n amountCents: number\n }\n /**\n * The workspace's default payment method changed, or its type did.\n *\n * `defaultType` is Stripe's own type string (`card`, `us_bank_account`,\n * `link`, …), or `null` when the customer has no default. Core states the\n * fact and takes no view: which types a plugin will extend credit against\n * is the plugin's decision, not the platform's.\n */\n 'billing.paymentMethod.changed': {\n orgId: string\n defaultType: string | null\n }\n /**\n * Records were REMOVED from a collection, by a path that is not the\n * owning plugin's own (AGL-3330): a deletion through the public API, a\n * person's erasure.\n *\n * Raised after the delete has landed, with each removed document's data\n * as it stood, so a plugin that keeps a figure over those rows — a counter\n * no increment can take back — can recount what they fed. Core names the\n * collection and takes no view of what it holds.\n *\n * `hostIds` are the sites the records were on: the one site for a site's\n * collection, every site of the organization for an organization row.\n */\n 'host.records.removed': {\n orgId: string | null\n hostIds: readonly string[]\n collection: string\n records: ReadonlyArray<{ id: string; data: Readonly<Record<string, unknown>> }>\n }\n /**\n * A site's email was opened, or a link in it clicked (AGL-3605): the\n * delivery events one webhook delivery recorded for a send tagged with the\n * site, the FIRST of each type per message only — the delivery log decides\n * which those are, so a replay or a reader opening one email six times is\n * one event. Raised by the plugin whose webhook reads the provider, after\n * the log has recorded them; a subscriber that counts engagement of its own\n * reads it here rather than subscribing to the provider.\n */\n 'host.email.engaged': {\n hostId: string\n events: ReadonlyArray<{\n /** The recipient, lowercased. */\n to: string\n /** `opened` or `clicked`. */\n type: string\n /** When it happened, epoch ms. */\n at: number\n firstOfType: boolean\n }>\n }\n /**\n * A custom domain was connected to a site and the hosting provider accepted\n * it (AGL-3629). Raised once the claim is written, so `domain` is the site's\n * `cname` and no other site's. The site may still be awaiting a certificate\n * or a DNS change; a plugin that needs the domain registered somewhere — a\n * payment provider, a search console — does that now rather than waiting\n * for traffic.\n */\n 'host.domain.attached': {\n orgId: string | null\n hostId: string\n domain: string\n }\n /**\n * A domain stopped serving a site (AGL-3629): its custom domain was\n * disconnected, or the site was deleted — which releases its platform\n * subdomain too, raised as a second event with that name. A plugin undoes\n * whatever it registered for the name, and must check the name is not\n * still served by ANOTHER site first (a `www.` twin, a reused subdomain).\n */\n 'host.domain.released': {\n orgId: string | null\n hostId: string\n domain: string\n reason: 'detached' | 'site-deleted'\n }\n /**\n * A site collaborator's per-site toggles were written through the platform's\n * own door (AGL-4047), with their verdict for every per-site key after it.\n * `memberId` is their row on the site's collaborators roster\n * (`hosts/{hostId}/members/{memberId}`), for a plugin that keeps a display\n * copy of its own keys there.\n */\n 'host.member.permissions.changed': {\n orgId: string\n hostId: string\n memberId: string\n uid: string\n permissions: Record<string, boolean>\n }\n /**\n * The workspace's own logo changed (AGL-4048): the logo or dark-mode logo\n * on its organization profile, or the logo of its white-label brand.\n * Raised once the save has answered, never in front of it, so a handler\n * that redraws the logo somewhere of its own — a device's start-up\n * screen, a generated image — does so now rather than on its next pass,\n * and the person saving waits on none of it. `changed` names which logos\n * moved; a handler that draws only some of them may ignore the rest.\n */\n 'org.logo.changed': {\n orgId: string\n actor: PluginEventActor\n changed: ReadonlyArray<'logo' | 'logoDark' | 'brandLogo' | 'brandLogoDark'>\n }\n /**\n * A workspace was created for its owner, by either door that creates one\n * (the new-workspace page, or an account's first site). Raised once the\n * workspace exists and its welcome is sent, so a plugin holding something\n * for that person until they had a workspace — a purchase made before they\n * signed up — hands it over now. `emailVerified` says whether the owner's\n * address is proven; a plugin that matches by address waits for it.\n */\n 'org.created': {\n orgId: string\n owner: { uid: string; email: string | null; emailVerified: boolean }\n via: 'workspace-page' | 'first-site'\n }\n /** One permission key moved on a subject of the org. */\n 'org.permissions.changed': {\n orgId: string\n actor: PluginEventActor\n subject: {\n type: 'org' | 'role' | 'member' | 'host'\n id?: string | null\n name?: string | null\n }\n permission: string\n granted: boolean\n }\n}\n\nexport type PluginEventName = keyof PluginEventPayloads\n\nexport type PluginEventHandler<E extends PluginEventName> = (\n payload: PluginEventPayloads[E],\n) => void | Promise<void>\n\ninterface Registration {\n pluginId: string\n handler: PluginEventHandler<PluginEventName>\n}\n\n/**\n * On `globalThis` under `Symbol.for`, never at module scope (AGL-3947).\n * The handlers are registered at boot, from `instrumentation.ts`, and Next\n * compiles that file apart from the routes, so the boot and a route each hold\n * their own copy of this module. At module scope, the boot filled its copy and\n * every route read an empty one (AGL-3412, AGL-3868).\n */\nconst REGISTRY_KEY = Symbol.for('@aglyn/aglyn:plugin-event-handlers')\nconst globalScope = globalThis as typeof globalThis & {\n [REGISTRY_KEY]?: Map<PluginEventName, Registration[]>\n}\nconst handlers: Map<PluginEventName, Registration[]> =\n globalScope[REGISTRY_KEY] ?? (globalScope[REGISTRY_KEY] = new Map())\n\n/**\n * Subscribes to an event. Owner = the loader's marker inside a register fn,\n * else `options.pluginId`; a subscription with neither throws. A plugin\n * re-subscribing replaces its earlier handler for that event, so a module\n * evaluated twice does not write two rows.\n */\nexport function registerPluginEventHandler<E extends PluginEventName>(\n event: E,\n handler: PluginEventHandler<E>,\n options?: { pluginId?: string },\n): void {\n const pluginId = (getRegisteringPluginId() ?? options?.pluginId ?? '').trim()\n if (!pluginId) {\n throw new Error(\n `plugin event \"${event}\" subscribed with no owner: pass { pluginId } ` +\n 'when subscribing outside a plugin register fn',\n )\n }\n const list = (handlers.get(event) ?? []).filter((entry) => entry.pluginId !== pluginId)\n list.push({ pluginId, handler: handler as PluginEventHandler<PluginEventName> })\n handlers.set(event, list)\n}\n\n/**\n * Raises an event. Every handler runs, in subscription order; a throw is\n * logged with the plugin's id and does not stop the others.\n */\nexport async function runPluginEventHandlers<E extends PluginEventName>(\n event: E,\n payload: PluginEventPayloads[E],\n): Promise<{ handled: number; failed: string[] }> {\n let handled = 0\n const failed: string[] = []\n for (const { pluginId, handler } of handlers.get(event) ?? []) {\n try {\n await handler(payload)\n handled += 1\n } catch (error) {\n failed.push(pluginId)\n console.error(`[plugins] ${pluginId} failed on event ${event}`, error)\n }\n }\n return { handled, failed }\n}\n\n/** The plugins subscribed to an event, for diagnostics. */\nexport function listPluginEventHandlers(event: PluginEventName): string[] {\n return (handlers.get(event) ?? []).map((entry) => entry.pluginId)\n}\n\n/** Test seam: forget every subscription. */\nexport function resetPluginEventHandlersForTests(): void {\n handlers.clear()\n}\n"],"names":["globalScope","getRegisteringPluginId","REGISTRY_KEY","Symbol","for","globalThis","handlers","Map","registerPluginEventHandler","event","handler","options","pluginId","trim","Error","list","get","filter","entry","push","set","runPluginEventHandlers","payload","handled","failed","error","console","listPluginEventHandlers","map","resetPluginEventHandlersForTests","clear"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;CAkBC,OAmOCA;AAhOF,SAASC,sBAAsB,QAAQ,qCAAiC;AAoNxE;;;;;;CAMC,GACD,MAAMC,eAAeC,OAAOC,GAAG,CAAC;AAChC,MAAMJ,cAAcK;AAGpB,MAAMC,YACJN,4BAAAA,WAAW,CAACE,aAAa,YAAzBF,4BAA8BA,WAAW,CAACE,aAAa,GAAG,IAAIK;AAEhE;;;;;CAKC,GACD,OAAO,SAASC,2BACdC,KAAQ,EACRC,OAA8B,EAC9BC,OAA+B;QAEbV,MAAAA,yBAOJK;IAPd,MAAMM,WAAW,EAACX,QAAAA,0BAAAA,oCAAAA,0BAA4BU,2BAAAA,QAASC,QAAQ,YAA7CX,OAAiD,IAAIY,IAAI;IAC3E,IAAI,CAACD,UAAU;QACb,MAAM,IAAIE,MACR,CAAC,cAAc,EAAEL,MAAM,8CAA8C,CAAC,GACpE;IAEN;IACA,MAAMM,OAAO,EAACT,gBAAAA,SAASU,GAAG,CAACP,kBAAbH,gBAAuB,EAAE,EAAEW,MAAM,CAAC,CAACC,QAAUA,MAAMN,QAAQ,KAAKA;IAC9EG,KAAKI,IAAI,CAAC;QAAEP;QAAUF,SAASA;IAA+C;IAC9EJ,SAASc,GAAG,CAACX,OAAOM;AACtB;AAEA;;;CAGC,GACD,OAAO,eAAeM,uBACpBZ,KAAQ,EACRa,OAA+B;QAIKhB;IAFpC,IAAIiB,UAAU;IACd,MAAMC,SAAmB,EAAE;IAC3B,KAAK,MAAM,EAAEZ,QAAQ,EAAEF,OAAO,EAAE,KAAIJ,gBAAAA,SAASU,GAAG,CAACP,kBAAbH,gBAAuB,EAAE,CAAE;QAC7D,IAAI;YACF,MAAMI,QAAQY;YACdC,WAAW;QACb,EAAE,OAAOE,OAAO;YACdD,OAAOL,IAAI,CAACP;YACZc,QAAQD,KAAK,CAAC,CAAC,UAAU,EAAEb,SAAS,iBAAiB,EAAEH,OAAO,EAAEgB;QAClE;IACF;IACA,OAAO;QAAEF;QAASC;IAAO;AAC3B;AAEA,yDAAyD,GACzD,OAAO,SAASG,wBAAwBlB,KAAsB;QACpDH;IAAR,OAAO,EAACA,gBAAAA,SAASU,GAAG,CAACP,kBAAbH,gBAAuB,EAAE,EAAEsB,GAAG,CAAC,CAACV,QAAUA,MAAMN,QAAQ;AAClE;AAEA,0CAA0C,GAC1C,OAAO,SAASiB;IACdvB,SAASwB,KAAK;AAChB"}
@@ -42,7 +42,51 @@ export interface PluginPermission {
42
42
  group?: string;
43
43
  /** The heading of a group of the plugin's own; absent: the plugin's catalog label. */
44
44
  groupLabel?: string;
45
+ /**
46
+ * A broader key this one is a finer part of (AGL-4047), such as one action
47
+ * of a register the broader key opens. A refining key is never granted
48
+ * while the key it refines is not, and when no custom role or override
49
+ * sets it, it follows that key — so splitting a key into finer ones leaves
50
+ * every member with exactly the access they had. Its `defaults` should be
51
+ * the broader key's.
52
+ */
53
+ refines?: string;
54
+ /**
55
+ * A key this one takes its verdict from where no custom role or override
56
+ * sets it, WITHOUT needing it (AGL-4047). Unlike `refines`, it may be
57
+ * granted while that key is refused — buying hardware for a store whose
58
+ * register the member does not work — so a key that stops refining one and
59
+ * follows it instead leaves every member with exactly the access they had,
60
+ * and can then be granted on its own. Its `defaults` should be the followed
61
+ * key's. Ignored on a key that also `refines`.
62
+ */
63
+ follows?: string;
64
+ /**
65
+ * A site can narrow this key (AGL-4047). Where a member who works across
66
+ * the whole workspace has narrower access to one site than their workspace
67
+ * role (an editor given Viewer or Author access there), a key that narrows
68
+ * is, on that site, what the narrower access holds by default, never more
69
+ * than the workspace grants, unless the site grants it to them. A key a
70
+ * site's own work needs, such as working its register, offers this; a key
71
+ * about the workspace as a whole leaves it out.
72
+ *
73
+ * Declaring it offers a per-key CONTROL ("On sites where their role is
74
+ * narrower: Follow the site role / Keep this permission") on custom roles
75
+ * and per-member overrides, and the value here is the built-in roles'
76
+ * default: `true` narrows for every built-in role, `false` keeps for every
77
+ * one (the control is still offered), and a map names each built-in role's
78
+ * default, a role it leaves out keeping. Absent, the key never narrows and
79
+ * no control is offered. Members with no narrower access anywhere are
80
+ * untouched whatever it says.
81
+ */
82
+ narrowsPerSite?: boolean | Partial<Record<'owner' | 'admin' | 'editor' | 'viewer', boolean>>;
45
83
  }
84
+ /**
85
+ * A key, the key it takes its verdict from, and how
86
+ * (`PluginPermission.refines` / `.follows`): a two-entry pair refines its
87
+ * broader key, one marked `'follows'` only follows it.
88
+ */
89
+ export type PermissionRefinement = readonly [key: string, refines: string, kind?: 'follows'];
46
90
  /** Calls `listener` after every registration; answers the unsubscribe. */
47
91
  export declare function subscribePluginPermissions(listener: () => void): () => void;
48
92
  /**
@@ -81,3 +125,33 @@ export declare function listPluginPermissions(): PluginPermission[];
81
125
  export declare function listPluginPermissionKeys(): string[];
82
126
  /** Tier defaults for every registered plugin permission. */
83
127
  export declare function pluginPermissionDefaults(tier: 'admin' | 'editor' | 'viewer'): Record<string, boolean>;
128
+ /** The refinements the per-tier registry holds (AGL-4047), as declared. */
129
+ export declare function pluginPermissionRefinements(): PermissionRefinement[];
130
+ /**
131
+ * One declaration's refinement, or null: the key it refines, else the key it
132
+ * follows, never itself. Shared by both declaration APIs' catalogs.
133
+ */
134
+ export declare function permissionRefinementOf(declaration: {
135
+ key: string;
136
+ refines?: string;
137
+ follows?: string;
138
+ }): PermissionRefinement | null;
139
+ /**
140
+ * A resolved permission map with every refining key held to the key it
141
+ * refines (AGL-4047). A refining key is granted only while its broader key
142
+ * is.
143
+ *
144
+ * With `layers` — the stored decisions the map was resolved from, in the
145
+ * order they apply (a custom role's map, then a member's overrides) — a
146
+ * refining key that no layer sets FOLLOWS its broader key: granted exactly
147
+ * when that key is, whatever its own role default says. Without them, the
148
+ * map's own verdict for the key stands, narrowed by the broader key; that is
149
+ * the pass for a map whose refining keys were already resolved with every
150
+ * layer, and then had stored values laid back over them.
151
+ *
152
+ * A key that only FOLLOWS (`'follows'`) takes the followed key's verdict
153
+ * where no layer sets it, and a layer's own value where one does, granted or
154
+ * not, whatever the followed key says. Without layers it is left as the map
155
+ * has it.
156
+ */
157
+ export declare function refinePermissionMap<T extends Record<string, boolean>>(map: T, refinements: readonly PermissionRefinement[], layers?: ReadonlyArray<Readonly<Partial<Record<string, unknown>>> | null | undefined>): T;
@@ -21,7 +21,8 @@
21
21
  * `ConsolePluginPageProps.permissions` and API-side resolution carry them
22
22
  * with zero app changes. Register at module scope from both the client
23
23
  * barrel and the /server entry (the AGL-428 pattern).
24
- */ var _globalScope_REGISTRY_KEY;
24
+ */ import { _ as _extends } from "@swc/helpers/_/_extends";
25
+ var _globalScope_REGISTRY_KEY;
25
26
  /*
26
27
  * One registry per process, keyed on `globalThis` under `Symbol.for`, like
27
28
  * `plugin-services.ts`. Next compiles the boot file and the route handlers
@@ -119,5 +120,94 @@ export function listPluginPermissions() {
119
120
  }
120
121
  return defaults;
121
122
  }
123
+ /** The refinements the per-tier registry holds (AGL-4047), as declared. */ export function pluginPermissionRefinements() {
124
+ const refinements = [];
125
+ for (const permission of pluginPermissions.values()){
126
+ const refinement = permissionRefinementOf(permission);
127
+ if (refinement) refinements.push(refinement);
128
+ }
129
+ return refinements;
130
+ }
131
+ /**
132
+ * One declaration's refinement, or null: the key it refines, else the key it
133
+ * follows, never itself. Shared by both declaration APIs' catalogs.
134
+ */ export function permissionRefinementOf(declaration) {
135
+ if (declaration.refines && declaration.refines !== declaration.key) {
136
+ return [
137
+ declaration.key,
138
+ declaration.refines
139
+ ];
140
+ }
141
+ if (declaration.follows && declaration.follows !== declaration.key) {
142
+ return [
143
+ declaration.key,
144
+ declaration.follows,
145
+ 'follows'
146
+ ];
147
+ }
148
+ return null;
149
+ }
150
+ /**
151
+ * The refinements in the order they must be applied: a key whose broader key
152
+ * is itself a refinement after that one, so a chain settles in one pass. A
153
+ * refinement that loops back on itself is dropped rather than resolved.
154
+ */ function orderRefinements(refinements) {
155
+ const broader = new Map(refinements.map(([key, parent])=>[
156
+ key,
157
+ parent
158
+ ]));
159
+ const depth = (key)=>{
160
+ const seen = new Set();
161
+ let steps = 0;
162
+ let current = key;
163
+ while(broader.has(current)){
164
+ if (seen.has(current)) return -1;
165
+ seen.add(current);
166
+ current = broader.get(current);
167
+ steps += 1;
168
+ }
169
+ return steps;
170
+ };
171
+ return refinements.map((refinement)=>({
172
+ refinement,
173
+ steps: depth(refinement[0])
174
+ })).filter((entry)=>entry.steps > 0).sort((a, b)=>a.steps - b.steps).map((entry)=>entry.refinement);
175
+ }
176
+ /**
177
+ * A resolved permission map with every refining key held to the key it
178
+ * refines (AGL-4047). A refining key is granted only while its broader key
179
+ * is.
180
+ *
181
+ * With `layers` — the stored decisions the map was resolved from, in the
182
+ * order they apply (a custom role's map, then a member's overrides) — a
183
+ * refining key that no layer sets FOLLOWS its broader key: granted exactly
184
+ * when that key is, whatever its own role default says. Without them, the
185
+ * map's own verdict for the key stands, narrowed by the broader key; that is
186
+ * the pass for a map whose refining keys were already resolved with every
187
+ * layer, and then had stored values laid back over them.
188
+ *
189
+ * A key that only FOLLOWS (`'follows'`) takes the followed key's verdict
190
+ * where no layer sets it, and a layer's own value where one does, granted or
191
+ * not, whatever the followed key says. Without layers it is left as the map
192
+ * has it.
193
+ */ export function refinePermissionMap(map, refinements, layers) {
194
+ if (!refinements.length) return map;
195
+ const refined = _extends({}, map);
196
+ for (const [key, parent, kind] of orderRefinements(refinements)){
197
+ const held = refined[parent] === true;
198
+ if (layers) {
199
+ let stored;
200
+ for (const layer of layers){
201
+ const value = layer == null ? void 0 : layer[key];
202
+ if (typeof value === 'boolean') stored = value;
203
+ }
204
+ if (kind === 'follows') refined[key] = stored === undefined ? held : stored;
205
+ else refined[key] = held && (stored === undefined ? true : stored);
206
+ } else if (kind !== 'follows') {
207
+ refined[key] = held && refined[key] === true;
208
+ }
209
+ }
210
+ return refined;
211
+ }
122
212
 
123
213
  //# sourceMappingURL=plugin-permissions.js.map
@@ -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 * 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"}
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 * A broader key this one is a finer part of (AGL-4047), such as one action\n * of a register the broader key opens. A refining key is never granted\n * while the key it refines is not, and when no custom role or override\n * sets it, it follows that key — so splitting a key into finer ones leaves\n * every member with exactly the access they had. Its `defaults` should be\n * the broader key's.\n */\n refines?: string\n /**\n * A key this one takes its verdict from where no custom role or override\n * sets it, WITHOUT needing it (AGL-4047). Unlike `refines`, it may be\n * granted while that key is refused — buying hardware for a store whose\n * register the member does not work — so a key that stops refining one and\n * follows it instead leaves every member with exactly the access they had,\n * and can then be granted on its own. Its `defaults` should be the followed\n * key's. Ignored on a key that also `refines`.\n */\n follows?: string\n /**\n * A site can narrow this key (AGL-4047). Where a member who works across\n * the whole workspace has narrower access to one site than their workspace\n * role (an editor given Viewer or Author access there), a key that narrows\n * is, on that site, what the narrower access holds by default, never more\n * than the workspace grants, unless the site grants it to them. A key a\n * site's own work needs, such as working its register, offers this; a key\n * about the workspace as a whole leaves it out.\n *\n * Declaring it offers a per-key CONTROL (\"On sites where their role is\n * narrower: Follow the site role / Keep this permission\") on custom roles\n * and per-member overrides, and the value here is the built-in roles'\n * default: `true` narrows for every built-in role, `false` keeps for every\n * one (the control is still offered), and a map names each built-in role's\n * default, a role it leaves out keeping. Absent, the key never narrows and\n * no control is offered. Members with no narrower access anywhere are\n * untouched whatever it says.\n */\n narrowsPerSite?: boolean | Partial<Record<'owner' | 'admin' | 'editor' | 'viewer', boolean>>\n}\n\n/**\n * A key, the key it takes its verdict from, and how\n * (`PluginPermission.refines` / `.follows`): a two-entry pair refines its\n * broader key, one marked `'follows'` only follows it.\n */\nexport type PermissionRefinement = readonly [key: string, refines: string, kind?: 'follows']\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\n/** The refinements the per-tier registry holds (AGL-4047), as declared. */\nexport function pluginPermissionRefinements(): PermissionRefinement[] {\n const refinements: PermissionRefinement[] = []\n for (const permission of pluginPermissions.values()) {\n const refinement = permissionRefinementOf(permission)\n if (refinement) refinements.push(refinement)\n }\n return refinements\n}\n\n/**\n * One declaration's refinement, or null: the key it refines, else the key it\n * follows, never itself. Shared by both declaration APIs' catalogs.\n */\nexport function permissionRefinementOf(declaration: {\n key: string\n refines?: string\n follows?: string\n}): PermissionRefinement | null {\n if (declaration.refines && declaration.refines !== declaration.key) {\n return [declaration.key, declaration.refines]\n }\n if (declaration.follows && declaration.follows !== declaration.key) {\n return [declaration.key, declaration.follows, 'follows']\n }\n return null\n}\n\n/**\n * The refinements in the order they must be applied: a key whose broader key\n * is itself a refinement after that one, so a chain settles in one pass. A\n * refinement that loops back on itself is dropped rather than resolved.\n */\nfunction orderRefinements(\n refinements: readonly PermissionRefinement[],\n): PermissionRefinement[] {\n const broader = new Map<string, string>(refinements.map(([key, parent]) => [key, parent]))\n const depth = (key: string): number => {\n const seen = new Set<string>()\n let steps = 0\n let current = key\n while (broader.has(current)) {\n if (seen.has(current)) return -1\n seen.add(current)\n current = broader.get(current) as string\n steps += 1\n }\n return steps\n }\n return refinements\n .map((refinement) => ({ refinement, steps: depth(refinement[0]) }))\n .filter((entry) => entry.steps > 0)\n .sort((a, b) => a.steps - b.steps)\n .map((entry) => entry.refinement)\n}\n\n/**\n * A resolved permission map with every refining key held to the key it\n * refines (AGL-4047). A refining key is granted only while its broader key\n * is.\n *\n * With `layers` — the stored decisions the map was resolved from, in the\n * order they apply (a custom role's map, then a member's overrides) — a\n * refining key that no layer sets FOLLOWS its broader key: granted exactly\n * when that key is, whatever its own role default says. Without them, the\n * map's own verdict for the key stands, narrowed by the broader key; that is\n * the pass for a map whose refining keys were already resolved with every\n * layer, and then had stored values laid back over them.\n *\n * A key that only FOLLOWS (`'follows'`) takes the followed key's verdict\n * where no layer sets it, and a layer's own value where one does, granted or\n * not, whatever the followed key says. Without layers it is left as the map\n * has it.\n */\nexport function refinePermissionMap<T extends Record<string, boolean>>(\n map: T,\n refinements: readonly PermissionRefinement[],\n layers?: ReadonlyArray<Readonly<Partial<Record<string, unknown>>> | null | undefined>,\n): T {\n if (!refinements.length) return map\n const refined: Record<string, boolean> = { ...map }\n for (const [key, parent, kind] of orderRefinements(refinements)) {\n const held = refined[parent] === true\n if (layers) {\n let stored: boolean | undefined\n for (const layer of layers) {\n const value = layer?.[key]\n if (typeof value === 'boolean') stored = value\n }\n if (kind === 'follows') refined[key] = stored === undefined ? held : stored\n else refined[key] = held && (stored === undefined ? true : stored)\n } else if (kind !== 'follows') {\n refined[key] = held && refined[key] === true\n }\n }\n return refined as T\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","pluginPermissionRefinements","refinements","refinement","permissionRefinementOf","push","declaration","refines","follows","orderRefinements","broader","map","parent","depth","seen","steps","current","has","filter","entry","sort","a","b","refinePermissionMap","layers","length","refined","kind","held","stored","layer"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;CAQC;IAqFCA;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;AAEA,yEAAyE,GACzE,OAAO,SAASC;IACd,MAAMC,cAAsC,EAAE;IAC9C,KAAK,MAAMjB,cAAcR,kBAAkBkB,MAAM,GAAI;QACnD,MAAMQ,aAAaC,uBAAuBnB;QAC1C,IAAIkB,YAAYD,YAAYG,IAAI,CAACF;IACnC;IACA,OAAOD;AACT;AAEA;;;CAGC,GACD,OAAO,SAASE,uBAAuBE,WAItC;IACC,IAAIA,YAAYC,OAAO,IAAID,YAAYC,OAAO,KAAKD,YAAYlB,GAAG,EAAE;QAClE,OAAO;YAACkB,YAAYlB,GAAG;YAAEkB,YAAYC,OAAO;SAAC;IAC/C;IACA,IAAID,YAAYE,OAAO,IAAIF,YAAYE,OAAO,KAAKF,YAAYlB,GAAG,EAAE;QAClE,OAAO;YAACkB,YAAYlB,GAAG;YAAEkB,YAAYE,OAAO;YAAE;SAAU;IAC1D;IACA,OAAO;AACT;AAEA;;;;CAIC,GACD,SAASC,iBACPP,WAA4C;IAE5C,MAAMQ,UAAU,IAAItC,IAAoB8B,YAAYS,GAAG,CAAC,CAAC,CAACvB,KAAKwB,OAAO,GAAK;YAACxB;YAAKwB;SAAO;IACxF,MAAMC,QAAQ,CAACzB;QACb,MAAM0B,OAAO,IAAIxC;QACjB,IAAIyC,QAAQ;QACZ,IAAIC,UAAU5B;QACd,MAAOsB,QAAQO,GAAG,CAACD,SAAU;YAC3B,IAAIF,KAAKG,GAAG,CAACD,UAAU,OAAO,CAAC;YAC/BF,KAAKlC,GAAG,CAACoC;YACTA,UAAUN,QAAQvB,GAAG,CAAC6B;YACtBD,SAAS;QACX;QACA,OAAOA;IACT;IACA,OAAOb,YACJS,GAAG,CAAC,CAACR,aAAgB,CAAA;YAAEA;YAAYY,OAAOF,MAAMV,UAAU,CAAC,EAAE;QAAE,CAAA,GAC/De,MAAM,CAAC,CAACC,QAAUA,MAAMJ,KAAK,GAAG,GAChCK,IAAI,CAAC,CAACC,GAAGC,IAAMD,EAAEN,KAAK,GAAGO,EAAEP,KAAK,EAChCJ,GAAG,CAAC,CAACQ,QAAUA,MAAMhB,UAAU;AACpC;AAEA;;;;;;;;;;;;;;;;;CAiBC,GACD,OAAO,SAASoB,oBACdZ,GAAM,EACNT,WAA4C,EAC5CsB,MAAqF;IAErF,IAAI,CAACtB,YAAYuB,MAAM,EAAE,OAAOd;IAChC,MAAMe,UAAmC,aAAKf;IAC9C,KAAK,MAAM,CAACvB,KAAKwB,QAAQe,KAAK,IAAIlB,iBAAiBP,aAAc;QAC/D,MAAM0B,OAAOF,OAAO,CAACd,OAAO,KAAK;QACjC,IAAIY,QAAQ;YACV,IAAIK;YACJ,KAAK,MAAMC,SAASN,OAAQ;gBAC1B,MAAMhD,QAAQsD,yBAAAA,KAAO,CAAC1C,IAAI;gBAC1B,IAAI,OAAOZ,UAAU,WAAWqD,SAASrD;YAC3C;YACA,IAAImD,SAAS,WAAWD,OAAO,CAACtC,IAAI,GAAGyC,WAAWxC,YAAYuC,OAAOC;iBAChEH,OAAO,CAACtC,IAAI,GAAGwC,QAASC,CAAAA,WAAWxC,YAAY,OAAOwC,MAAK;QAClE,OAAO,IAAIF,SAAS,WAAW;YAC7BD,OAAO,CAACtC,IAAI,GAAGwC,QAAQF,OAAO,CAACtC,IAAI,KAAK;QAC1C;IACF;IACA,OAAOsC;AACT"}
@@ -63,6 +63,13 @@ export interface PluginSiteStarterSource {
63
63
  status: 402 | 403 | 404;
64
64
  priceUsd?: number;
65
65
  }>;
66
+ /**
67
+ * Records that a site was created from a starter; best-effort, never throws.
68
+ * Optional, so a source that does not count installs still satisfies the seam.
69
+ */
70
+ installed?(choice: SiteStarterChoice, viewer: SiteStarterViewer, site: {
71
+ hostId: string;
72
+ }): Promise<void>;
66
73
  }
67
74
  export declare const PLUGIN_SITE_STARTERS: import("./plugin-services").PluginServiceContract<PluginSiteStarterSource>;
68
75
  /** Registers the plugin that serves site starters. One per deployment; a second is refused. */
@@ -1 +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"}
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 * Records that a site was created from a starter; best-effort, never throws.\n * Optional, so a source that does not count installs still satisfies the seam.\n */\n installed?(choice: SiteStarterChoice, viewer: SiteStarterViewer, site: { hostId: string }): Promise<void>\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;AA0D1B,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"}
@@ -36,6 +36,27 @@
36
36
  * figures, its tables and figures for the screen, its blocks for the export,
37
37
  * and its own summary and rows carried whole for the audit.
38
38
  *
39
+ * A plugin that records two KINDS of sale apart — sales it facilitated for
40
+ * merchants and sales the operator made itself, say — answers one section
41
+ * per kind, as a list. Each is its own card, with its own truncation, its own
42
+ * undated rows and its own findings, because one card over two collections
43
+ * could only say "these rows exceeded the cap" without saying which. The
44
+ * list is all or nothing: an empty list or one malformed section refuses the
45
+ * plugin, so a half-answered plugin never reads as a complete one.
46
+ *
47
+ * ## THE OPERATOR'S OWN SALES FOLD INTO THE FILING FIGURES (AGL-4038)
48
+ *
49
+ * The filing figures sum the operator's own invoices. A plugin may also hold
50
+ * sales the operator made AS THE SELLER — goods sold from the operator's own
51
+ * store, hardware sold to workspaces — and those are as much the operator's
52
+ * receipts as an invoice is. A source states them as
53
+ * {@link TaxReturnOperatorSalesPart}s, by jurisdiction and under a label for
54
+ * each kind, and the platform ADDS them to the filing figures and prints a
55
+ * breakdown beside them by part and jurisdiction, so the sum can be checked.
56
+ * A facilitated sale — somebody else's money — never goes there: it stays on
57
+ * the source's own card. A source counts each sale under ONE part only, and
58
+ * a part id is unique on the return, so nothing is folded in twice.
59
+ *
39
60
  * ## ⛔ AN UNREAD SOURCE REFUSES, IT NEVER READS AS ZERO
40
61
  *
41
62
  * Decided before anything else, because this is a legal filing. A source
@@ -183,6 +204,45 @@ export interface TaxReturnExportBlock {
183
204
  placement: 'jurisdictions' | 'sections';
184
205
  rows: string[][];
185
206
  }
207
+ /**
208
+ * The operator's own sales of one kind in one jurisdiction, for one period.
209
+ * Integer cents. Sales are placed in the period they were paid; refunds in
210
+ * the period they were made, stated beside the sales and never netted.
211
+ */
212
+ export interface TaxReturnOperatorFigures {
213
+ /** Sales paid in the period. */
214
+ transactionCount: number;
215
+ /** Receipts excluding the tax itself. */
216
+ totalSalesCents: number;
217
+ /** The base the tax was charged on. */
218
+ taxableSalesCents: number;
219
+ taxCollectedCents: number;
220
+ /** Refunds made in the period, whenever the sale was paid. */
221
+ refundCount: number;
222
+ /** Refunded receipts excluding the tax. */
223
+ refundedSalesCents: number;
224
+ refundedTaxCents: number;
225
+ }
226
+ /**
227
+ * One kind of the operator's own sales a source holds, which the platform
228
+ * folds into the filing figures (see the module note).
229
+ */
230
+ export interface TaxReturnOperatorSalesPart {
231
+ /** Stable, and unique on the return. */
232
+ id: string;
233
+ /** How the breakdown beside the filing figures names it. */
234
+ label: string;
235
+ /**
236
+ * By the jurisdiction keys the platform's own figures use (`US-TX`, `GB`,
237
+ * `unknown` for a sale with no region). Every jurisdiction is listed in the
238
+ * breakdown; only the filing jurisdiction's figures are added to the form.
239
+ */
240
+ byJurisdiction: Record<string, TaxReturnOperatorFigures>;
241
+ }
242
+ /** The figure fields of {@link TaxReturnOperatorFigures}, in print order. */
243
+ export declare const TAX_RETURN_OPERATOR_FIGURE_FIELDS: readonly ["transactionCount", "totalSalesCents", "taxableSalesCents", "taxCollectedCents", "refundCount", "refundedSalesCents", "refundedTaxCents"];
244
+ /** A jurisdiction with nothing in it. */
245
+ export declare function emptyTaxReturnOperatorFigures(): TaxReturnOperatorFigures;
186
246
  /** What a source answers for one period. */
187
247
  export interface TaxReturnSourceAnswer {
188
248
  /** Stable key for the source's findings and its card, unique on the return. */
@@ -203,6 +263,12 @@ export interface TaxReturnSourceAnswer {
203
263
  findings: TaxReturnFinding[];
204
264
  /** Lines beneath the filing figures, for the request's `filing.form`. */
205
265
  filingLines: TaxReturnFilingLine[];
266
+ /**
267
+ * The operator's OWN sales this section holds, which the platform adds to
268
+ * the filing figures (see the module note). Absent or empty for a section
269
+ * of facilitated sales only.
270
+ */
271
+ operatorSales?: TaxReturnOperatorSalesPart[];
206
272
  tables: TaxReturnSectionTable[];
207
273
  figures: TaxReturnSectionFigure[];
208
274
  exports: TaxReturnExportBlock[];
@@ -225,12 +291,13 @@ export type TaxReturnSection = ({
225
291
  };
226
292
  export interface TaxReturnSource {
227
293
  /**
228
- * This plugin's sales for one period. May throw: the return refuses the
229
- * source rather than read a throw as nothing sold.
294
+ * This plugin's sales for one period: one section, or one per kind of sale
295
+ * it records apart (see the module note). May throw: the return refuses
296
+ * the source rather than read a throw as nothing sold.
230
297
  */
231
- read(request: TaxReturnSourceRequest): Promise<TaxReturnSourceAnswer>;
298
+ read(request: TaxReturnSourceRequest): Promise<TaxReturnSourceAnswer | readonly TaxReturnSourceAnswer[]>;
232
299
  }
233
- /** Every source answers; one per plugin. */
300
+ /** Every source answers; one per plugin, with one section or several. */
234
301
  export declare const TAX_RETURN_SOURCES: import("./plugin-services").PluginServiceContract<TaxReturnSource>;
235
302
  /**
236
303
  * Installs a plugin's source. Owner = the loader's marker inside a register
@@ -242,6 +309,22 @@ export declare function registerTaxReturnSource(source: TaxReturnSource, options
242
309
  }): void;
243
310
  /** The plugins with a registered source, in the order they answer. */
244
311
  export declare function listTaxReturnSources(): string[];
312
+ /**
313
+ * The operator's own sales every answered section states, as the platform
314
+ * folds them: one row per part and jurisdiction, in the sections' order.
315
+ */
316
+ export interface TaxReturnOperatorSalesRow extends TaxReturnOperatorFigures {
317
+ pluginId: string;
318
+ partId: string;
319
+ label: string;
320
+ jurisdiction: string;
321
+ }
322
+ export declare function taxReturnOperatorSalesRows(sections: readonly TaxReturnSection[] | null | undefined): TaxReturnOperatorSalesRow[];
323
+ /**
324
+ * The operator's own sales in ONE jurisdiction, summed over every part —
325
+ * what the platform adds to its own figures for that jurisdiction.
326
+ */
327
+ export declare function taxReturnOperatorSalesIn(sections: readonly TaxReturnSection[] | null | undefined, jurisdiction: string): TaxReturnOperatorFigures;
245
328
  /**
246
329
  * Every source's answer for one period — declared sources first, in the
247
330
  * compiled order, then any other registered source in registration order.