@aglyn/aglyn 1.0.0-beta.229 → 1.0.0-beta.231

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 (123) hide show
  1. package/package.json +11 -11
  2. package/src/lib/app-utils/analytics-events.d.ts +18 -0
  3. package/src/lib/app-utils/analytics-events.js +2 -0
  4. package/src/lib/app-utils/analytics-events.js.map +1 -1
  5. package/src/lib/app-utils/crm.d.ts +14 -1
  6. package/src/lib/app-utils/crm.js +19 -2
  7. package/src/lib/app-utils/crm.js.map +1 -1
  8. package/src/lib/app-utils/docs-help.generated.d.ts +111 -9
  9. package/src/lib/app-utils/docs-help.generated.js +272 -3
  10. package/src/lib/app-utils/docs-help.generated.js.map +1 -1
  11. package/src/lib/app-utils/docs-index.generated.js +810 -61
  12. package/src/lib/app-utils/docs-index.generated.js.map +1 -1
  13. package/src/lib/app-utils/host-status.d.ts +85 -0
  14. package/src/lib/app-utils/host-status.js +115 -0
  15. package/src/lib/app-utils/host-status.js.map +1 -0
  16. package/src/lib/app-utils/lockdown.js +1 -1
  17. package/src/lib/app-utils/lockdown.js.map +1 -1
  18. package/src/lib/app-utils/media-filter.d.ts +136 -0
  19. package/src/lib/app-utils/media-filter.js +400 -0
  20. package/src/lib/app-utils/media-filter.js.map +1 -0
  21. package/src/lib/app-utils/mobile-push.d.ts +98 -0
  22. package/src/lib/app-utils/mobile-push.js +97 -0
  23. package/src/lib/app-utils/mobile-push.js.map +1 -0
  24. package/src/lib/app-utils/notification-push.d.ts +38 -0
  25. package/src/lib/app-utils/notification-push.js +54 -0
  26. package/src/lib/app-utils/notification-push.js.map +1 -0
  27. package/src/lib/app-utils/notifications.d.ts +7 -0
  28. package/src/lib/app-utils/notifications.js.map +1 -1
  29. package/src/lib/app-utils/organizations.js +5 -2
  30. package/src/lib/app-utils/organizations.js.map +1 -1
  31. package/src/lib/app-utils/plan-entitlements.js +20 -0
  32. package/src/lib/app-utils/plan-entitlements.js.map +1 -1
  33. package/src/lib/app-utils/plugin-host-events.generated.d.ts +1 -1
  34. package/src/lib/app-utils/plugin-host-events.generated.js +164 -0
  35. package/src/lib/app-utils/plugin-host-events.generated.js.map +1 -1
  36. package/src/lib/app-utils/plugin-release-flags.generated.d.ts +1 -1
  37. package/src/lib/app-utils/plugin-release-flags.generated.js +7 -0
  38. package/src/lib/app-utils/plugin-release-flags.generated.js.map +1 -1
  39. package/src/lib/app-utils/realm-host-surface.generated.js +3 -0
  40. package/src/lib/app-utils/realm-host-surface.generated.js.map +1 -1
  41. package/src/lib/app-utils/release-flags.js +6 -2
  42. package/src/lib/app-utils/release-flags.js.map +1 -1
  43. package/src/lib/app-utils/scope-tokens.d.ts +16 -1
  44. package/src/lib/app-utils/scope-tokens.js +15 -1
  45. package/src/lib/app-utils/scope-tokens.js.map +1 -1
  46. package/src/lib/app-utils/site-journey.d.ts +143 -0
  47. package/src/lib/app-utils/site-journey.js +282 -0
  48. package/src/lib/app-utils/site-journey.js.map +1 -0
  49. package/src/lib/app-utils/site-list-query.d.ts +47 -0
  50. package/src/lib/app-utils/site-list-query.js +142 -0
  51. package/src/lib/app-utils/site-list-query.js.map +1 -0
  52. package/src/lib/app-utils/site-wide-outbox.d.ts +95 -0
  53. package/src/lib/app-utils/site-wide-outbox.js +117 -0
  54. package/src/lib/app-utils/site-wide-outbox.js.map +1 -0
  55. package/src/lib/app-utils/transfer-launcher-context.d.ts +6 -0
  56. package/src/lib/app-utils/transfer-launcher-context.js.map +1 -1
  57. package/src/lib/app-utils/upload-inspection.js +7 -0
  58. package/src/lib/app-utils/upload-inspection.js.map +1 -1
  59. package/src/lib/app-utils/webhook-delivery.js +4 -1
  60. package/src/lib/app-utils/webhook-delivery.js.map +1 -1
  61. package/src/lib/foundation/definitions/org-billing.types.d.ts +24 -0
  62. package/src/lib/foundation/definitions/org-billing.types.js.map +1 -1
  63. package/src/lib/foundation/definitions/organization.types.d.ts +18 -7
  64. package/src/lib/foundation/definitions/organization.types.js.map +1 -1
  65. package/src/lib/foundation/definitions/write-deny-coverage.util.d.ts +4 -1
  66. package/src/lib/foundation/definitions/write-deny-coverage.util.js +12 -2
  67. package/src/lib/foundation/definitions/write-deny-coverage.util.js.map +1 -1
  68. package/src/lib/plugin-manager/enabled-plugins.js +4 -2
  69. package/src/lib/plugin-manager/enabled-plugins.js.map +1 -1
  70. package/src/lib/plugin-manager/feature-plugins.d.ts +173 -0
  71. package/src/lib/plugin-manager/feature-plugins.js +62 -1
  72. package/src/lib/plugin-manager/feature-plugins.js.map +1 -1
  73. package/src/lib/plugin-manager/first-party-plugins.generated.js +251 -2
  74. package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
  75. package/src/lib/plugin-manager/plugin-ai-capabilities.d.ts +192 -0
  76. package/src/lib/plugin-manager/plugin-ai-capabilities.js +157 -0
  77. package/src/lib/plugin-manager/plugin-ai-capabilities.js.map +1 -0
  78. package/src/lib/plugin-manager/plugin-checkout-extras.d.ts +168 -0
  79. package/src/lib/plugin-manager/plugin-checkout-extras.js +172 -0
  80. package/src/lib/plugin-manager/plugin-checkout-extras.js.map +1 -0
  81. package/src/lib/plugin-manager/plugin-contributions.d.ts +7 -0
  82. package/src/lib/plugin-manager/plugin-contributions.js +1 -1
  83. package/src/lib/plugin-manager/plugin-contributions.js.map +1 -1
  84. package/src/lib/plugin-manager/plugin-domain-events.d.ts +138 -0
  85. package/src/lib/plugin-manager/plugin-domain-events.js +148 -0
  86. package/src/lib/plugin-manager/plugin-domain-events.js.map +1 -0
  87. package/src/lib/plugin-manager/plugin-events.d.ts +51 -0
  88. package/src/lib/plugin-manager/plugin-events.js +4 -0
  89. package/src/lib/plugin-manager/plugin-events.js.map +1 -1
  90. package/src/lib/plugin-manager/plugin-fulfillment-providers.d.ts +101 -0
  91. package/src/lib/plugin-manager/plugin-fulfillment-providers.js +83 -0
  92. package/src/lib/plugin-manager/plugin-fulfillment-providers.js.map +1 -0
  93. package/src/lib/plugin-manager/plugin-permissions.js +21 -5
  94. package/src/lib/plugin-manager/plugin-permissions.js.map +1 -1
  95. package/src/lib/plugin-manager/plugin-person-records.d.ts +90 -0
  96. package/src/lib/plugin-manager/plugin-person-records.js +26 -0
  97. package/src/lib/plugin-manager/plugin-person-records.js.map +1 -1
  98. package/src/lib/plugin-manager/plugin-product-catalog.d.ts +204 -0
  99. package/src/lib/plugin-manager/plugin-product-catalog.js +43 -0
  100. package/src/lib/plugin-manager/plugin-product-catalog.js.map +1 -0
  101. package/src/lib/plugin-manager/plugin-shipment-records.d.ts +210 -0
  102. package/src/lib/plugin-manager/plugin-shipment-records.js +63 -0
  103. package/src/lib/plugin-manager/plugin-shipment-records.js.map +1 -0
  104. package/src/lib/plugin-manager/plugin-shipping-rates.d.ts +151 -0
  105. package/src/lib/plugin-manager/plugin-shipping-rates.js +62 -0
  106. package/src/lib/plugin-manager/plugin-shipping-rates.js.map +1 -0
  107. package/src/lib/plugin-manager/plugin-sms-messaging.d.ts +103 -0
  108. package/src/lib/plugin-manager/plugin-sms-messaging.js +39 -0
  109. package/src/lib/plugin-manager/plugin-sms-messaging.js.map +1 -0
  110. package/src/lib/plugin-manager/plugin-stock-levels.d.ts +81 -0
  111. package/src/lib/plugin-manager/plugin-stock-levels.js +32 -0
  112. package/src/lib/plugin-manager/plugin-stock-levels.js.map +1 -0
  113. package/src/lib/plugin-manager/plugin-tax-profile.d.ts +154 -0
  114. package/src/lib/plugin-manager/plugin-tax-profile.js +56 -0
  115. package/src/lib/plugin-manager/plugin-tax-profile.js.map +1 -1
  116. package/src/lib/plugin-manager/plugin-theme-font-catalog.d.ts +59 -0
  117. package/src/lib/plugin-manager/plugin-theme-font-catalog.js +40 -0
  118. package/src/lib/plugin-manager/plugin-theme-font-catalog.js.map +1 -0
  119. package/src/lib/plugin-manager/plugin-tracking-pages.d.ts +55 -0
  120. package/src/lib/plugin-manager/plugin-tracking-pages.js +76 -0
  121. package/src/lib/plugin-manager/plugin-tracking-pages.js.map +1 -0
  122. package/src/lib/plugin-manager/realm-host-aglyn.generated.js +3 -0
  123. package/src/lib/plugin-manager/realm-host-aglyn.generated.js.map +1 -1
