@aglyn/aglyn 1.0.0-beta.230 → 1.0.0-beta.232

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 (150) hide show
  1. package/package.json +11 -11
  2. package/src/lib/app-utils/account-acquisition.d.ts +15 -2
  3. package/src/lib/app-utils/account-acquisition.js +20 -1
  4. package/src/lib/app-utils/account-acquisition.js.map +1 -1
  5. package/src/lib/app-utils/artifact-list-queries.d.ts +35 -0
  6. package/src/lib/app-utils/artifact-list-queries.js +203 -0
  7. package/src/lib/app-utils/artifact-list-queries.js.map +1 -0
  8. package/src/lib/app-utils/business-profile.d.ts +171 -0
  9. package/src/lib/app-utils/business-profile.js +277 -0
  10. package/src/lib/app-utils/business-profile.js.map +1 -0
  11. package/src/lib/app-utils/child-contract.d.ts +4 -1
  12. package/src/lib/app-utils/child-contract.js +5 -2
  13. package/src/lib/app-utils/child-contract.js.map +1 -1
  14. package/src/lib/app-utils/console-routes.d.ts +5 -0
  15. package/src/lib/app-utils/console-routes.js +1 -0
  16. package/src/lib/app-utils/console-routes.js.map +1 -1
  17. package/src/lib/app-utils/content-schema-type.d.ts +9 -0
  18. package/src/lib/app-utils/content-schema-type.js +4 -0
  19. package/src/lib/app-utils/content-schema-type.js.map +1 -1
  20. package/src/lib/app-utils/crm.d.ts +14 -1
  21. package/src/lib/app-utils/crm.js +19 -2
  22. package/src/lib/app-utils/crm.js.map +1 -1
  23. package/src/lib/app-utils/docs-help.generated.d.ts +119 -5
  24. package/src/lib/app-utils/docs-help.generated.js +290 -2
  25. package/src/lib/app-utils/docs-help.generated.js.map +1 -1
  26. package/src/lib/app-utils/docs-index.generated.js +697 -32
  27. package/src/lib/app-utils/docs-index.generated.js.map +1 -1
  28. package/src/lib/app-utils/entry-list-declaration.d.ts +33 -0
  29. package/src/lib/app-utils/entry-list-declaration.js +172 -0
  30. package/src/lib/app-utils/entry-list-declaration.js.map +1 -0
  31. package/src/lib/app-utils/first-version-seed.d.ts +19 -0
  32. package/src/lib/app-utils/first-version-seed.js +53 -0
  33. package/src/lib/app-utils/first-version-seed.js.map +1 -0
  34. package/src/lib/app-utils/host-status.d.ts +85 -0
  35. package/src/lib/app-utils/host-status.js +115 -0
  36. package/src/lib/app-utils/host-status.js.map +1 -0
  37. package/src/lib/app-utils/local-business.d.ts +8 -0
  38. package/src/lib/app-utils/local-business.js +4 -0
  39. package/src/lib/app-utils/local-business.js.map +1 -1
  40. package/src/lib/app-utils/lockdown.js +1 -1
  41. package/src/lib/app-utils/lockdown.js.map +1 -1
  42. package/src/lib/app-utils/media-filter.d.ts +136 -0
  43. package/src/lib/app-utils/media-filter.js +400 -0
  44. package/src/lib/app-utils/media-filter.js.map +1 -0
  45. package/src/lib/app-utils/mobile-push.d.ts +98 -0
  46. package/src/lib/app-utils/mobile-push.js +97 -0
  47. package/src/lib/app-utils/mobile-push.js.map +1 -0
  48. package/src/lib/app-utils/notification-push.d.ts +38 -0
  49. package/src/lib/app-utils/notification-push.js +54 -0
  50. package/src/lib/app-utils/notification-push.js.map +1 -0
  51. package/src/lib/app-utils/notifications.d.ts +7 -0
  52. package/src/lib/app-utils/notifications.js.map +1 -1
  53. package/src/lib/app-utils/organizations.js +5 -2
  54. package/src/lib/app-utils/organizations.js.map +1 -1
  55. package/src/lib/app-utils/page-markdown.js +3 -0
  56. package/src/lib/app-utils/page-markdown.js.map +1 -1
  57. package/src/lib/app-utils/plugin-host-events.generated.d.ts +1 -1
  58. package/src/lib/app-utils/plugin-host-events.generated.js +149 -0
  59. package/src/lib/app-utils/plugin-host-events.generated.js.map +1 -1
  60. package/src/lib/app-utils/plugin-release-flags.generated.d.ts +1 -1
  61. package/src/lib/app-utils/plugin-release-flags.generated.js +7 -0
  62. package/src/lib/app-utils/plugin-release-flags.generated.js.map +1 -1
  63. package/src/lib/app-utils/realm-host-surface.generated.js +3 -0
  64. package/src/lib/app-utils/realm-host-surface.generated.js.map +1 -1
  65. package/src/lib/app-utils/release-flags.js +6 -2
  66. package/src/lib/app-utils/release-flags.js.map +1 -1
  67. package/src/lib/app-utils/scope-tokens.d.ts +16 -1
  68. package/src/lib/app-utils/scope-tokens.js +15 -1
  69. package/src/lib/app-utils/scope-tokens.js.map +1 -1
  70. package/src/lib/app-utils/site-list-query.d.ts +47 -0
  71. package/src/lib/app-utils/site-list-query.js +142 -0
  72. package/src/lib/app-utils/site-list-query.js.map +1 -0
  73. package/src/lib/app-utils/site-wide-outbox.d.ts +95 -0
  74. package/src/lib/app-utils/site-wide-outbox.js +117 -0
  75. package/src/lib/app-utils/site-wide-outbox.js.map +1 -0
  76. package/src/lib/app-utils/transfer-launcher-context.d.ts +6 -0
  77. package/src/lib/app-utils/transfer-launcher-context.js.map +1 -1
  78. package/src/lib/app-utils/upload-inspection.js +7 -0
  79. package/src/lib/app-utils/upload-inspection.js.map +1 -1
  80. package/src/lib/app-utils/webhook-delivery.js +4 -1
  81. package/src/lib/app-utils/webhook-delivery.js.map +1 -1
  82. package/src/lib/foundation/definitions/org-billing.types.d.ts +8 -0
  83. package/src/lib/foundation/definitions/org-billing.types.js.map +1 -1
  84. package/src/lib/foundation/definitions/organization.types.d.ts +18 -7
  85. package/src/lib/foundation/definitions/organization.types.js.map +1 -1
  86. package/src/lib/foundation/definitions/write-deny-coverage.util.d.ts +4 -1
  87. package/src/lib/foundation/definitions/write-deny-coverage.util.js +12 -2
  88. package/src/lib/foundation/definitions/write-deny-coverage.util.js.map +1 -1
  89. package/src/lib/plugin-manager/enabled-plugins.js +4 -2
  90. package/src/lib/plugin-manager/enabled-plugins.js.map +1 -1
  91. package/src/lib/plugin-manager/feature-plugins.d.ts +107 -0
  92. package/src/lib/plugin-manager/feature-plugins.js +52 -0
  93. package/src/lib/plugin-manager/feature-plugins.js.map +1 -1
  94. package/src/lib/plugin-manager/first-party-plugins.generated.js +326 -2
  95. package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
  96. package/src/lib/plugin-manager/plugin-channel-orders.d.ts +262 -0
  97. package/src/lib/plugin-manager/plugin-channel-orders.js +32 -0
  98. package/src/lib/plugin-manager/plugin-channel-orders.js.map +1 -0
  99. package/src/lib/plugin-manager/plugin-checkout-extras.d.ts +168 -0
  100. package/src/lib/plugin-manager/plugin-checkout-extras.js +172 -0
  101. package/src/lib/plugin-manager/plugin-checkout-extras.js.map +1 -0
  102. package/src/lib/plugin-manager/plugin-contributions.d.ts +7 -0
  103. package/src/lib/plugin-manager/plugin-contributions.js +1 -1
  104. package/src/lib/plugin-manager/plugin-contributions.js.map +1 -1
  105. package/src/lib/plugin-manager/plugin-domain-events.d.ts +138 -0
  106. package/src/lib/plugin-manager/plugin-domain-events.js +148 -0
  107. package/src/lib/plugin-manager/plugin-domain-events.js.map +1 -0
  108. package/src/lib/plugin-manager/plugin-events.d.ts +30 -0
  109. package/src/lib/plugin-manager/plugin-events.js +4 -0
  110. package/src/lib/plugin-manager/plugin-events.js.map +1 -1
  111. package/src/lib/plugin-manager/plugin-fulfillment-providers.d.ts +101 -0
  112. package/src/lib/plugin-manager/plugin-fulfillment-providers.js +83 -0
  113. package/src/lib/plugin-manager/plugin-fulfillment-providers.js.map +1 -0
  114. package/src/lib/plugin-manager/plugin-permissions.js +21 -5
  115. package/src/lib/plugin-manager/plugin-permissions.js.map +1 -1
  116. package/src/lib/plugin-manager/plugin-person-records.d.ts +90 -0
  117. package/src/lib/plugin-manager/plugin-person-records.js +26 -0
  118. package/src/lib/plugin-manager/plugin-person-records.js.map +1 -1
  119. package/src/lib/plugin-manager/plugin-product-catalog.d.ts +209 -0
  120. package/src/lib/plugin-manager/plugin-product-catalog.js +43 -0
  121. package/src/lib/plugin-manager/plugin-product-catalog.js.map +1 -0
  122. package/src/lib/plugin-manager/plugin-product-writer.d.ts +166 -0
  123. package/src/lib/plugin-manager/plugin-product-writer.js +31 -0
  124. package/src/lib/plugin-manager/plugin-product-writer.js.map +1 -0
  125. package/src/lib/plugin-manager/plugin-shipment-records.d.ts +210 -0
  126. package/src/lib/plugin-manager/plugin-shipment-records.js +63 -0
  127. package/src/lib/plugin-manager/plugin-shipment-records.js.map +1 -0
  128. package/src/lib/plugin-manager/plugin-shipping-rates.d.ts +151 -0
  129. package/src/lib/plugin-manager/plugin-shipping-rates.js +62 -0
  130. package/src/lib/plugin-manager/plugin-shipping-rates.js.map +1 -0
  131. package/src/lib/plugin-manager/plugin-sms-messaging.d.ts +103 -0
  132. package/src/lib/plugin-manager/plugin-sms-messaging.js +39 -0
  133. package/src/lib/plugin-manager/plugin-sms-messaging.js.map +1 -0
  134. package/src/lib/plugin-manager/plugin-stock-levels.d.ts +81 -0
  135. package/src/lib/plugin-manager/plugin-stock-levels.js +32 -0
  136. package/src/lib/plugin-manager/plugin-stock-levels.js.map +1 -0
  137. package/src/lib/plugin-manager/plugin-tax-profile.d.ts +154 -0
  138. package/src/lib/plugin-manager/plugin-tax-profile.js +56 -0
  139. package/src/lib/plugin-manager/plugin-tax-profile.js.map +1 -1
  140. package/src/lib/plugin-manager/plugin-theme-font-catalog.d.ts +59 -0
  141. package/src/lib/plugin-manager/plugin-theme-font-catalog.js +40 -0
  142. package/src/lib/plugin-manager/plugin-theme-font-catalog.js.map +1 -0
  143. package/src/lib/plugin-manager/plugin-theme-presets.d.ts +40 -0
  144. package/src/lib/plugin-manager/plugin-theme-presets.js +44 -0
  145. package/src/lib/plugin-manager/plugin-theme-presets.js.map +1 -0
  146. package/src/lib/plugin-manager/plugin-tracking-pages.d.ts +55 -0
  147. package/src/lib/plugin-manager/plugin-tracking-pages.js +76 -0
  148. package/src/lib/plugin-manager/plugin-tracking-pages.js.map +1 -0
  149. package/src/lib/plugin-manager/realm-host-aglyn.generated.js +3 -0
  150. package/src/lib/plugin-manager/realm-host-aglyn.generated.js.map +1 -1
