@aglyn/aglyn 1.0.0-beta.269 → 1.0.0-beta.270

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 (59) hide show
  1. package/package.json +11 -11
  2. package/src/lib/app-utils/account-preferences.d.ts +248 -0
  3. package/src/lib/app-utils/account-preferences.js +434 -0
  4. package/src/lib/app-utils/account-preferences.js.map +1 -0
  5. package/src/lib/app-utils/country-options.d.ts +27 -0
  6. package/src/lib/app-utils/country-options.js +96 -0
  7. package/src/lib/app-utils/country-options.js.map +1 -0
  8. package/src/lib/app-utils/docs-help.generated.d.ts +6 -0
  9. package/src/lib/app-utils/docs-help.generated.js +13 -0
  10. package/src/lib/app-utils/docs-help.generated.js.map +1 -1
  11. package/src/lib/app-utils/docs-index.generated.js +16 -9
  12. package/src/lib/app-utils/docs-index.generated.js.map +1 -1
  13. package/src/lib/app-utils/mobile-push.d.ts +8 -0
  14. package/src/lib/app-utils/mobile-push.js +4 -1
  15. package/src/lib/app-utils/mobile-push.js.map +1 -1
  16. package/src/lib/app-utils/native-app-association.d.ts +3 -2
  17. package/src/lib/app-utils/native-app-association.js +4 -2
  18. package/src/lib/app-utils/native-app-association.js.map +1 -1
  19. package/src/lib/app-utils/native-nav.d.ts +265 -0
  20. package/src/lib/app-utils/native-nav.js +741 -0
  21. package/src/lib/app-utils/native-nav.js.map +1 -0
  22. package/src/lib/app-utils/native-themes.d.ts +249 -0
  23. package/src/lib/app-utils/native-themes.js +732 -0
  24. package/src/lib/app-utils/native-themes.js.map +1 -0
  25. package/src/lib/app-utils/notifications.d.ts +1 -1
  26. package/src/lib/app-utils/notifications.js +10 -0
  27. package/src/lib/app-utils/notifications.js.map +1 -1
  28. package/src/lib/app-utils/one-box-search.d.ts +108 -0
  29. package/src/lib/app-utils/one-box-search.js +167 -0
  30. package/src/lib/app-utils/one-box-search.js.map +1 -0
  31. package/src/lib/app-utils/org-permissions.js +10 -61
  32. package/src/lib/app-utils/org-permissions.js.map +1 -1
  33. package/src/lib/app-utils/org-role-grants.d.ts +95 -0
  34. package/src/lib/app-utils/org-role-grants.js +193 -0
  35. package/src/lib/app-utils/org-role-grants.js.map +1 -0
  36. package/src/lib/app-utils/plan-entitlements.d.ts +41 -1
  37. package/src/lib/app-utils/plan-entitlements.js +65 -3
  38. package/src/lib/app-utils/plan-entitlements.js.map +1 -1
  39. package/src/lib/app-utils/tenant-dns-targets.d.ts +62 -0
  40. package/src/lib/app-utils/tenant-dns-targets.js +80 -0
  41. package/src/lib/app-utils/tenant-dns-targets.js.map +1 -0
  42. package/src/lib/foundation/definitions/org-billing.types.d.ts +14 -0
  43. package/src/lib/foundation/definitions/org-billing.types.js.map +1 -1
  44. package/src/lib/foundation/definitions/platform.types.d.ts +9 -0
  45. package/src/lib/foundation/definitions/platform.types.js.map +1 -1
  46. package/src/lib/plugin-manager/feature-plugins.d.ts +38 -0
  47. package/src/lib/plugin-manager/feature-plugins.js +20 -0
  48. package/src/lib/plugin-manager/feature-plugins.js.map +1 -1
  49. package/src/lib/plugin-manager/first-party-plugins.generated.js +22 -1
  50. package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
  51. package/src/lib/plugin-manager/plugin-domain-events.d.ts +4 -3
  52. package/src/lib/plugin-manager/plugin-domain-events.js.map +1 -1
  53. package/src/lib/plugin-manager/plugin-media-ingest.d.ts +25 -0
  54. package/src/lib/plugin-manager/plugin-media-ingest.js.map +1 -1
  55. package/src/lib/plugin-manager/plugin-search-sources.d.ts +85 -0
  56. package/src/lib/plugin-manager/plugin-search-sources.js +91 -0
  57. package/src/lib/plugin-manager/plugin-search-sources.js.map +1 -0
  58. package/src/lib/plugin-manager/plugin-services.js +6 -0
  59. package/src/lib/plugin-manager/plugin-services.js.map +1 -1
@@ -33,9 +33,10 @@
33
33
  * carries `id`, stable across retries, for a subscriber to dedupe on.
34
34
  *
35
35
  * TYPED BY TOKEN. `definePluginDomainEvent<Payload>(name)` gives the raiser
36
- * and a subscriber the same payload type; a subscriber in another plugin
37
- * restates the shape it reads and defines its own token under the same
38
- * name, as a widget restates a zone's props.
36
+ * and a subscriber the same payload type. The raising plugin publishes its
37
+ * tokens and payloads in its contracts module, and a subscriber in another
38
+ * plugin imports them from `@aglyn/plugins-<id>/contracts` and names the
39
+ * raiser in its `uses` (AGL-3980, docs/PACKAGES.md); it never restates them.
39
40
  *
40
41
  * The registry lives on `globalThis` (AGL-3412): subscriptions register from