@@ -0,0 +1,172 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ import { getRegisteringPluginId } from "../app-utils/registering-plugin.js";
18
+ import { definePluginServiceContract, registerPluginService, resolvePluginServices } from "./plugin-services.js";
19
+ /** The most extras one checkout carries. */ export const MAX_CHECKOUT_EXTRAS = 3;
20
+ const KEY = /^[a-z][a-z0-9-]{1,39}$/;
21
+ const PLUGIN_ID = /^[a-z][a-z0-9-]{0,63}$/;
22
+ const PLUGIN_CHECKOUT_EXTRAS = definePluginServiceContract('core.checkout-extras', {
23
+ multiple: true
24
+ });
25
+ /** Joins the providers. Re-registering under the same plugin replaces its own. */ export function registerPluginCheckoutExtra(provider, options) {
26
+ var _getRegisteringPluginId;
27
+ const pluginId = (_getRegisteringPluginId = getRegisteringPluginId()) != null ? _getRegisteringPluginId : options == null ? void 0 : options.pluginId;
28
+ registerPluginService(PLUGIN_CHECKOUT_EXTRAS, provider, _extends({}, pluginId ? {
29
+ pluginId
30
+ } : {}));
31
+ }
32
+ /** Whether any plugin offers extras: a seller with none skips the question. */ export function hasPluginCheckoutExtras() {
33
+ return resolvePluginServices(PLUGIN_CHECKOUT_EXTRAS).length > 0;
34
+ }
35
+ function cleanText(value, max) {
36
+ // Control characters become spaces: an offer's words are drawn on a page.
37
+ return Array.from(String(value != null ? value : ''), (char)=>char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127 ? ' ' : char).join('').replace(/\s+/g, ' ').trim().slice(0, max);
38
+ }
39
+ /**
40
+ * An offer held to the contract, or `null`: a positive integer amount in the
41
+ * request's currency, a key and a label, and nothing else a page would render
42
+ * that it should not.
43
+ */ export function normalizePluginCheckoutExtra(pluginId, offer, currency) {
44
+ var _offer_key, _offer_currency, _offer_termsUrl;
45
+ if (!offer || !PLUGIN_ID.test(pluginId)) return null;
46
+ const key = String((_offer_key = offer.key) != null ? _offer_key : '');
47
+ const label = cleanText(offer.label, 60);
48
+ const amountCents = offer.amountCents;
49
+ if (!KEY.test(key) || !label) return null;
50
+ if (typeof amountCents !== 'number' || !Number.isSafeInteger(amountCents) || amountCents <= 0) return null;
51
+ if (String((_offer_currency = offer.currency) != null ? _offer_currency : '').toLowerCase() !== String(currency != null ? currency : '').toLowerCase()) return null;
52
+ const description = cleanText(offer.description, 240);
53
+ const quoteRef = cleanText(offer.quoteRef, 64);
54
+ const termsUrl = /^https:\/\/[^\s"'<>]+$/i.test(String((_offer_termsUrl = offer.termsUrl) != null ? _offer_termsUrl : '')) ? String(offer.termsUrl) : '';
55
+ return _extends({
56
+ id: `${pluginId}.${key}`,
57
+ pluginId,
58
+ key,
59
+ label
60
+ }, description ? {
61
+ description
62
+ } : {}, {
63
+ amountCents,
64
+ currency: currency.toLowerCase(),
65
+ defaultSelected: offer.defaultSelected === true
66
+ }, quoteRef ? {
67
+ quoteRef
68
+ } : {}, termsUrl ? {
69
+ termsUrl
70
+ } : {});
71
+ }
72
+ /**
73
+ * Asks every provider at once and keeps what answered within `timeoutMs`,
74
+ * each one held to the contract. Never throws; a provider that throws, is
75
+ * late or answers nonsense is simply absent. At most
76
+ * {@link MAX_CHECKOUT_EXTRAS}, in the providers' resolve order.
77
+ */ export async function quotePluginCheckoutExtras(request, options) {
78
+ const providers = resolvePluginServices(PLUGIN_CHECKOUT_EXTRAS);
79
+ if (!providers.length) return [];
80
+ const controller = new AbortController();
81
+ let timer;
82
+ const deadline = new Promise((resolve)=>{
83
+ timer = setTimeout(()=>{
84
+ controller.abort();
85
+ resolve('late');
86
+ }, Math.max(0, options.timeoutMs));
87
+ });
88
+ try {
89
+ const answers = await Promise.all(providers.map(async (entry)=>{
90
+ const answer = await Promise.race([
91
+ entry.impl.offer(_extends({}, request, {
92
+ signal: controller.signal
93
+ })).catch((error)=>{
94
+ console.error(`[checkout-extras] provider "${entry.pluginId}" failed for ${request.hostId}`, error);
95
+ return null;
96
+ }),
97
+ deadline
98
+ ]);
99
+ return answer === 'late' ? null : normalizePluginCheckoutExtra(entry.pluginId, answer, request.currency);
100
+ }));
101
+ const seen = new Set();
102
+ return answers.filter((answer)=>Boolean(answer)).filter((answer)=>seen.has(answer.id) ? false : (seen.add(answer.id), true)).slice(0, MAX_CHECKOUT_EXTRAS);
103
+ } finally{
104
+ if (timer) clearTimeout(timer);
105
+ }
106
+ }
107
+ /**
108
+ * The buyer's choice, read from a request body: the ids of offers they took,
109
+ * de-duplicated and bounded. Ids only; the amounts are always asked again.
110
+ */ export function readChosenCheckoutExtras(value) {
111
+ if (!Array.isArray(value)) return [];
112
+ const ids = value.map((entry)=>String(entry != null ? entry : '').trim()).filter((entry)=>/^[a-z][a-z0-9-]{0,63}\.[a-z][a-z0-9-]{1,39}$/.test(entry));
113
+ return [
114
+ ...new Set(ids)
115
+ ].slice(0, MAX_CHECKOUT_EXTRAS);
116
+ }
117
+ /** The metadata keys the extras ride under: `extra0`, `extra1`, `extra2`. */ export const CHECKOUT_EXTRA_METADATA_PREFIX = 'extra';
118
+ /**
119
+ * The extras a sale carries, packed for a payment processor's metadata: one
120
+ * key per extra (`extra0` …), each `[id, cents, label, quoteRef?]` as JSON,
121
+ * well inside a 500-character value. One key each, so a long quote id can
122
+ * never truncate another extra out of the record.
123
+ */ export function encodeCheckoutExtrasMetadata(extras) {
124
+ const packed = {};
125
+ extras.slice(0, MAX_CHECKOUT_EXTRAS).forEach((extra, index)=>{
126
+ const entry = [
127
+ extra.id,
128
+ extra.amountCents,
129
+ extra.label.slice(0, 60)
130
+ ];
131
+ if (extra.quoteRef) entry.push(extra.quoteRef.slice(0, 64));
132
+ packed[`${CHECKOUT_EXTRA_METADATA_PREFIX}${index}`] = JSON.stringify(entry);
133
+ });
134
+ return packed;
135
+ }
136
+ /**
137
+ * Reads {@link encodeCheckoutExtrasMetadata} back off the metadata object.
138
+ * Anything unreadable is dropped, never guessed.
139
+ */ export function decodeCheckoutExtrasMetadata(metadata) {
140
+ const sold = [];
141
+ for(let index = 0; index < MAX_CHECKOUT_EXTRAS; index += 1){
142
+ const raw = metadata == null ? void 0 : metadata[`${CHECKOUT_EXTRA_METADATA_PREFIX}${index}`];
143
+ if (raw === undefined || raw === null || raw === '') continue;
144
+ let entry;
145
+ try {
146
+ entry = JSON.parse(String(raw));
147
+ } catch (unused) {
148
+ continue;
149
+ }
150
+ if (!Array.isArray(entry)) continue;
151
+ const [id, cents, label, quoteRef] = entry;
152
+ const match = /^([a-z][a-z0-9-]{0,63})\.([a-z][a-z0-9-]{1,39})$/.exec(String(id != null ? id : ''));
153
+ if (!match || typeof cents !== 'number' || !Number.isSafeInteger(cents) || cents <= 0) continue;
154
+ if (sold.some((extra)=>extra.id === id)) continue;
155
+ const ref = cleanText(quoteRef, 64);
156
+ sold.push(_extends({
157
+ id: String(id),
158
+ pluginId: match[1],
159
+ key: match[2],
160
+ label: cleanText(label, 60) || 'Extra',
161
+ amountCents: cents
162
+ }, ref ? {
163
+ quoteRef: ref
164
+ } : {}));
165
+ }
166
+ return sold;
167
+ }
168
+ /** The cents a sale's extras add up to. */ export function checkoutExtrasCents(extras) {
169
+ return (extras != null ? extras : []).reduce((sum, extra)=>sum + (Number.isSafeInteger(extra == null ? void 0 : extra.amountCents) && extra.amountCents > 0 ? extra.amountCents : 0), 0);
170
+ }
171
+
172
+ //# sourceMappingURL=plugin-checkout-extras.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-checkout-extras.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { getRegisteringPluginId } from '../app-utils/registering-plugin'\nimport {\n definePluginServiceContract,\n registerPluginService,\n resolvePluginServices,\n} from './plugin-services'\n\n/**\n * Optional lines a buyer may add at checkout, offered by another plugin\n * (AGL-3635).\n *\n * Package protection is the first: a plugin that insures parcels quotes a\n * premium for the basket, the seller shows it as a box the buyer ticks, and\n * the seller charges it as one more line. The seller never learns which\n * insurer answered and the insurer never reads the seller's documents: the\n * offer crosses here, and what was bought is recorded on the sale, which the\n * seller's own events then carry.\n *\n * ## Money\n *\n * Every amount is integer cents in the request's currency. An offer is\n * advisory until the sale: the seller asks again when the buyer pays and\n * charges THAT answer, so a stale price in a drawer is never what is\n * charged. An offer whose amount is not a positive integer, or whose\n * currency differs from the request's, is dropped rather than rounded.\n *\n * ## Nobody home is an empty list\n *\n * A provider that is not configured, not switched on for the site, slow or\n * failing answers nothing, and the seller sells exactly as it did before.\n * {@link quotePluginCheckoutExtras} never throws.\n *\n * Import this module by its own subpath\n * (`@aglyn/aglyn/plugin-manager/plugin-checkout-extras`); it is not in the\n * barrel.\n */\n\n/** One line of the basket, as a provider prices it. */\nexport interface PluginCheckoutExtraLine {\n /** The seller's id for the item, when it has one. */\n itemId?: string\n name: string\n sku?: string\n quantity: number\n /** Per unit, integer cents, before discounts. */\n unitCents: number\n /** Whether the line travels in a parcel. */\n ships: boolean\n}\n\n/** What a seller asks: the basket a buyer is about to pay for. */\nexport interface PluginCheckoutExtraRequest {\n hostId: string\n /** ISO-4217, lower case. */\n currency: string\n /** The goods' value, integer cents, before discounts and shipping. */\n itemsCents: number\n lines: PluginCheckoutExtraLine[]\n /** Where the buyer said it goes, when they said. */\n destination?: { country?: string; postalCode?: string }\n /** Aborted when the seller stops waiting; a provider passes it to its fetches. */\n signal?: AbortSignal\n}\n\n/** What a provider offers for that basket. */\nexport interface PluginCheckoutExtraOffer {\n /** Stable within the provider: `package-protection`. Lower-case words and dashes. */\n key: string\n /** What the buyer reads beside the box: `Package protection`. */\n label: string\n /** One sentence under it. */\n description?: string\n /** Integer cents, above zero. */\n amountCents: number\n /** ISO-4217, lower case; must be the request's. */\n currency: string\n /** Whether the box starts ticked. The merchant decides; default unticked. */\n defaultSelected?: boolean\n /** The provider's id for this quote, carried onto the sale (64 characters at most). */\n quoteRef?: string\n /** A page the buyer can read about it, `https:` only. */\n termsUrl?: string\n}\n\nexport interface PluginCheckoutExtraProvider {\n /** The offer for this basket, or `null`. Throwing reads as `null`. */\n offer(request: PluginCheckoutExtraRequest): Promise<PluginCheckoutExtraOffer | null>\n}\n\n/** An offer as the seller sees it: whose it is, and the id it is chosen by. */\nexport interface QuotedPluginCheckoutExtra extends Required<Pick<PluginCheckoutExtraOffer, 'defaultSelected'>> {\n /** `{pluginId}.{key}`: what a buyer's choice names. */\n id: string\n pluginId: string\n key: string\n label: string\n description?: string\n amountCents: number\n currency: string\n quoteRef?: string\n termsUrl?: string\n}\n\n/**\n * What a sale recorded of one extra the buyer took. The seller stores this\n * on the sale and puts it in the events it raises about the sale, so the\n * provider learns what was bought from the seller's own facts.\n */\nexport interface PluginCheckoutExtraSold {\n id: string\n pluginId: string\n key: string\n label: string\n amountCents: number\n quoteRef?: string\n}\n\n/** The most extras one checkout carries. */\nexport const MAX_CHECKOUT_EXTRAS = 3\n\nconst KEY = /^[a-z][a-z0-9-]{1,39}$/\nconst PLUGIN_ID = /^[a-z][a-z0-9-]{0,63}$/\n\nconst PLUGIN_CHECKOUT_EXTRAS = definePluginServiceContract<PluginCheckoutExtraProvider>(\n 'core.checkout-extras',\n { multiple: true },\n)\n\n/** Joins the providers. Re-registering under the same plugin replaces its own. */\nexport function registerPluginCheckoutExtra(\n provider: PluginCheckoutExtraProvider,\n options?: { pluginId?: string },\n): void {\n const pluginId = getRegisteringPluginId() ?? options?.pluginId\n registerPluginService(PLUGIN_CHECKOUT_EXTRAS, provider, {\n ...(pluginId ? { pluginId } : {}),\n })\n}\n\n/** Whether any plugin offers extras: a seller with none skips the question. */\nexport function hasPluginCheckoutExtras(): boolean {\n return resolvePluginServices(PLUGIN_CHECKOUT_EXTRAS).length > 0\n}\n\nfunction cleanText(value: unknown, max: number): string {\n // Control characters become spaces: an offer's words are drawn on a page.\n return Array.from(String(value ?? ''), (char) => (char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127 ? ' ' : char))\n .join('')\n .replace(/\\s+/g, ' ')\n .trim()\n .slice(0, max)\n}\n\n/**\n * An offer held to the contract, or `null`: a positive integer amount in the\n * request's currency, a key and a label, and nothing else a page would render\n * that it should not.\n */\nexport function normalizePluginCheckoutExtra(\n pluginId: string,\n offer: PluginCheckoutExtraOffer | null | undefined,\n currency: string,\n): QuotedPluginCheckoutExtra | null {\n if (!offer || !PLUGIN_ID.test(pluginId)) return null\n const key = String(offer.key ?? '')\n const label = cleanText(offer.label, 60)\n const amountCents = offer.amountCents\n if (!KEY.test(key) || !label) return null\n if (typeof amountCents !== 'number' || !Number.isSafeInteger(amountCents) || amountCents <= 0) return null\n if (String(offer.currency ?? '').toLowerCase() !== String(currency ?? '').toLowerCase()) return null\n const description = cleanText(offer.description, 240)\n const quoteRef = cleanText(offer.quoteRef, 64)\n const termsUrl = /^https:\\/\\/[^\\s\"'<>]+$/i.test(String(offer.termsUrl ?? '')) ? String(offer.termsUrl) : ''\n return {\n id: `${pluginId}.${key}`,\n pluginId,\n key,\n label,\n ...(description ? { description } : {}),\n amountCents,\n currency: currency.toLowerCase(),\n defaultSelected: offer.defaultSelected === true,\n ...(quoteRef ? { quoteRef } : {}),\n ...(termsUrl ? { termsUrl } : {}),\n }\n}\n\n/**\n * Asks every provider at once and keeps what answered within `timeoutMs`,\n * each one held to the contract. Never throws; a provider that throws, is\n * late or answers nonsense is simply absent. At most\n * {@link MAX_CHECKOUT_EXTRAS}, in the providers' resolve order.\n */\nexport async function quotePluginCheckoutExtras(\n request: Omit<PluginCheckoutExtraRequest, 'signal'>,\n options: { timeoutMs: number },\n): Promise<QuotedPluginCheckoutExtra[]> {\n const providers = resolvePluginServices(PLUGIN_CHECKOUT_EXTRAS)\n if (!providers.length) return []\n const controller = new AbortController()\n let timer: ReturnType<typeof setTimeout> | undefined\n const deadline = new Promise<'late'>((resolve) => {\n timer = setTimeout(() => {\n controller.abort()\n resolve('late')\n }, Math.max(0, options.timeoutMs))\n })\n try {\n const answers = await Promise.all(\n providers.map(async (entry) => {\n const answer = await Promise.race([\n entry.impl.offer({ ...request, signal: controller.signal }).catch((error: unknown): null => {\n console.error(`[checkout-extras] provider \"${entry.pluginId}\" failed for ${request.hostId}`, error)\n return null\n }),\n deadline,\n ])\n return answer === 'late' ? null : normalizePluginCheckoutExtra(entry.pluginId, answer, request.currency)\n }),\n )\n const seen = new Set<string>()\n return answers\n .filter((answer): answer is QuotedPluginCheckoutExtra => Boolean(answer))\n .filter((answer) => (seen.has(answer.id) ? false : (seen.add(answer.id), true)))\n .slice(0, MAX_CHECKOUT_EXTRAS)\n } finally {\n if (timer) clearTimeout(timer)\n }\n}\n\n/**\n * The buyer's choice, read from a request body: the ids of offers they took,\n * de-duplicated and bounded. Ids only; the amounts are always asked again.\n */\nexport function readChosenCheckoutExtras(value: unknown): string[] {\n if (!Array.isArray(value)) return []\n const ids = value\n .map((entry) => String(entry ?? '').trim())\n .filter((entry) => /^[a-z][a-z0-9-]{0,63}\\.[a-z][a-z0-9-]{1,39}$/.test(entry))\n return [...new Set(ids)].slice(0, MAX_CHECKOUT_EXTRAS)\n}\n\n/** The metadata keys the extras ride under: `extra0`, `extra1`, `extra2`. */\nexport const CHECKOUT_EXTRA_METADATA_PREFIX = 'extra'\n\n/**\n * The extras a sale carries, packed for a payment processor's metadata: one\n * key per extra (`extra0` …), each `[id, cents, label, quoteRef?]` as JSON,\n * well inside a 500-character value. One key each, so a long quote id can\n * never truncate another extra out of the record.\n */\nexport function encodeCheckoutExtrasMetadata(\n extras: readonly QuotedPluginCheckoutExtra[],\n): Record<string, string> {\n const packed: Record<string, string> = {}\n extras.slice(0, MAX_CHECKOUT_EXTRAS).forEach((extra, index) => {\n const entry: Array<string | number> = [extra.id, extra.amountCents, extra.label.slice(0, 60)]\n if (extra.quoteRef) entry.push(extra.quoteRef.slice(0, 64))\n packed[`${CHECKOUT_EXTRA_METADATA_PREFIX}${index}`] = JSON.stringify(entry)\n })\n return packed\n}\n\n/**\n * Reads {@link encodeCheckoutExtrasMetadata} back off the metadata object.\n * Anything unreadable is dropped, never guessed.\n */\nexport function decodeCheckoutExtrasMetadata(\n metadata: Record<string, unknown> | null | undefined,\n): PluginCheckoutExtraSold[] {\n const sold: PluginCheckoutExtraSold[] = []\n for (let index = 0; index < MAX_CHECKOUT_EXTRAS; index += 1) {\n const raw = metadata?.[`${CHECKOUT_EXTRA_METADATA_PREFIX}${index}`]\n if (raw === undefined || raw === null || raw === '') continue\n let entry: unknown\n try {\n entry = JSON.parse(String(raw))\n } catch {\n continue\n }\n if (!Array.isArray(entry)) continue\n const [id, cents, label, quoteRef] = entry\n const match = /^([a-z][a-z0-9-]{0,63})\\.([a-z][a-z0-9-]{1,39})$/.exec(String(id ?? ''))\n if (!match || typeof cents !== 'number' || !Number.isSafeInteger(cents) || cents <= 0) continue\n if (sold.some((extra) => extra.id === id)) continue\n const ref = cleanText(quoteRef, 64)\n sold.push({\n id: String(id),\n pluginId: match[1],\n key: match[2],\n label: cleanText(label, 60) || 'Extra',\n amountCents: cents,\n ...(ref ? { quoteRef: ref } : {}),\n })\n }\n return sold\n}\n\n/** The cents a sale's extras add up to. */\nexport function checkoutExtrasCents(extras: ReadonlyArray<{ amountCents: number }> | null | undefined): number {\n return (extras ?? []).reduce(\n (sum, extra) => sum + (Number.isSafeInteger(extra?.amountCents) && extra.amountCents > 0 ? extra.amountCents : 0),\n 0,\n )\n}\n"],"names":["getRegisteringPluginId","definePluginServiceContract","registerPluginService","resolvePluginServices","MAX_CHECKOUT_EXTRAS","KEY","PLUGIN_ID","PLUGIN_CHECKOUT_EXTRAS","multiple","registerPluginCheckoutExtra","provider","options","pluginId","hasPluginCheckoutExtras","length","cleanText","value","max","Array","from","String","char","charCodeAt","join","replace","trim","slice","normalizePluginCheckoutExtra","offer","currency","test","key","label","amountCents","Number","isSafeInteger","toLowerCase","description","quoteRef","termsUrl","id","defaultSelected","quotePluginCheckoutExtras","request","providers","controller","AbortController","timer","deadline","Promise","resolve","setTimeout","abort","Math","timeoutMs","answers","all","map","entry","answer","race","impl","signal","catch","error","console","hostId","seen","Set","filter","Boolean","has","add","clearTimeout","readChosenCheckoutExtras","isArray","ids","CHECKOUT_EXTRA_METADATA_PREFIX","encodeCheckoutExtrasMetadata","extras","packed","forEach","extra","index","push","JSON","stringify","decodeCheckoutExtrasMetadata","metadata","sold","raw","undefined","parse","cents","match","exec","some","ref","checkoutExtrasCents","reduce","sum"],"mappings":"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"}
@@ -92,6 +92,13 @@ export interface PluginConsoleContributions {
92
92
  routes?: string[];
93
93
  /** Organization-level console routes it serves (`/outreach`). */
94
94
  orgRoutes?: string[];
95
+ /**
96
+ * Public console pages it serves (`/display`), at `/kiosk/{plugin id}`
97
+ * with no staff session (AGL-3608) — see `ConsolePublicPage`. The public
98
+ * route reads only the console's first-party manifest, so a marketplace
99
+ * plugin's declaration of these is accepted and serves nothing.
100
+ */
101
+ publicRoutes?: string[];
95
102
  /**
96
103
  * The shell draws something of it on every screen of a workspace: a nav
97
104
  * tab, an organization tab, a staff tab or a provider. Such a plugin loads
@@ -143,7 +143,7 @@
143
143
  if (consoleInput['shell']) next.shell = true;
144
144
  continue;
145
145
  }
146
- if (key !== 'slots' && key !== 'routes' && key !== 'orgRoutes') {
146
+ if (key !== 'slots' && key !== 'routes' && key !== 'orgRoutes' && key !== 'publicRoutes') {
147
147
  return {
148
148
  ok: false,
149
149
  error: `contributes.console.${key} is not a known contribution`
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-contributions.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 contributes, and where (AGL-3116).\n *\n * A plugin loads only where something uses it. Installing one loads nothing:\n * a published page fetches a plugin's code when the page places one of its\n * elements or the site runs one of its features, and a console screen\n * fetches it when the screen renders one of its slots or routes, or when the\n * plugin draws something the shell renders on every screen.\n *\n * The loaders cannot learn that by running a plugin, because running it is\n * the cost being avoided. So each plugin DECLARES its contributions:\n * first-party plugins in `plugins.config.json`, marketplace plugins in the\n * manifest they publish (`contributes`). The declaration is the whole of what\n * a loader reads, and it is data: this module is dependency-free so the\n * server, both apps, the verifier and the build tools share one reading.\n *\n * A declared surface only narrows where to look. Presence is the rule: a\n * plugin that declares a component loads on the pages that place it, not on\n * every page of the site that installed it.\n *\n * ## A plugin that declares nothing\n *\n * Every marketplace version published before this contract has no\n * `contributes`. Such a plugin keeps working under one default:\n *\n * - **On a published page it loads only where its element is placed**: a\n * node whose `pluginId` names the plugin (its manifest id or its listing\n * id). That is the one presence signal a page carries without running the\n * bundle, and it is the only way a page can show an element the plugin\n * registers. A site runtime of an undeclared plugin does not load, and the\n * publish pipeline refuses a bundle that registers one without declaring\n * it, so the only plugins this default governs are the ones already\n * published.\n * - **In the console it loads with the workspace shell**, as it always did,\n * because a nav tab or a provider is only discoverable by running\n * `register()`. Nothing on a published page depends on this.\n */\n\n/** What a plugin adds to a published site. */\nexport interface PluginSiteContributions {\n /**\n * Canvas component ids the plugin registers: the elements a page places. A\n * published page loads the plugin only when its node tree places one.\n */\n components?: string[]\n /**\n * Site features: runtimes the plugin mounts on every page of a site that\n * switched it on (an announcement bar, an experiment runner), named by the\n * `runtimeId` it registers. A plugin with a feature loads on every page of\n * such a site, which is what a feature is.\n */\n features?: string[]\n /**\n * The elements that run a site function in the visitor's browser (AGL-3393),\n * each keyed by its component id and naming the prop that holds the\n * function's name. Compose hands such a node the function's definition and\n * the site variables it reads; no other node is given either.\n *\n * Declared DATA rather than read off a registered schema: compose runs where\n * no plugin code loads — the tenant's server components — so the only thing\n * it can consult is a declaration. A first-party plugin declares it in\n * `plugins.config.json`, a marketplace plugin in its manifest, and both reach\n * compose as the same map. Every key must also be one of `components`.\n */\n functionBindings?: Record<string, string>\n}\n\n/** What a plugin adds to the console. */\nexport interface PluginConsoleContributions {\n /**\n * The widget slots its widgets fill: the `CONSOLE_WIDGET_SLOTS` zones, the\n * panels the shell draws as slots (the console dock `consoleDock`, the\n * besigner's `besignerInspector`), and zones another plugin hosts. A screen\n * that renders one of them loads the plugin.\n */\n slots?: string[]\n /** Site-level console routes it serves (`/forms`), beneath `/hosts/[host]`. */\n routes?: string[]\n /** Organization-level console routes it serves (`/outreach`). */\n orgRoutes?: string[]\n /**\n * The shell draws something of it on every screen of a workspace: a nav\n * tab, an organization tab, a staff tab or a provider. Such a plugin loads\n * with the shell, because every screen renders it.\n */\n shell?: boolean\n}\n\nexport interface PluginContributions {\n site?: PluginSiteContributions\n console?: PluginConsoleContributions\n}\n\n/** Bound on each declared list, so a manifest cannot hand a loader a novel. */\nexport const PLUGIN_MAX_CONTRIBUTIONS = 128\n\n/**\n * A component, feature or slot id: an identifier with the separators ids in\n * the platform use (`muiTypography`, `marketplacePlugin`, `ai.assist`).\n */\nconst CONTRIBUTION_ID = /^[A-Za-z_][A-Za-z0-9_.:-]{0,79}$/\n\n/**\n * A console route: one or more path segments, as a nav item's `href` names\n * them. Never a URL, never `..`.\n */\nconst CONTRIBUTION_ROUTE = /^(\\/[A-Za-z0-9_-]+){1,8}$/\n\n/** A prop name a function binding may name: a plain identifier. */\nconst BINDING_PROP = /^[A-Za-z_$][A-Za-z0-9_$]{0,63}$/\n\ntype Sanitized =\n | { ok: true; contributions: PluginContributions }\n | { ok: false; error: string }\n\n/**\n * Validates a declared `contributes` block.\n *\n * Malformed input is REFUSED rather than trimmed: a loader reads this to\n * decide where a plugin runs, so a declaration that silently lost an entry\n * would stop the plugin loading where it is used, and nobody would see why.\n * Duplicates are collapsed and empty lists dropped, because neither changes\n * what the declaration means.\n *\n * `{}` is valid and means \"contributes nothing anywhere\", which is a\n * different statement from an absent block (see the module note).\n */\nexport function sanitizePluginContributions(input: unknown): Sanitized {\n if (!isPlainObject(input)) {\n return { ok: false, error: 'contributes must be an object' }\n }\n const out: PluginContributions = {}\n for (const key of Object.keys(input)) {\n if (key !== 'site' && key !== 'console') {\n return { ok: false, error: `contributes.${key} is not a known surface` }\n }\n }\n\n if (input['site'] !== undefined) {\n const site = input['site']\n if (!isPlainObject(site)) {\n return { ok: false, error: 'contributes.site must be an object' }\n }\n const next: PluginSiteContributions = {}\n for (const key of Object.keys(site)) {\n if (key === 'functionBindings') continue\n if (key !== 'components' && key !== 'features') {\n return { ok: false, error: `contributes.site.${key} is not a known contribution` }\n }\n const list = idList(site[key], `contributes.site.${key}`, CONTRIBUTION_ID)\n if (list.ok === false) return { ok: false, error: list.error }\n if (list.values.length) next[key] = list.values\n }\n if (site['functionBindings'] !== undefined) {\n const bindings = functionBindingMap(\n site['functionBindings'],\n next.components ?? [],\n )\n if (bindings.ok === false) return { ok: false, error: bindings.error }\n if (Object.keys(bindings.value).length) {\n next.functionBindings = bindings.value\n }\n }\n if (Object.keys(next).length) out.site = next\n }\n\n if (input['console'] !== undefined) {\n const consoleInput = input['console']\n if (!isPlainObject(consoleInput)) {\n return { ok: false, error: 'contributes.console must be an object' }\n }\n const next: PluginConsoleContributions = {}\n for (const key of Object.keys(consoleInput)) {\n if (key === 'shell') {\n if (typeof consoleInput['shell'] !== 'boolean') {\n return { ok: false, error: 'contributes.console.shell must be true or false' }\n }\n if (consoleInput['shell']) next.shell = true\n continue\n }\n if (key !== 'slots' && key !== 'routes' && key !== 'orgRoutes') {\n return {\n ok: false,\n error: `contributes.console.${key} is not a known contribution`,\n }\n }\n const list = idList(\n consoleInput[key],\n `contributes.console.${key}`,\n key === 'slots' ? CONTRIBUTION_ID : CONTRIBUTION_ROUTE,\n )\n if (list.ok === false) return { ok: false, error: list.error }\n if (list.values.length) next[key] = list.values\n }\n if (Object.keys(next).length) out.console = next\n }\n\n return { ok: true, contributions: out }\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return Boolean(value) && typeof value === 'object' && !Array.isArray(value)\n}\n\nfunction idList(\n value: unknown,\n where: string,\n pattern: RegExp,\n): { ok: true; values: string[] } | { ok: false; error: string } {\n if (!Array.isArray(value)) return { ok: false, error: `${where} must be a list` }\n if (value.length > PLUGIN_MAX_CONTRIBUTIONS) {\n return { ok: false, error: `${where} lists more than ${PLUGIN_MAX_CONTRIBUTIONS}` }\n }\n const values: string[] = []\n for (const entry of value) {\n if (typeof entry !== 'string' || !pattern.test(entry)) {\n return { ok: false, error: `${where} has an invalid entry \"${String(entry)}\"` }\n }\n if (!values.includes(entry)) values.push(entry)\n }\n return { ok: true, values }\n}\n\n/**\n * `contributes.site.functionBindings`, validated against the components the\n * same block declares: a binding on an element the plugin does not register\n * would hand a function's definition, and the variable values it reads, to\n * whichever element happens to own that id.\n */\nfunction functionBindingMap(\n value: unknown,\n components: readonly string[],\n): { ok: true; value: Record<string, string> } | { ok: false; error: string } {\n const where = 'contributes.site.functionBindings'\n if (!isPlainObject(value)) {\n return { ok: false, error: `${where} must be an object` }\n }\n const entries = Object.entries(value)\n if (entries.length > PLUGIN_MAX_CONTRIBUTIONS) {\n return { ok: false, error: `${where} lists more than ${PLUGIN_MAX_CONTRIBUTIONS}` }\n }\n const out: Record<string, string> = {}\n for (const [componentId, prop] of entries) {\n if (!components.includes(componentId)) {\n return {\n ok: false,\n error: `${where} names \"${componentId}\", which contributes.site.components does not declare`,\n }\n }\n if (typeof prop !== 'string' || !BINDING_PROP.test(prop)) {\n return {\n ok: false,\n error: `${where}.${componentId} must name a prop, not \"${String(prop)}\"`,\n }\n }\n out[componentId] = prop\n }\n return { ok: true, value: out }\n}\n\n/**\n * A declared `contributes` block as a loader may trust it: the sanitized\n * block, or `undefined` for one that is absent or does not validate.\n *\n * Loaders read this from stored documents (a pinned version's manifest), and\n * a malformed stored block is treated as UNDECLARED — the default applies —\n * never as \"contributes nothing\", which would stop a plugin that is in use.\n */\nexport function readPluginContributions(\n input: unknown,\n): PluginContributions | undefined {\n if (input === undefined || input === null) return undefined\n const verdict = sanitizePluginContributions(input)\n return verdict.ok ? verdict.contributions : undefined\n}\n\n/**\n * What the surface a plugin is being registered into actually uses\n * (AGL-3141).\n *\n * The loaders carry this from the surface that computed it to each plugin's\n * register function, unchanged. A plugin that can register a part of itself\n * reads it; one that cannot ignores it, and the platform's behavior is the\n * same either way.\n *\n * An absent field means \"everything of that kind\". A surface that cannot say\n * what it uses must be handed all of it, because an element the surface\n * places and the set omits has no registered component and renders NOTHING,\n * silently (the blank-canvas invariant, AGL-52). Narrowing is an optimization\n * and must fail toward the whole.\n */\nexport interface PluginUse {\n /**\n * Canvas component ids the surface places, read from the FULL composed\n * document the way `pagePresence` reads it — withheld lazy-panel subtrees\n * included, because an element inside a panel the visitor opens later is\n * still this page's element.\n */\n componentIds?: readonly string[]\n}\n\n/** A node as presence reads it: the two ids a placed element carries. */\nexport interface PresenceNode {\n componentId?: string\n pluginId?: string\n}\n\n/** What a published page places: its component ids and the plugin ids stamped on its nodes. */\nexport interface PagePresence {\n componentIds: ReadonlySet<string>\n pluginIds: ReadonlySet<string>\n}\n\n/**\n * The component ids and plugin ids a node tree places.\n *\n * Read from the FULL composed document — layouts, grafted reusable\n * components and withheld lazy panels included — because an element inside\n * a panel the visitor opens later is still this page's element.\n */\nexport function pagePresence(\n nodes: Record<string, PresenceNode | null | undefined> | null | undefined,\n): PagePresence {\n const componentIds = new Set<string>()\n const pluginIds = new Set<string>()\n for (const node of Object.values(nodes ?? {})) {\n if (typeof node?.componentId === 'string' && node.componentId) {\n componentIds.add(node.componentId)\n }\n if (typeof node?.pluginId === 'string' && node.pluginId) {\n pluginIds.add(node.pluginId)\n }\n }\n return { componentIds, pluginIds }\n}\n\n/** A plugin as the presence rules see it. */\nexport interface PresenceSubject {\n /** The plugin's own id (a manifest `id`, or a first-party catalog id). */\n pluginId?: string\n /** A marketplace plugin's listing id, which its elements may carry instead. */\n listingId?: string\n /**\n * A marketplace plugin's identity, `<publisher handle>.<manifest id>`\n * (AGL-3390): the `pluginId` its namespaced elements carry.\n */\n identity?: string\n /** Its declaration; `undefined` when it declares nothing. */\n contributes?: PluginContributions\n}\n\n/**\n * Whether a published page uses a plugin, so the page must load it.\n *\n * - An element of it is placed: a node stamped with the plugin's id, or,\n * for a declared plugin, a node whose component id it declares.\n * - It declares a site feature. The caller only asks about plugins the site\n * has switched on, and a feature runs on every page of such a site.\n *\n * A plugin that declares nothing is present only through its stamped\n * elements — the default in the module note.\n */\nexport function isPluginUsedOnPage(\n subject: PresenceSubject,\n page: PagePresence,\n): boolean {\n if (subject.pluginId && page.pluginIds.has(subject.pluginId)) return true\n if (subject.listingId && page.pluginIds.has(subject.listingId)) return true\n if (subject.identity && page.pluginIds.has(subject.identity)) return true\n const site = subject.contributes?.site\n if (!site) return false\n if (site.features?.length) return true\n return (site.components ?? []).some((id) => page.componentIds.has(id))\n}\n\n/**\n * Whether a plugin declares anything a site draws: an element an author can\n * place, or a feature that runs on the site's pages. The editor loads exactly\n * these (AGL-3391). A plugin that declares nothing is not among them: its\n * site half is only discoverable by running `register()`, and the console\n * shell already does that for it.\n */\nexport function declaresSiteElements(\n contributes: PluginContributions | undefined,\n): boolean {\n const site = contributes?.site\n return Boolean(site?.components?.length || site?.features?.length)\n}\n\n/**\n * Every id a plugin's elements and presets may carry as their `pluginId`: its\n * own id and, for a marketplace plugin, its listing id and its identity. A\n * surface that filters registry entries by plugin set needs all of them, or it\n * hides the elements of a plugin it just loaded.\n */\nexport function presenceIds(subject: PresenceSubject): string[] {\n const ids: string[] = []\n for (const id of [subject.identity, subject.pluginId, subject.listingId]) {\n if (id && !ids.includes(id)) ids.push(id)\n }\n return ids\n}\n\n/**\n * Function bindings (AGL-3393): component id → the prop naming the site\n * function that element runs.\n */\nexport type FunctionBindings = Readonly<Record<string, string>>\n\n/**\n * The function bindings a set of plugins declares, merged into one map.\n *\n * Where two plugins bind the same component id the FIRST one listed wins, so a\n * caller that passes first-party declarations ahead of marketplace ones keeps\n * a marketplace manifest from rebinding a first-party element. Marketplace ids\n * are namespaced by their publisher (see {@link isNamespacedComponentId}), so\n * a collision is a mistake, never a feature.\n */\nexport function mergeFunctionBindings(\n ...sources: ReadonlyArray<\n FunctionBindings | { contributes?: PluginContributions } | null | undefined\n >\n): FunctionBindings {\n const merged: Record<string, string> = {}\n for (const source of sources) {\n if (!source) continue\n const declared: FunctionBindings | undefined =\n 'contributes' in source\n ? (source.contributes as PluginContributions | undefined)?.site\n ?.functionBindings\n : (source as FunctionBindings)\n for (const [componentId, prop] of Object.entries(declared ?? {})) {\n if (typeof prop !== 'string') continue\n if (!Object.prototype.hasOwnProperty.call(merged, componentId)) {\n merged[componentId] = prop\n }\n }\n }\n return merged\n}\n\n/**\n * Whether a component id belongs to a marketplace plugin's namespace\n * (AGL-3387): `<publisher handle>.<plugin id>.<role>`. A `.` is reserved for\n * those ids — no first-party component carries one, which the manifest\n * generator enforces — so compose can tell, without loading anything, that a\n * tree places an element only an installed plugin can declare.\n */\nexport function isNamespacedComponentId(componentId: unknown): boolean {\n return typeof componentId === 'string' && componentId.includes('.')\n}\n\n/**\n * Where a plugin's console code loads, resolved with the default applied: a\n * plugin that declares nothing loads with the shell, as it always did.\n */\nexport interface ConsoleLoadPoints {\n shell: boolean\n slots: readonly string[]\n routes: readonly string[]\n orgRoutes: readonly string[]\n}\n\nexport function consoleLoadPoints(\n contributes: PluginContributions | undefined,\n): ConsoleLoadPoints {\n if (!contributes) return { shell: true, slots: [], routes: [], orgRoutes: [] }\n const declared = contributes.console ?? {}\n return {\n shell: Boolean(declared.shell),\n slots: declared.slots ?? [],\n routes: declared.routes ?? [],\n orgRoutes: declared.orgRoutes ?? [],\n }\n}\n\n/**\n * Whether a declared route serves `href`: the route itself, or a path beneath\n * it on a segment boundary, the way the shell's page resolver matches a nav\n * item that owns its subtree. `/products` serves `/products/orders` and never\n * `/products-archive`.\n */\nexport function routeServes(route: string, href: string): boolean {\n return href === route || href.startsWith(`${route}/`)\n}\n\n/**\n * A place in the console that loads plugins, as the surface drawing it names\n * itself (AGL-3142).\n *\n * - `shell`: the workspace chrome every screen has. It draws whatever a\n * plugin adds to every screen — a nav tab, an organization tab, a staff\n * tab, a provider — so a plugin that declares any of those loads here, and\n * so does one that declares nothing at all.\n * - `slots`: the zones a screen renders. The zones it names, not the plugins\n * it expects in them: a screen must never hold a plugin id.\n * - `route`: the path the reader has open, plugin-relative (`/products`,\n * `/products/orders`), on the level its route tree serves — `site` beneath\n * `/hosts/[host]`, `org` beneath the organization.\n * - `editor`: the Besigner, which draws a site's elements and offers them in\n * its Elements panel (AGL-3391). A plugin loads here when it declares site\n * elements or site features, whether or not the page open uses them, since\n * the author is choosing what to place.\n */\nexport type ConsoleLoadWhere =\n | { at: 'shell' }\n | { at: 'slots'; slots: readonly string[] }\n | { at: 'route'; href: string; level: 'site' | 'org' }\n | { at: 'editor' }\n\n/**\n * Whether a console surface uses a plugin, so it must load it.\n *\n * The console twin of {@link isPluginUsedOnPage}, and the same rule: a\n * declaration says where a plugin's code belongs, and the surface that draws\n * that place is the one that fetches it. A screen asks about the zones it\n * renders and the path it serves; it never asks about a plugin by name.\n *\n * A plugin that declares nothing loads with the shell — the default in the\n * module note, and the reason the shell branch reads\n * {@link consoleLoadPoints} rather than the raw block. Its nav tab or its\n * provider is only discoverable by running `register()`, so narrowing it\n * anywhere else would take a tab away with no way to notice.\n */\nexport function isPluginUsedInConsole(\n contributes: PluginContributions | undefined,\n where: ConsoleLoadWhere,\n): boolean {\n if (where.at === 'editor') return declaresSiteElements(contributes)\n const points = consoleLoadPoints(contributes)\n if (where.at === 'shell') return points.shell\n if (where.at === 'slots') {\n return points.slots.some((slot) => where.slots.includes(slot))\n }\n const routes = where.level === 'org' ? points.orgRoutes : points.routes\n return routes.some((route) => routeServes(route, where.href))\n}\n"],"names":["PLUGIN_MAX_CONTRIBUTIONS","CONTRIBUTION_ID","CONTRIBUTION_ROUTE","BINDING_PROP","sanitizePluginContributions","input","isPlainObject","ok","error","out","key","Object","keys","undefined","site","next","list","idList","values","length","bindings","functionBindingMap","components","value","functionBindings","consoleInput","shell","console","contributions","Boolean","Array","isArray","where","pattern","entry","test","String","includes","push","entries","componentId","prop","readPluginContributions","verdict","pagePresence","nodes","componentIds","Set","pluginIds","node","add","pluginId","isPluginUsedOnPage","subject","page","has","listingId","identity","contributes","features","some","id","declaresSiteElements","presenceIds","ids","mergeFunctionBindings","sources","merged","source","declared","prototype","hasOwnProperty","call","isNamespacedComponentId","consoleLoadPoints","slots","routes","orgRoutes","routeServes","route","href","startsWith","isPluginUsedInConsole","at","points","slot","level"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-contributions.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 contributes, and where (AGL-3116).\n *\n * A plugin loads only where something uses it. Installing one loads nothing:\n * a published page fetches a plugin's code when the page places one of its\n * elements or the site runs one of its features, and a console screen\n * fetches it when the screen renders one of its slots or routes, or when the\n * plugin draws something the shell renders on every screen.\n *\n * The loaders cannot learn that by running a plugin, because running it is\n * the cost being avoided. So each plugin DECLARES its contributions:\n * first-party plugins in `plugins.config.json`, marketplace plugins in the\n * manifest they publish (`contributes`). The declaration is the whole of what\n * a loader reads, and it is data: this module is dependency-free so the\n * server, both apps, the verifier and the build tools share one reading.\n *\n * A declared surface only narrows where to look. Presence is the rule: a\n * plugin that declares a component loads on the pages that place it, not on\n * every page of the site that installed it.\n *\n * ## A plugin that declares nothing\n *\n * Every marketplace version published before this contract has no\n * `contributes`. Such a plugin keeps working under one default:\n *\n * - **On a published page it loads only where its element is placed**: a\n * node whose `pluginId` names the plugin (its manifest id or its listing\n * id). That is the one presence signal a page carries without running the\n * bundle, and it is the only way a page can show an element the plugin\n * registers. A site runtime of an undeclared plugin does not load, and the\n * publish pipeline refuses a bundle that registers one without declaring\n * it, so the only plugins this default governs are the ones already\n * published.\n * - **In the console it loads with the workspace shell**, as it always did,\n * because a nav tab or a provider is only discoverable by running\n * `register()`. Nothing on a published page depends on this.\n */\n\n/** What a plugin adds to a published site. */\nexport interface PluginSiteContributions {\n /**\n * Canvas component ids the plugin registers: the elements a page places. A\n * published page loads the plugin only when its node tree places one.\n */\n components?: string[]\n /**\n * Site features: runtimes the plugin mounts on every page of a site that\n * switched it on (an announcement bar, an experiment runner), named by the\n * `runtimeId` it registers. A plugin with a feature loads on every page of\n * such a site, which is what a feature is.\n */\n features?: string[]\n /**\n * The elements that run a site function in the visitor's browser (AGL-3393),\n * each keyed by its component id and naming the prop that holds the\n * function's name. Compose hands such a node the function's definition and\n * the site variables it reads; no other node is given either.\n *\n * Declared DATA rather than read off a registered schema: compose runs where\n * no plugin code loads — the tenant's server components — so the only thing\n * it can consult is a declaration. A first-party plugin declares it in\n * `plugins.config.json`, a marketplace plugin in its manifest, and both reach\n * compose as the same map. Every key must also be one of `components`.\n */\n functionBindings?: Record<string, string>\n}\n\n/** What a plugin adds to the console. */\nexport interface PluginConsoleContributions {\n /**\n * The widget slots its widgets fill: the `CONSOLE_WIDGET_SLOTS` zones, the\n * panels the shell draws as slots (the console dock `consoleDock`, the\n * besigner's `besignerInspector`), and zones another plugin hosts. A screen\n * that renders one of them loads the plugin.\n */\n slots?: string[]\n /** Site-level console routes it serves (`/forms`), beneath `/hosts/[host]`. */\n routes?: string[]\n /** Organization-level console routes it serves (`/outreach`). */\n orgRoutes?: string[]\n /**\n * Public console pages it serves (`/display`), at `/kiosk/{plugin id}`\n * with no staff session (AGL-3608) — see `ConsolePublicPage`. The public\n * route reads only the console's first-party manifest, so a marketplace\n * plugin's declaration of these is accepted and serves nothing.\n */\n publicRoutes?: string[]\n /**\n * The shell draws something of it on every screen of a workspace: a nav\n * tab, an organization tab, a staff tab or a provider. Such a plugin loads\n * with the shell, because every screen renders it.\n */\n shell?: boolean\n}\n\nexport interface PluginContributions {\n site?: PluginSiteContributions\n console?: PluginConsoleContributions\n}\n\n/** Bound on each declared list, so a manifest cannot hand a loader a novel. */\nexport const PLUGIN_MAX_CONTRIBUTIONS = 128\n\n/**\n * A component, feature or slot id: an identifier with the separators ids in\n * the platform use (`muiTypography`, `marketplacePlugin`, `ai.assist`).\n */\nconst CONTRIBUTION_ID = /^[A-Za-z_][A-Za-z0-9_.:-]{0,79}$/\n\n/**\n * A console route: one or more path segments, as a nav item's `href` names\n * them. Never a URL, never `..`.\n */\nconst CONTRIBUTION_ROUTE = /^(\\/[A-Za-z0-9_-]+){1,8}$/\n\n/** A prop name a function binding may name: a plain identifier. */\nconst BINDING_PROP = /^[A-Za-z_$][A-Za-z0-9_$]{0,63}$/\n\ntype Sanitized =\n | { ok: true; contributions: PluginContributions }\n | { ok: false; error: string }\n\n/**\n * Validates a declared `contributes` block.\n *\n * Malformed input is REFUSED rather than trimmed: a loader reads this to\n * decide where a plugin runs, so a declaration that silently lost an entry\n * would stop the plugin loading where it is used, and nobody would see why.\n * Duplicates are collapsed and empty lists dropped, because neither changes\n * what the declaration means.\n *\n * `{}` is valid and means \"contributes nothing anywhere\", which is a\n * different statement from an absent block (see the module note).\n */\nexport function sanitizePluginContributions(input: unknown): Sanitized {\n if (!isPlainObject(input)) {\n return { ok: false, error: 'contributes must be an object' }\n }\n const out: PluginContributions = {}\n for (const key of Object.keys(input)) {\n if (key !== 'site' && key !== 'console') {\n return { ok: false, error: `contributes.${key} is not a known surface` }\n }\n }\n\n if (input['site'] !== undefined) {\n const site = input['site']\n if (!isPlainObject(site)) {\n return { ok: false, error: 'contributes.site must be an object' }\n }\n const next: PluginSiteContributions = {}\n for (const key of Object.keys(site)) {\n if (key === 'functionBindings') continue\n if (key !== 'components' && key !== 'features') {\n return { ok: false, error: `contributes.site.${key} is not a known contribution` }\n }\n const list = idList(site[key], `contributes.site.${key}`, CONTRIBUTION_ID)\n if (list.ok === false) return { ok: false, error: list.error }\n if (list.values.length) next[key] = list.values\n }\n if (site['functionBindings'] !== undefined) {\n const bindings = functionBindingMap(\n site['functionBindings'],\n next.components ?? [],\n )\n if (bindings.ok === false) return { ok: false, error: bindings.error }\n if (Object.keys(bindings.value).length) {\n next.functionBindings = bindings.value\n }\n }\n if (Object.keys(next).length) out.site = next\n }\n\n if (input['console'] !== undefined) {\n const consoleInput = input['console']\n if (!isPlainObject(consoleInput)) {\n return { ok: false, error: 'contributes.console must be an object' }\n }\n const next: PluginConsoleContributions = {}\n for (const key of Object.keys(consoleInput)) {\n if (key === 'shell') {\n if (typeof consoleInput['shell'] !== 'boolean') {\n return { ok: false, error: 'contributes.console.shell must be true or false' }\n }\n if (consoleInput['shell']) next.shell = true\n continue\n }\n if (\n key !== 'slots' &&\n key !== 'routes' &&\n key !== 'orgRoutes' &&\n key !== 'publicRoutes'\n ) {\n return {\n ok: false,\n error: `contributes.console.${key} is not a known contribution`,\n }\n }\n const list = idList(\n consoleInput[key],\n `contributes.console.${key}`,\n key === 'slots' ? CONTRIBUTION_ID : CONTRIBUTION_ROUTE,\n )\n if (list.ok === false) return { ok: false, error: list.error }\n if (list.values.length) next[key] = list.values\n }\n if (Object.keys(next).length) out.console = next\n }\n\n return { ok: true, contributions: out }\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return Boolean(value) && typeof value === 'object' && !Array.isArray(value)\n}\n\nfunction idList(\n value: unknown,\n where: string,\n pattern: RegExp,\n): { ok: true; values: string[] } | { ok: false; error: string } {\n if (!Array.isArray(value)) return { ok: false, error: `${where} must be a list` }\n if (value.length > PLUGIN_MAX_CONTRIBUTIONS) {\n return { ok: false, error: `${where} lists more than ${PLUGIN_MAX_CONTRIBUTIONS}` }\n }\n const values: string[] = []\n for (const entry of value) {\n if (typeof entry !== 'string' || !pattern.test(entry)) {\n return { ok: false, error: `${where} has an invalid entry \"${String(entry)}\"` }\n }\n if (!values.includes(entry)) values.push(entry)\n }\n return { ok: true, values }\n}\n\n/**\n * `contributes.site.functionBindings`, validated against the components the\n * same block declares: a binding on an element the plugin does not register\n * would hand a function's definition, and the variable values it reads, to\n * whichever element happens to own that id.\n */\nfunction functionBindingMap(\n value: unknown,\n components: readonly string[],\n): { ok: true; value: Record<string, string> } | { ok: false; error: string } {\n const where = 'contributes.site.functionBindings'\n if (!isPlainObject(value)) {\n return { ok: false, error: `${where} must be an object` }\n }\n const entries = Object.entries(value)\n if (entries.length > PLUGIN_MAX_CONTRIBUTIONS) {\n return { ok: false, error: `${where} lists more than ${PLUGIN_MAX_CONTRIBUTIONS}` }\n }\n const out: Record<string, string> = {}\n for (const [componentId, prop] of entries) {\n if (!components.includes(componentId)) {\n return {\n ok: false,\n error: `${where} names \"${componentId}\", which contributes.site.components does not declare`,\n }\n }\n if (typeof prop !== 'string' || !BINDING_PROP.test(prop)) {\n return {\n ok: false,\n error: `${where}.${componentId} must name a prop, not \"${String(prop)}\"`,\n }\n }\n out[componentId] = prop\n }\n return { ok: true, value: out }\n}\n\n/**\n * A declared `contributes` block as a loader may trust it: the sanitized\n * block, or `undefined` for one that is absent or does not validate.\n *\n * Loaders read this from stored documents (a pinned version's manifest), and\n * a malformed stored block is treated as UNDECLARED — the default applies —\n * never as \"contributes nothing\", which would stop a plugin that is in use.\n */\nexport function readPluginContributions(\n input: unknown,\n): PluginContributions | undefined {\n if (input === undefined || input === null) return undefined\n const verdict = sanitizePluginContributions(input)\n return verdict.ok ? verdict.contributions : undefined\n}\n\n/**\n * What the surface a plugin is being registered into actually uses\n * (AGL-3141).\n *\n * The loaders carry this from the surface that computed it to each plugin's\n * register function, unchanged. A plugin that can register a part of itself\n * reads it; one that cannot ignores it, and the platform's behavior is the\n * same either way.\n *\n * An absent field means \"everything of that kind\". A surface that cannot say\n * what it uses must be handed all of it, because an element the surface\n * places and the set omits has no registered component and renders NOTHING,\n * silently (the blank-canvas invariant, AGL-52). Narrowing is an optimization\n * and must fail toward the whole.\n */\nexport interface PluginUse {\n /**\n * Canvas component ids the surface places, read from the FULL composed\n * document the way `pagePresence` reads it — withheld lazy-panel subtrees\n * included, because an element inside a panel the visitor opens later is\n * still this page's element.\n */\n componentIds?: readonly string[]\n}\n\n/** A node as presence reads it: the two ids a placed element carries. */\nexport interface PresenceNode {\n componentId?: string\n pluginId?: string\n}\n\n/** What a published page places: its component ids and the plugin ids stamped on its nodes. */\nexport interface PagePresence {\n componentIds: ReadonlySet<string>\n pluginIds: ReadonlySet<string>\n}\n\n/**\n * The component ids and plugin ids a node tree places.\n *\n * Read from the FULL composed document — layouts, grafted reusable\n * components and withheld lazy panels included — because an element inside\n * a panel the visitor opens later is still this page's element.\n */\nexport function pagePresence(\n nodes: Record<string, PresenceNode | null | undefined> | null | undefined,\n): PagePresence {\n const componentIds = new Set<string>()\n const pluginIds = new Set<string>()\n for (const node of Object.values(nodes ?? {})) {\n if (typeof node?.componentId === 'string' && node.componentId) {\n componentIds.add(node.componentId)\n }\n if (typeof node?.pluginId === 'string' && node.pluginId) {\n pluginIds.add(node.pluginId)\n }\n }\n return { componentIds, pluginIds }\n}\n\n/** A plugin as the presence rules see it. */\nexport interface PresenceSubject {\n /** The plugin's own id (a manifest `id`, or a first-party catalog id). */\n pluginId?: string\n /** A marketplace plugin's listing id, which its elements may carry instead. */\n listingId?: string\n /**\n * A marketplace plugin's identity, `<publisher handle>.<manifest id>`\n * (AGL-3390): the `pluginId` its namespaced elements carry.\n */\n identity?: string\n /** Its declaration; `undefined` when it declares nothing. */\n contributes?: PluginContributions\n}\n\n/**\n * Whether a published page uses a plugin, so the page must load it.\n *\n * - An element of it is placed: a node stamped with the plugin's id, or,\n * for a declared plugin, a node whose component id it declares.\n * - It declares a site feature. The caller only asks about plugins the site\n * has switched on, and a feature runs on every page of such a site.\n *\n * A plugin that declares nothing is present only through its stamped\n * elements — the default in the module note.\n */\nexport function isPluginUsedOnPage(\n subject: PresenceSubject,\n page: PagePresence,\n): boolean {\n if (subject.pluginId && page.pluginIds.has(subject.pluginId)) return true\n if (subject.listingId && page.pluginIds.has(subject.listingId)) return true\n if (subject.identity && page.pluginIds.has(subject.identity)) return true\n const site = subject.contributes?.site\n if (!site) return false\n if (site.features?.length) return true\n return (site.components ?? []).some((id) => page.componentIds.has(id))\n}\n\n/**\n * Whether a plugin declares anything a site draws: an element an author can\n * place, or a feature that runs on the site's pages. The editor loads exactly\n * these (AGL-3391). A plugin that declares nothing is not among them: its\n * site half is only discoverable by running `register()`, and the console\n * shell already does that for it.\n */\nexport function declaresSiteElements(\n contributes: PluginContributions | undefined,\n): boolean {\n const site = contributes?.site\n return Boolean(site?.components?.length || site?.features?.length)\n}\n\n/**\n * Every id a plugin's elements and presets may carry as their `pluginId`: its\n * own id and, for a marketplace plugin, its listing id and its identity. A\n * surface that filters registry entries by plugin set needs all of them, or it\n * hides the elements of a plugin it just loaded.\n */\nexport function presenceIds(subject: PresenceSubject): string[] {\n const ids: string[] = []\n for (const id of [subject.identity, subject.pluginId, subject.listingId]) {\n if (id && !ids.includes(id)) ids.push(id)\n }\n return ids\n}\n\n/**\n * Function bindings (AGL-3393): component id → the prop naming the site\n * function that element runs.\n */\nexport type FunctionBindings = Readonly<Record<string, string>>\n\n/**\n * The function bindings a set of plugins declares, merged into one map.\n *\n * Where two plugins bind the same component id the FIRST one listed wins, so a\n * caller that passes first-party declarations ahead of marketplace ones keeps\n * a marketplace manifest from rebinding a first-party element. Marketplace ids\n * are namespaced by their publisher (see {@link isNamespacedComponentId}), so\n * a collision is a mistake, never a feature.\n */\nexport function mergeFunctionBindings(\n ...sources: ReadonlyArray<\n FunctionBindings | { contributes?: PluginContributions } | null | undefined\n >\n): FunctionBindings {\n const merged: Record<string, string> = {}\n for (const source of sources) {\n if (!source) continue\n const declared: FunctionBindings | undefined =\n 'contributes' in source\n ? (source.contributes as PluginContributions | undefined)?.site\n ?.functionBindings\n : (source as FunctionBindings)\n for (const [componentId, prop] of Object.entries(declared ?? {})) {\n if (typeof prop !== 'string') continue\n if (!Object.prototype.hasOwnProperty.call(merged, componentId)) {\n merged[componentId] = prop\n }\n }\n }\n return merged\n}\n\n/**\n * Whether a component id belongs to a marketplace plugin's namespace\n * (AGL-3387): `<publisher handle>.<plugin id>.<role>`. A `.` is reserved for\n * those ids — no first-party component carries one, which the manifest\n * generator enforces — so compose can tell, without loading anything, that a\n * tree places an element only an installed plugin can declare.\n */\nexport function isNamespacedComponentId(componentId: unknown): boolean {\n return typeof componentId === 'string' && componentId.includes('.')\n}\n\n/**\n * Where a plugin's console code loads, resolved with the default applied: a\n * plugin that declares nothing loads with the shell, as it always did.\n */\nexport interface ConsoleLoadPoints {\n shell: boolean\n slots: readonly string[]\n routes: readonly string[]\n orgRoutes: readonly string[]\n}\n\nexport function consoleLoadPoints(\n contributes: PluginContributions | undefined,\n): ConsoleLoadPoints {\n if (!contributes) return { shell: true, slots: [], routes: [], orgRoutes: [] }\n const declared = contributes.console ?? {}\n return {\n shell: Boolean(declared.shell),\n slots: declared.slots ?? [],\n routes: declared.routes ?? [],\n orgRoutes: declared.orgRoutes ?? [],\n }\n}\n\n/**\n * Whether a declared route serves `href`: the route itself, or a path beneath\n * it on a segment boundary, the way the shell's page resolver matches a nav\n * item that owns its subtree. `/products` serves `/products/orders` and never\n * `/products-archive`.\n */\nexport function routeServes(route: string, href: string): boolean {\n return href === route || href.startsWith(`${route}/`)\n}\n\n/**\n * A place in the console that loads plugins, as the surface drawing it names\n * itself (AGL-3142).\n *\n * - `shell`: the workspace chrome every screen has. It draws whatever a\n * plugin adds to every screen — a nav tab, an organization tab, a staff\n * tab, a provider — so a plugin that declares any of those loads here, and\n * so does one that declares nothing at all.\n * - `slots`: the zones a screen renders. The zones it names, not the plugins\n * it expects in them: a screen must never hold a plugin id.\n * - `route`: the path the reader has open, plugin-relative (`/products`,\n * `/products/orders`), on the level its route tree serves — `site` beneath\n * `/hosts/[host]`, `org` beneath the organization.\n * - `editor`: the Besigner, which draws a site's elements and offers them in\n * its Elements panel (AGL-3391). A plugin loads here when it declares site\n * elements or site features, whether or not the page open uses them, since\n * the author is choosing what to place.\n */\nexport type ConsoleLoadWhere =\n | { at: 'shell' }\n | { at: 'slots'; slots: readonly string[] }\n | { at: 'route'; href: string; level: 'site' | 'org' }\n | { at: 'editor' }\n\n/**\n * Whether a console surface uses a plugin, so it must load it.\n *\n * The console twin of {@link isPluginUsedOnPage}, and the same rule: a\n * declaration says where a plugin's code belongs, and the surface that draws\n * that place is the one that fetches it. A screen asks about the zones it\n * renders and the path it serves; it never asks about a plugin by name.\n *\n * A plugin that declares nothing loads with the shell — the default in the\n * module note, and the reason the shell branch reads\n * {@link consoleLoadPoints} rather than the raw block. Its nav tab or its\n * provider is only discoverable by running `register()`, so narrowing it\n * anywhere else would take a tab away with no way to notice.\n */\nexport function isPluginUsedInConsole(\n contributes: PluginContributions | undefined,\n where: ConsoleLoadWhere,\n): boolean {\n if (where.at === 'editor') return declaresSiteElements(contributes)\n const points = consoleLoadPoints(contributes)\n if (where.at === 'shell') return points.shell\n if (where.at === 'slots') {\n return points.slots.some((slot) => where.slots.includes(slot))\n }\n const routes = where.level === 'org' ? points.orgRoutes : points.routes\n return routes.some((route) => routeServes(route, where.href))\n}\n"],"names":["PLUGIN_MAX_CONTRIBUTIONS","CONTRIBUTION_ID","CONTRIBUTION_ROUTE","BINDING_PROP","sanitizePluginContributions","input","isPlainObject","ok","error","out","key","Object","keys","undefined","site","next","list","idList","values","length","bindings","functionBindingMap","components","value","functionBindings","consoleInput","shell","console","contributions","Boolean","Array","isArray","where","pattern","entry","test","String","includes","push","entries","componentId","prop","readPluginContributions","verdict","pagePresence","nodes","componentIds","Set","pluginIds","node","add","pluginId","isPluginUsedOnPage","subject","page","has","listingId","identity","contributes","features","some","id","declaresSiteElements","presenceIds","ids","mergeFunctionBindings","sources","merged","source","declared","prototype","hasOwnProperty","call","isNamespacedComponentId","consoleLoadPoints","slots","routes","orgRoutes","routeServes","route","href","startsWith","isPluginUsedInConsole","at","points","slot","level"],"mappings":"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"}
@@ -0,0 +1,138 @@
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
+ * Events a PLUGIN raises for other plugins (AGL-3611).
19
+ *
20
+ * `plugin-events.ts` carries the platform's own facts — a seat add-on, a paid
21
+ * invoice — with payloads core declares. This is the same idea for facts
22
+ * that belong to a plugin: one plugin declares an event and its payload,
23
+ * raises it when the thing happens, and any other plugin subscribes by the
24
+ * event's name without importing the plugin that raises it. Core names
25
+ * nothing a plugin raises.
26
+ *
27
+ * DELIVERY IS RELIABLE, NOT FIRE-AND-FORGET. Raising an event writes an
28
+ * outbox document (`plugin-event-outbox.ts` in the admin data lib), in the
29
+ * same transaction as the write it reports where the raiser has one, and a
30
+ * scheduled drain hands it to each subscriber in turn. A subscriber that
31
+ * throws is retried with backoff, alone: the ones that already took the
32
+ * event are not called again. Delivery is AT LEAST ONCE, so every envelope
33
+ * carries `id`, stable across retries, for a subscriber to dedupe on.
34
+ *
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.
39
+ *
40
+ * The registry lives on `globalThis` (AGL-3412): subscriptions register from
41
+ * the plugins' server declarations at boot, in a module graph the routes and
42
+ * the job runner do not share.
43
+ */
44
+ /** A typed handle on one event name. */
45
+ export interface PluginDomainEvent<Payload> {
46
+ readonly id: string;
47
+ /** Type-level only: the payload the event carries. */
48
+ readonly __payload?: Payload;
49
+ }
50
+ /** The payload an event token carries. */
51
+ export type PluginDomainEventPayload<Event> = Event extends PluginDomainEvent<infer Payload> ? Payload : never;
52
+ /**
53
+ * A token for one event name: lower-case words joined by dots, the first
54
+ * naming what the event is about (`order.paid`, `return.requested`).
55
+ */
56
+ export declare function definePluginDomainEvent<Payload>(id: string): PluginDomainEvent<Payload>;
57
+ /** How a plugin describes an event it raises, for pickers and the docs. */
58
+ export interface PluginDomainEventDeclaration {
59
+ event: PluginDomainEvent<unknown> | string;
60
+ /** "Order paid". */
61
+ label: string;
62
+ /** One sentence: when it is raised. */
63
+ description: string;
64
+ /** The top-level payload keys, for a person writing a subscriber. */
65
+ payloadKeys?: readonly string[];
66
+ }
67
+ /** A declared event, with the plugin that raises it. */
68
+ export interface DeclaredPluginDomainEvent {
69
+ pluginId: string;
70
+ event: string;
71
+ label: string;
72
+ description: string;
73
+ payloadKeys: readonly string[];
74
+ }
75
+ /** What a subscriber is handed. */
76
+ export interface PluginDomainEventEnvelope<Payload = unknown> {
77
+ /** Stable across retries: the idempotency key for a subscriber. */
78
+ id: string;
79
+ event: string;
80
+ /** The site the event happened on. */
81
+ hostId: string;
82
+ /** The site's org, when the raiser knew it. */
83
+ orgId: string | null;
84
+ /** When the fact happened, not when it was delivered. */
85
+ occurredAtMs: number;
86
+ /** 1 on the first delivery to this subscriber, then 2, 3, … */
87
+ attempt: number;
88
+ payload: Payload;
89
+ }
90
+ export type PluginDomainEventHandler<Payload = unknown> = (envelope: PluginDomainEventEnvelope<Payload>) => void | Promise<void>;
91
+ /**
92
+ * Declares the events a plugin raises. One plugin owns a name: a second
93
+ * plugin declaring it throws, so two plugins cannot raise one event with two
94
+ * payloads. The owner declaring again replaces its entry.
95
+ */
96
+ export declare function declarePluginDomainEvents(declarations: readonly PluginDomainEventDeclaration[], options?: {
97
+ pluginId?: string;
98
+ }): void;
99
+ /** Every declared event, sorted by name. */
100
+ export declare function listPluginDomainEvents(): DeclaredPluginDomainEvent[];
101
+ /** The declaration of one event, or `null` when no plugin declares it. */
102
+ export declare function pluginDomainEventDeclaration(event: string): DeclaredPluginDomainEvent | null;
103
+ /**
104
+ * Subscribes to an event. A subscriber subscribing again to the same event
105
+ * replaces its earlier handler, so a module evaluated twice delivers once.
106
+ *
107
+ * The subscriber is named by plugin id, or `pluginId:name` when a plugin
108
+ * subscribes more than one handler to an event (its webhooks and its
109
+ * workflow triggers, say) — and that name is what the outbox records as
110
+ * delivered, so it must be stable.
111
+ */
112
+ export declare function subscribePluginDomainEvent<Payload>(event: PluginDomainEvent<Payload> | string, handler: PluginDomainEventHandler<Payload>, options?: {
113
+ pluginId?: string;
114
+ name?: string;
115
+ }): void;
116
+ /** The plugins subscribed to an event, in subscription order. */
117
+ export declare function listPluginDomainEventSubscribers(event: string): string[];
118
+ /** What one delivery pass came to. */
119
+ export interface PluginDomainEventDeliveryResult {
120
+ /** Subscribers that took the event on this pass. */
121
+ delivered: string[];
122
+ /** Subscribers that threw, with what they said. */
123
+ failed: Array<{
124
+ pluginId: string;
125
+ error: string;
126
+ }>;
127
+ }
128
+ /**
129
+ * Hands an envelope to every subscriber not in `skip`, one after another. A
130
+ * throw is caught and reported and does not stop the others. The outbox
131
+ * drain calls this; a raiser never does.
132
+ */
133
+ export declare function deliverPluginDomainEvent(envelope: Omit<PluginDomainEventEnvelope, 'attempt'>, options?: {
134
+ skip?: ReadonlySet<string>;
135
+ attempts?: Readonly<Record<string, number>>;
136
+ }): Promise<PluginDomainEventDeliveryResult>;
137
+ /** Test seam: forget every declaration and subscription. */
138
+ export declare function resetPluginDomainEventsForTests(): void;