@aglyn/aglyn 1.0.0-beta.199 → 1.0.0-beta.201

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 (84) hide show
  1. package/package.json +11 -11
  2. package/src/lib/app-utils/activity-search.d.ts +39 -0
  3. package/src/lib/app-utils/activity-search.js +85 -0
  4. package/src/lib/app-utils/activity-search.js.map +1 -0
  5. package/src/lib/app-utils/admin-audit-index.d.ts +104 -0
  6. package/src/lib/app-utils/admin-audit-index.js +177 -0
  7. package/src/lib/app-utils/admin-audit-index.js.map +1 -0
  8. package/src/lib/app-utils/artifact-list-keys.d.ts +51 -0
  9. package/src/lib/app-utils/artifact-list-keys.js +154 -0
  10. package/src/lib/app-utils/artifact-list-keys.js.map +1 -0
  11. package/src/lib/app-utils/content-query-fields.d.ts +69 -0
  12. package/src/lib/app-utils/content-query-fields.js +79 -0
  13. package/src/lib/app-utils/content-query-fields.js.map +1 -0
  14. package/src/lib/app-utils/crm.d.ts +170 -1
  15. package/src/lib/app-utils/crm.js +414 -1
  16. package/src/lib/app-utils/crm.js.map +1 -1
  17. package/src/lib/app-utils/dataset-models.d.ts +75 -24
  18. package/src/lib/app-utils/dataset-models.js +107 -40
  19. package/src/lib/app-utils/dataset-models.js.map +1 -1
  20. package/src/lib/app-utils/docs-help.generated.d.ts +4 -4
  21. package/src/lib/app-utils/docs-help.generated.js +10 -0
  22. package/src/lib/app-utils/docs-help.generated.js.map +1 -1
  23. package/src/lib/app-utils/docs-index.generated.js +89 -75
  24. package/src/lib/app-utils/docs-index.generated.js.map +1 -1
  25. package/src/lib/app-utils/email-search.d.ts +36 -0
  26. package/src/lib/app-utils/email-search.js +53 -0
  27. package/src/lib/app-utils/email-search.js.map +1 -0
  28. package/src/lib/app-utils/email-state.d.ts +7 -3
  29. package/src/lib/app-utils/email-state.js +22 -6
  30. package/src/lib/app-utils/email-state.js.map +1 -1
  31. package/src/lib/app-utils/forms.d.ts +178 -6
  32. package/src/lib/app-utils/forms.js +197 -0
  33. package/src/lib/app-utils/forms.js.map +1 -1
  34. package/src/lib/app-utils/health-report.d.ts +3 -3
  35. package/src/lib/app-utils/health-report.js +9 -9
  36. package/src/lib/app-utils/health-report.js.map +1 -1
  37. package/src/lib/app-utils/host-naming.d.ts +12 -7
  38. package/src/lib/app-utils/host-naming.js +18 -3
  39. package/src/lib/app-utils/host-naming.js.map +1 -1
  40. package/src/lib/app-utils/lockdown.d.ts +19 -0
  41. package/src/lib/app-utils/lockdown.js +21 -0
  42. package/src/lib/app-utils/lockdown.js.map +1 -1
  43. package/src/lib/app-utils/marketplace-listing-visibility.d.ts +7 -6
  44. package/src/lib/app-utils/marketplace-listing-visibility.js +12 -10
  45. package/src/lib/app-utils/marketplace-listing-visibility.js.map +1 -1
  46. package/src/lib/app-utils/media-embedded-fields.d.ts +40 -0
  47. package/src/lib/app-utils/media-embedded-fields.js +77 -0
  48. package/src/lib/app-utils/media-embedded-fields.js.map +1 -1
  49. package/src/lib/app-utils/media-metadata.d.ts +95 -0
  50. package/src/lib/app-utils/media-metadata.js +94 -0
  51. package/src/lib/app-utils/media-metadata.js.map +1 -1
  52. package/src/lib/app-utils/message-search.d.ts +40 -0
  53. package/src/lib/app-utils/message-search.js +168 -0
  54. package/src/lib/app-utils/message-search.js.map +1 -0
  55. package/src/lib/app-utils/name-search.d.ts +83 -0
  56. package/src/lib/app-utils/name-search.js +97 -0
  57. package/src/lib/app-utils/name-search.js.map +1 -1
  58. package/src/lib/app-utils/payment-risk.d.ts +75 -0
  59. package/src/lib/app-utils/payment-risk.js +139 -0
  60. package/src/lib/app-utils/payment-risk.js.map +1 -0
  61. package/src/lib/app-utils/plugin-api-request-host.d.ts +29 -0
  62. package/src/lib/app-utils/plugin-api-request-host.js +90 -0
  63. package/src/lib/app-utils/plugin-api-request-host.js.map +1 -0
  64. package/src/lib/app-utils/plugin-bundle-checks.d.ts +3 -2
  65. package/src/lib/app-utils/plugin-bundle-checks.js +76 -5
  66. package/src/lib/app-utils/plugin-bundle-checks.js.map +1 -1
  67. package/src/lib/app-utils/server.d.ts +2 -0
  68. package/src/lib/app-utils/server.js +2 -0
  69. package/src/lib/app-utils/server.js.map +1 -1
  70. package/src/lib/app-utils/webhook-delivery.js +4 -1
  71. package/src/lib/app-utils/webhook-delivery.js.map +1 -1
  72. package/src/lib/plugin-manager/billing-webhook-hooks.d.ts +9 -0
  73. package/src/lib/plugin-manager/billing-webhook-hooks.js +5 -1
  74. package/src/lib/plugin-manager/billing-webhook-hooks.js.map +1 -1
  75. package/src/lib/plugin-manager/plugin-activity-actions.d.ts +9 -1
  76. package/src/lib/plugin-manager/plugin-activity-actions.js +11 -1
  77. package/src/lib/plugin-manager/plugin-activity-actions.js.map +1 -1
  78. package/src/lib/plugin-manager/plugin-contact-capture.d.ts +8 -0
  79. package/src/lib/plugin-manager/plugin-contact-capture.js.map +1 -1
  80. package/src/lib/plugin-manager/plugin-events.d.ts +22 -0
  81. package/src/lib/plugin-manager/plugin-events.js.map +1 -1
  82. package/src/lib/plugin-manager/plugin-jobs.d.ts +8 -4
  83. package/src/lib/plugin-manager/plugin-jobs.js +8 -4
  84. package/src/lib/plugin-manager/plugin-jobs.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 /** 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;AAyGxE,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 * 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 /** 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;AA4HxE,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"}
@@ -61,10 +61,14 @@ export interface PluginJobHostGate {
61
61
  isLocked(hostId: string): Promise<boolean>;
62
62
  }
63
63
  /**
64
- * The gate as the runner builds it. Exported because the MANUAL doors —
65
- * `commerce/process-abandoned`, `bookings/reminders` and the other
66
- * `x-cron-secret` entry points that still exist for ops — call the same scan
67
- * functions and must ask the same question. A forced pass is still a pass.
64
+ * The gate as the runner builds it.
65
+ *
66
+ * The MANUAL doors — `commerce/process-abandoned`, `commerce/process-restock`,
67
+ * `bookings/reminders` — do NOT use it (AGL-3356). They are served by the
68
+ * console, which registers no resolver, so this answered "not locked" for
69
+ * every site there and a forced pass mailed a suspended site's customers.
70
+ * They take `siteLockdownJobGate()` from the admin lib instead: the same
71
+ * function the tenant registers here, needing no registration at all.
68
72
  *
69
73
  * NO resolver registered answers "not locked", loudly and once. That is the
70
74
  * fail-open direction on purpose: a deployment that has not wired a carrier
@@ -85,10 +85,14 @@
85
85
  hostLockedResolver = null;
86
86
  }
87
87
  /**
88
- * The gate as the runner builds it. Exported because the MANUAL doors —
89
- * `commerce/process-abandoned`, `bookings/reminders` and the other
90
- * `x-cron-secret` entry points that still exist for ops — call the same scan
91
- * functions and must ask the same question. A forced pass is still a pass.
88
+ * The gate as the runner builds it.
89
+ *
90
+ * The MANUAL doors — `commerce/process-abandoned`, `commerce/process-restock`,
91
+ * `bookings/reminders` — do NOT use it (AGL-3356). They are served by the
92
+ * console, which registers no resolver, so this answered "not locked" for
93
+ * every site there and a forced pass mailed a suspended site's customers.
94
+ * They take `siteLockdownJobGate()` from the admin lib instead: the same
95
+ * function the tenant registers here, needing no registration at all.
92
96
  *
93
97
  * NO resolver registered answers "not locked", loudly and once. That is the
94
98
  * fail-open direction on purpose: a deployment that has not wired a carrier
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-jobs.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 scheduled jobs (AGL-435, Strapi cron parity): `/server` entries\n * register named jobs with an interval; a guarded platform runner route\n * (`/api/plugins/run-jobs`, secret header, invoked by the deployment's\n * scheduler) executes the due ones. The REGISTRY is pure — due-ness and\n * last-run persistence belong to the runner, so core stays storage-free.\n * Handlers must be idempotent and bounded (they share the API process).\n */\n\n/*==========================================\n * THE LOCKDOWN GATE FOR BACKGROUND WORK (AGL-2495, from the AGL-1621 drill).\n *\n * A job runs on PLATFORM credentials, from a secret-gated route, with no\n * visitor, no session and no org — so none of the gates that cover the\n * visitor-facing write paths are anywhere near it. That is exactly the shape\n * the drill found in `publish-schedule-job.ts`: a scheduled publish firing on\n * a locked host. The commerce and bookings beats are the sharper version of\n * the same thing, because they touch orders, stock and money for a host that\n * may be suspended for abuse, non-payment or a legal takedown.\n *\n * ## Why the gate lives HERE and not at the six call sites\n *\n * Six call-site edits close six holes and guarantee the seventh is forgotten.\n * Gating in the job contract means the seventh job is covered the day it is\n * written: `lockdown` below is REQUIRED, so a new `registerPluginJob({…})`\n * does not compile until its author has answered \"what does this touch\".\n *\n * (The nx boundary is NOT why. `libs/plugins/*` CAN import\n * `@aglyn/tenant-data-admin` — it carries `scope:data` and `scope:aglyn`,\n * both on the `aglyn:addons` allowlist, and 196 files in `libs/plugins`\n * already do. The AGL-2495 sweep recorded the opposite as its reason for\n * deferring; it was wrong about the constraint and right about the design.)\n *\n * ## Why core cannot just call `getSiteLockdown` itself\n *\n * `@aglyn/tenant-data-admin` imports `@aglyn/aglyn/server`, so a core import\n * of it is a project CYCLE. Hence the same registry shape the platform uses\n * for every other app↔core edge (`registerOrderFulfilmentService`,\n * `registerBillingWebhookHandler`, `registerSitePageResolver`): the host app\n * registers the resolver from a place that may import the admin lib, and core\n * looks it up. Nothing is imported statically in either direction and a\n * self-host build with a different carrier registers its own.\n *\n * ## Fail-open is INHERITED, not re-decided\n *\n * The gate asks whatever the app registered — on the tenant that is\n * `getSiteLockdown`, which owns the fail-open/fail-closed decision (and the\n * takedown-class ratchet on top of it). This module deliberately holds no\n * parallel notion of what a lock is: it has one question and no opinion.\n *=========================================*/\n\n/**\n * The one question a beat asks before touching a host: is a lock active that\n * forbids this write? Resolved by the host app, never by core.\n */\nexport type PluginJobHostLockedResolver = (\n hostId: string,\n) => Promise<boolean> | boolean\n\nlet hostLockedResolver: PluginJobHostLockedResolver | null = null\n\n/**\n * Registers the platform's per-host lockdown verdict for background work,\n * from a place that may import the admin data lib — on this platform,\n * `apps/tenant/utils/plugin-job-lockdown.ts`, loaded for its side effect by\n * the runner route beside the core job registrations.\n *\n * Idempotent and last-registration-wins, unlike `registerOrderFulfilmentService`\n * next door: that one holds a capability with a plugin OWNER, where a second\n * claimant is a misconfiguration. This is infrastructure the deployment\n * supplies exactly once, and a process that loads the module twice must not\n * throw on the second pass.\n */\nexport function registerPluginJobHostLockdown(\n resolver: PluginJobHostLockedResolver,\n): void {\n hostLockedResolver = resolver\n}\n\n/** Has the deployment wired a verdict? Reported by the runner route. */\nexport function hasPluginJobHostLockdown(): boolean {\n return hostLockedResolver !== null\n}\n\n/** Test seam: forget the registration. */\nexport function resetPluginJobHostLockdownForTests(): void {\n hostLockedResolver = null\n}\n\n/**\n * The gate handed to a `per-host` job. One method, so there is exactly one\n * thing to call and one thing for the coverage guard to look for:\n *\n * if (await gate.isLocked(hostId)) continue\n *\n * SKIPPED, NOT DROPPED is the caller's obligation and the reason this\n * answers a question rather than performing the skip: `continue` leaves the\n * row exactly as it was, so the work lands on the next beat after the lift.\n * A gate that swallowed the row — stamping it done, deleting it, advancing a\n * cursor past it — would turn a pause into a cancellation, which is what\n * `publish-schedule-job.ts` argued at length and what every job here copies.\n */\nexport interface PluginJobHostGate {\n isLocked(hostId: string): Promise<boolean>\n}\n\n/**\n * The gate as the runner builds it. Exported because the MANUAL doors —\n * `commerce/process-abandoned`, `bookings/reminders` and the other\n * `x-cron-secret` entry points that still exist for ops — call the same scan\n * functions and must ask the same question. A forced pass is still a pass.\n *\n * NO resolver registered answers \"not locked\", loudly and once. That is the\n * fail-open direction on purpose: a deployment that has not wired a carrier\n * (a self-host, a test process) must not have every background job welded\n * shut by infrastructure it never asked for. The loss is real and is why\n * `hasPluginJobHostLockdown` is reported by the runner route rather than\n * left to be inferred from nothing happening.\n */\nexport function pluginJobHostGate(): PluginJobHostGate {\n return {\n isLocked: async (hostId: string) => {\n if (!hostId) return false\n if (!hostLockedResolver) {\n warnMissingResolver()\n return false\n }\n return (await hostLockedResolver(hostId)) === true\n },\n }\n}\n\nlet warnedMissingResolver = false\nfunction warnMissingResolver(): void {\n if (warnedMissingResolver) return\n warnedMissingResolver = true\n console.error(\n '[plugin-jobs] no host lockdown resolver is registered — background ' +\n 'jobs are running UNGATED. Register one with ' +\n 'registerPluginJobHostLockdown() from the app that owns the runner.',\n )\n}\n\n/** Test seam: let the \"warned once\" latch be re-armed. */\nexport function resetPluginJobLockdownWarningForTests(): void {\n warnedMissingResolver = false\n}\n\n/**\n * What a job touches, and therefore what the runner owes it. REQUIRED on\n * every registration — the point of the field is that a new job cannot\n * compile without its author answering.\n */\nexport type PluginJobLockdown =\n /**\n * The job acts FOR HOSTS. The runner injects a {@link PluginJobHostGate}\n * and the handler must ask it for every host it is about to touch.\n */\n | { scope: 'per-host' }\n /**\n * The job touches nothing a lock could be about. `reason` is the argument,\n * and `lockdown-tenant-api-coverage.spec.ts` holds the set by name so a new\n * one has to be argued rather than declared.\n */\n | { scope: 'platform'; reason: string }\n\nexport interface PluginJob {\n pluginId: string\n /** Stable job name, unique within the plugin ('expire-stale-holds'). */\n name: string\n intervalMinutes: number\n description?: string\n /** See {@link PluginJobLockdown}. Required, deliberately. */\n lockdown: PluginJobLockdown\n handler: (gate: PluginJobHostGate) => Promise<void> | void\n}\n\nconst jobs = new Map<string, PluginJob>()\n\nexport const pluginJobKey = (job: Pick<PluginJob, 'pluginId' | 'name'>) =>\n `${job.pluginId}:${job.name}`\n\n/** Idempotent per pluginId:name. */\nexport function registerPluginJob(job: PluginJob): void {\n jobs.set(pluginJobKey(job), job)\n}\n\nexport function listPluginJobs(): PluginJob[] {\n return [...jobs.values()]\n}\n\nexport interface PluginJobResult {\n key: string\n ok: boolean\n error?: string\n}\n\n/**\n * The gate a `platform`-scoped job gets: one that REFUSES to answer.\n *\n * A declaration of `platform` scope is a claim that there is no host to ask\n * about. If such a job asks anyway, the claim is false and the registration\n * is the thing that needs fixing — so the runner surfaces it as that job's\n * own failure (isolated, like every other job error) instead of quietly\n * answering \"not locked\" and letting a mislabelled job mutate a locked site.\n *\n * This is the one place the contract is enforced at RUNTIME rather than by\n * the type or by the guard, and it is enforceable precisely because asking\n * is the only way to get an answer.\n */\nfunction refusingGate(key: string): PluginJobHostGate {\n return {\n isLocked: async (hostId: string) => {\n throw new Error(\n `plugin job ${key} declared lockdown scope 'platform' but asked ` +\n `about host \"${hostId}\". Declare { scope: 'per-host' } instead.`,\n )\n },\n }\n}\n\n/**\n * Runs the jobs `due` selects (default: all), sequentially with error\n * isolation — one broken job never blocks the rest.\n *\n * Every handler is handed a {@link PluginJobHostGate} (AGL-2495). It is a\n * parameter rather than something the handler imports so that the runner\n * stays the single place the verdict comes from: swap what\n * `registerPluginJobHostLockdown` holds and every job on the beat changes\n * behaviour together, including one written next month.\n */\nexport async function runPluginJobs(\n due?: (job: PluginJob) => boolean,\n): Promise<PluginJobResult[]> {\n const results: PluginJobResult[] = []\n for (const job of jobs.values()) {\n if (due && !due(job)) continue\n const key = pluginJobKey(job)\n try {\n await job.handler(\n job.lockdown?.scope === 'platform'\n ? refusingGate(key)\n : pluginJobHostGate(),\n )\n results.push({ key, ok: true })\n } catch (error) {\n console.error(`plugin job ${key} failed:`, error)\n results.push({ key, ok: false, error: String(error) })\n }\n }\n return results\n}\n"],"names":["hostLockedResolver","registerPluginJobHostLockdown","resolver","hasPluginJobHostLockdown","resetPluginJobHostLockdownForTests","pluginJobHostGate","isLocked","hostId","warnMissingResolver","warnedMissingResolver","console","error","resetPluginJobLockdownWarningForTests","jobs","Map","pluginJobKey","job","pluginId","name","registerPluginJob","set","listPluginJobs","values","refusingGate","key","Error","runPluginJobs","due","results","handler","lockdown","scope","push","ok","String"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;CAOC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;2CAwC2C,GAE3C;;;CAGC,GAKD,IAAIA,qBAAyD;AAE7D;;;;;;;;;;;CAWC,GACD,OAAO,SAASC,8BACdC,QAAqC;IAErCF,qBAAqBE;AACvB;AAEA,sEAAsE,GACtE,OAAO,SAASC;IACd,OAAOH,uBAAuB;AAChC;AAEA,wCAAwC,GACxC,OAAO,SAASI;IACdJ,qBAAqB;AACvB;AAmBA;;;;;;;;;;;;CAYC,GACD,OAAO,SAASK;IACd,OAAO;QACLC,UAAU,OAAOC;YACf,IAAI,CAACA,QAAQ,OAAO;YACpB,IAAI,CAACP,oBAAoB;gBACvBQ;gBACA,OAAO;YACT;YACA,OAAO,AAAC,MAAMR,mBAAmBO,YAAa;QAChD;IACF;AACF;AAEA,IAAIE,wBAAwB;AAC5B,SAASD;IACP,IAAIC,uBAAuB;IAC3BA,wBAAwB;IACxBC,QAAQC,KAAK,CACX,wEACE,iDACA;AAEN;AAEA,wDAAwD,GACxD,OAAO,SAASC;IACdH,wBAAwB;AAC1B;AA+BA,MAAMI,OAAO,IAAIC;AAEjB,OAAO,MAAMC,eAAe,CAACC,MAC3B,GAAGA,IAAIC,QAAQ,CAAC,CAAC,EAAED,IAAIE,IAAI,EAAE,CAAA;AAE/B,kCAAkC,GAClC,OAAO,SAASC,kBAAkBH,GAAc;IAC9CH,KAAKO,GAAG,CAACL,aAAaC,MAAMA;AAC9B;AAEA,OAAO,SAASK;IACd,OAAO;WAAIR,KAAKS,MAAM;KAAG;AAC3B;AAQA;;;;;;;;;;;;CAYC,GACD,SAASC,aAAaC,GAAW;IAC/B,OAAO;QACLlB,UAAU,OAAOC;YACf,MAAM,IAAIkB,MACR,CAAC,WAAW,EAAED,IAAI,8CAA8C,CAAC,GAC/D,CAAC,YAAY,EAAEjB,OAAO,yCAAyC,CAAC;QAEtE;IACF;AACF;AAEA;;;;;;;;;CASC,GACD,OAAO,eAAemB,cACpBC,GAAiC;IAEjC,MAAMC,UAA6B,EAAE;IACrC,KAAK,MAAMZ,OAAOH,KAAKS,MAAM,GAAI;QAC/B,IAAIK,OAAO,CAACA,IAAIX,MAAM;QACtB,MAAMQ,MAAMT,aAAaC;QACzB,IAAI;gBAEAA;YADF,MAAMA,IAAIa,OAAO,CACfb,EAAAA,gBAAAA,IAAIc,QAAQ,qBAAZd,cAAce,KAAK,MAAK,aACpBR,aAAaC,OACbnB;YAENuB,QAAQI,IAAI,CAAC;gBAAER;gBAAKS,IAAI;YAAK;QAC/B,EAAE,OAAOtB,OAAO;YACdD,QAAQC,KAAK,CAAC,CAAC,WAAW,EAAEa,IAAI,QAAQ,CAAC,EAAEb;YAC3CiB,QAAQI,IAAI,CAAC;gBAAER;gBAAKS,IAAI;gBAAOtB,OAAOuB,OAAOvB;YAAO;QACtD;IACF;IACA,OAAOiB;AACT"}
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-jobs.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 scheduled jobs (AGL-435, Strapi cron parity): `/server` entries\n * register named jobs with an interval; a guarded platform runner route\n * (`/api/plugins/run-jobs`, secret header, invoked by the deployment's\n * scheduler) executes the due ones. The REGISTRY is pure — due-ness and\n * last-run persistence belong to the runner, so core stays storage-free.\n * Handlers must be idempotent and bounded (they share the API process).\n */\n\n/*==========================================\n * THE LOCKDOWN GATE FOR BACKGROUND WORK (AGL-2495, from the AGL-1621 drill).\n *\n * A job runs on PLATFORM credentials, from a secret-gated route, with no\n * visitor, no session and no org — so none of the gates that cover the\n * visitor-facing write paths are anywhere near it. That is exactly the shape\n * the drill found in `publish-schedule-job.ts`: a scheduled publish firing on\n * a locked host. The commerce and bookings beats are the sharper version of\n * the same thing, because they touch orders, stock and money for a host that\n * may be suspended for abuse, non-payment or a legal takedown.\n *\n * ## Why the gate lives HERE and not at the six call sites\n *\n * Six call-site edits close six holes and guarantee the seventh is forgotten.\n * Gating in the job contract means the seventh job is covered the day it is\n * written: `lockdown` below is REQUIRED, so a new `registerPluginJob({…})`\n * does not compile until its author has answered \"what does this touch\".\n *\n * (The nx boundary is NOT why. `libs/plugins/*` CAN import\n * `@aglyn/tenant-data-admin` — it carries `scope:data` and `scope:aglyn`,\n * both on the `aglyn:addons` allowlist, and 196 files in `libs/plugins`\n * already do. The AGL-2495 sweep recorded the opposite as its reason for\n * deferring; it was wrong about the constraint and right about the design.)\n *\n * ## Why core cannot just call `getSiteLockdown` itself\n *\n * `@aglyn/tenant-data-admin` imports `@aglyn/aglyn/server`, so a core import\n * of it is a project CYCLE. Hence the same registry shape the platform uses\n * for every other app↔core edge (`registerOrderFulfilmentService`,\n * `registerBillingWebhookHandler`, `registerSitePageResolver`): the host app\n * registers the resolver from a place that may import the admin lib, and core\n * looks it up. Nothing is imported statically in either direction and a\n * self-host build with a different carrier registers its own.\n *\n * ## Fail-open is INHERITED, not re-decided\n *\n * The gate asks whatever the app registered — on the tenant that is\n * `getSiteLockdown`, which owns the fail-open/fail-closed decision (and the\n * takedown-class ratchet on top of it). This module deliberately holds no\n * parallel notion of what a lock is: it has one question and no opinion.\n *=========================================*/\n\n/**\n * The one question a beat asks before touching a host: is a lock active that\n * forbids this write? Resolved by the host app, never by core.\n */\nexport type PluginJobHostLockedResolver = (\n hostId: string,\n) => Promise<boolean> | boolean\n\nlet hostLockedResolver: PluginJobHostLockedResolver | null = null\n\n/**\n * Registers the platform's per-host lockdown verdict for background work,\n * from a place that may import the admin data lib — on this platform,\n * `apps/tenant/utils/plugin-job-lockdown.ts`, loaded for its side effect by\n * the runner route beside the core job registrations.\n *\n * Idempotent and last-registration-wins, unlike `registerOrderFulfilmentService`\n * next door: that one holds a capability with a plugin OWNER, where a second\n * claimant is a misconfiguration. This is infrastructure the deployment\n * supplies exactly once, and a process that loads the module twice must not\n * throw on the second pass.\n */\nexport function registerPluginJobHostLockdown(\n resolver: PluginJobHostLockedResolver,\n): void {\n hostLockedResolver = resolver\n}\n\n/** Has the deployment wired a verdict? Reported by the runner route. */\nexport function hasPluginJobHostLockdown(): boolean {\n return hostLockedResolver !== null\n}\n\n/** Test seam: forget the registration. */\nexport function resetPluginJobHostLockdownForTests(): void {\n hostLockedResolver = null\n}\n\n/**\n * The gate handed to a `per-host` job. One method, so there is exactly one\n * thing to call and one thing for the coverage guard to look for:\n *\n * if (await gate.isLocked(hostId)) continue\n *\n * SKIPPED, NOT DROPPED is the caller's obligation and the reason this\n * answers a question rather than performing the skip: `continue` leaves the\n * row exactly as it was, so the work lands on the next beat after the lift.\n * A gate that swallowed the row — stamping it done, deleting it, advancing a\n * cursor past it — would turn a pause into a cancellation, which is what\n * `publish-schedule-job.ts` argued at length and what every job here copies.\n */\nexport interface PluginJobHostGate {\n isLocked(hostId: string): Promise<boolean>\n}\n\n/**\n * The gate as the runner builds it.\n *\n * The MANUAL doors — `commerce/process-abandoned`, `commerce/process-restock`,\n * `bookings/reminders` — do NOT use it (AGL-3356). They are served by the\n * console, which registers no resolver, so this answered \"not locked\" for\n * every site there and a forced pass mailed a suspended site's customers.\n * They take `siteLockdownJobGate()` from the admin lib instead: the same\n * function the tenant registers here, needing no registration at all.\n *\n * NO resolver registered answers \"not locked\", loudly and once. That is the\n * fail-open direction on purpose: a deployment that has not wired a carrier\n * (a self-host, a test process) must not have every background job welded\n * shut by infrastructure it never asked for. The loss is real and is why\n * `hasPluginJobHostLockdown` is reported by the runner route rather than\n * left to be inferred from nothing happening.\n */\nexport function pluginJobHostGate(): PluginJobHostGate {\n return {\n isLocked: async (hostId: string) => {\n if (!hostId) return false\n if (!hostLockedResolver) {\n warnMissingResolver()\n return false\n }\n return (await hostLockedResolver(hostId)) === true\n },\n }\n}\n\nlet warnedMissingResolver = false\nfunction warnMissingResolver(): void {\n if (warnedMissingResolver) return\n warnedMissingResolver = true\n console.error(\n '[plugin-jobs] no host lockdown resolver is registered — background ' +\n 'jobs are running UNGATED. Register one with ' +\n 'registerPluginJobHostLockdown() from the app that owns the runner.',\n )\n}\n\n/** Test seam: let the \"warned once\" latch be re-armed. */\nexport function resetPluginJobLockdownWarningForTests(): void {\n warnedMissingResolver = false\n}\n\n/**\n * What a job touches, and therefore what the runner owes it. REQUIRED on\n * every registration — the point of the field is that a new job cannot\n * compile without its author answering.\n */\nexport type PluginJobLockdown =\n /**\n * The job acts FOR HOSTS. The runner injects a {@link PluginJobHostGate}\n * and the handler must ask it for every host it is about to touch.\n */\n | { scope: 'per-host' }\n /**\n * The job touches nothing a lock could be about. `reason` is the argument,\n * and `lockdown-tenant-api-coverage.spec.ts` holds the set by name so a new\n * one has to be argued rather than declared.\n */\n | { scope: 'platform'; reason: string }\n\nexport interface PluginJob {\n pluginId: string\n /** Stable job name, unique within the plugin ('expire-stale-holds'). */\n name: string\n intervalMinutes: number\n description?: string\n /** See {@link PluginJobLockdown}. Required, deliberately. */\n lockdown: PluginJobLockdown\n handler: (gate: PluginJobHostGate) => Promise<void> | void\n}\n\nconst jobs = new Map<string, PluginJob>()\n\nexport const pluginJobKey = (job: Pick<PluginJob, 'pluginId' | 'name'>) =>\n `${job.pluginId}:${job.name}`\n\n/** Idempotent per pluginId:name. */\nexport function registerPluginJob(job: PluginJob): void {\n jobs.set(pluginJobKey(job), job)\n}\n\nexport function listPluginJobs(): PluginJob[] {\n return [...jobs.values()]\n}\n\nexport interface PluginJobResult {\n key: string\n ok: boolean\n error?: string\n}\n\n/**\n * The gate a `platform`-scoped job gets: one that REFUSES to answer.\n *\n * A declaration of `platform` scope is a claim that there is no host to ask\n * about. If such a job asks anyway, the claim is false and the registration\n * is the thing that needs fixing — so the runner surfaces it as that job's\n * own failure (isolated, like every other job error) instead of quietly\n * answering \"not locked\" and letting a mislabelled job mutate a locked site.\n *\n * This is the one place the contract is enforced at RUNTIME rather than by\n * the type or by the guard, and it is enforceable precisely because asking\n * is the only way to get an answer.\n */\nfunction refusingGate(key: string): PluginJobHostGate {\n return {\n isLocked: async (hostId: string) => {\n throw new Error(\n `plugin job ${key} declared lockdown scope 'platform' but asked ` +\n `about host \"${hostId}\". Declare { scope: 'per-host' } instead.`,\n )\n },\n }\n}\n\n/**\n * Runs the jobs `due` selects (default: all), sequentially with error\n * isolation — one broken job never blocks the rest.\n *\n * Every handler is handed a {@link PluginJobHostGate} (AGL-2495). It is a\n * parameter rather than something the handler imports so that the runner\n * stays the single place the verdict comes from: swap what\n * `registerPluginJobHostLockdown` holds and every job on the beat changes\n * behaviour together, including one written next month.\n */\nexport async function runPluginJobs(\n due?: (job: PluginJob) => boolean,\n): Promise<PluginJobResult[]> {\n const results: PluginJobResult[] = []\n for (const job of jobs.values()) {\n if (due && !due(job)) continue\n const key = pluginJobKey(job)\n try {\n await job.handler(\n job.lockdown?.scope === 'platform'\n ? refusingGate(key)\n : pluginJobHostGate(),\n )\n results.push({ key, ok: true })\n } catch (error) {\n console.error(`plugin job ${key} failed:`, error)\n results.push({ key, ok: false, error: String(error) })\n }\n }\n return results\n}\n"],"names":["hostLockedResolver","registerPluginJobHostLockdown","resolver","hasPluginJobHostLockdown","resetPluginJobHostLockdownForTests","pluginJobHostGate","isLocked","hostId","warnMissingResolver","warnedMissingResolver","console","error","resetPluginJobLockdownWarningForTests","jobs","Map","pluginJobKey","job","pluginId","name","registerPluginJob","set","listPluginJobs","values","refusingGate","key","Error","runPluginJobs","due","results","handler","lockdown","scope","push","ok","String"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;CAOC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;2CAwC2C,GAE3C;;;CAGC,GAKD,IAAIA,qBAAyD;AAE7D;;;;;;;;;;;CAWC,GACD,OAAO,SAASC,8BACdC,QAAqC;IAErCF,qBAAqBE;AACvB;AAEA,sEAAsE,GACtE,OAAO,SAASC;IACd,OAAOH,uBAAuB;AAChC;AAEA,wCAAwC,GACxC,OAAO,SAASI;IACdJ,qBAAqB;AACvB;AAmBA;;;;;;;;;;;;;;;;CAgBC,GACD,OAAO,SAASK;IACd,OAAO;QACLC,UAAU,OAAOC;YACf,IAAI,CAACA,QAAQ,OAAO;YACpB,IAAI,CAACP,oBAAoB;gBACvBQ;gBACA,OAAO;YACT;YACA,OAAO,AAAC,MAAMR,mBAAmBO,YAAa;QAChD;IACF;AACF;AAEA,IAAIE,wBAAwB;AAC5B,SAASD;IACP,IAAIC,uBAAuB;IAC3BA,wBAAwB;IACxBC,QAAQC,KAAK,CACX,wEACE,iDACA;AAEN;AAEA,wDAAwD,GACxD,OAAO,SAASC;IACdH,wBAAwB;AAC1B;AA+BA,MAAMI,OAAO,IAAIC;AAEjB,OAAO,MAAMC,eAAe,CAACC,MAC3B,GAAGA,IAAIC,QAAQ,CAAC,CAAC,EAAED,IAAIE,IAAI,EAAE,CAAA;AAE/B,kCAAkC,GAClC,OAAO,SAASC,kBAAkBH,GAAc;IAC9CH,KAAKO,GAAG,CAACL,aAAaC,MAAMA;AAC9B;AAEA,OAAO,SAASK;IACd,OAAO;WAAIR,KAAKS,MAAM;KAAG;AAC3B;AAQA;;;;;;;;;;;;CAYC,GACD,SAASC,aAAaC,GAAW;IAC/B,OAAO;QACLlB,UAAU,OAAOC;YACf,MAAM,IAAIkB,MACR,CAAC,WAAW,EAAED,IAAI,8CAA8C,CAAC,GAC/D,CAAC,YAAY,EAAEjB,OAAO,yCAAyC,CAAC;QAEtE;IACF;AACF;AAEA;;;;;;;;;CASC,GACD,OAAO,eAAemB,cACpBC,GAAiC;IAEjC,MAAMC,UAA6B,EAAE;IACrC,KAAK,MAAMZ,OAAOH,KAAKS,MAAM,GAAI;QAC/B,IAAIK,OAAO,CAACA,IAAIX,MAAM;QACtB,MAAMQ,MAAMT,aAAaC;QACzB,IAAI;gBAEAA;YADF,MAAMA,IAAIa,OAAO,CACfb,EAAAA,gBAAAA,IAAIc,QAAQ,qBAAZd,cAAce,KAAK,MAAK,aACpBR,aAAaC,OACbnB;YAENuB,QAAQI,IAAI,CAAC;gBAAER;gBAAKS,IAAI;YAAK;QAC/B,EAAE,OAAOtB,OAAO;YACdD,QAAQC,KAAK,CAAC,CAAC,WAAW,EAAEa,IAAI,QAAQ,CAAC,EAAEb;YAC3CiB,QAAQI,IAAI,CAAC;gBAAER;gBAAKS,IAAI;gBAAOtB,OAAOuB,OAAOvB;YAAO;QACtD;IACF;IACA,OAAOiB;AACT"}