41
42
  * the plugins' server declarations at boot, in a module graph the routes and
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-domain-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\nimport { getRegisteringPluginId } from '../app-utils/registering-plugin'\n\n/**\n * Events a PLUGIN raises for other plugins (AGL-3611).\n *\n * `plugin-events.ts` carries the platform's own facts — a seat add-on, a paid\n * invoice — with payloads core declares. This is the same idea for facts\n * that belong to a plugin: one plugin declares an event and its payload,\n * raises it when the thing happens, and any other plugin subscribes by the\n * event's name without importing the plugin that raises it. Core names\n * nothing a plugin raises.\n *\n * DELIVERY IS RELIABLE, NOT FIRE-AND-FORGET. Raising an event writes an\n * outbox document (`plugin-event-outbox.ts` in the admin data lib), in the\n * same transaction as the write it reports where the raiser has one, and a\n * scheduled drain hands it to each subscriber in turn. A subscriber that\n * throws is retried with backoff, alone: the ones that already took the\n * event are not called again. Delivery is AT LEAST ONCE, so every envelope\n * carries `id`, stable across retries, for a subscriber to dedupe on.\n *\n * TYPED BY TOKEN. `definePluginDomainEvent<Payload>(name)` gives the raiser\n * and a subscriber the same payload type; a subscriber in another plugin\n * restates the shape it reads and defines its own token under the same\n * name, as a widget restates a zone's props.\n *\n * The registry lives on `globalThis` (AGL-3412): subscriptions register from\n * the plugins' server declarations at boot, in a module graph the routes and\n * the job runner do not share.\n */\n\n/** A typed handle on one event name. */\nexport interface PluginDomainEvent<Payload> {\n readonly id: string\n /** Type-level only: the payload the event carries. */\n readonly __payload?: Payload\n}\n\n/** The payload an event token carries. */\nexport type PluginDomainEventPayload<Event> = Event extends PluginDomainEvent<infer Payload>\n ? Payload\n : never\n\nconst EVENT_NAME = /^[a-z][a-zA-Z0-9]*(\\.[a-z][a-zA-Z0-9_]*)+$/\n\n/**\n * A token for one event name: lower-case words joined by dots, the first\n * naming what the event is about (`order.paid`, `return.requested`).\n */\nexport function definePluginDomainEvent<Payload>(id: string): PluginDomainEvent<Payload> {\n const name = String(id ?? '').trim()\n if (!EVENT_NAME.test(name)) {\n throw new Error(`plugin event name \"${id}\" must be dotted lower-case words, e.g. \"order.paid\"`)\n }\n return { id: name }\n}\n\n/** How a plugin describes an event it raises, for pickers and the docs. */\nexport interface PluginDomainEventDeclaration {\n event: PluginDomainEvent<unknown> | string\n /** \"Order paid\". */\n label: string\n /** One sentence: when it is raised. */\n description: string\n /** The top-level payload keys, for a person writing a subscriber. */\n payloadKeys?: readonly string[]\n}\n\n/** A declared event, with the plugin that raises it. */\nexport interface DeclaredPluginDomainEvent {\n pluginId: string\n event: string\n label: string\n description: string\n payloadKeys: readonly string[]\n}\n\n/** What a subscriber is handed. */\nexport interface PluginDomainEventEnvelope<Payload = unknown> {\n /** Stable across retries: the idempotency key for a subscriber. */\n id: string\n event: string\n /** The site the event happened on. */\n hostId: string\n /** The site's org, when the raiser knew it. */\n orgId: string | null\n /** When the fact happened, not when it was delivered. */\n occurredAtMs: number\n /** 1 on the first delivery to this subscriber, then 2, 3, … */\n attempt: number\n payload: Payload\n}\n\nexport type PluginDomainEventHandler<Payload = unknown> = (\n envelope: PluginDomainEventEnvelope<Payload>,\n) => void | Promise<void>\n\ninterface Subscription {\n pluginId: string\n handler: PluginDomainEventHandler<unknown>\n}\n\ninterface Registry {\n declarations: Map<string, DeclaredPluginDomainEvent>\n subscriptions: Map<string, Subscription[]>\n}\n\nconst REGISTRY_KEY = Symbol.for('@aglyn/aglyn:plugin-domain-events')\nconst globalScope = globalThis as typeof globalThis & { [REGISTRY_KEY]?: Registry }\n\nfunction registry(): Registry {\n if (!globalScope[REGISTRY_KEY]) {\n globalScope[REGISTRY_KEY] = { declarations: new Map(), subscriptions: new Map() }\n }\n return globalScope[REGISTRY_KEY] as Registry\n}\n\nconst nameOf = (event: PluginDomainEvent<unknown> | string): string =>\n typeof event === 'string' ? event : event.id\n\nfunction ownerOf(kind: string, name: string, options?: { pluginId?: string }): string {\n const pluginId = (getRegisteringPluginId() ?? options?.pluginId ?? '').trim()\n if (!pluginId) {\n throw new Error(\n `plugin event \"${name}\" ${kind} with no owner: pass { pluginId } outside a plugin register fn`,\n )\n }\n return pluginId\n}\n\n/**\n * Declares the events a plugin raises. One plugin owns a name: a second\n * plugin declaring it throws, so two plugins cannot raise one event with two\n * payloads. The owner declaring again replaces its entry.\n */\nexport function declarePluginDomainEvents(\n declarations: readonly PluginDomainEventDeclaration[],\n options?: { pluginId?: string },\n): void {\n for (const declaration of declarations) {\n const name = nameOf(declaration.event)\n if (!EVENT_NAME.test(name)) {\n throw new Error(`plugin event name \"${name}\" must be dotted lower-case words`)\n }\n const pluginId = ownerOf('declared', name, options)\n const existing = registry().declarations.get(name)\n if (existing && existing.pluginId !== pluginId) {\n throw new Error(\n `plugin event \"${name}\" is already declared by \"${existing.pluginId}\"; \"${pluginId}\" cannot declare it too`,\n )\n }\n registry().declarations.set(name, {\n pluginId,\n event: name,\n label: declaration.label,\n description: declaration.description,\n payloadKeys: [...(declaration.payloadKeys ?? [])],\n })\n }\n}\n\n/** Every declared event, sorted by name. */\nexport function listPluginDomainEvents(): DeclaredPluginDomainEvent[] {\n return [...registry().declarations.values()].sort((a, b) => a.event.localeCompare(b.event))\n}\n\n/** The declaration of one event, or `null` when no plugin declares it. */\nexport function pluginDomainEventDeclaration(event: string): DeclaredPluginDomainEvent | null {\n return registry().declarations.get(event) ?? null\n}\n\n/**\n * Subscribes to an event. A subscriber subscribing again to the same event\n * replaces its earlier handler, so a module evaluated twice delivers once.\n *\n * The subscriber is named by plugin id, or `pluginId:name` when a plugin\n * subscribes more than one handler to an event (its webhooks and its\n * workflow triggers, say) — and that name is what the outbox records as\n * delivered, so it must be stable.\n */\nexport function subscribePluginDomainEvent<Payload>(\n event: PluginDomainEvent<Payload> | string,\n handler: PluginDomainEventHandler<Payload>,\n options?: { pluginId?: string; name?: string },\n): void {\n const name = nameOf(event as PluginDomainEvent<unknown> | string)\n const owner = ownerOf('subscribed', name, options)\n const pluginId = options?.name ? `${owner}:${options.name}` : owner\n const list = (registry().subscriptions.get(name) ?? []).filter((entry) => entry.pluginId !== pluginId)\n list.push({ pluginId, handler: handler as PluginDomainEventHandler<unknown> })\n registry().subscriptions.set(name, list)\n}\n\n/** The plugins subscribed to an event, in subscription order. */\nexport function listPluginDomainEventSubscribers(event: string): string[] {\n return (registry().subscriptions.get(event) ?? []).map((entry) => entry.pluginId)\n}\n\n/** What one delivery pass came to. */\nexport interface PluginDomainEventDeliveryResult {\n /** Subscribers that took the event on this pass. */\n delivered: string[]\n /** Subscribers that threw, with what they said. */\n failed: Array<{ pluginId: string; error: string }>\n}\n\n/**\n * Hands an envelope to every subscriber not in `skip`, one after another. A\n * throw is caught and reported and does not stop the others. The outbox\n * drain calls this; a raiser never does.\n */\nexport async function deliverPluginDomainEvent(\n envelope: Omit<PluginDomainEventEnvelope, 'attempt'>,\n options: { skip?: ReadonlySet<string>; attempts?: Readonly<Record<string, number>> } = {},\n): Promise<PluginDomainEventDeliveryResult> {\n const result: PluginDomainEventDeliveryResult = { delivered: [], failed: [] }\n for (const { pluginId, handler } of registry().subscriptions.get(envelope.event) ?? []) {\n if (options.skip?.has(pluginId)) continue\n try {\n await handler({ ...envelope, attempt: (options.attempts?.[pluginId] ?? 0) + 1 })\n result.delivered.push(pluginId)\n } catch (error) {\n const message = String((error as Error)?.message ?? error).slice(0, 500)\n result.failed.push({ pluginId, error: message })\n console.error(`[plugin-events] ${pluginId} failed on ${envelope.event} ${envelope.id}`, error)\n }\n }\n return result\n}\n\n/** Test seam: forget every declaration and subscription. */\nexport function resetPluginDomainEventsForTests(): void {\n registry().declarations.clear()\n registry().subscriptions.clear()\n}\n"],"names":["getRegisteringPluginId","EVENT_NAME","definePluginDomainEvent","id","name","String","trim","test","Error","REGISTRY_KEY","Symbol","for","globalScope","globalThis","registry","declarations","Map","subscriptions","nameOf","event","ownerOf","kind","options","pluginId","declarePluginDomainEvents","declaration","existing","get","set","label","description","payloadKeys","listPluginDomainEvents","values","sort","a","b","localeCompare","pluginDomainEventDeclaration","subscribePluginDomainEvent","handler","owner","list","filter","entry","push","listPluginDomainEventSubscribers","map","deliverPluginDomainEvent","envelope","result","delivered","failed","skip","has","attempt","attempts","error","message","slice","console","resetPluginDomainEventsForTests","clear"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-domain-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\nimport { getRegisteringPluginId } from '../app-utils/registering-plugin'\n\n/**\n * Events a PLUGIN raises for other plugins (AGL-3611).\n *\n * `plugin-events.ts` carries the platform's own facts — a seat add-on, a paid\n * invoice — with payloads core declares. This is the same idea for facts\n * that belong to a plugin: one plugin declares an event and its payload,\n * raises it when the thing happens, and any other plugin subscribes by the\n * event's name without importing the plugin that raises it. Core names\n * nothing a plugin raises.\n *\n * DELIVERY IS RELIABLE, NOT FIRE-AND-FORGET. Raising an event writes an\n * outbox document (`plugin-event-outbox.ts` in the admin data lib), in the\n * same transaction as the write it reports where the raiser has one, and a\n * scheduled drain hands it to each subscriber in turn. A subscriber that\n * throws is retried with backoff, alone: the ones that already took the\n * event are not called again. Delivery is AT LEAST ONCE, so every envelope\n * carries `id`, stable across retries, for a subscriber to dedupe on.\n *\n * TYPED BY TOKEN. `definePluginDomainEvent<Payload>(name)` gives the raiser\n * and a subscriber the same payload type. The raising plugin publishes its\n * tokens and payloads in its contracts module, and a subscriber in another\n * plugin imports them from `@aglyn/plugins-<id>/contracts` and names the\n * raiser in its `uses` (AGL-3980, docs/PACKAGES.md); it never restates them.\n *\n * The registry lives on `globalThis` (AGL-3412): subscriptions register from\n * the plugins' server declarations at boot, in a module graph the routes and\n * the job runner do not share.\n */\n\n/** A typed handle on one event name. */\nexport interface PluginDomainEvent<Payload> {\n readonly id: string\n /** Type-level only: the payload the event carries. */\n readonly __payload?: Payload\n}\n\n/** The payload an event token carries. */\nexport type PluginDomainEventPayload<Event> = Event extends PluginDomainEvent<infer Payload>\n ? Payload\n : never\n\nconst EVENT_NAME = /^[a-z][a-zA-Z0-9]*(\\.[a-z][a-zA-Z0-9_]*)+$/\n\n/**\n * A token for one event name: lower-case words joined by dots, the first\n * naming what the event is about (`order.paid`, `return.requested`).\n */\nexport function definePluginDomainEvent<Payload>(id: string): PluginDomainEvent<Payload> {\n const name = String(id ?? '').trim()\n if (!EVENT_NAME.test(name)) {\n throw new Error(`plugin event name \"${id}\" must be dotted lower-case words, e.g. \"order.paid\"`)\n }\n return { id: name }\n}\n\n/** How a plugin describes an event it raises, for pickers and the docs. */\nexport interface PluginDomainEventDeclaration {\n event: PluginDomainEvent<unknown> | string\n /** \"Order paid\". */\n label: string\n /** One sentence: when it is raised. */\n description: string\n /** The top-level payload keys, for a person writing a subscriber. */\n payloadKeys?: readonly string[]\n}\n\n/** A declared event, with the plugin that raises it. */\nexport interface DeclaredPluginDomainEvent {\n pluginId: string\n event: string\n label: string\n description: string\n payloadKeys: readonly string[]\n}\n\n/** What a subscriber is handed. */\nexport interface PluginDomainEventEnvelope<Payload = unknown> {\n /** Stable across retries: the idempotency key for a subscriber. */\n id: string\n event: string\n /** The site the event happened on. */\n hostId: string\n /** The site's org, when the raiser knew it. */\n orgId: string | null\n /** When the fact happened, not when it was delivered. */\n occurredAtMs: number\n /** 1 on the first delivery to this subscriber, then 2, 3, … */\n attempt: number\n payload: Payload\n}\n\nexport type PluginDomainEventHandler<Payload = unknown> = (\n envelope: PluginDomainEventEnvelope<Payload>,\n) => void | Promise<void>\n\ninterface Subscription {\n pluginId: string\n handler: PluginDomainEventHandler<unknown>\n}\n\ninterface Registry {\n declarations: Map<string, DeclaredPluginDomainEvent>\n subscriptions: Map<string, Subscription[]>\n}\n\nconst REGISTRY_KEY = Symbol.for('@aglyn/aglyn:plugin-domain-events')\nconst globalScope = globalThis as typeof globalThis & { [REGISTRY_KEY]?: Registry }\n\nfunction registry(): Registry {\n if (!globalScope[REGISTRY_KEY]) {\n globalScope[REGISTRY_KEY] = { declarations: new Map(), subscriptions: new Map() }\n }\n return globalScope[REGISTRY_KEY] as Registry\n}\n\nconst nameOf = (event: PluginDomainEvent<unknown> | string): string =>\n typeof event === 'string' ? event : event.id\n\nfunction ownerOf(kind: string, name: string, options?: { pluginId?: string }): string {\n const pluginId = (getRegisteringPluginId() ?? options?.pluginId ?? '').trim()\n if (!pluginId) {\n throw new Error(\n `plugin event \"${name}\" ${kind} with no owner: pass { pluginId } outside a plugin register fn`,\n )\n }\n return pluginId\n}\n\n/**\n * Declares the events a plugin raises. One plugin owns a name: a second\n * plugin declaring it throws, so two plugins cannot raise one event with two\n * payloads. The owner declaring again replaces its entry.\n */\nexport function declarePluginDomainEvents(\n declarations: readonly PluginDomainEventDeclaration[],\n options?: { pluginId?: string },\n): void {\n for (const declaration of declarations) {\n const name = nameOf(declaration.event)\n if (!EVENT_NAME.test(name)) {\n throw new Error(`plugin event name \"${name}\" must be dotted lower-case words`)\n }\n const pluginId = ownerOf('declared', name, options)\n const existing = registry().declarations.get(name)\n if (existing && existing.pluginId !== pluginId) {\n throw new Error(\n `plugin event \"${name}\" is already declared by \"${existing.pluginId}\"; \"${pluginId}\" cannot declare it too`,\n )\n }\n registry().declarations.set(name, {\n pluginId,\n event: name,\n label: declaration.label,\n description: declaration.description,\n payloadKeys: [...(declaration.payloadKeys ?? [])],\n })\n }\n}\n\n/** Every declared event, sorted by name. */\nexport function listPluginDomainEvents(): DeclaredPluginDomainEvent[] {\n return [...registry().declarations.values()].sort((a, b) => a.event.localeCompare(b.event))\n}\n\n/** The declaration of one event, or `null` when no plugin declares it. */\nexport function pluginDomainEventDeclaration(event: string): DeclaredPluginDomainEvent | null {\n return registry().declarations.get(event) ?? null\n}\n\n/**\n * Subscribes to an event. A subscriber subscribing again to the same event\n * replaces its earlier handler, so a module evaluated twice delivers once.\n *\n * The subscriber is named by plugin id, or `pluginId:name` when a plugin\n * subscribes more than one handler to an event (its webhooks and its\n * workflow triggers, say) — and that name is what the outbox records as\n * delivered, so it must be stable.\n */\nexport function subscribePluginDomainEvent<Payload>(\n event: PluginDomainEvent<Payload> | string,\n handler: PluginDomainEventHandler<Payload>,\n options?: { pluginId?: string; name?: string },\n): void {\n const name = nameOf(event as PluginDomainEvent<unknown> | string)\n const owner = ownerOf('subscribed', name, options)\n const pluginId = options?.name ? `${owner}:${options.name}` : owner\n const list = (registry().subscriptions.get(name) ?? []).filter((entry) => entry.pluginId !== pluginId)\n list.push({ pluginId, handler: handler as PluginDomainEventHandler<unknown> })\n registry().subscriptions.set(name, list)\n}\n\n/** The plugins subscribed to an event, in subscription order. */\nexport function listPluginDomainEventSubscribers(event: string): string[] {\n return (registry().subscriptions.get(event) ?? []).map((entry) => entry.pluginId)\n}\n\n/** What one delivery pass came to. */\nexport interface PluginDomainEventDeliveryResult {\n /** Subscribers that took the event on this pass. */\n delivered: string[]\n /** Subscribers that threw, with what they said. */\n failed: Array<{ pluginId: string; error: string }>\n}\n\n/**\n * Hands an envelope to every subscriber not in `skip`, one after another. A\n * throw is caught and reported and does not stop the others. The outbox\n * drain calls this; a raiser never does.\n */\nexport async function deliverPluginDomainEvent(\n envelope: Omit<PluginDomainEventEnvelope, 'attempt'>,\n options: { skip?: ReadonlySet<string>; attempts?: Readonly<Record<string, number>> } = {},\n): Promise<PluginDomainEventDeliveryResult> {\n const result: PluginDomainEventDeliveryResult = { delivered: [], failed: [] }\n for (const { pluginId, handler } of registry().subscriptions.get(envelope.event) ?? []) {\n if (options.skip?.has(pluginId)) continue\n try {\n await handler({ ...envelope, attempt: (options.attempts?.[pluginId] ?? 0) + 1 })\n result.delivered.push(pluginId)\n } catch (error) {\n const message = String((error as Error)?.message ?? error).slice(0, 500)\n result.failed.push({ pluginId, error: message })\n console.error(`[plugin-events] ${pluginId} failed on ${envelope.event} ${envelope.id}`, error)\n }\n }\n return result\n}\n\n/** Test seam: forget every declaration and subscription. */\nexport function resetPluginDomainEventsForTests(): void {\n registry().declarations.clear()\n registry().subscriptions.clear()\n}\n"],"names":["getRegisteringPluginId","EVENT_NAME","definePluginDomainEvent","id","name","String","trim","test","Error","REGISTRY_KEY","Symbol","for","globalScope","globalThis","registry","declarations","Map","subscriptions","nameOf","event","ownerOf","kind","options","pluginId","declarePluginDomainEvents","declaration","existing","get","set","label","description","payloadKeys","listPluginDomainEvents","values","sort","a","b","localeCompare","pluginDomainEventDeclaration","subscribePluginDomainEvent","handler","owner","list","filter","entry","push","listPluginDomainEventSubscribers","map","deliverPluginDomainEvent","envelope","result","delivered","failed","skip","has","attempt","attempts","error","message","slice","console","resetPluginDomainEventsForTests","clear"],"mappings":"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"}
@@ -50,6 +50,18 @@ export interface PluginMediaIngestRequest {
50
50
  description?: string;
51
51
  /** Where a photo copied from a stock library came from (`stockPhoto` on the asset). */
52
52
  stockPhoto?: Omit<AglynHostMediaStockSource, 'importedAt'>;
53
+ /**
54
+ * The library folder the asset is filed in (AGL-3957); absent or null,
55
+ * the library's root. An id the library does not hold files the asset at
56
+ * the root, as an upload naming a stale folder does.
57
+ */
58
+ folderId?: string | null;
59
+ /**
60
+ * The asset is the business's own photo, sent through a site photo link
61
+ * (AGL-3957) — the id of the link it came through. Stamps `ownerPhoto` on
62
+ * the asset, which is what the AI reads its owner's pictures by.
63
+ */
64
+ ownerPhotoLinkId?: string;
53
65
  }