@@ -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 * 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 /** 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\nconst handlers = new Map<PluginEventName, Registration[]>()\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":["getRegisteringPluginId","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;;;;;;;;;;;;;;CAcC,GAGD,SAASA,sBAAsB,QAAQ,qCAAiC;AAiJxE,MAAMC,WAAW,IAAIC;AAErB;;;;;CAKC,GACD,OAAO,SAASC,2BACdC,KAAQ,EACRC,OAA8B,EAC9BC,OAA+B;QAEbN,MAAAA,yBAOJC;IAPd,MAAMM,WAAW,EAACP,QAAAA,0BAAAA,oCAAAA,0BAA4BM,2BAAAA,QAASC,QAAQ,YAA7CP,OAAiD,IAAIQ,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 /** 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\nconst handlers = new Map<PluginEventName, Registration[]>()\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":["getRegisteringPluginId","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,GAGD,SAASA,sBAAsB,QAAQ,qCAAiC;AA2KxE,MAAMC,WAAW,IAAIC;AAErB;;;;;CAKC,GACD,OAAO,SAASC,2BACdC,KAAQ,EACRC,OAA8B,EAC9BC,OAA+B;QAEbN,MAAAA,yBAOJC;IAPd,MAAMM,WAAW,EAACP,QAAAA,0BAAAA,oCAAAA,0BAA4BM,2BAAAA,QAASC,QAAQ,YAA7CP,OAAiD,IAAIQ,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"}
@@ -0,0 +1,101 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ /**
18
+ * Outside parties that fulfill a seller's lines (AGL-3634): a fulfillment
19
+ * network that picks and packs from its own warehouses, a print-on-demand or
20
+ * dropship supplier that makes and sends the goods itself.
21
+ *
22
+ * Each one takes some of a record's lines away from the merchant's own
23
+ * packing table. Whoever else ships that record — a label buyer, another
24
+ * provider, the merchant by hand — must know which units are already spoken
25
+ * for, or the customer gets two parcels and the merchant pays twice. The
26
+ * provider plugins never import one another and never import the seller, so
27
+ * each registers a {@link PluginFulfillmentProvider} here and answers one
28
+ * question: which units of this record do you hold that have not shipped?
29
+ *
30
+ * A HOLD IS ONLY WHAT HAS NOT SHIPPED. Once the provider ships, it writes the
31
+ * shipment onto the record through `core.shipment-records`, the record's own
32
+ * unshipped count drops, and the hold drops with it. A canceled or failed
33
+ * hand-off is no hold: the units are the merchant's again.
34
+ *
35
+ * A provider that cannot answer is reported by name in `unanswered` rather
36
+ * than read as holding nothing, so a caller deciding whether to ship can say
37
+ * why it is waiting instead of shipping a parcel that may already be on its
38
+ * way from somewhere else.
39
+ *
40
+ * Import this module by its own subpath
41
+ * (`@aglyn/aglyn/plugin-manager/plugin-fulfillment-providers`); it is not in
42
+ * the barrel.
43
+ */
44
+ /** Units of one line a provider has taken and not yet shipped. */
45
+ export interface PluginFulfillmentHold {
46
+ /** The provider's id, unique across plugins: `shipbob`, `printful`. */
47
+ providerId: string;
48
+ /** What a person calls it. */
49
+ providerLabel: string;
50
+ /** The line's position on the record, as `core.shipment-records` names it. */
51
+ lineIndex: number;
52
+ /** Units of the line the provider holds that have not shipped. */
53
+ quantity: number;
54
+ /**
55
+ * `pending` — taken and not yet accepted by the provider (queued, or being
56
+ * sent); `accepted` — the provider has it and is fulfilling it.
57
+ */
58
+ state: 'pending' | 'accepted';
59
+ /** The provider's own reference for the hand-off, for a person to look it up there. */
60
+ reference?: string;
61
+ }
62
+ export interface PluginFulfillmentProvider {
63
+ /** Stable and unique across plugins; a plugin may register several. */
64
+ id: string;
65
+ label: string;
66
+ /** The units of one record this provider holds; empty when none. */
67
+ holds(hostId: string, recordId: string): Promise<PluginFulfillmentHold[]>;
68
+ }
69
+ /**
70
+ * Registers a provider. A plugin registers each of its providers under the
71
+ * provider's id, and registering the same id again replaces it.
72
+ */
73
+ export declare function registerPluginFulfillmentProvider(provider: PluginFulfillmentProvider, options?: {
74
+ pluginId?: string;
75
+ }): void;
76
+ /** Every registered provider, with the plugin that registered it. */
77
+ export declare function listPluginFulfillmentProviders(): Array<{
78
+ pluginId: string;
79
+ id: string;
80
+ label: string;
81
+ }>;
82
+ /** What every provider holds of one record. */
83
+ export interface PluginFulfillmentHolds {
84
+ holds: PluginFulfillmentHold[];
85
+ /** Providers that threw: they may hold units nobody can see. */
86
+ unanswered: Array<{
87
+ providerId: string;
88
+ providerLabel: string;
89
+ }>;
90
+ }
91
+ /**
92
+ * Asks every provider — or every provider but those of `exceptPluginId`, so
93
+ * a provider deciding what it may take does not count itself — what it holds
94
+ * of one record. Each is isolated: one that throws is logged and named in
95
+ * `unanswered`, and the rest still answer. A hold of no units is dropped.
96
+ */
97
+ export declare function pluginFulfillmentHolds(hostId: string, recordId: string, options?: {
98
+ exceptPluginId?: string;
99
+ }): Promise<PluginFulfillmentHolds>;
100
+ /** Units held per line, summed across providers. */
101
+ export declare function heldQuantitiesByLine(holds: readonly PluginFulfillmentHold[]): Map<number, number>;
@@ -0,0 +1,83 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ import { getRegisteringPluginId } from "../app-utils/registering-plugin.js";
18
+ import { definePluginServiceContract, registerPluginService, resolvePluginServices } from "./plugin-services.js";
19
+ const PLUGIN_FULFILLMENT_PROVIDERS = definePluginServiceContract('core.fulfillment-providers', {
20
+ multiple: true
21
+ });
22
+ /**
23
+ * Registers a provider. A plugin registers each of its providers under the
24
+ * provider's id, and registering the same id again replaces it.
25
+ */ export function registerPluginFulfillmentProvider(provider, options) {
26
+ var _ref, _getRegisteringPluginId;
27
+ const id = String((_ref = provider == null ? void 0 : provider.id) != null ? _ref : '').trim();
28
+ if (!id) throw new Error('a fulfillment provider needs an id');
29
+ const pluginId = (_getRegisteringPluginId = getRegisteringPluginId()) != null ? _getRegisteringPluginId : options == null ? void 0 : options.pluginId;
30
+ registerPluginService(PLUGIN_FULFILLMENT_PROVIDERS, provider, _extends({
31
+ key: id
32
+ }, pluginId ? {
33
+ pluginId
34
+ } : {}));
35
+ }
36
+ /** Every registered provider, with the plugin that registered it. */ export function listPluginFulfillmentProviders() {
37
+ return resolvePluginServices(PLUGIN_FULFILLMENT_PROVIDERS).map((entry)=>({
38
+ pluginId: entry.pluginId,
39
+ id: entry.impl.id,
40
+ label: entry.impl.label
41
+ }));
42
+ }
43
+ /**
44
+ * Asks every provider — or every provider but those of `exceptPluginId`, so
45
+ * a provider deciding what it may take does not count itself — what it holds
46
+ * of one record. Each is isolated: one that throws is logged and named in
47
+ * `unanswered`, and the rest still answer. A hold of no units is dropped.
48
+ */ export async function pluginFulfillmentHolds(hostId, recordId, options) {
49
+ const result = {
50
+ holds: [],
51
+ unanswered: []
52
+ };
53
+ const entries = resolvePluginServices(PLUGIN_FULFILLMENT_PROVIDERS).filter((entry)=>!(options == null ? void 0 : options.exceptPluginId) || entry.pluginId !== options.exceptPluginId);
54
+ await Promise.all(entries.map(async (entry)=>{
55
+ try {
56
+ const holds = await entry.impl.holds(hostId, recordId);
57
+ for (const hold of holds != null ? holds : []){
58
+ const quantity = Math.floor(Number(hold == null ? void 0 : hold.quantity));
59
+ const lineIndex = Math.floor(Number(hold == null ? void 0 : hold.lineIndex));
60
+ if (!(quantity > 0) || !(lineIndex >= 0)) continue;
61
+ result.holds.push(_extends({}, hold, {
62
+ quantity,
63
+ lineIndex
64
+ }));
65
+ }
66
+ } catch (error) {
67
+ console.error(`[fulfillment-providers] "${entry.impl.id}" of "${entry.pluginId}" failed for ${hostId}/${recordId}`, error);
68
+ result.unanswered.push({
69
+ providerId: entry.impl.id,
70
+ providerLabel: entry.impl.label
71
+ });
72
+ }
73
+ }));
74
+ return result;
75
+ }
76
+ /** Units held per line, summed across providers. */ export function heldQuantitiesByLine(holds) {
77
+ var _held_get;
78
+ const held = new Map();
79
+ for (const hold of holds)held.set(hold.lineIndex, ((_held_get = held.get(hold.lineIndex)) != null ? _held_get : 0) + hold.quantity);
80
+ return held;
81
+ }
82
+
83
+ //# sourceMappingURL=plugin-fulfillment-providers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-fulfillment-providers.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 { getRegisteringPluginId } from '../app-utils/registering-plugin'\nimport {\n definePluginServiceContract,\n registerPluginService,\n resolvePluginServices,\n} from './plugin-services'\n\n/**\n * Outside parties that fulfill a seller's lines (AGL-3634): a fulfillment\n * network that picks and packs from its own warehouses, a print-on-demand or\n * dropship supplier that makes and sends the goods itself.\n *\n * Each one takes some of a record's lines away from the merchant's own\n * packing table. Whoever else ships that record — a label buyer, another\n * provider, the merchant by hand — must know which units are already spoken\n * for, or the customer gets two parcels and the merchant pays twice. The\n * provider plugins never import one another and never import the seller, so\n * each registers a {@link PluginFulfillmentProvider} here and answers one\n * question: which units of this record do you hold that have not shipped?\n *\n * A HOLD IS ONLY WHAT HAS NOT SHIPPED. Once the provider ships, it writes the\n * shipment onto the record through `core.shipment-records`, the record's own\n * unshipped count drops, and the hold drops with it. A canceled or failed\n * hand-off is no hold: the units are the merchant's again.\n *\n * A provider that cannot answer is reported by name in `unanswered` rather\n * than read as holding nothing, so a caller deciding whether to ship can say\n * why it is waiting instead of shipping a parcel that may already be on its\n * way from somewhere else.\n *\n * Import this module by its own subpath\n * (`@aglyn/aglyn/plugin-manager/plugin-fulfillment-providers`); it is not in\n * the barrel.\n */\n\n/** Units of one line a provider has taken and not yet shipped. */\nexport interface PluginFulfillmentHold {\n /** The provider's id, unique across plugins: `shipbob`, `printful`. */\n providerId: string\n /** What a person calls it. */\n providerLabel: string\n /** The line's position on the record, as `core.shipment-records` names it. */\n lineIndex: number\n /** Units of the line the provider holds that have not shipped. */\n quantity: number\n /**\n * `pending` — taken and not yet accepted by the provider (queued, or being\n * sent); `accepted` — the provider has it and is fulfilling it.\n */\n state: 'pending' | 'accepted'\n /** The provider's own reference for the hand-off, for a person to look it up there. */\n reference?: string\n}\n\nexport interface PluginFulfillmentProvider {\n /** Stable and unique across plugins; a plugin may register several. */\n id: string\n label: string\n /** The units of one record this provider holds; empty when none. */\n holds(hostId: string, recordId: string): Promise<PluginFulfillmentHold[]>\n}\n\nconst PLUGIN_FULFILLMENT_PROVIDERS = definePluginServiceContract<PluginFulfillmentProvider>(\n 'core.fulfillment-providers',\n { multiple: true },\n)\n\n/**\n * Registers a provider. A plugin registers each of its providers under the\n * provider's id, and registering the same id again replaces it.\n */\nexport function registerPluginFulfillmentProvider(\n provider: PluginFulfillmentProvider,\n options?: { pluginId?: string },\n): void {\n const id = String(provider?.id ?? '').trim()\n if (!id) throw new Error('a fulfillment provider needs an id')\n const pluginId = getRegisteringPluginId() ?? options?.pluginId\n registerPluginService(PLUGIN_FULFILLMENT_PROVIDERS, provider, {\n key: id,\n ...(pluginId ? { pluginId } : {}),\n })\n}\n\n/** Every registered provider, with the plugin that registered it. */\nexport function listPluginFulfillmentProviders(): Array<{ pluginId: string; id: string; label: string }> {\n return resolvePluginServices(PLUGIN_FULFILLMENT_PROVIDERS).map((entry) => ({\n pluginId: entry.pluginId,\n id: entry.impl.id,\n label: entry.impl.label,\n }))\n}\n\n/** What every provider holds of one record. */\nexport interface PluginFulfillmentHolds {\n holds: PluginFulfillmentHold[]\n /** Providers that threw: they may hold units nobody can see. */\n unanswered: Array<{ providerId: string; providerLabel: string }>\n}\n\n/**\n * Asks every provider — or every provider but those of `exceptPluginId`, so\n * a provider deciding what it may take does not count itself — what it holds\n * of one record. Each is isolated: one that throws is logged and named in\n * `unanswered`, and the rest still answer. A hold of no units is dropped.\n */\nexport async function pluginFulfillmentHolds(\n hostId: string,\n recordId: string,\n options?: { exceptPluginId?: string },\n): Promise<PluginFulfillmentHolds> {\n const result: PluginFulfillmentHolds = { holds: [], unanswered: [] }\n const entries = resolvePluginServices(PLUGIN_FULFILLMENT_PROVIDERS).filter(\n (entry) => !options?.exceptPluginId || entry.pluginId !== options.exceptPluginId,\n )\n await Promise.all(\n entries.map(async (entry) => {\n try {\n const holds = await entry.impl.holds(hostId, recordId)\n for (const hold of holds ?? []) {\n const quantity = Math.floor(Number(hold?.quantity))\n const lineIndex = Math.floor(Number(hold?.lineIndex))\n if (!(quantity > 0) || !(lineIndex >= 0)) continue\n result.holds.push({ ...hold, quantity, lineIndex })\n }\n } catch (error) {\n console.error(\n `[fulfillment-providers] \"${entry.impl.id}\" of \"${entry.pluginId}\" failed for ${hostId}/${recordId}`,\n error,\n )\n result.unanswered.push({ providerId: entry.impl.id, providerLabel: entry.impl.label })\n }\n }),\n )\n return result\n}\n\n/** Units held per line, summed across providers. */\nexport function heldQuantitiesByLine(holds: readonly PluginFulfillmentHold[]): Map<number, number> {\n const held = new Map<number, number>()\n for (const hold of holds) held.set(hold.lineIndex, (held.get(hold.lineIndex) ?? 0) + hold.quantity)\n return held\n}\n"],"names":["getRegisteringPluginId","definePluginServiceContract","registerPluginService","resolvePluginServices","PLUGIN_FULFILLMENT_PROVIDERS","multiple","registerPluginFulfillmentProvider","provider","options","id","String","trim","Error","pluginId","key","listPluginFulfillmentProviders","map","entry","impl","label","pluginFulfillmentHolds","hostId","recordId","result","holds","unanswered","entries","filter","exceptPluginId","Promise","all","hold","quantity","Math","floor","Number","lineIndex","push","error","console","providerId","providerLabel","heldQuantitiesByLine","held","Map","set","get"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,sBAAsB,QAAQ,qCAAiC;AACxE,SACEC,2BAA2B,EAC3BC,qBAAqB,EACrBC,qBAAqB,QAChB,uBAAmB;AAyD1B,MAAMC,+BAA+BH,4BACnC,8BACA;IAAEI,UAAU;AAAK;AAGnB;;;CAGC,GACD,OAAO,SAASC,kCACdC,QAAmC,EACnCC,OAA+B;cAIdR;IAFjB,MAAMS,KAAKC,eAAOH,4BAAAA,SAAUE,EAAE,mBAAI,IAAIE,IAAI;IAC1C,IAAI,CAACF,IAAI,MAAM,IAAIG,MAAM;IACzB,MAAMC,YAAWb,0BAAAA,oCAAAA,0BAA4BQ,2BAAAA,QAASK,QAAQ;IAC9DX,sBAAsBE,8BAA8BG,UAAU;QAC5DO,KAAKL;OACDI,WAAW;QAAEA;IAAS,IAAI,CAAC;AAEnC;AAEA,mEAAmE,GACnE,OAAO,SAASE;IACd,OAAOZ,sBAAsBC,8BAA8BY,GAAG,CAAC,CAACC,QAAW,CAAA;YACzEJ,UAAUI,MAAMJ,QAAQ;YACxBJ,IAAIQ,MAAMC,IAAI,CAACT,EAAE;YACjBU,OAAOF,MAAMC,IAAI,CAACC,KAAK;QACzB,CAAA;AACF;AASA;;;;;CAKC,GACD,OAAO,eAAeC,uBACpBC,MAAc,EACdC,QAAgB,EAChBd,OAAqC;IAErC,MAAMe,SAAiC;QAAEC,OAAO,EAAE;QAAEC,YAAY,EAAE;IAAC;IACnE,MAAMC,UAAUvB,sBAAsBC,8BAA8BuB,MAAM,CACxE,CAACV,QAAU,EAACT,2BAAAA,QAASoB,cAAc,KAAIX,MAAMJ,QAAQ,KAAKL,QAAQoB,cAAc;IAElF,MAAMC,QAAQC,GAAG,CACfJ,QAAQV,GAAG,CAAC,OAAOC;QACjB,IAAI;YACF,MAAMO,QAAQ,MAAMP,MAAMC,IAAI,CAACM,KAAK,CAACH,QAAQC;YAC7C,KAAK,MAAMS,QAAQP,gBAAAA,QAAS,EAAE,CAAE;gBAC9B,MAAMQ,WAAWC,KAAKC,KAAK,CAACC,OAAOJ,wBAAAA,KAAMC,QAAQ;gBACjD,MAAMI,YAAYH,KAAKC,KAAK,CAACC,OAAOJ,wBAAAA,KAAMK,SAAS;gBACnD,IAAI,CAAEJ,CAAAA,WAAW,CAAA,KAAM,CAAEI,CAAAA,aAAa,CAAA,GAAI;gBAC1Cb,OAAOC,KAAK,CAACa,IAAI,CAAC,aAAKN;oBAAMC;oBAAUI;;YACzC;QACF,EAAE,OAAOE,OAAO;YACdC,QAAQD,KAAK,CACX,CAAC,yBAAyB,EAAErB,MAAMC,IAAI,CAACT,EAAE,CAAC,MAAM,EAAEQ,MAAMJ,QAAQ,CAAC,aAAa,EAAEQ,OAAO,CAAC,EAAEC,UAAU,EACpGgB;YAEFf,OAAOE,UAAU,CAACY,IAAI,CAAC;gBAAEG,YAAYvB,MAAMC,IAAI,CAACT,EAAE;gBAAEgC,eAAexB,MAAMC,IAAI,CAACC,KAAK;YAAC;QACtF;IACF;IAEF,OAAOI;AACT;AAEA,kDAAkD,GAClD,OAAO,SAASmB,qBAAqBlB,KAAuC;QAEtBmB;IADpD,MAAMA,OAAO,IAAIC;IACjB,KAAK,MAAMb,QAAQP,MAAOmB,KAAKE,GAAG,CAACd,KAAKK,SAAS,EAAE,EAACO,YAAAA,KAAKG,GAAG,CAACf,KAAKK,SAAS,aAAvBO,YAA4B,KAAKZ,KAAKC,QAAQ;IAClG,OAAOW;AACT"}
@@ -21,7 +21,24 @@
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
- */ const pluginPermissions = new Map();
24
+ */ var _globalScope_REGISTRY_KEY;
25
+ /*
26
+ * One registry per process, keyed on `globalThis` under `Symbol.for`, like
27
+ * `plugin-services.ts`. Next compiles the boot file and the route handlers
28
+ * apart, so each can hold its own copy of this module: with the map at module
29
+ * scope, a plugin's keys registered by one copy were absent from the copy
30
+ * `resolveOrgPermissions` read, an absent key reads as refused, and the
31
+ * register's `managePos` gate answered 403 to the site's own owner.
32
+ */ const REGISTRY_KEY = Symbol.for('@aglyn/aglyn:plugin-permissions');
33
+ const globalScope = globalThis;
34
+ const tables = (_globalScope_REGISTRY_KEY = globalScope[REGISTRY_KEY]) != null ? _globalScope_REGISTRY_KEY : globalScope[REGISTRY_KEY] = {
35
+ permissions: new Map(),
36
+ listeners: new Set(),
37
+ version: {
38
+ value: 0
39
+ }
40
+ };
41
+ const pluginPermissions = tables.permissions;
25
42
  /*
26
43
  * Whoever resolved a permission map before a plugin registered holds a map
27
44
  * without that plugin's keys, and a key that is absent reads as refused
@@ -31,8 +48,7 @@
31
48
  * they hold by default until the next full render. So registration is
32
49
  * observable, the way `subscribePluginEntitlements` makes the org catalog's
33
50
  * registrations observable, and the provider re-reads on every change.
34
- */ let registrationVersion = 0;
35
- const listeners = new Set();
51
+ */ const listeners = tables.listeners;
36
52
  /** Calls `listener` after every registration; answers the unsubscribe. */ export function subscribePluginPermissions(listener) {
37
53
  listeners.add(listener);
38
54
  return ()=>{
@@ -43,10 +59,10 @@ const listeners = new Set();
43
59
  * A number that changes whenever the registry does — the snapshot for a
44
60
  * `useSyncExternalStore` over `subscribePluginPermissions`.
45
61
  */ export function pluginPermissionsVersion() {
46
- return registrationVersion;
62
+ return tables.version.value;
47
63
  }
48
64
  function notifyListeners() {
49
- registrationVersion += 1;
65
+ tables.version.value += 1;
50
66
  for (const listener of [
51
67
  ...listeners
52
68
  ])listener();
@@ -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\nconst pluginPermissions = new Map<string, PluginPermission>()\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 */\nlet registrationVersion = 0\nconst listeners = new Set<() => void>()\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 registrationVersion\n}\n\nfunction notifyListeners(): void {\n registrationVersion += 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":["pluginPermissions","Map","registrationVersion","listeners","Set","subscribePluginPermissions","listener","add","delete","pluginPermissionsVersion","notifyListeners","registerPluginPermissions","permissions","permission","incumbent","get","key","undefined","pluginId","console","error","set","listPluginPermissions","values","listPluginPermissionKeys","keys","pluginPermissionDefaults","tier","defaults"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;CAQC,GAYD,MAAMA,oBAAoB,IAAIC;AAE9B;;;;;;;;;CASC,GACD,IAAIC,sBAAsB;AAC1B,MAAMC,YAAY,IAAIC;AAEtB,wEAAwE,GACxE,OAAO,SAASC,2BAA2BC,QAAoB;IAC7DH,UAAUI,GAAG,CAACD;IACd,OAAO;QACLH,UAAUK,MAAM,CAACF;IACnB;AACF;AAEA;;;CAGC,GACD,OAAO,SAASG;IACd,OAAOP;AACT;AAEA,SAASQ;IACPR,uBAAuB;IACvB,KAAK,MAAMI,YAAY;WAAIH;KAAU,CAAEG;AACzC;AAEA;;;;;;;;;;;;;;;;CAgBC,GACD,OAAO,SAASK,0BACdC,WAAwC;IAExC,KAAK,MAAMC,cAAcD,YAAa;QACpC,MAAME,YAAYd,kBAAkBe,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;QACAlB,kBAAkBqB,GAAG,CAACR,WAAWG,GAAG,EAAEH;IACxC;IACAH;AACF;AAEA,OAAO,SAASY;IACd,OAAO;WAAItB,kBAAkBuB,MAAM;KAAG;AACxC;AAEA;;;;;;;;CAQC,GACD,OAAO,SAASC;IACd,OAAO;WAAIxB,kBAAkByB,IAAI;KAAG;AACtC;AAEA,0DAA0D,GAC1D,OAAO,SAASC,yBACdC,IAAmC;IAEnC,MAAMC,WAAoC,CAAC;IAC3C,KAAK,MAAMf,cAAcb,kBAAkBuB,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\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"}
@@ -190,11 +190,75 @@ export type PluginPersonViewPeople = {
190
190
  reason: 'not-found' | 'not-people' | 'unsupported';
191
191
  unsupported?: string[];
192
192
  };
193
+ /**
194
+ * People a site may see whose name, address or phone starts with what was
195
+ * typed (AGL-3609) — a cashier looking a customer up at the register, a
196
+ * picker anywhere a person is chosen by hand. One word: the owner answers it
197
+ * from its own search index and never by scanning its records.
198
+ */
199
+ export interface PluginPersonSearchRequest {
200
+ /** The site searching: only records it may see are answered. */
201
+ hostId: string;
202
+ /** The organization, when the caller knows it; the owner reads it off `hostId` otherwise. */
203
+ orgId?: string | null;
204
+ /** What was typed. Raw: the owner normalizes it, and reads a run of digits as a phone. */
205
+ text: string;
206
+ /** The most people answered. The owner may answer fewer. */
207
+ limit: number;
208
+ }
193
209
  /** Records to ask about, by the refs the owner handed out. */
194
210
  export interface PluginPersonWroteInRequest {
195
211
  orgId: string;
196
212
  records: readonly PluginPersonRecordRef[];
197
213
  }
214
+ /**
215
+ * The people one site holds, oldest change first, after a cursor (AGL-3639)
216
+ * — what a connector that mirrors a workspace's people into another system
217
+ * walks, once from the start as a backfill and then from where it stopped.
218
+ */
219
+ export interface PluginPersonChangesRequest {
220
+ /** The site the people are taken for: only records it may see are answered. */
221
+ hostId: string;
222
+ orgId?: string | null;
223
+ /**
224
+ * Where the last page stopped, exactly as the owner handed it out, or
225
+ * `null` to start from the earliest change. Opaque: only the owner reads
226
+ * it, so it can hold a change time finer than a millisecond and the id
227
+ * that breaks a tie, and a walk neither skips a person nor repeats one.
228
+ */
229
+ after: string | null;
230
+ /** The most people answered; the owner may answer fewer. */
231
+ limit: number;
232
+ }
233
+ /**
234
+ * A person as a connector copies them out: the platform's own fields on the
235
+ * record (`data`, read with the platform's readers — the consent basis
236
+ * above all), and the owner's profile of them as this site holds it, in the
237
+ * platform's words rather than the owner's storage.
238
+ */
239
+ export interface PluginPersonChange extends PluginPersonRecord {
240
+ /** When the record last changed, epoch ms. */
241
+ changedAtMs: number;
242
+ profile: {
243
+ name: string | null;
244
+ phone: string | null;
245
+ tags: string[];
246
+ /** What the person has spent at this site, integer cents, when the owner keeps it. */
247
+ lifetimeValueCents: number | null;
248
+ ordersCount: number | null;
249
+ };
250
+ }
251
+ export interface PluginPersonChangesPage {
252
+ people: PluginPersonChange[];
253
+ /**
254
+ * Where the next page starts — the cursor to store. It moves past every
255
+ * record the owner READ, including one it left out for the site, so a page
256
+ * can be empty and still move on. `null` when nothing changed after the
257
+ * cursor: the walk has reached the latest change, and the caller keeps the
258
+ * cursor it asked with.
259
+ */
260
+ next: string | null;
261
+ }
198
262
  export interface PluginPersonRecords {
199
263
  /** The person an address belongs to, or `null`. A read; may throw, and the caller decides which way a failure falls. */
200
264
  find(request: PluginPersonFindRequest): Promise<PluginPersonRecord | null>;
@@ -218,6 +282,17 @@ export interface PluginPersonRecords {
218
282
  * could not answer. Optional.
219
283
  */
220
284
  wroteIn?(request: PluginPersonWroteInRequest): Promise<Array<boolean | null>>;
285
+ /**
286
+ * People a site may see that match one typed word, best first. Optional:
287
+ * an owner that keeps no search index answers nothing. A read; may throw.
288
+ */
289
+ search?(request: PluginPersonSearchRequest): Promise<PluginPersonRecord[]>;
290
+ /**
291
+ * The people a site holds that changed after a cursor, oldest first.
292
+ * Optional: an owner that cannot walk its records by change answers
293
+ * nothing. A failed read throws.
294
+ */
295
+ changedSince?(request: PluginPersonChangesRequest): Promise<PluginPersonChangesPage>;
221
296
  }
222
297
  export declare const PLUGIN_PERSON_RECORDS: import("./plugin-services").PluginServiceContract<PluginPersonRecords>;
223
298
  /**
@@ -273,3 +348,18 @@ export declare function pluginPeopleInView(request: PluginPersonViewRequest): Pr
273
348
  * caller reads an unknown as unknown.
274
349
  */
275
350
  export declare function pluginPeopleWroteIn(request: PluginPersonWroteInRequest): Promise<Array<boolean | null> | null>;
351
+ /**
352
+ * People a site may see matching what was typed, or `null` when no plugin
353
+ * keeps people here or the one that does keeps no search — which is not
354
+ * "nobody matched". Blank text answers no one without asking. A failed read
355
+ * throws, and the caller decides how to say so.
356
+ */
357
+ export declare function searchPluginPeople(request: PluginPersonSearchRequest): Promise<PluginPersonRecord[] | null>;
358
+ /**
359
+ * The people a site holds that changed after a cursor, oldest first, or
360
+ * `null` when no plugin keeps people here or the one that does cannot walk
361
+ * them by change. The limit is clamped to 1–500. A failed read throws: a
362
+ * connector that skipped a page it could not read would advance past people
363
+ * it never copied.
364
+ */
365
+ export declare function pluginPeopleChangedSince(request: PluginPersonChangesRequest): Promise<PluginPersonChangesPage | null>;
@@ -103,5 +103,31 @@ export const PLUGIN_PERSON_RECORDS = definePluginServiceContract('core.person-re
103
103
  return null;
104
104
  }
105
105
  }
106
+ /**
107
+ * People a site may see matching what was typed, or `null` when no plugin
108
+ * keeps people here or the one that does keeps no search — which is not
109
+ * "nobody matched". Blank text answers no one without asking. A failed read
110
+ * throws, and the caller decides how to say so.
111
+ */ export async function searchPluginPeople(request) {
112
+ var _request_text;
113
+ const resolved = pluginPersonRecords();
114
+ if (!(resolved == null ? void 0 : resolved.records.search)) return null;
115
+ if (!String((_request_text = request.text) != null ? _request_text : '').trim() || !(request.limit > 0)) return [];
116
+ return await resolved.records.search(request);
117
+ }
118
+ /**
119
+ * The people a site holds that changed after a cursor, oldest first, or
120
+ * `null` when no plugin keeps people here or the one that does cannot walk
121
+ * them by change. The limit is clamped to 1–500. A failed read throws: a
122
+ * connector that skipped a page it could not read would advance past people
123
+ * it never copied.
124
+ */ export async function pluginPeopleChangedSince(request) {
125
+ const resolved = pluginPersonRecords();
126
+ if (!(resolved == null ? void 0 : resolved.records.changedSince)) return null;
127
+ const limit = Math.min(500, Math.max(1, Math.floor(Number(request.limit) || 0)));
128
+ return await resolved.records.changedSince(_extends({}, request, {
129
+ limit
130
+ }));
131
+ }
106
132
 
107
133
  //# sourceMappingURL=plugin-person-records.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-person-records.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 * THE PEOPLE A WORKSPACE KEEPS, asked for by the plugins that do not keep\n * them (AGL-3080).\n *\n * `plugin-contact-capture` is how a silo hands somebody it MET to the plugin\n * that keeps people. This is the other half: a plugin that has an address, or\n * a record the owner handed it, and needs the person behind it.\n *\n * - a flow email asking whether the person agreed to marketing mail;\n * - a campaign filling in the name a merge tag reads;\n * - a refund taking money back off what a customer is worth;\n * - an automation filing somebody under a campaign;\n * - a sales sequence reading the people a rep enrolls.\n *\n * Each of those used to open the owner's collection itself — its path, its\n * address index, which site may see which document — and broke the day the\n * owner changed one. None of it is visible to `check:lib-boundaries`, because\n * a collection name is not an import.\n *\n * So the plugin that keeps people registers ONE service here, and every rule\n * stays the owner's: how an address is keyed, what an alternate address a\n * merge folded in still answers to, which site may see which record, and what\n * a refund or a filing writes.\n *\n * ## What a record is\n *\n * A {@link PluginPersonRecord} is a kind and an id in the owner's own words —\n * the words `plugin-record-routes` addresses the record by — the person's\n * primary address, and the record as the owner stores it. The fields the\n * PLATFORM defines on a person record (the consent basis\n * `marketing-consent.ts` reads, the `emailState` verdict, the `visibleTo`\n * scope) are read off it with the platform's own readers; anything else is\n * the owner's and is documented with its registration.\n *\n * ## Not registered is an answer\n *\n * Every reader answers `null` when no plugin keeps people in this process,\n * which is a workspace with no record system and is not \"nobody found\". A\n * caller treats the two differently: a send with no record system has no\n * basis to read, which is not the same as a person who declined.\n *\n * A single-implementation contract: a workspace keeps one set of people, so\n * a second plugin's service is refused naming both and the incumbent keeps\n * serving. Registered from the owner's server declarations, so it is in place\n * in a process that never loaded the owner's API surface.\n *\n * ## The caller proves who is asking\n *\n * Like the capture and timeline seams, the registry authenticates nobody. A\n * caller decides in its own terms, before it asks, that the read or the write\n * is the workspace's to make.\n *\n * Server-side: reached by its own subpath, never through\n * `plugin-manager/index.ts`.\n */\n\n/** A person record, by the owner's word for its kind and its id. */\nexport interface PluginPersonRecordRef {\n /** The owner's word for the record — the kind `plugin-record-routes` addresses it by. */\n kind: string\n id: string\n}\n\n/** One person record, as the plugin that keeps people shares it. */\nexport interface PluginPersonRecord extends PluginPersonRecordRef {\n /** The record's primary address, normalized, or `null` when it holds none. */\n email: string | null\n /**\n * The record as the owner stores it. The platform's own fields on it are\n * read with the platform's readers; the rest is the owner's, documented\n * with its registration.\n */\n data: Readonly<Record<string, unknown>>\n}\n\n/** Who to find by address, and where. */\nexport interface PluginPersonFindRequest {\n /**\n * The organization whose people are searched. A caller that knows only the\n * site names `hostId` instead, and the owner reads the organization off it.\n */\n orgId?: string | null\n /** The site asking. Required with {@link onlyVisibleToSite}. */\n hostId?: string | null\n /** Raw: the owner normalizes it, alternate addresses included. */\n email: unknown\n /**\n * Answer only a record `hostId` may see. A record the address names but the\n * site cannot see is `null`, never a reason to look further — one address\n * names one person.\n */\n onlyVisibleToSite?: boolean\n /**\n * Look at every kind of record the owner keeps a person as, in the owner's\n * order, instead of only the one a known person is held as. A send deciding\n * whether it may mail somebody asks for this: a person not yet qualified is\n * still a person whose answer counts.\n */\n anyKind?: boolean\n}\n\n/** Records to read, by the refs the owner handed out. */\nexport interface PluginPersonReadRequest {\n orgId: string\n records: readonly PluginPersonRecordRef[]\n}\n\n/**\n * File a person under containers (`plugin-containers.ts`) — the merchant's\n * own act, through an automation step or an enrollment. Filing is not\n * consent.\n */\nexport interface PluginPersonFileRequest {\n /** The site filing them: the person is filed under the containers as this site holds them. */\n hostId: string\n orgId?: string | null\n record: PluginPersonRecordRef\n /** The container kind, as `plugin-containers.ts` declares it. */\n containerKind: string\n ids: readonly string[]\n}\n\n/**\n * Money handed back to a person a silo took it from: a refund, or a dispute\n * lost. The owner records it beside what the person is worth; it never\n * creates a person to hold it.\n */\nexport interface PluginPersonRefundRequest {\n /** The site the money was taken on. */\n hostId: string\n /** The buyer's address as the sale recorded it. Raw: the owner normalizes it. */\n email: unknown\n /** Cents reversed by THIS refund, never the sale's total. */\n amountCents: number\n /** The silo's own document the money went back on — what a timeline entry points at. */\n refId: string\n /** True only for the refund that moved the sale into fully refunded. */\n closedTheSale: boolean\n /** `chargeback` for a dispute lost; a refund otherwise. */\n reason?: 'refund' | 'chargeback'\n}\n\n/**\n * What became of a refund. `recorded`; `no-email` for a sale that never\n * named its buyer; `no-person` for an address the workspace holds nobody at;\n * `gone` for a person removed between the sale and the refund.\n */\nexport type PluginPersonRefundOutcome = 'recorded' | 'no-email' | 'no-person' | 'gone'\n\n/**\n * The people a saved selection the owner keeps names — a saved view of one of\n * its lists — taken for one site, as the people a sequence or a send works\n * through.\n */\nexport interface PluginPersonViewRequest {\n orgId: string\n /** The site the people are taken for: only records it holds are answered. */\n hostId: string\n /** The view, by the id the owner's record list handed out (`plugin-record-lists`). */\n viewId: string\n /** The member asking: a view they may not list does not exist for them. */\n viewerUid: string\n /** The most people answered. */\n limit: number\n}\n\nexport type PluginPersonViewPeople =\n | {\n ok: true\n people: PluginPersonRecordRef[]\n /** How many the view selects, as far as the owner read it. */\n total: number\n /** The view selects more than `limit`, or more than the owner read. */\n truncated: boolean\n }\n | {\n ok: false\n /**\n * `not-found`: no such view, or not this member's to list.\n * `not-people`: a view of something that is not people.\n * `unsupported`: the view narrows by something the owner cannot apply\n * outside its own list, so taking it would select more people than the\n * view shows; `unsupported` names what, in the owner's words.\n */\n reason: 'not-found' | 'not-people' | 'unsupported'\n unsupported?: string[]\n }\n\n/** Records to ask about, by the refs the owner handed out. */\nexport interface PluginPersonWroteInRequest {\n orgId: string\n records: readonly PluginPersonRecordRef[]\n}\n\nexport interface PluginPersonRecords {\n /** The person an address belongs to, or `null`. A read; may throw, and the caller decides which way a failure falls. */\n find(request: PluginPersonFindRequest): Promise<PluginPersonRecord | null>\n /** The records named, in the order asked; `null` for one that is gone. */\n read(request: PluginPersonReadRequest): Promise<Array<PluginPersonRecord | null>>\n /**\n * Files the person under the containers. Optional: an owner that keeps no\n * containers answers nothing. `filed: false` is a record the owner could\n * not find for the site.\n */\n fileUnder?(request: PluginPersonFileRequest): Promise<{ filed: boolean }>\n /** Records money handed back. Optional. Never throws: the money has already moved. */\n recordRefund?(request: PluginPersonRefundRequest): Promise<PluginPersonRefundOutcome>\n /** The people a saved view selects for a site. Optional: an owner that keeps no views answers nothing. */\n peopleInView?(request: PluginPersonViewRequest): Promise<PluginPersonViewPeople>\n /**\n * Whether each person has ever written to the workspace — an email they\n * sent, on their record — in the order asked; `null` for one the owner\n * could not answer. Optional.\n */\n wroteIn?(request: PluginPersonWroteInRequest): Promise<Array<boolean | null>>\n}\n\nexport const PLUGIN_PERSON_RECORDS = definePluginServiceContract<PluginPersonRecords>(\n 'core.person-records',\n { multiple: false },\n)\n\n/**\n * Registers the plugin that keeps people. The owner is the loader's marker\n * when a register fn is running, else `options.pluginId`; with neither the\n * registration throws, and a second plugin's service is refused naming both —\n * the incumbent keeps serving.\n */\nexport function registerPluginPersonRecords(\n records: PluginPersonRecords,\n options?: { pluginId?: string },\n): void {\n registerPluginService(PLUGIN_PERSON_RECORDS, records, {\n ...(options?.pluginId ? { pluginId: options.pluginId } : {}),\n })\n}\n\nexport interface ResolvedPluginPersonRecords {\n /** The plugin that keeps people. */\n pluginId: string\n records: PluginPersonRecords\n}\n\n/** The service with its owner, or `null` when no plugin keeps people here. */\nexport function pluginPersonRecords(): ResolvedPluginPersonRecords | null {\n const entry = resolvePluginServices(PLUGIN_PERSON_RECORDS)[0]\n return entry ? { pluginId: entry.pluginId, records: entry.impl } : null\n}\n\n/**\n * The person an address belongs to, or `null` — for nobody found, and for a\n * workspace no plugin keeps people for. A failed read is the owner's to\n * throw and the caller's to decide which way it falls.\n */\nexport async function findPluginPerson(\n request: PluginPersonFindRequest,\n): Promise<PluginPersonRecord | null> {\n const resolved = pluginPersonRecords()\n if (!resolved) return null\n return await resolved.records.find(request)\n}\n\n/**\n * The records named, in the order asked, `null` for each that is gone — or\n * `null` whole when no plugin keeps people here, which is not \"all gone\".\n */\nexport async function readPluginPeople(\n request: PluginPersonReadRequest,\n): Promise<Array<PluginPersonRecord | null> | null> {\n const resolved = pluginPersonRecords()\n if (!resolved) return null\n if (!request.records.length) return []\n return await resolved.records.read(request)\n}\n\n/**\n * Files the person under the containers through whichever plugin keeps\n * people, or answers `null` when none does or the one that does keeps no\n * containers. A failed write throws, as the caller's own write would have.\n */\nexport async function filePluginPersonUnder(\n request: PluginPersonFileRequest,\n): Promise<{ filed: boolean } | null> {\n const resolved = pluginPersonRecords()\n if (!resolved?.records.fileUnder) return null\n if (!request.ids.length) return { filed: false }\n return await resolved.records.fileUnder(request)\n}\n\n/**\n * Hands a refund to whichever plugin keeps people, or answers `null` when\n * none does. Never throws: every caller has already moved the money and\n * recorded it on its own document, and a person's figure is derived from it.\n */\nexport async function recordPluginPersonRefund(\n request: PluginPersonRefundRequest,\n): Promise<PluginPersonRefundOutcome | null> {\n const resolved = pluginPersonRecords()\n if (!resolved?.records.recordRefund) return null\n try {\n return await resolved.records.recordRefund(request)\n } catch (error) {\n console.error('[person-records] the record system could not record a refund', error)\n return null\n }\n}\n\n/**\n * The people a saved view selects for a site, or `null` when no plugin keeps\n * people here or the one that does keeps no views. A failed read throws.\n */\nexport async function pluginPeopleInView(\n request: PluginPersonViewRequest,\n): Promise<PluginPersonViewPeople | null> {\n const resolved = pluginPersonRecords()\n if (!resolved?.records.peopleInView) return null\n return await resolved.records.peopleInView(request)\n}\n\n/**\n * Whether each person has ever written to the workspace, in the order asked\n * — `null` for each the owner could not answer, and `null` whole when no\n * plugin keeps people here or the one that does cannot say. Never throws: a\n * caller reads an unknown as unknown.\n */\nexport async function pluginPeopleWroteIn(\n request: PluginPersonWroteInRequest,\n): Promise<Array<boolean | null> | null> {\n const resolved = pluginPersonRecords()\n if (!resolved?.records.wroteIn) return null\n if (!request.records.length) return []\n try {\n return await resolved.records.wroteIn(request)\n } catch (error) {\n console.error('[person-records] the record system could not say who wrote in', error)\n return null\n }\n}\n"],"names":["definePluginServiceContract","registerPluginService","resolvePluginServices","PLUGIN_PERSON_RECORDS","multiple","registerPluginPersonRecords","records","options","pluginId","pluginPersonRecords","entry","impl","findPluginPerson","request","resolved","find","readPluginPeople","length","read","filePluginPersonUnder","fileUnder","ids","filed","recordPluginPersonRefund","recordRefund","error","console","pluginPeopleInView","peopleInView","pluginPeopleWroteIn","wroteIn"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,2BAA2B,EAC3BC,qBAAqB,EACrBC,qBAAqB,QAChB,uBAAmB;AA2N1B,OAAO,MAAMC,wBAAwBH,4BACnC,uBACA;IAAEI,UAAU;AAAM,GACnB;AAED;;;;;CAKC,GACD,OAAO,SAASC,4BACdC,OAA4B,EAC5BC,OAA+B;IAE/BN,sBAAsBE,uBAAuBG,SAAS,aAChDC,CAAAA,2BAAAA,QAASC,QAAQ,IAAG;QAAEA,UAAUD,QAAQC,QAAQ;IAAC,IAAI,CAAC;AAE9D;AAQA,4EAA4E,GAC5E,OAAO,SAASC;IACd,MAAMC,QAAQR,sBAAsBC,sBAAsB,CAAC,EAAE;IAC7D,OAAOO,QAAQ;QAAEF,UAAUE,MAAMF,QAAQ;QAAEF,SAASI,MAAMC,IAAI;IAAC,IAAI;AACrE;AAEA;;;;CAIC,GACD,OAAO,eAAeC,iBACpBC,OAAgC;IAEhC,MAAMC,WAAWL;IACjB,IAAI,CAACK,UAAU,OAAO;IACtB,OAAO,MAAMA,SAASR,OAAO,CAACS,IAAI,CAACF;AACrC;AAEA;;;CAGC,GACD,OAAO,eAAeG,iBACpBH,OAAgC;IAEhC,MAAMC,WAAWL;IACjB,IAAI,CAACK,UAAU,OAAO;IACtB,IAAI,CAACD,QAAQP,OAAO,CAACW,MAAM,EAAE,OAAO,EAAE;IACtC,OAAO,MAAMH,SAASR,OAAO,CAACY,IAAI,CAACL;AACrC;AAEA;;;;CAIC,GACD,OAAO,eAAeM,sBACpBN,OAAgC;IAEhC,MAAMC,WAAWL;IACjB,IAAI,EAACK,4BAAAA,SAAUR,OAAO,CAACc,SAAS,GAAE,OAAO;IACzC,IAAI,CAACP,QAAQQ,GAAG,CAACJ,MAAM,EAAE,OAAO;QAAEK,OAAO;IAAM;IAC/C,OAAO,MAAMR,SAASR,OAAO,CAACc,SAAS,CAACP;AAC1C;AAEA;;;;CAIC,GACD,OAAO,eAAeU,yBACpBV,OAAkC;IAElC,MAAMC,WAAWL;IACjB,IAAI,EAACK,4BAAAA,SAAUR,OAAO,CAACkB,YAAY,GAAE,OAAO;IAC5C,IAAI;QACF,OAAO,MAAMV,SAASR,OAAO,CAACkB,YAAY,CAACX;IAC7C,EAAE,OAAOY,OAAO;QACdC,QAAQD,KAAK,CAAC,gEAAgEA;QAC9E,OAAO;IACT;AACF;AAEA;;;CAGC,GACD,OAAO,eAAeE,mBACpBd,OAAgC;IAEhC,MAAMC,WAAWL;IACjB,IAAI,EAACK,4BAAAA,SAAUR,OAAO,CAACsB,YAAY,GAAE,OAAO;IAC5C,OAAO,MAAMd,SAASR,OAAO,CAACsB,YAAY,CAACf;AAC7C;AAEA;;;;;CAKC,GACD,OAAO,eAAegB,oBACpBhB,OAAmC;IAEnC,MAAMC,WAAWL;IACjB,IAAI,EAACK,4BAAAA,SAAUR,OAAO,CAACwB,OAAO,GAAE,OAAO;IACvC,IAAI,CAACjB,QAAQP,OAAO,CAACW,MAAM,EAAE,OAAO,EAAE;IACtC,IAAI;QACF,OAAO,MAAMH,SAASR,OAAO,CAACwB,OAAO,CAACjB;IACxC,EAAE,OAAOY,OAAO;QACdC,QAAQD,KAAK,CAAC,iEAAiEA;QAC/E,OAAO;IACT;AACF"}
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-person-records.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 * THE PEOPLE A WORKSPACE KEEPS, asked for by the plugins that do not keep\n * them (AGL-3080).\n *\n * `plugin-contact-capture` is how a silo hands somebody it MET to the plugin\n * that keeps people. This is the other half: a plugin that has an address, or\n * a record the owner handed it, and needs the person behind it.\n *\n * - a flow email asking whether the person agreed to marketing mail;\n * - a campaign filling in the name a merge tag reads;\n * - a refund taking money back off what a customer is worth;\n * - an automation filing somebody under a campaign;\n * - a sales sequence reading the people a rep enrolls.\n *\n * Each of those used to open the owner's collection itself — its path, its\n * address index, which site may see which document — and broke the day the\n * owner changed one. None of it is visible to `check:lib-boundaries`, because\n * a collection name is not an import.\n *\n * So the plugin that keeps people registers ONE service here, and every rule\n * stays the owner's: how an address is keyed, what an alternate address a\n * merge folded in still answers to, which site may see which record, and what\n * a refund or a filing writes.\n *\n * ## What a record is\n *\n * A {@link PluginPersonRecord} is a kind and an id in the owner's own words —\n * the words `plugin-record-routes` addresses the record by — the person's\n * primary address, and the record as the owner stores it. The fields the\n * PLATFORM defines on a person record (the consent basis\n * `marketing-consent.ts` reads, the `emailState` verdict, the `visibleTo`\n * scope) are read off it with the platform's own readers; anything else is\n * the owner's and is documented with its registration.\n *\n * ## Not registered is an answer\n *\n * Every reader answers `null` when no plugin keeps people in this process,\n * which is a workspace with no record system and is not \"nobody found\". A\n * caller treats the two differently: a send with no record system has no\n * basis to read, which is not the same as a person who declined.\n *\n * A single-implementation contract: a workspace keeps one set of people, so\n * a second plugin's service is refused naming both and the incumbent keeps\n * serving. Registered from the owner's server declarations, so it is in place\n * in a process that never loaded the owner's API surface.\n *\n * ## The caller proves who is asking\n *\n * Like the capture and timeline seams, the registry authenticates nobody. A\n * caller decides in its own terms, before it asks, that the read or the write\n * is the workspace's to make.\n *\n * Server-side: reached by its own subpath, never through\n * `plugin-manager/index.ts`.\n */\n\n/** A person record, by the owner's word for its kind and its id. */\nexport interface PluginPersonRecordRef {\n /** The owner's word for the record — the kind `plugin-record-routes` addresses it by. */\n kind: string\n id: string\n}\n\n/** One person record, as the plugin that keeps people shares it. */\nexport interface PluginPersonRecord extends PluginPersonRecordRef {\n /** The record's primary address, normalized, or `null` when it holds none. */\n email: string | null\n /**\n * The record as the owner stores it. The platform's own fields on it are\n * read with the platform's readers; the rest is the owner's, documented\n * with its registration.\n */\n data: Readonly<Record<string, unknown>>\n}\n\n/** Who to find by address, and where. */\nexport interface PluginPersonFindRequest {\n /**\n * The organization whose people are searched. A caller that knows only the\n * site names `hostId` instead, and the owner reads the organization off it.\n */\n orgId?: string | null\n /** The site asking. Required with {@link onlyVisibleToSite}. */\n hostId?: string | null\n /** Raw: the owner normalizes it, alternate addresses included. */\n email: unknown\n /**\n * Answer only a record `hostId` may see. A record the address names but the\n * site cannot see is `null`, never a reason to look further — one address\n * names one person.\n */\n onlyVisibleToSite?: boolean\n /**\n * Look at every kind of record the owner keeps a person as, in the owner's\n * order, instead of only the one a known person is held as. A send deciding\n * whether it may mail somebody asks for this: a person not yet qualified is\n * still a person whose answer counts.\n */\n anyKind?: boolean\n}\n\n/** Records to read, by the refs the owner handed out. */\nexport interface PluginPersonReadRequest {\n orgId: string\n records: readonly PluginPersonRecordRef[]\n}\n\n/**\n * File a person under containers (`plugin-containers.ts`) — the merchant's\n * own act, through an automation step or an enrollment. Filing is not\n * consent.\n */\nexport interface PluginPersonFileRequest {\n /** The site filing them: the person is filed under the containers as this site holds them. */\n hostId: string\n orgId?: string | null\n record: PluginPersonRecordRef\n /** The container kind, as `plugin-containers.ts` declares it. */\n containerKind: string\n ids: readonly string[]\n}\n\n/**\n * Money handed back to a person a silo took it from: a refund, or a dispute\n * lost. The owner records it beside what the person is worth; it never\n * creates a person to hold it.\n */\nexport interface PluginPersonRefundRequest {\n /** The site the money was taken on. */\n hostId: string\n /** The buyer's address as the sale recorded it. Raw: the owner normalizes it. */\n email: unknown\n /** Cents reversed by THIS refund, never the sale's total. */\n amountCents: number\n /** The silo's own document the money went back on — what a timeline entry points at. */\n refId: string\n /** True only for the refund that moved the sale into fully refunded. */\n closedTheSale: boolean\n /** `chargeback` for a dispute lost; a refund otherwise. */\n reason?: 'refund' | 'chargeback'\n}\n\n/**\n * What became of a refund. `recorded`; `no-email` for a sale that never\n * named its buyer; `no-person` for an address the workspace holds nobody at;\n * `gone` for a person removed between the sale and the refund.\n */\nexport type PluginPersonRefundOutcome = 'recorded' | 'no-email' | 'no-person' | 'gone'\n\n/**\n * The people a saved selection the owner keeps names — a saved view of one of\n * its lists — taken for one site, as the people a sequence or a send works\n * through.\n */\nexport interface PluginPersonViewRequest {\n orgId: string\n /** The site the people are taken for: only records it holds are answered. */\n hostId: string\n /** The view, by the id the owner's record list handed out (`plugin-record-lists`). */\n viewId: string\n /** The member asking: a view they may not list does not exist for them. */\n viewerUid: string\n /** The most people answered. */\n limit: number\n}\n\nexport type PluginPersonViewPeople =\n | {\n ok: true\n people: PluginPersonRecordRef[]\n /** How many the view selects, as far as the owner read it. */\n total: number\n /** The view selects more than `limit`, or more than the owner read. */\n truncated: boolean\n }\n | {\n ok: false\n /**\n * `not-found`: no such view, or not this member's to list.\n * `not-people`: a view of something that is not people.\n * `unsupported`: the view narrows by something the owner cannot apply\n * outside its own list, so taking it would select more people than the\n * view shows; `unsupported` names what, in the owner's words.\n */\n reason: 'not-found' | 'not-people' | 'unsupported'\n unsupported?: string[]\n }\n\n/**\n * People a site may see whose name, address or phone starts with what was\n * typed (AGL-3609) — a cashier looking a customer up at the register, a\n * picker anywhere a person is chosen by hand. One word: the owner answers it\n * from its own search index and never by scanning its records.\n */\nexport interface PluginPersonSearchRequest {\n /** The site searching: only records it may see are answered. */\n hostId: string\n /** The organization, when the caller knows it; the owner reads it off `hostId` otherwise. */\n orgId?: string | null\n /** What was typed. Raw: the owner normalizes it, and reads a run of digits as a phone. */\n text: string\n /** The most people answered. The owner may answer fewer. */\n limit: number\n}\n\n/** Records to ask about, by the refs the owner handed out. */\nexport interface PluginPersonWroteInRequest {\n orgId: string\n records: readonly PluginPersonRecordRef[]\n}\n\n/**\n * The people one site holds, oldest change first, after a cursor (AGL-3639)\n * — what a connector that mirrors a workspace's people into another system\n * walks, once from the start as a backfill and then from where it stopped.\n */\nexport interface PluginPersonChangesRequest {\n /** The site the people are taken for: only records it may see are answered. */\n hostId: string\n orgId?: string | null\n /**\n * Where the last page stopped, exactly as the owner handed it out, or\n * `null` to start from the earliest change. Opaque: only the owner reads\n * it, so it can hold a change time finer than a millisecond and the id\n * that breaks a tie, and a walk neither skips a person nor repeats one.\n */\n after: string | null\n /** The most people answered; the owner may answer fewer. */\n limit: number\n}\n\n/**\n * A person as a connector copies them out: the platform's own fields on the\n * record (`data`, read with the platform's readers — the consent basis\n * above all), and the owner's profile of them as this site holds it, in the\n * platform's words rather than the owner's storage.\n */\nexport interface PluginPersonChange extends PluginPersonRecord {\n /** When the record last changed, epoch ms. */\n changedAtMs: number\n profile: {\n name: string | null\n phone: string | null\n tags: string[]\n /** What the person has spent at this site, integer cents, when the owner keeps it. */\n lifetimeValueCents: number | null\n ordersCount: number | null\n }\n}\n\nexport interface PluginPersonChangesPage {\n people: PluginPersonChange[]\n /**\n * Where the next page starts — the cursor to store. It moves past every\n * record the owner READ, including one it left out for the site, so a page\n * can be empty and still move on. `null` when nothing changed after the\n * cursor: the walk has reached the latest change, and the caller keeps the\n * cursor it asked with.\n */\n next: string | null\n}\n\nexport interface PluginPersonRecords {\n /** The person an address belongs to, or `null`. A read; may throw, and the caller decides which way a failure falls. */\n find(request: PluginPersonFindRequest): Promise<PluginPersonRecord | null>\n /** The records named, in the order asked; `null` for one that is gone. */\n read(request: PluginPersonReadRequest): Promise<Array<PluginPersonRecord | null>>\n /**\n * Files the person under the containers. Optional: an owner that keeps no\n * containers answers nothing. `filed: false` is a record the owner could\n * not find for the site.\n */\n fileUnder?(request: PluginPersonFileRequest): Promise<{ filed: boolean }>\n /** Records money handed back. Optional. Never throws: the money has already moved. */\n recordRefund?(request: PluginPersonRefundRequest): Promise<PluginPersonRefundOutcome>\n /** The people a saved view selects for a site. Optional: an owner that keeps no views answers nothing. */\n peopleInView?(request: PluginPersonViewRequest): Promise<PluginPersonViewPeople>\n /**\n * Whether each person has ever written to the workspace — an email they\n * sent, on their record — in the order asked; `null` for one the owner\n * could not answer. Optional.\n */\n wroteIn?(request: PluginPersonWroteInRequest): Promise<Array<boolean | null>>\n /**\n * People a site may see that match one typed word, best first. Optional:\n * an owner that keeps no search index answers nothing. A read; may throw.\n */\n search?(request: PluginPersonSearchRequest): Promise<PluginPersonRecord[]>\n /**\n * The people a site holds that changed after a cursor, oldest first.\n * Optional: an owner that cannot walk its records by change answers\n * nothing. A failed read throws.\n */\n changedSince?(request: PluginPersonChangesRequest): Promise<PluginPersonChangesPage>\n}\n\nexport const PLUGIN_PERSON_RECORDS = definePluginServiceContract<PluginPersonRecords>(\n 'core.person-records',\n { multiple: false },\n)\n\n/**\n * Registers the plugin that keeps people. The owner is the loader's marker\n * when a register fn is running, else `options.pluginId`; with neither the\n * registration throws, and a second plugin's service is refused naming both —\n * the incumbent keeps serving.\n */\nexport function registerPluginPersonRecords(\n records: PluginPersonRecords,\n options?: { pluginId?: string },\n): void {\n registerPluginService(PLUGIN_PERSON_RECORDS, records, {\n ...(options?.pluginId ? { pluginId: options.pluginId } : {}),\n })\n}\n\nexport interface ResolvedPluginPersonRecords {\n /** The plugin that keeps people. */\n pluginId: string\n records: PluginPersonRecords\n}\n\n/** The service with its owner, or `null` when no plugin keeps people here. */\nexport function pluginPersonRecords(): ResolvedPluginPersonRecords | null {\n const entry = resolvePluginServices(PLUGIN_PERSON_RECORDS)[0]\n return entry ? { pluginId: entry.pluginId, records: entry.impl } : null\n}\n\n/**\n * The person an address belongs to, or `null` — for nobody found, and for a\n * workspace no plugin keeps people for. A failed read is the owner's to\n * throw and the caller's to decide which way it falls.\n */\nexport async function findPluginPerson(\n request: PluginPersonFindRequest,\n): Promise<PluginPersonRecord | null> {\n const resolved = pluginPersonRecords()\n if (!resolved) return null\n return await resolved.records.find(request)\n}\n\n/**\n * The records named, in the order asked, `null` for each that is gone — or\n * `null` whole when no plugin keeps people here, which is not \"all gone\".\n */\nexport async function readPluginPeople(\n request: PluginPersonReadRequest,\n): Promise<Array<PluginPersonRecord | null> | null> {\n const resolved = pluginPersonRecords()\n if (!resolved) return null\n if (!request.records.length) return []\n return await resolved.records.read(request)\n}\n\n/**\n * Files the person under the containers through whichever plugin keeps\n * people, or answers `null` when none does or the one that does keeps no\n * containers. A failed write throws, as the caller's own write would have.\n */\nexport async function filePluginPersonUnder(\n request: PluginPersonFileRequest,\n): Promise<{ filed: boolean } | null> {\n const resolved = pluginPersonRecords()\n if (!resolved?.records.fileUnder) return null\n if (!request.ids.length) return { filed: false }\n return await resolved.records.fileUnder(request)\n}\n\n/**\n * Hands a refund to whichever plugin keeps people, or answers `null` when\n * none does. Never throws: every caller has already moved the money and\n * recorded it on its own document, and a person's figure is derived from it.\n */\nexport async function recordPluginPersonRefund(\n request: PluginPersonRefundRequest,\n): Promise<PluginPersonRefundOutcome | null> {\n const resolved = pluginPersonRecords()\n if (!resolved?.records.recordRefund) return null\n try {\n return await resolved.records.recordRefund(request)\n } catch (error) {\n console.error('[person-records] the record system could not record a refund', error)\n return null\n }\n}\n\n/**\n * The people a saved view selects for a site, or `null` when no plugin keeps\n * people here or the one that does keeps no views. A failed read throws.\n */\nexport async function pluginPeopleInView(\n request: PluginPersonViewRequest,\n): Promise<PluginPersonViewPeople | null> {\n const resolved = pluginPersonRecords()\n if (!resolved?.records.peopleInView) return null\n return await resolved.records.peopleInView(request)\n}\n\n/**\n * Whether each person has ever written to the workspace, in the order asked\n * — `null` for each the owner could not answer, and `null` whole when no\n * plugin keeps people here or the one that does cannot say. Never throws: a\n * caller reads an unknown as unknown.\n */\nexport async function pluginPeopleWroteIn(\n request: PluginPersonWroteInRequest,\n): Promise<Array<boolean | null> | null> {\n const resolved = pluginPersonRecords()\n if (!resolved?.records.wroteIn) return null\n if (!request.records.length) return []\n try {\n return await resolved.records.wroteIn(request)\n } catch (error) {\n console.error('[person-records] the record system could not say who wrote in', error)\n return null\n }\n}\n\n/**\n * People a site may see matching what was typed, or `null` when no plugin\n * keeps people here or the one that does keeps no search — which is not\n * \"nobody matched\". Blank text answers no one without asking. A failed read\n * throws, and the caller decides how to say so.\n */\nexport async function searchPluginPeople(\n request: PluginPersonSearchRequest,\n): Promise<PluginPersonRecord[] | null> {\n const resolved = pluginPersonRecords()\n if (!resolved?.records.search) return null\n if (!String(request.text ?? '').trim() || !(request.limit > 0)) return []\n return await resolved.records.search(request)\n}\n\n/**\n * The people a site holds that changed after a cursor, oldest first, or\n * `null` when no plugin keeps people here or the one that does cannot walk\n * them by change. The limit is clamped to 1–500. A failed read throws: a\n * connector that skipped a page it could not read would advance past people\n * it never copied.\n */\nexport async function pluginPeopleChangedSince(\n request: PluginPersonChangesRequest,\n): Promise<PluginPersonChangesPage | null> {\n const resolved = pluginPersonRecords()\n if (!resolved?.records.changedSince) return null\n const limit = Math.min(500, Math.max(1, Math.floor(Number(request.limit) || 0)))\n return await resolved.records.changedSince({ ...request, limit })\n}\n"],"names":["definePluginServiceContract","registerPluginService","resolvePluginServices","PLUGIN_PERSON_RECORDS","multiple","registerPluginPersonRecords","records","options","pluginId","pluginPersonRecords","entry","impl","findPluginPerson","request","resolved","find","readPluginPeople","length","read","filePluginPersonUnder","fileUnder","ids","filed","recordPluginPersonRefund","recordRefund","error","console","pluginPeopleInView","peopleInView","pluginPeopleWroteIn","wroteIn","searchPluginPeople","search","String","text","trim","limit","pluginPeopleChangedSince","changedSince","Math","min","max","floor","Number"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,2BAA2B,EAC3BC,qBAAqB,EACrBC,qBAAqB,QAChB,uBAAmB;AA0S1B,OAAO,MAAMC,wBAAwBH,4BACnC,uBACA;IAAEI,UAAU;AAAM,GACnB;AAED;;;;;CAKC,GACD,OAAO,SAASC,4BACdC,OAA4B,EAC5BC,OAA+B;IAE/BN,sBAAsBE,uBAAuBG,SAAS,aAChDC,CAAAA,2BAAAA,QAASC,QAAQ,IAAG;QAAEA,UAAUD,QAAQC,QAAQ;IAAC,IAAI,CAAC;AAE9D;AAQA,4EAA4E,GAC5E,OAAO,SAASC;IACd,MAAMC,QAAQR,sBAAsBC,sBAAsB,CAAC,EAAE;IAC7D,OAAOO,QAAQ;QAAEF,UAAUE,MAAMF,QAAQ;QAAEF,SAASI,MAAMC,IAAI;IAAC,IAAI;AACrE;AAEA;;;;CAIC,GACD,OAAO,eAAeC,iBACpBC,OAAgC;IAEhC,MAAMC,WAAWL;IACjB,IAAI,CAACK,UAAU,OAAO;IACtB,OAAO,MAAMA,SAASR,OAAO,CAACS,IAAI,CAACF;AACrC;AAEA;;;CAGC,GACD,OAAO,eAAeG,iBACpBH,OAAgC;IAEhC,MAAMC,WAAWL;IACjB,IAAI,CAACK,UAAU,OAAO;IACtB,IAAI,CAACD,QAAQP,OAAO,CAACW,MAAM,EAAE,OAAO,EAAE;IACtC,OAAO,MAAMH,SAASR,OAAO,CAACY,IAAI,CAACL;AACrC;AAEA;;;;CAIC,GACD,OAAO,eAAeM,sBACpBN,OAAgC;IAEhC,MAAMC,WAAWL;IACjB,IAAI,EAACK,4BAAAA,SAAUR,OAAO,CAACc,SAAS,GAAE,OAAO;IACzC,IAAI,CAACP,QAAQQ,GAAG,CAACJ,MAAM,EAAE,OAAO;QAAEK,OAAO;IAAM;IAC/C,OAAO,MAAMR,SAASR,OAAO,CAACc,SAAS,CAACP;AAC1C;AAEA;;;;CAIC,GACD,OAAO,eAAeU,yBACpBV,OAAkC;IAElC,MAAMC,WAAWL;IACjB,IAAI,EAACK,4BAAAA,SAAUR,OAAO,CAACkB,YAAY,GAAE,OAAO;IAC5C,IAAI;QACF,OAAO,MAAMV,SAASR,OAAO,CAACkB,YAAY,CAACX;IAC7C,EAAE,OAAOY,OAAO;QACdC,QAAQD,KAAK,CAAC,gEAAgEA;QAC9E,OAAO;IACT;AACF;AAEA;;;CAGC,GACD,OAAO,eAAeE,mBACpBd,OAAgC;IAEhC,MAAMC,WAAWL;IACjB,IAAI,EAACK,4BAAAA,SAAUR,OAAO,CAACsB,YAAY,GAAE,OAAO;IAC5C,OAAO,MAAMd,SAASR,OAAO,CAACsB,YAAY,CAACf;AAC7C;AAEA;;;;;CAKC,GACD,OAAO,eAAegB,oBACpBhB,OAAmC;IAEnC,MAAMC,WAAWL;IACjB,IAAI,EAACK,4BAAAA,SAAUR,OAAO,CAACwB,OAAO,GAAE,OAAO;IACvC,IAAI,CAACjB,QAAQP,OAAO,CAACW,MAAM,EAAE,OAAO,EAAE;IACtC,IAAI;QACF,OAAO,MAAMH,SAASR,OAAO,CAACwB,OAAO,CAACjB;IACxC,EAAE,OAAOY,OAAO;QACdC,QAAQD,KAAK,CAAC,iEAAiEA;QAC/E,OAAO;IACT;AACF;AAEA;;;;;CAKC,GACD,OAAO,eAAeM,mBACpBlB,OAAkC;QAItBA;IAFZ,MAAMC,WAAWL;IACjB,IAAI,EAACK,4BAAAA,SAAUR,OAAO,CAAC0B,MAAM,GAAE,OAAO;IACtC,IAAI,CAACC,QAAOpB,gBAAAA,QAAQqB,IAAI,YAAZrB,gBAAgB,IAAIsB,IAAI,MAAM,CAAEtB,CAAAA,QAAQuB,KAAK,GAAG,CAAA,GAAI,OAAO,EAAE;IACzE,OAAO,MAAMtB,SAASR,OAAO,CAAC0B,MAAM,CAACnB;AACvC;AAEA;;;;;;CAMC,GACD,OAAO,eAAewB,yBACpBxB,OAAmC;IAEnC,MAAMC,WAAWL;IACjB,IAAI,EAACK,4BAAAA,SAAUR,OAAO,CAACgC,YAAY,GAAE,OAAO;IAC5C,MAAMF,QAAQG,KAAKC,GAAG,CAAC,KAAKD,KAAKE,GAAG,CAAC,GAAGF,KAAKG,KAAK,CAACC,OAAO9B,QAAQuB,KAAK,KAAK;IAC5E,OAAO,MAAMtB,SAASR,OAAO,CAACgC,YAAY,CAAC,aAAKzB;QAASuB;;AAC3D"}