54
66
  /** A stored asset, as a page names it. */
55
67
  export interface PluginMediaAsset {
@@ -58,6 +70,8 @@ export interface PluginMediaAsset {
58
70
  src: string;
59
71
  width?: number;
60
72
  height?: number;
73
+ /** What the asset's own alt text says it shows, where a member wrote one. */
74
+ alt?: string;
61
75
  }
62
76
  export type PluginMediaIngestResult = ({
63
77
  ok: true;
@@ -77,6 +91,17 @@ export interface PluginMediaIngest {
77
91
  hostId: string;
78
92
  sourceKey: string;
79
93
  }): Promise<PluginMediaAsset | null>;
94
+ /**
95
+ * The site's live, public photos of the business itself (AGL-3957): what
96
+ * came through a site photo link, newest first, at most `limit`. A photo
97
+ * of the owner's own work is placed before any stock photo is searched
98
+ * for. Optional: an implementation registered before the link existed
99
+ * answers nothing, and a caller treats that as an empty library.
100
+ */
101
+ listOwnerPhotos?(input: {
102
+ hostId: string;
103
+ limit?: number;
104
+ }): Promise<PluginMediaAsset[]>;
80
105
  }
81
106
  /** One implementation: the app that holds the media ingress. */
82
107
  export declare const PLUGIN_MEDIA_INGEST: import("./plugin-services").PluginServiceContract<PluginMediaIngest>;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-media-ingest.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { AglynHostMediaStockSource } from '../foundation/definitions/platform.types'\nimport {\n definePluginServiceContract,\n registerPluginService,\n resolvePluginService,\n} from './plugin-services'\n\n/**\n * A SERVER PROCESS STORES A PICTURE IN A SITE'S MEDIA LIBRARY (AGL-3660).\n *\n * The media library takes bytes through the console's own ingress — the\n * upload routes and the v1 API — which check the caller, the site's\n * lockdown, the bytes' structure, the takedown list, the plan and the\n * storage band before anything is written. A plugin that holds a picture on\n * the server with no browser request behind it (an AI job copying a stock\n * photo) has no route to post to. This is that route's door for it: the\n * console registers the one implementation at boot (`instrumentation.ts`),\n * the same direction as `core.site-cache`, and it applies the ingress checks\n * as the member named, so a plugin never writes a media document or a\n * Storage object itself.\n *\n * Images only: a server-held video or document has no caller yet.\n *\n * A process with no implementation answers `null` from\n * {@link pluginMediaIngest}, and a caller keeps its own fallback.\n */\n\nexport interface PluginMediaIngestRequest {\n hostId: string\n /**\n * The member the asset is stored as. Their access to the site, the site's\n * lockdown and the workspace's plan and storage band are checked as for\n * an upload they made themselves.\n */\n uid: string\n fileName: string\n /** A raster image type: `image/jpeg`, `image/png`, `image/webp` or `image/gif`. */\n contentType: string\n bytes: Uint8Array\n alt?: string\n description?: string\n /** Where a photo copied from a stock library came from (`stockPhoto` on the asset). */\n stockPhoto?: Omit<AglynHostMediaStockSource, 'importedAt'>\n}\n\n/** A stored asset, as a page names it. */\nexport interface PluginMediaAsset {\n mediaId: string\n /** What an `image` node's `src` holds: the asset's media reference, else its URL. */\n src: string\n width?: number\n height?: number\n}\n\nexport type PluginMediaIngestResult =\n | ({ ok: true } & PluginMediaAsset)\n | { ok: false; status: number; reason: string }\n\nexport interface PluginMediaIngest {\n /** Stores one picture in the site's own library. */\n ingest(request: PluginMediaIngestRequest): Promise<PluginMediaIngestResult>\n /**\n * The site's live, public asset copied from a stock photo with this source\n * key (`{provider}:{id}`), so a photo is never stored twice in one library.\n */\n findStockPhoto(input: { hostId: string; sourceKey: string }): Promise<PluginMediaAsset | null>\n}\n\n/** One implementation: the app that holds the media ingress. */\nexport const PLUGIN_MEDIA_INGEST = definePluginServiceContract<PluginMediaIngest>(\n 'core.media-ingest',\n { multiple: false },\n)\n\nexport function registerPluginMediaIngest(\n ingest: PluginMediaIngest,\n options?: { pluginId?: string },\n): void {\n if (typeof ingest?.ingest !== 'function' || typeof ingest?.findStockPhoto !== 'function') {\n throw new Error('a media ingest needs ingest and findStockPhoto')\n }\n registerPluginService(PLUGIN_MEDIA_INGEST, ingest, {\n ...(options?.pluginId ? { pluginId: options.pluginId } : {}),\n })\n}\n\n/** The registered ingest, or `null` in a process that registered none. */\nexport function pluginMediaIngest(): PluginMediaIngest | null {\n return resolvePluginService(PLUGIN_MEDIA_INGEST) ?? null\n}\n"],"names":["definePluginServiceContract","registerPluginService","resolvePluginService","PLUGIN_MEDIA_INGEST","multiple","registerPluginMediaIngest","ingest","options","findStockPhoto","Error","pluginId","pluginMediaIngest"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC;AAGD,SACEA,2BAA2B,EAC3BC,qBAAqB,EACrBC,oBAAoB,QACf,uBAAmB;AA+D1B,8DAA8D,GAC9D,OAAO,MAAMC,sBAAsBH,4BACjC,qBACA;IAAEI,UAAU;AAAM,GACnB;AAED,OAAO,SAASC,0BACdC,MAAyB,EACzBC,OAA+B;IAE/B,IAAI,QAAOD,0BAAAA,OAAQA,MAAM,MAAK,cAAc,QAAOA,0BAAAA,OAAQE,cAAc,MAAK,YAAY;QACxF,MAAM,IAAIC,MAAM;IAClB;IACAR,sBAAsBE,qBAAqBG,QAAQ,aAC7CC,CAAAA,2BAAAA,QAASG,QAAQ,IAAG;QAAEA,UAAUH,QAAQG,QAAQ;IAAC,IAAI,CAAC;AAE9D;AAEA,wEAAwE,GACxE,OAAO,SAASC;QACPT;IAAP,QAAOA,wBAAAA,qBAAqBC,gCAArBD,wBAA6C;AACtD"}
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-media-ingest.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { AglynHostMediaStockSource } from '../foundation/definitions/platform.types'\nimport {\n definePluginServiceContract,\n registerPluginService,\n resolvePluginService,\n} from './plugin-services'\n\n/**\n * A SERVER PROCESS STORES A PICTURE IN A SITE'S MEDIA LIBRARY (AGL-3660).\n *\n * The media library takes bytes through the console's own ingress — the\n * upload routes and the v1 API — which check the caller, the site's\n * lockdown, the bytes' structure, the takedown list, the plan and the\n * storage band before anything is written. A plugin that holds a picture on\n * the server with no browser request behind it (an AI job copying a stock\n * photo) has no route to post to. This is that route's door for it: the\n * console registers the one implementation at boot (`instrumentation.ts`),\n * the same direction as `core.site-cache`, and it applies the ingress checks\n * as the member named, so a plugin never writes a media document or a\n * Storage object itself.\n *\n * Images only: a server-held video or document has no caller yet.\n *\n * A process with no implementation answers `null` from\n * {@link pluginMediaIngest}, and a caller keeps its own fallback.\n */\n\nexport interface PluginMediaIngestRequest {\n hostId: string\n /**\n * The member the asset is stored as. Their access to the site, the site's\n * lockdown and the workspace's plan and storage band are checked as for\n * an upload they made themselves.\n */\n uid: string\n fileName: string\n /** A raster image type: `image/jpeg`, `image/png`, `image/webp` or `image/gif`. */\n contentType: string\n bytes: Uint8Array\n alt?: string\n description?: string\n /** Where a photo copied from a stock library came from (`stockPhoto` on the asset). */\n stockPhoto?: Omit<AglynHostMediaStockSource, 'importedAt'>\n /**\n * The library folder the asset is filed in (AGL-3957); absent or null,\n * the library's root. An id the library does not hold files the asset at\n * the root, as an upload naming a stale folder does.\n */\n folderId?: string | null\n /**\n * The asset is the business's own photo, sent through a site photo link\n * (AGL-3957) — the id of the link it came through. Stamps `ownerPhoto` on\n * the asset, which is what the AI reads its owner's pictures by.\n */\n ownerPhotoLinkId?: string\n}\n\n/** A stored asset, as a page names it. */\nexport interface PluginMediaAsset {\n mediaId: string\n /** What an `image` node's `src` holds: the asset's media reference, else its URL. */\n src: string\n width?: number\n height?: number\n /** What the asset's own alt text says it shows, where a member wrote one. */\n alt?: string\n}\n\nexport type PluginMediaIngestResult =\n | ({ ok: true } & PluginMediaAsset)\n | { ok: false; status: number; reason: string }\n\nexport interface PluginMediaIngest {\n /** Stores one picture in the site's own library. */\n ingest(request: PluginMediaIngestRequest): Promise<PluginMediaIngestResult>\n /**\n * The site's live, public asset copied from a stock photo with this source\n * key (`{provider}:{id}`), so a photo is never stored twice in one library.\n */\n findStockPhoto(input: { hostId: string; sourceKey: string }): Promise<PluginMediaAsset | null>\n /**\n * The site's live, public photos of the business itself (AGL-3957): what\n * came through a site photo link, newest first, at most `limit`. A photo\n * of the owner's own work is placed before any stock photo is searched\n * for. Optional: an implementation registered before the link existed\n * answers nothing, and a caller treats that as an empty library.\n */\n listOwnerPhotos?(input: { hostId: string; limit?: number }): Promise<PluginMediaAsset[]>\n}\n\n/** One implementation: the app that holds the media ingress. */\nexport const PLUGIN_MEDIA_INGEST = definePluginServiceContract<PluginMediaIngest>(\n 'core.media-ingest',\n { multiple: false },\n)\n\nexport function registerPluginMediaIngest(\n ingest: PluginMediaIngest,\n options?: { pluginId?: string },\n): void {\n if (typeof ingest?.ingest !== 'function' || typeof ingest?.findStockPhoto !== 'function') {\n throw new Error('a media ingest needs ingest and findStockPhoto')\n }\n registerPluginService(PLUGIN_MEDIA_INGEST, ingest, {\n ...(options?.pluginId ? { pluginId: options.pluginId } : {}),\n })\n}\n\n/** The registered ingest, or `null` in a process that registered none. */\nexport function pluginMediaIngest(): PluginMediaIngest | null {\n return resolvePluginService(PLUGIN_MEDIA_INGEST) ?? null\n}\n"],"names":["definePluginServiceContract","registerPluginService","resolvePluginService","PLUGIN_MEDIA_INGEST","multiple","registerPluginMediaIngest","ingest","options","findStockPhoto","Error","pluginId","pluginMediaIngest"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC;AAGD,SACEA,2BAA2B,EAC3BC,qBAAqB,EACrBC,oBAAoB,QACf,uBAAmB;AAqF1B,8DAA8D,GAC9D,OAAO,MAAMC,sBAAsBH,4BACjC,qBACA;IAAEI,UAAU;AAAM,GACnB;AAED,OAAO,SAASC,0BACdC,MAAyB,EACzBC,OAA+B;IAE/B,IAAI,QAAOD,0BAAAA,OAAQA,MAAM,MAAK,cAAc,QAAOA,0BAAAA,OAAQE,cAAc,MAAK,YAAY;QACxF,MAAM,IAAIC,MAAM;IAClB;IACAR,sBAAsBE,qBAAqBG,QAAQ,aAC7CC,CAAAA,2BAAAA,QAASG,QAAQ,IAAG;QAAEA,UAAUH,QAAQG,QAAQ;IAAC,IAAI,CAAC;AAE9D;AAEA,wEAAwE,GACxE,OAAO,SAASC;QACPT;IAAP,QAAOA,wBAAAA,qBAAqBC,gCAArBD,wBAA6C;AACtD"}
@@ -0,0 +1,85 @@
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
+ * WHAT A PLUGIN LETS THE ONE-BOX SEARCH FIND, ON THE SERVER (AGL-3943).
19
+ *
20
+ * The console's palette reads a plugin's `ConsoleSearchSource`s in the
21
+ * browser, through the Firestore rules. The native apps' one box asks the
22
+ * console's `/api/search` instead, which reads with the Admin SDK, so it
23
+ * needs the same sources in a server process that never loads a plugin's
24
+ * console bundle. A plugin registers them here from its
25
+ * `consoleServerDeclarations`, usually the very objects its console
26
+ * extension lists, with the extension's own gates beside them.
27
+ *
28
+ * The Admin SDK bypasses the rules, so the route applies every gate itself:
29
+ * the plugin on for the workspace and the site, the plan flag and quota, the
30
+ * permission, the reader's host role (`hostAccess`), and each org-shared
31
+ * row's `visibleTo` against the reader's reach.
32
+ *
33
+ * The core never imports a plugin; this is how its search reaches one.
34
+ */
35
+ import type { ListQueryDeclaration } from '@aglyn/shared-util-tools/list-query/list-query-plan';
36
+ import type { ConsoleSearchSource } from './feature-plugins';
37
+ /** A plugin's search source as the server reads it. */
38
+ export interface PluginSearchSource extends ConsoleSearchSource {
39
+ /**
40
+ * Who on the site may find these rows: any member (the default), or only
41
+ * the members who may read the site's sensitive documents (admin and
42
+ * editor, the rules' `canReadHostSensitive`), as for orders.
43
+ */
44
+ hostAccess?: 'member' | 'sensitive';
45
+ /**
46
+ * Read through the list's own query rather than a capped window: the
47
+ * search word goes on `listQuery.search` (a stored token array), planned
48
+ * by `planListQuery`, so the rows are the list's own matches, newest first,
49
+ * on the index the list already uses.
50
+ */
51
+ listQuery?: ListQueryDeclaration;
52
+ /** The row's label when no name field says it (an order's number). */
53
+ label?(row: Readonly<Record<string, unknown>>): string;
54
+ /** One line under the label (an order's buyer). */
55
+ detail?(row: Readonly<Record<string, unknown>>): string | null;
56
+ }
57
+ /** The gates the plugin's console extension puts on every source it lists. */
58
+ export interface PluginSearchSourceGates {
59
+ /** The plan flag the extension needs (`ConsoleExtension.featureFlag`). */
60
+ featureFlag?: string;
61
+ /** The permission the extension needs (`ConsoleExtension.permission`). */
62
+ permission?: string;
63
+ }
64
+ export interface RegisteredPluginSearchSource {
65
+ pluginId: string;
66
+ gates: PluginSearchSourceGates;
67
+ source: PluginSearchSource;
68
+ }
69
+ /**
70
+ * Registers a plugin's server search sources. Owner = the loader's marker
71
+ * inside a register fn, else `options.pluginId`; neither throws. Registering
72
+ * again replaces the plugin's earlier sources, so a module evaluated twice
73
+ * does not list a group twice.
74
+ */
75
+ export declare function registerPluginSearchSources(sources: readonly PluginSearchSource[], options?: PluginSearchSourceGates & {
76
+ pluginId?: string;
77
+ }): void;
78
+ /**
79
+ * The sources of the plugins in `enabledPluginIds` (every registered one
80
+ * when omitted), in registration order. A second source with an id already
81
+ * listed is dropped, as the palette drops it.
82
+ */
83
+ export declare function listPluginSearchSources(enabledPluginIds?: readonly string[]): RegisteredPluginSearchSource[];
84
+ /** Only for specs: forgets every source. */
85
+ export declare function resetPluginSearchSourcesForTests(): void;
@@ -0,0 +1,91 @@
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
+ * WHAT A PLUGIN LETS THE ONE-BOX SEARCH FIND, ON THE SERVER (AGL-3943).
18
+ *
19
+ * The console's palette reads a plugin's `ConsoleSearchSource`s in the
20
+ * browser, through the Firestore rules. The native apps' one box asks the
21
+ * console's `/api/search` instead, which reads with the Admin SDK, so it
22
+ * needs the same sources in a server process that never loads a plugin's
23
+ * console bundle. A plugin registers them here from its
24
+ * `consoleServerDeclarations`, usually the very objects its console
25
+ * extension lists, with the extension's own gates beside them.
26
+ *
27
+ * The Admin SDK bypasses the rules, so the route applies every gate itself:
28
+ * the plugin on for the workspace and the site, the plan flag and quota, the
29
+ * permission, the reader's host role (`hostAccess`), and each org-shared
30
+ * row's `visibleTo` against the reader's reach.
31
+ *
32
+ * The core never imports a plugin; this is how its search reaches one.
33
+ */ import { _ as _extends } from "@swc/helpers/_/_extends";
34
+ var _globalScope_REGISTRY_KEY;
35
+ import { getRegisteringPluginId } from "../app-utils/registering-plugin.js";
36
+ /**
37
+ * On `globalThis` under `Symbol.for`, never at module scope (AGL-3947). The
38
+ * sources are registered at boot, from `instrumentation.ts`, which Next
39
+ * compiles apart from the routes, so the boot and `/api/search` each hold
40
+ * their own copy of this module.
41
+ */ const REGISTRY_KEY = Symbol.for('@aglyn/aglyn:plugin-search-sources');
42
+ const globalScope = globalThis;
43
+ const registrations = (_globalScope_REGISTRY_KEY = globalScope[REGISTRY_KEY]) != null ? _globalScope_REGISTRY_KEY : globalScope[REGISTRY_KEY] = [];
44
+ /**
45
+ * Registers a plugin's server search sources. Owner = the loader's marker
46
+ * inside a register fn, else `options.pluginId`; neither throws. Registering
47
+ * again replaces the plugin's earlier sources, so a module evaluated twice
48
+ * does not list a group twice.
49
+ */ export function registerPluginSearchSources(sources, options = {}) {
50
+ var _ref, _getRegisteringPluginId;
51
+ const pluginId = ((_ref = (_getRegisteringPluginId = getRegisteringPluginId()) != null ? _getRegisteringPluginId : options.pluginId) != null ? _ref : '').trim();
52
+ if (!pluginId) {
53
+ throw new Error('plugin search sources were registered with no owner: pass { pluginId } ' + 'when registering outside a plugin register fn');
54
+ }
55
+ const gates = _extends({}, options.featureFlag ? {
56
+ featureFlag: options.featureFlag
57
+ } : {}, options.permission ? {
58
+ permission: options.permission
59
+ } : {});
60
+ for(let index = registrations.length - 1; index >= 0; index -= 1){
61
+ if (registrations[index].pluginId === pluginId) registrations.splice(index, 1);
62
+ }
63
+ for (const source of sources){
64
+ if (source == null ? void 0 : source.id) registrations.push({
65
+ pluginId,
66
+ gates,
67
+ source
68
+ });
69
+ }
70
+ }
71
+ /**
72
+ * The sources of the plugins in `enabledPluginIds` (every registered one
73
+ * when omitted), in registration order. A second source with an id already
74
+ * listed is dropped, as the palette drops it.
75
+ */ export function listPluginSearchSources(enabledPluginIds) {
76
+ const on = enabledPluginIds ? new Set(enabledPluginIds) : null;
77
+ const seen = new Set();
78
+ const out = [];
79
+ for (const entry of registrations){
80
+ if (on && !on.has(entry.pluginId)) continue;
81
+ if (seen.has(entry.source.id)) continue;
82
+ seen.add(entry.source.id);
83
+ out.push(entry);
84
+ }
85
+ return out;
86
+ }
87
+ /** Only for specs: forgets every source. */ export function resetPluginSearchSourcesForTests() {
88
+ registrations.length = 0;
89
+ }
90
+
91
+ //# sourceMappingURL=plugin-search-sources.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-search-sources.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 * WHAT A PLUGIN LETS THE ONE-BOX SEARCH FIND, ON THE SERVER (AGL-3943).\n *\n * The console's palette reads a plugin's `ConsoleSearchSource`s in the\n * browser, through the Firestore rules. The native apps' one box asks the\n * console's `/api/search` instead, which reads with the Admin SDK, so it\n * needs the same sources in a server process that never loads a plugin's\n * console bundle. A plugin registers them here from its\n * `consoleServerDeclarations`, usually the very objects its console\n * extension lists, with the extension's own gates beside them.\n *\n * The Admin SDK bypasses the rules, so the route applies every gate itself:\n * the plugin on for the workspace and the site, the plan flag and quota, the\n * permission, the reader's host role (`hostAccess`), and each org-shared\n * row's `visibleTo` against the reader's reach.\n *\n * The core never imports a plugin; this is how its search reaches one.\n */\n\nimport type { ListQueryDeclaration } from '@aglyn/shared-util-tools/list-query/list-query-plan'\nimport { getRegisteringPluginId } from '../app-utils/registering-plugin'\nimport type { ConsoleSearchSource } from './feature-plugins'\n\n/** A plugin's search source as the server reads it. */\nexport interface PluginSearchSource extends ConsoleSearchSource {\n /**\n * Who on the site may find these rows: any member (the default), or only\n * the members who may read the site's sensitive documents (admin and\n * editor, the rules' `canReadHostSensitive`), as for orders.\n */\n hostAccess?: 'member' | 'sensitive'\n /**\n * Read through the list's own query rather than a capped window: the\n * search word goes on `listQuery.search` (a stored token array), planned\n * by `planListQuery`, so the rows are the list's own matches, newest first,\n * on the index the list already uses.\n */\n listQuery?: ListQueryDeclaration\n /** The row's label when no name field says it (an order's number). */\n label?(row: Readonly<Record<string, unknown>>): string\n /** One line under the label (an order's buyer). */\n detail?(row: Readonly<Record<string, unknown>>): string | null\n}\n\n/** The gates the plugin's console extension puts on every source it lists. */\nexport interface PluginSearchSourceGates {\n /** The plan flag the extension needs (`ConsoleExtension.featureFlag`). */\n featureFlag?: string\n /** The permission the extension needs (`ConsoleExtension.permission`). */\n permission?: string\n}\n\nexport interface RegisteredPluginSearchSource {\n pluginId: string\n gates: PluginSearchSourceGates\n source: PluginSearchSource\n}\n\n/**\n * On `globalThis` under `Symbol.for`, never at module scope (AGL-3947). The\n * sources are registered at boot, from `instrumentation.ts`, which Next\n * compiles apart from the routes, so the boot and `/api/search` each hold\n * their own copy of this module.\n */\nconst REGISTRY_KEY = Symbol.for('@aglyn/aglyn:plugin-search-sources')\nconst globalScope = globalThis as typeof globalThis & {\n [REGISTRY_KEY]?: RegisteredPluginSearchSource[]\n}\nconst registrations: RegisteredPluginSearchSource[] =\n globalScope[REGISTRY_KEY] ?? (globalScope[REGISTRY_KEY] = [])\n\n/**\n * Registers a plugin's server search sources. Owner = the loader's marker\n * inside a register fn, else `options.pluginId`; neither throws. Registering\n * again replaces the plugin's earlier sources, so a module evaluated twice\n * does not list a group twice.\n */\nexport function registerPluginSearchSources(\n sources: readonly PluginSearchSource[],\n options: PluginSearchSourceGates & { pluginId?: string } = {},\n): void {\n const pluginId = (getRegisteringPluginId() ?? options.pluginId ?? '').trim()\n if (!pluginId) {\n throw new Error(\n 'plugin search sources were registered with no owner: pass { pluginId } ' +\n 'when registering outside a plugin register fn',\n )\n }\n const gates: PluginSearchSourceGates = {\n ...(options.featureFlag ? { featureFlag: options.featureFlag } : {}),\n ...(options.permission ? { permission: options.permission } : {}),\n }\n for (let index = registrations.length - 1; index >= 0; index -= 1) {\n if (registrations[index].pluginId === pluginId) registrations.splice(index, 1)\n }\n for (const source of sources) {\n if (source?.id) registrations.push({ pluginId, gates, source })\n }\n}\n\n/**\n * The sources of the plugins in `enabledPluginIds` (every registered one\n * when omitted), in registration order. A second source with an id already\n * listed is dropped, as the palette drops it.\n */\nexport function listPluginSearchSources(\n enabledPluginIds?: readonly string[],\n): RegisteredPluginSearchSource[] {\n const on = enabledPluginIds ? new Set(enabledPluginIds) : null\n const seen = new Set<string>()\n const out: RegisteredPluginSearchSource[] = []\n for (const entry of registrations) {\n if (on && !on.has(entry.pluginId)) continue\n if (seen.has(entry.source.id)) continue\n seen.add(entry.source.id)\n out.push(entry)\n }\n return out\n}\n\n/** Only for specs: forgets every source. */\nexport function resetPluginSearchSourcesForTests(): void {\n registrations.length = 0\n}\n"],"names":["globalScope","getRegisteringPluginId","REGISTRY_KEY","Symbol","for","globalThis","registrations","registerPluginSearchSources","sources","options","pluginId","trim","Error","gates","featureFlag","permission","index","length","splice","source","id","push","listPluginSearchSources","enabledPluginIds","on","Set","seen","out","entry","has","add","resetPluginSearchSourcesForTests"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;CAiBC;IAoDCA;AAjDF,SAASC,sBAAsB,QAAQ,qCAAiC;AAsCxE;;;;;CAKC,GACD,MAAMC,eAAeC,OAAOC,GAAG,CAAC;AAChC,MAAMJ,cAAcK;AAGpB,MAAMC,iBACJN,4BAAAA,WAAW,CAACE,aAAa,YAAzBF,4BAA8BA,WAAW,CAACE,aAAa,GAAG,EAAE;AAE9D;;;;;CAKC,GACD,OAAO,SAASK,4BACdC,OAAsC,EACtCC,UAA2D,CAAC,CAAC;QAE3CR,MAAAA;IAAlB,MAAMS,WAAW,EAACT,QAAAA,0BAAAA,oCAAAA,0BAA4BQ,QAAQC,QAAQ,YAA5CT,OAAgD,IAAIU,IAAI;IAC1E,IAAI,CAACD,UAAU;QACb,MAAM,IAAIE,MACR,4EACE;IAEN;IACA,MAAMC,QAAiC,aACjCJ,QAAQK,WAAW,GAAG;QAAEA,aAAaL,QAAQK,WAAW;IAAC,IAAI,CAAC,GAC9DL,QAAQM,UAAU,GAAG;QAAEA,YAAYN,QAAQM,UAAU;IAAC,IAAI,CAAC;IAEjE,IAAK,IAAIC,QAAQV,cAAcW,MAAM,GAAG,GAAGD,SAAS,GAAGA,SAAS,EAAG;QACjE,IAAIV,aAAa,CAACU,MAAM,CAACN,QAAQ,KAAKA,UAAUJ,cAAcY,MAAM,CAACF,OAAO;IAC9E;IACA,KAAK,MAAMG,UAAUX,QAAS;QAC5B,IAAIW,0BAAAA,OAAQC,EAAE,EAAEd,cAAce,IAAI,CAAC;YAAEX;YAAUG;YAAOM;QAAO;IAC/D;AACF;AAEA;;;;CAIC,GACD,OAAO,SAASG,wBACdC,gBAAoC;IAEpC,MAAMC,KAAKD,mBAAmB,IAAIE,IAAIF,oBAAoB;IAC1D,MAAMG,OAAO,IAAID;IACjB,MAAME,MAAsC,EAAE;IAC9C,KAAK,MAAMC,SAAStB,cAAe;QACjC,IAAIkB,MAAM,CAACA,GAAGK,GAAG,CAACD,MAAMlB,QAAQ,GAAG;QACnC,IAAIgB,KAAKG,GAAG,CAACD,MAAMT,MAAM,CAACC,EAAE,GAAG;QAC/BM,KAAKI,GAAG,CAACF,MAAMT,MAAM,CAACC,EAAE;QACxBO,IAAIN,IAAI,CAACO;IACX;IACA,OAAOD;AACT;AAEA,0CAA0C,GAC1C,OAAO,SAASI;IACdzB,cAAcW,MAAM,GAAG;AACzB"}
@@ -32,6 +32,12 @@ var _globalScope_PLUGIN_SERVICES_KEY;
32
32
  * contract token carries the implementation type with it, so a seam another
33
33
  * plugin invents needs no core edit at all.
34
34
  *
35
+ * A plugin's token lives in its contracts module, which another plugin
36
+ * imports as `@aglyn/plugins-<id>/contracts` after naming that plugin in its
37
+ * `uses` (AGL-3980, docs/PACKAGES.md), so both sides share one id and one
38
+ * type. Declaring the same id twice still works, but a second declaration is
39
+ * a copy that drifts.
40
+ *
35
41
  * Attribution follows every other registry: the owner is the plugin whose
36
42
  * register fn is running (`setRegisteringPluginId`), or the `pluginId` the
37
43
  * caller names for a registration made at module scope, where the loader's
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-services.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 * Typed plugin service registry (AGL-2940).\n *\n * A plugin that wants OTHER plugins to extend it — a provider behind a\n * contract, a tool a generator can call, a generator kind — declares a\n * contract token here, and any plugin (first-party or marketplace) registers\n * an implementation against the token. The declaring plugin resolves the\n * implementations when it needs them and never imports the plugin that\n * supplied one, so the dependency runs only from an adopter to a contract.\n *\n * `registerBillingWebhookHandler` and `registerSiteRuntime` are the\n * precedent: one registry per extension seam, filled at registration time,\n * read lazily. What those cannot do is carry a TYPE for the thing registered\n * beyond their one shape, and each new seam meant a new module in core. A\n * contract token carries the implementation type with it, so a seam another\n * plugin invents needs no core edit at all.\n *\n * Attribution follows every other registry: the owner is the plugin whose\n * register fn is running (`setRegisteringPluginId`), or the `pluginId` the\n * caller names for a registration made at module scope, where the loader's\n * marker is not yet set. An implementation with no owner is refused —\n * ownership is what lets a duplicate single-service registration name both\n * claimants rather than silently taking the later one.\n */\n\nimport { getRegisteringPluginId } from '../app-utils/registering-plugin'\n\n/**\n * A contract token. `T` is the implementation type and is carried at the\n * type level only; `__type` never holds a value.\n */\nexport interface PluginServiceContract<T> {\n readonly id: string\n /**\n * Whether more than one plugin may register against the contract. A\n * single-implementation contract (`false`) is a slot — an AI provider\n * routing table, a payment processor — where a second registration from a\n * different plugin is a conflict, not a list. A multiple contract is a set\n * every implementation joins.\n */\n readonly multiple: boolean\n readonly __type?: T\n}\n\nexport interface PluginServiceRegistration<T> {\n contractId: string\n pluginId: string\n /**\n * Which of a plugin's implementations this is, on a multiple contract\n * where one plugin registers several (two provider adapters from one\n * plugin). Absent when the plugin registers one.\n */\n key?: string\n impl: T\n /** Higher resolves first among several implementations; default 0. */\n priority: number\n}\n\n/**\n * ONE REGISTRY PER PROCESS, not per module copy — the `aglyn.ts` pattern\n * (AGL-53), and for the same reason.\n *\n * Next.js compiles `instrumentation.ts` apart from the routes, so the boot\n * file and a route handler each hold their own copy of this module. With the\n * maps at module scope, what the boot registered (the console's trust signer,\n * its site cache, the declarations repair) went into the boot's copy, and\n * every route resolved an empty one: production answered \"Trust signing is\n * not available on this deployment\" with the key deployed and the signer\n * registered. Keyed on `globalThis` under `Symbol.for`, every copy in a\n * realm reads the same maps.\n */\nconst PLUGIN_SERVICES_KEY = Symbol.for('@aglyn/aglyn:plugin-services')\n\ninterface PluginServiceTables {\n contracts: Map<string, PluginServiceContract<unknown>>\n registrations: Map<string, PluginServiceRegistration<unknown>[]>\n}\n\nconst globalScope = globalThis as typeof globalThis & {\n [PLUGIN_SERVICES_KEY]?: PluginServiceTables\n}\n\nconst tables: PluginServiceTables =\n globalScope[PLUGIN_SERVICES_KEY] ??\n (globalScope[PLUGIN_SERVICES_KEY] = {\n contracts: new Map(),\n registrations: new Map(),\n })\nconst { contracts, registrations } = tables\n\n/**\n * Declares a contract. Idempotent by id for the same `multiple` setting, so\n * a hot reload or a second surface re-evaluating the declaring module gets\n * the token back; declaring the same id with a DIFFERENT setting is a\n * programming error and throws, because two plugins would then disagree\n * about whether the seam is a slot or a set.\n */\nexport function definePluginServiceContract<T>(\n id: string,\n options: { multiple: boolean },\n): PluginServiceContract<T> {\n const key = id.trim()\n if (!key) throw new Error('a plugin service contract needs an id')\n const existing = contracts.get(key)\n if (existing) {\n if (existing.multiple !== options.multiple) {\n throw new Error(\n `plugin service contract \"${key}\" is already defined with ` +\n `multiple=${existing.multiple}; cannot redefine it with ` +\n `multiple=${options.multiple}`,\n )\n }\n return existing as PluginServiceContract<T>\n }\n const contract: PluginServiceContract<T> = { id: key, multiple: options.multiple }\n contracts.set(key, contract)\n return contract\n}\n\nfunction requireContract<T>(contract: PluginServiceContract<T>): PluginServiceContract<T> {\n const known = contracts.get(contract.id)\n if (!known) {\n throw new Error(\n `plugin service contract \"${contract.id}\" is not defined; call ` +\n 'definePluginServiceContract before registering against it',\n )\n }\n return known as PluginServiceContract<T>\n}\n\n/**\n * Registers an implementation. The owner is the loader's marker when a\n * register fn is running, else `options.pluginId`; a registration with\n * neither throws.\n *\n * Re-registration by the SAME plugin under the same `key` replaces its\n * previous entry (hot reload, a second surface, a repeated init); a plugin\n * registering several implementations on a multiple contract tells them\n * apart with `key`. On a single-implementation contract a registration\n * from a DIFFERENT plugin throws naming both, and the incumbent keeps\n * serving.\n */\nexport function registerPluginService<T>(\n contract: PluginServiceContract<T>,\n impl: T,\n options?: { pluginId?: string; priority?: number; key?: string },\n): void {\n const known = requireContract(contract)\n const pluginId = (getRegisteringPluginId() ?? options?.pluginId ?? '').trim()\n if (!pluginId) {\n throw new Error(\n `plugin service \"${known.id}\" registered with no owner: pass ` +\n '{ pluginId } when registering outside a plugin register fn',\n )\n }\n const key = options?.key?.trim() || undefined\n const list = registrations.get(known.id) ?? []\n const foreign = list.find((entry) => entry.pluginId !== pluginId)\n if (!known.multiple && foreign) {\n throw new Error(\n `plugin service \"${known.id}\" is a single-implementation contract ` +\n `already registered by \"${foreign.pluginId}\"; refused \"${pluginId}\"`,\n )\n }\n const kept = list.filter(\n (entry) => !(entry.pluginId === pluginId && (entry.key ?? null) === (key ?? null)),\n )\n kept.push({\n contractId: known.id,\n pluginId,\n ...(key ? { key } : {}),\n impl,\n priority: options?.priority ?? 0,\n })\n registrations.set(known.id, kept)\n}\n\n/**\n * Every implementation of a contract, highest priority first and\n * registration order within a priority. Empty when nothing registered.\n */\nexport function resolvePluginServices<T>(\n contract: PluginServiceContract<T>,\n): PluginServiceRegistration<T>[] {\n const known = requireContract(contract)\n const list = (registrations.get(known.id) ?? []) as PluginServiceRegistration<T>[]\n return [...list].sort((a, b) => b.priority - a.priority)\n}\n\n/**\n * The one implementation of a single contract — or, on a multiple\n * contract, the highest-priority one — or `undefined`.\n */\nexport function resolvePluginService<T>(\n contract: PluginServiceContract<T>,\n): T | undefined {\n return resolvePluginServices(contract)[0]?.impl\n}\n\n/** Whether any plugin has registered against the contract. */\nexport function hasPluginService<T>(contract: PluginServiceContract<T>): boolean {\n return (registrations.get(requireContract(contract).id) ?? []).length > 0\n}\n\n/** Every declared contract id, for diagnostics. */\nexport function listPluginServiceContracts(): string[] {\n return [...contracts.keys()]\n}\n\n/**\n * Drops a plugin's registrations across every contract — what the loader's\n * reverse (a bundle unloading) would call, and what a spec calls to give\n * one fake plugin back its slot.\n */\nexport function unregisterPluginServices(pluginId: string): void {\n for (const [id, list] of registrations) {\n registrations.set(\n id,\n list.filter((entry) => entry.pluginId !== pluginId),\n )\n }\n}\n\n/** Test seam: forget every registration; contracts stay defined. */\nexport function resetPluginServicesForTests(): void {\n registrations.clear()\n}\n"],"names":["globalScope","getRegisteringPluginId","PLUGIN_SERVICES_KEY","Symbol","for","globalThis","tables","contracts","Map","registrations","definePluginServiceContract","id","options","key","trim","Error","existing","get","multiple","contract","set","requireContract","known","registerPluginService","impl","pluginId","undefined","list","foreign","find","entry","kept","filter","push","contractId","priority","resolvePluginServices","sort","a","b","resolvePluginService","hasPluginService","length","listPluginServiceContracts","keys","unregisterPluginServices","resetPluginServicesForTests","clear"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-services.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 * Typed plugin service registry (AGL-2940).\n *\n * A plugin that wants OTHER plugins to extend it — a provider behind a\n * contract, a tool a generator can call, a generator kind — declares a\n * contract token here, and any plugin (first-party or marketplace) registers\n * an implementation against the token. The declaring plugin resolves the\n * implementations when it needs them and never imports the plugin that\n * supplied one, so the dependency runs only from an adopter to a contract.\n *\n * `registerBillingWebhookHandler` and `registerSiteRuntime` are the\n * precedent: one registry per extension seam, filled at registration time,\n * read lazily. What those cannot do is carry a TYPE for the thing registered\n * beyond their one shape, and each new seam meant a new module in core. A\n * contract token carries the implementation type with it, so a seam another\n * plugin invents needs no core edit at all.\n *\n * A plugin's token lives in its contracts module, which another plugin\n * imports as `@aglyn/plugins-<id>/contracts` after naming that plugin in its\n * `uses` (AGL-3980, docs/PACKAGES.md), so both sides share one id and one\n * type. Declaring the same id twice still works, but a second declaration is\n * a copy that drifts.\n *\n * Attribution follows every other registry: the owner is the plugin whose\n * register fn is running (`setRegisteringPluginId`), or the `pluginId` the\n * caller names for a registration made at module scope, where the loader's\n * marker is not yet set. An implementation with no owner is refused —\n * ownership is what lets a duplicate single-service registration name both\n * claimants rather than silently taking the later one.\n */\n\nimport { getRegisteringPluginId } from '../app-utils/registering-plugin'\n\n/**\n * A contract token. `T` is the implementation type and is carried at the\n * type level only; `__type` never holds a value.\n */\nexport interface PluginServiceContract<T> {\n readonly id: string\n /**\n * Whether more than one plugin may register against the contract. A\n * single-implementation contract (`false`) is a slot — an AI provider\n * routing table, a payment processor — where a second registration from a\n * different plugin is a conflict, not a list. A multiple contract is a set\n * every implementation joins.\n */\n readonly multiple: boolean\n readonly __type?: T\n}\n\nexport interface PluginServiceRegistration<T> {\n contractId: string\n pluginId: string\n /**\n * Which of a plugin's implementations this is, on a multiple contract\n * where one plugin registers several (two provider adapters from one\n * plugin). Absent when the plugin registers one.\n */\n key?: string\n impl: T\n /** Higher resolves first among several implementations; default 0. */\n priority: number\n}\n\n/**\n * ONE REGISTRY PER PROCESS, not per module copy — the `aglyn.ts` pattern\n * (AGL-53), and for the same reason.\n *\n * Next.js compiles `instrumentation.ts` apart from the routes, so the boot\n * file and a route handler each hold their own copy of this module. With the\n * maps at module scope, what the boot registered (the console's trust signer,\n * its site cache, the declarations repair) went into the boot's copy, and\n * every route resolved an empty one: production answered \"Trust signing is\n * not available on this deployment\" with the key deployed and the signer\n * registered. Keyed on `globalThis` under `Symbol.for`, every copy in a\n * realm reads the same maps.\n */\nconst PLUGIN_SERVICES_KEY = Symbol.for('@aglyn/aglyn:plugin-services')\n\ninterface PluginServiceTables {\n contracts: Map<string, PluginServiceContract<unknown>>\n registrations: Map<string, PluginServiceRegistration<unknown>[]>\n}\n\nconst globalScope = globalThis as typeof globalThis & {\n [PLUGIN_SERVICES_KEY]?: PluginServiceTables\n}\n\nconst tables: PluginServiceTables =\n globalScope[PLUGIN_SERVICES_KEY] ??\n (globalScope[PLUGIN_SERVICES_KEY] = {\n contracts: new Map(),\n registrations: new Map(),\n })\nconst { contracts, registrations } = tables\n\n/**\n * Declares a contract. Idempotent by id for the same `multiple` setting, so\n * a hot reload or a second surface re-evaluating the declaring module gets\n * the token back; declaring the same id with a DIFFERENT setting is a\n * programming error and throws, because two plugins would then disagree\n * about whether the seam is a slot or a set.\n */\nexport function definePluginServiceContract<T>(\n id: string,\n options: { multiple: boolean },\n): PluginServiceContract<T> {\n const key = id.trim()\n if (!key) throw new Error('a plugin service contract needs an id')\n const existing = contracts.get(key)\n if (existing) {\n if (existing.multiple !== options.multiple) {\n throw new Error(\n `plugin service contract \"${key}\" is already defined with ` +\n `multiple=${existing.multiple}; cannot redefine it with ` +\n `multiple=${options.multiple}`,\n )\n }\n return existing as PluginServiceContract<T>\n }\n const contract: PluginServiceContract<T> = { id: key, multiple: options.multiple }\n contracts.set(key, contract)\n return contract\n}\n\nfunction requireContract<T>(contract: PluginServiceContract<T>): PluginServiceContract<T> {\n const known = contracts.get(contract.id)\n if (!known) {\n throw new Error(\n `plugin service contract \"${contract.id}\" is not defined; call ` +\n 'definePluginServiceContract before registering against it',\n )\n }\n return known as PluginServiceContract<T>\n}\n\n/**\n * Registers an implementation. The owner is the loader's marker when a\n * register fn is running, else `options.pluginId`; a registration with\n * neither throws.\n *\n * Re-registration by the SAME plugin under the same `key` replaces its\n * previous entry (hot reload, a second surface, a repeated init); a plugin\n * registering several implementations on a multiple contract tells them\n * apart with `key`. On a single-implementation contract a registration\n * from a DIFFERENT plugin throws naming both, and the incumbent keeps\n * serving.\n */\nexport function registerPluginService<T>(\n contract: PluginServiceContract<T>,\n impl: T,\n options?: { pluginId?: string; priority?: number; key?: string },\n): void {\n const known = requireContract(contract)\n const pluginId = (getRegisteringPluginId() ?? options?.pluginId ?? '').trim()\n if (!pluginId) {\n throw new Error(\n `plugin service \"${known.id}\" registered with no owner: pass ` +\n '{ pluginId } when registering outside a plugin register fn',\n )\n }\n const key = options?.key?.trim() || undefined\n const list = registrations.get(known.id) ?? []\n const foreign = list.find((entry) => entry.pluginId !== pluginId)\n if (!known.multiple && foreign) {\n throw new Error(\n `plugin service \"${known.id}\" is a single-implementation contract ` +\n `already registered by \"${foreign.pluginId}\"; refused \"${pluginId}\"`,\n )\n }\n const kept = list.filter(\n (entry) => !(entry.pluginId === pluginId && (entry.key ?? null) === (key ?? null)),\n )\n kept.push({\n contractId: known.id,\n pluginId,\n ...(key ? { key } : {}),\n impl,\n priority: options?.priority ?? 0,\n })\n registrations.set(known.id, kept)\n}\n\n/**\n * Every implementation of a contract, highest priority first and\n * registration order within a priority. Empty when nothing registered.\n */\nexport function resolvePluginServices<T>(\n contract: PluginServiceContract<T>,\n): PluginServiceRegistration<T>[] {\n const known = requireContract(contract)\n const list = (registrations.get(known.id) ?? []) as PluginServiceRegistration<T>[]\n return [...list].sort((a, b) => b.priority - a.priority)\n}\n\n/**\n * The one implementation of a single contract — or, on a multiple\n * contract, the highest-priority one — or `undefined`.\n */\nexport function resolvePluginService<T>(\n contract: PluginServiceContract<T>,\n): T | undefined {\n return resolvePluginServices(contract)[0]?.impl\n}\n\n/** Whether any plugin has registered against the contract. */\nexport function hasPluginService<T>(contract: PluginServiceContract<T>): boolean {\n return (registrations.get(requireContract(contract).id) ?? []).length > 0\n}\n\n/** Every declared contract id, for diagnostics. */\nexport function listPluginServiceContracts(): string[] {\n return [...contracts.keys()]\n}\n\n/**\n * Drops a plugin's registrations across every contract — what the loader's\n * reverse (a bundle unloading) would call, and what a spec calls to give\n * one fake plugin back its slot.\n */\nexport function unregisterPluginServices(pluginId: string): void {\n for (const [id, list] of registrations) {\n registrations.set(\n id,\n list.filter((entry) => entry.pluginId !== pluginId),\n )\n }\n}\n\n/** Test seam: forget every registration; contracts stay defined. */\nexport function resetPluginServicesForTests(): void {\n registrations.clear()\n}\n"],"names":["globalScope","getRegisteringPluginId","PLUGIN_SERVICES_KEY","Symbol","for","globalThis","tables","contracts","Map","registrations","definePluginServiceContract","id","options","key","trim","Error","existing","get","multiple","contract","set","requireContract","known","registerPluginService","impl","pluginId","undefined","list","foreign","find","entry","kept","filter","push","contractId","priority","resolvePluginServices","sort","a","b","resolvePluginService","hasPluginService","length","listPluginServiceContracts","keys","unregisterPluginServices","resetPluginServicesForTests","clear"],"mappings":"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"}