@aglyn/aglyn 1.0.0-beta.150 → 1.0.0-beta.152

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 (29) hide show
  1. package/package.json +10 -10
  2. package/src/lib/app-utils/console-routes.d.ts +0 -6
  3. package/src/lib/app-utils/console-routes.js +0 -2
  4. package/src/lib/app-utils/console-routes.js.map +1 -1
  5. package/src/lib/app-utils/docs-help.generated.d.ts +12 -0
  6. package/src/lib/app-utils/docs-help.generated.js +27 -0
  7. package/src/lib/app-utils/docs-help.generated.js.map +1 -1
  8. package/src/lib/app-utils/docs-index.generated.js +5 -5
  9. package/src/lib/app-utils/docs-index.generated.js.map +1 -1
  10. package/src/lib/app-utils/index.d.ts +1 -0
  11. package/src/lib/app-utils/index.js +1 -0
  12. package/src/lib/app-utils/index.js.map +1 -1
  13. package/src/lib/app-utils/notifications.d.ts +11 -0
  14. package/src/lib/app-utils/notifications.js +11 -1
  15. package/src/lib/app-utils/notifications.js.map +1 -1
  16. package/src/lib/app-utils/staff-role-gate.d.ts +56 -0
  17. package/src/lib/app-utils/staff-role-gate.js +46 -0
  18. package/src/lib/app-utils/staff-role-gate.js.map +1 -0
  19. package/src/lib/plugin-manager/feature-plugins.d.ts +61 -0
  20. package/src/lib/plugin-manager/feature-plugins.js.map +1 -1
  21. package/src/lib/plugin-manager/plugin-permissions.d.ts +7 -0
  22. package/src/lib/plugin-manager/plugin-permissions.js +30 -0
  23. package/src/lib/plugin-manager/plugin-permissions.js.map +1 -1
  24. package/src/lib/plugin-manager/plugin-site-cache.d.ts +78 -0
  25. package/src/lib/plugin-manager/plugin-site-cache.js +125 -0
  26. package/src/lib/plugin-manager/plugin-site-cache.js.map +1 -0
  27. package/src/lib/plugin-manager/plugin-trust-signing.d.ts +51 -0
  28. package/src/lib/plugin-manager/plugin-trust-signing.js +119 -0
  29. package/src/lib/plugin-manager/plugin-trust-signing.js.map +1 -0
@@ -0,0 +1,78 @@
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 the caller wants dropped, and why. */
18
+ export interface PluginSiteCacheRequest {
19
+ /**
20
+ * The sites whose rendered pages must go. Duplicates are the caller's to
21
+ * avoid or not — the implementation is free to collapse them.
22
+ */
23
+ hostIds: readonly string[];
24
+ /**
25
+ * Why, in a few words, for the fan-out's own logging: a truncated purge
26
+ * is only debuggable if the record says what it was purging for.
27
+ * `'marketplace listing revoked'`, not `'revoke'`.
28
+ */
29
+ reason: string;
30
+ }
31
+ export interface PluginSiteCacheResult {
32
+ /** Sites whose cached pages were dropped. */
33
+ dropped: number;
34
+ /**
35
+ * Sites deliberately left holding cached pages because the fan-out hit
36
+ * its cap. Not an error — a decision the shell made and logged — but a
37
+ * number a caller may want to surface, because those sites are still
38
+ * serving what was just stopped.
39
+ */
40
+ skipped: number;
41
+ /**
42
+ * Whether the drop certainly happened. FALSE when no implementation is
43
+ * registered or one threw, and a caller must not read `dropped: 0` as
44
+ * "there was nothing to drop" without checking this first.
45
+ */
46
+ complete: boolean;
47
+ }
48
+ export interface PluginSiteCache {
49
+ drop(request: PluginSiteCacheRequest): Promise<PluginSiteCacheResult>;
50
+ }
51
+ /**
52
+ * One implementation: the app's. Two would be two cache layers disagreeing
53
+ * about whether a page is gone, and the second to register would be the one
54
+ * nobody knew about.
55
+ */
56
+ export declare const PLUGIN_SITE_CACHE: import("./plugin-services").PluginServiceContract<PluginSiteCache>;
57
+ /**
58
+ * Installs the app's implementation. Called once, at server boot, by the app
59
+ * that owns the tenant's cache.
60
+ *
61
+ * `pluginId` is required from outside a plugin's register fn, as it is for
62
+ * every service: the app passes its own marker, and the contract refuses an
63
+ * anonymous registration rather than accepting one nothing can be traced to.
64
+ */
65
+ export declare function registerPluginSiteCache(cache: PluginSiteCache, options?: {
66
+ pluginId?: string;
67
+ }): void;
68
+ /** Whether an implementation is installed — for a caller that wants to say so. */
69
+ export declare function hasPluginSiteCache(): boolean;
70
+ /**
71
+ * Drops the cached pages of the named sites.
72
+ *
73
+ * NEVER THROWS and never silently succeeds: a failure comes back as
74
+ * `complete: false` with the reason logged, because the callers are stop
75
+ * paths that must complete their own work either way. Read `complete`
76
+ * before reading `dropped`.
77
+ */
78
+ export declare function dropPluginSiteCache(request: PluginSiteCacheRequest): Promise<PluginSiteCacheResult>;
@@ -0,0 +1,125 @@
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
+ */ /**
18
+ * HOW A PLUGIN DROPS A SITE'S CACHED PAGES (AGL-3080).
19
+ *
20
+ * The shell hands this one down rather than up. Every other contract in this
21
+ * directory is a plugin telling the shell something; this is the shell
22
+ * telling plugins it can do something — purge the rendered HTML of a site —
23
+ * because purging it takes the tenant's cache tags and revalidation paths,
24
+ * which live in the app and are not a plugin's to know.
25
+ *
26
+ * ## The case, and why it is a kill switch rather than a nicety
27
+ *
28
+ * A marketplace plugin can be REVOKED. Revoking writes a stop on the
29
+ * version, and the tenant refuses revoked bytes at render time — but a page
30
+ * already rendered and cached does not re-render to find that out. So the
31
+ * revoke has a second half: drop the cached pages of every site running it.
32
+ * Without it a revoked plugin keeps executing in visitors' browsers for as
33
+ * long as the cache holds, which is the whole failure the stop exists to
34
+ * prevent.
35
+ *
36
+ * ## ⛔ AN ABSENT IMPLEMENTATION IS AN ANSWER, AND IT IS `complete: false`
37
+ *
38
+ * The registry is filled by the APP at boot. A process that has not filled
39
+ * it resolves "no cache to drop" — indistinguishable, from the inside, from
40
+ * a workspace whose sites were all already fresh. That is the AGL-3025 shape
41
+ * and on this path it means a revoked plugin quietly kept serving.
42
+ *
43
+ * So there is no silent branch. {@link dropPluginSiteCache} always answers a
44
+ * {@link PluginSiteCacheResult}, and `complete` is false whenever the drop
45
+ * did not certainly happen — nothing registered, or the implementation
46
+ * threw. A caller acts on that: the revoke itself must still stand (a plugin
47
+ * left un-revoked because a cache purge failed is strictly worse), but the
48
+ * reader has to be told the purge did not run, and the log has to carry it.
49
+ *
50
+ * ## The split, in one line
51
+ *
52
+ * The plugin knows WHICH sites — that is its own install pins, its own
53
+ * collection. The shell knows HOW to drop one, and owns the fan-out policy:
54
+ * the cap, the concurrency, and saying out loud when a cap truncated the
55
+ * list. Neither half is guessable from the other side.
56
+ */ import { definePluginServiceContract, registerPluginService, resolvePluginService } from "./plugin-services.js";
57
+ /**
58
+ * One implementation: the app's. Two would be two cache layers disagreeing
59
+ * about whether a page is gone, and the second to register would be the one
60
+ * nobody knew about.
61
+ */ export const PLUGIN_SITE_CACHE = definePluginServiceContract('core.site-cache', {
62
+ multiple: false
63
+ });
64
+ /**
65
+ * Installs the app's implementation. Called once, at server boot, by the app
66
+ * that owns the tenant's cache.
67
+ *
68
+ * `pluginId` is required from outside a plugin's register fn, as it is for
69
+ * every service: the app passes its own marker, and the contract refuses an
70
+ * anonymous registration rather than accepting one nothing can be traced to.
71
+ */ export function registerPluginSiteCache(cache, options) {
72
+ if (typeof (cache == null ? void 0 : cache.drop) !== 'function') {
73
+ throw new Error('a site cache needs a drop function');
74
+ }
75
+ registerPluginService(PLUGIN_SITE_CACHE, cache, _extends({}, (options == null ? void 0 : options.pluginId) ? {
76
+ pluginId: options.pluginId
77
+ } : {}));
78
+ }
79
+ /** Whether an implementation is installed — for a caller that wants to say so. */ export function hasPluginSiteCache() {
80
+ return Boolean(resolvePluginService(PLUGIN_SITE_CACHE));
81
+ }
82
+ /**
83
+ * Drops the cached pages of the named sites.
84
+ *
85
+ * NEVER THROWS and never silently succeeds: a failure comes back as
86
+ * `complete: false` with the reason logged, because the callers are stop
87
+ * paths that must complete their own work either way. Read `complete`
88
+ * before reading `dropped`.
89
+ */ export async function dropPluginSiteCache(request) {
90
+ const hostIds = [
91
+ ...new Set(request.hostIds.filter(Boolean))
92
+ ];
93
+ if (!hostIds.length) {
94
+ // Nothing asked for is the one case where "nothing dropped" is complete
95
+ // on its own terms, whether or not anything is registered.
96
+ return {
97
+ dropped: 0,
98
+ skipped: 0,
99
+ complete: true
100
+ };
101
+ }
102
+ const cache = resolvePluginService(PLUGIN_SITE_CACHE);
103
+ if (!cache) {
104
+ console.error(`[site-cache] no implementation is installed, so ${hostIds.length} ` + `site(s) keep their cached pages after: ${request.reason}. The app ` + 'registers this at boot; a process that has not is serving stopped ' + 'content.');
105
+ return {
106
+ dropped: 0,
107
+ skipped: 0,
108
+ complete: false
109
+ };
110
+ }
111
+ try {
112
+ return await cache.drop(_extends({}, request, {
113
+ hostIds
114
+ }));
115
+ } catch (error) {
116
+ console.error(`[site-cache] the drop failed after: ${request.reason}`, error);
117
+ return {
118
+ dropped: 0,
119
+ skipped: 0,
120
+ complete: false
121
+ };
122
+ }
123
+ }
124
+
125
+ //# sourceMappingURL=plugin-site-cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-site-cache.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 * HOW A PLUGIN DROPS A SITE'S CACHED PAGES (AGL-3080).\n *\n * The shell hands this one down rather than up. Every other contract in this\n * directory is a plugin telling the shell something; this is the shell\n * telling plugins it can do something — purge the rendered HTML of a site —\n * because purging it takes the tenant's cache tags and revalidation paths,\n * which live in the app and are not a plugin's to know.\n *\n * ## The case, and why it is a kill switch rather than a nicety\n *\n * A marketplace plugin can be REVOKED. Revoking writes a stop on the\n * version, and the tenant refuses revoked bytes at render time — but a page\n * already rendered and cached does not re-render to find that out. So the\n * revoke has a second half: drop the cached pages of every site running it.\n * Without it a revoked plugin keeps executing in visitors' browsers for as\n * long as the cache holds, which is the whole failure the stop exists to\n * prevent.\n *\n * ## ⛔ AN ABSENT IMPLEMENTATION IS AN ANSWER, AND IT IS `complete: false`\n *\n * The registry is filled by the APP at boot. A process that has not filled\n * it resolves \"no cache to drop\" — indistinguishable, from the inside, from\n * a workspace whose sites were all already fresh. That is the AGL-3025 shape\n * and on this path it means a revoked plugin quietly kept serving.\n *\n * So there is no silent branch. {@link dropPluginSiteCache} always answers a\n * {@link PluginSiteCacheResult}, and `complete` is false whenever the drop\n * did not certainly happen — nothing registered, or the implementation\n * threw. A caller acts on that: the revoke itself must still stand (a plugin\n * left un-revoked because a cache purge failed is strictly worse), but the\n * reader has to be told the purge did not run, and the log has to carry it.\n *\n * ## The split, in one line\n *\n * The plugin knows WHICH sites — that is its own install pins, its own\n * collection. The shell knows HOW to drop one, and owns the fan-out policy:\n * the cap, the concurrency, and saying out loud when a cap truncated the\n * list. Neither half is guessable from the other side.\n */\n\nimport {\n definePluginServiceContract,\n registerPluginService,\n resolvePluginService,\n} from './plugin-services'\nimport { getRegisteringPluginId } from '../app-utils/registering-plugin'\n\n/** What the caller wants dropped, and why. */\nexport interface PluginSiteCacheRequest {\n /**\n * The sites whose rendered pages must go. Duplicates are the caller's to\n * avoid or not — the implementation is free to collapse them.\n */\n hostIds: readonly string[]\n /**\n * Why, in a few words, for the fan-out's own logging: a truncated purge\n * is only debuggable if the record says what it was purging for.\n * `'marketplace listing revoked'`, not `'revoke'`.\n */\n reason: string\n}\n\nexport interface PluginSiteCacheResult {\n /** Sites whose cached pages were dropped. */\n dropped: number\n /**\n * Sites deliberately left holding cached pages because the fan-out hit\n * its cap. Not an error — a decision the shell made and logged — but a\n * number a caller may want to surface, because those sites are still\n * serving what was just stopped.\n */\n skipped: number\n /**\n * Whether the drop certainly happened. FALSE when no implementation is\n * registered or one threw, and a caller must not read `dropped: 0` as\n * \"there was nothing to drop\" without checking this first.\n */\n complete: boolean\n}\n\nexport interface PluginSiteCache {\n drop(request: PluginSiteCacheRequest): Promise<PluginSiteCacheResult>\n}\n\n/**\n * One implementation: the app's. Two would be two cache layers disagreeing\n * about whether a page is gone, and the second to register would be the one\n * nobody knew about.\n */\nexport const PLUGIN_SITE_CACHE = definePluginServiceContract<PluginSiteCache>(\n 'core.site-cache',\n { multiple: false },\n)\n\n/**\n * Installs the app's implementation. Called once, at server boot, by the app\n * that owns the tenant's cache.\n *\n * `pluginId` is required from outside a plugin's register fn, as it is for\n * every service: the app passes its own marker, and the contract refuses an\n * anonymous registration rather than accepting one nothing can be traced to.\n */\nexport function registerPluginSiteCache(\n cache: PluginSiteCache,\n options?: { pluginId?: string },\n): void {\n if (typeof cache?.drop !== 'function') {\n throw new Error('a site cache needs a drop function')\n }\n registerPluginService(PLUGIN_SITE_CACHE, cache, {\n ...(options?.pluginId ? { pluginId: options.pluginId } : {}),\n })\n}\n\n/** Whether an implementation is installed — for a caller that wants to say so. */\nexport function hasPluginSiteCache(): boolean {\n return Boolean(resolvePluginService(PLUGIN_SITE_CACHE))\n}\n\n/**\n * Drops the cached pages of the named sites.\n *\n * NEVER THROWS and never silently succeeds: a failure comes back as\n * `complete: false` with the reason logged, because the callers are stop\n * paths that must complete their own work either way. Read `complete`\n * before reading `dropped`.\n */\nexport async function dropPluginSiteCache(\n request: PluginSiteCacheRequest,\n): Promise<PluginSiteCacheResult> {\n const hostIds = [...new Set(request.hostIds.filter(Boolean))]\n if (!hostIds.length) {\n // Nothing asked for is the one case where \"nothing dropped\" is complete\n // on its own terms, whether or not anything is registered.\n return { dropped: 0, skipped: 0, complete: true }\n }\n const cache = resolvePluginService(PLUGIN_SITE_CACHE)\n if (!cache) {\n console.error(\n `[site-cache] no implementation is installed, so ${hostIds.length} ` +\n `site(s) keep their cached pages after: ${request.reason}. The app ` +\n 'registers this at boot; a process that has not is serving stopped ' +\n 'content.',\n )\n return { dropped: 0, skipped: 0, complete: false }\n }\n try {\n return await cache.drop({ ...request, hostIds })\n } catch (error) {\n console.error(\n `[site-cache] the drop failed after: ${request.reason}`,\n error,\n )\n return { dropped: 0, skipped: 0, complete: false }\n }\n}\n"],"names":["definePluginServiceContract","registerPluginService","resolvePluginService","PLUGIN_SITE_CACHE","multiple","registerPluginSiteCache","cache","options","drop","Error","pluginId","hasPluginSiteCache","Boolean","dropPluginSiteCache","request","hostIds","Set","filter","length","dropped","skipped","complete","console","error","reason"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuCC,GAED,SACEA,2BAA2B,EAC3BC,qBAAqB,EACrBC,oBAAoB,QACf,uBAAmB;AAwC1B;;;;CAIC,GACD,OAAO,MAAMC,oBAAoBH,4BAC/B,mBACA;IAAEI,UAAU;AAAM,GACnB;AAED;;;;;;;CAOC,GACD,OAAO,SAASC,wBACdC,KAAsB,EACtBC,OAA+B;IAE/B,IAAI,QAAOD,yBAAAA,MAAOE,IAAI,MAAK,YAAY;QACrC,MAAM,IAAIC,MAAM;IAClB;IACAR,sBAAsBE,mBAAmBG,OAAO,aAC1CC,CAAAA,2BAAAA,QAASG,QAAQ,IAAG;QAAEA,UAAUH,QAAQG,QAAQ;IAAC,IAAI,CAAC;AAE9D;AAEA,gFAAgF,GAChF,OAAO,SAASC;IACd,OAAOC,QAAQV,qBAAqBC;AACtC;AAEA;;;;;;;CAOC,GACD,OAAO,eAAeU,oBACpBC,OAA+B;IAE/B,MAAMC,UAAU;WAAI,IAAIC,IAAIF,QAAQC,OAAO,CAACE,MAAM,CAACL;KAAU;IAC7D,IAAI,CAACG,QAAQG,MAAM,EAAE;QACnB,wEAAwE;QACxE,2DAA2D;QAC3D,OAAO;YAAEC,SAAS;YAAGC,SAAS;YAAGC,UAAU;QAAK;IAClD;IACA,MAAMf,QAAQJ,qBAAqBC;IACnC,IAAI,CAACG,OAAO;QACVgB,QAAQC,KAAK,CACX,CAAC,gDAAgD,EAAER,QAAQG,MAAM,CAAC,CAAC,CAAC,GAClE,CAAC,uCAAuC,EAAEJ,QAAQU,MAAM,CAAC,UAAU,CAAC,GACpE,uEACA;QAEJ,OAAO;YAAEL,SAAS;YAAGC,SAAS;YAAGC,UAAU;QAAM;IACnD;IACA,IAAI;QACF,OAAO,MAAMf,MAAME,IAAI,CAAC,aAAKM;YAASC;;IACxC,EAAE,OAAOQ,OAAO;QACdD,QAAQC,KAAK,CACX,CAAC,oCAAoC,EAAET,QAAQU,MAAM,EAAE,EACvDD;QAEF,OAAO;YAAEJ,SAAS;YAAGC,SAAS;YAAGC,UAAU;QAAM;IACnD;AACF"}
@@ -0,0 +1,51 @@
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
+ export interface PluginTrustSignature {
18
+ /** Base64 Ed25519 signature over the sha256 hex. Absent unless `signed`. */
19
+ signature?: string;
20
+ /**
21
+ * Whether a signature was produced. FALSE when nothing is registered, the
22
+ * deployment holds no key, or signing failed — and a caller must refuse
23
+ * the grant, never write one without a signature.
24
+ */
25
+ signed: boolean;
26
+ /** Why not, in the operator's terms, when `signed` is false. */
27
+ reason?: string;
28
+ }
29
+ export interface PluginTrustSigner {
30
+ /** Signs a version's sha256 hex with the platform's Ed25519 key. */
31
+ sign(sha256: string): Promise<PluginTrustSignature>;
32
+ }
33
+ /**
34
+ * One implementation: the app that holds the key. A second would be a second
35
+ * signing authority, and the loaders verify against ONE public key.
36
+ */
37
+ export declare const PLUGIN_TRUST_SIGNER: import("./plugin-services").PluginServiceContract<PluginTrustSigner>;
38
+ /** Installs the app's signer. Called once, at server boot. */
39
+ export declare function registerPluginTrustSigner(signer: PluginTrustSigner, options?: {
40
+ pluginId?: string;
41
+ }): void;
42
+ /** Whether this deployment can sign at all — for a surface that says so. */
43
+ export declare function hasPluginTrustSigner(): boolean;
44
+ /**
45
+ * Signs `sha256`, or says why it could not.
46
+ *
47
+ * NEVER THROWS, and never answers a signature it did not make. Read
48
+ * `signed` before reading `signature`; see the module docblock for why a
49
+ * caller must refuse rather than continue.
50
+ */
51
+ export declare function signPluginTrust(sha256: string): Promise<PluginTrustSignature>;
@@ -0,0 +1,119 @@
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
+ */ /**
18
+ * WHO HOLDS THE PLATFORM'S TRUST KEY (AGL-3080).
19
+ *
20
+ * Realm trust is the strongest grant in the platform: it drops a marketplace
21
+ * bundle into the app realm, where neither the sandbox iframe nor the
22
+ * manifest CSP stands between it and user data. A grant is an Ed25519
23
+ * signature, made with `PLUGIN_TRUST_PRIVATE_KEY`, over the reviewed
24
+ * version's sha256 — and every loader verifies it before a byte executes.
25
+ *
26
+ * ## Why this is a seam rather than a move
27
+ *
28
+ * The queue around it is the MARKETPLACE's: `marketplaceListings` is its
29
+ * collection, "a version must have passed review" is its rule, and the
30
+ * fields the grant writes are its documents. All of that belongs with the
31
+ * plugin, the way the review queue and the report queue already do.
32
+ *
33
+ * The KEY does not. It is deployed to the console and nowhere else — never
34
+ * to a tenant runtime — and this package is published to npm, where a
35
+ * self-hoster generates their own pair. A plugin that read the key directly
36
+ * would put the platform's signing authority inside a package anyone can
37
+ * install, to no benefit: the plugin has nothing to do with it beyond
38
+ * wanting one string signed.
39
+ *
40
+ * So the shell signs, and the marketplace decides what is worth signing.
41
+ * The sibling of `core.site-cache`, and the same direction: the shell
42
+ * offering a capability down.
43
+ *
44
+ * ## ⛔ AN ABSENT SIGNER REFUSES. IT DOES NOT GRANT.
45
+ *
46
+ * This is the opposite failure direction from every other contract here, and
47
+ * it is the one that matters. `core.site-cache` answers `complete: false`
48
+ * and the caller carries on, because a stale cache is survivable. An
49
+ * unsigned realm grant is not survivable: the loaders fail closed on a
50
+ * missing signature, so a "grant" without one produces a version that is
51
+ * marked trusted and cannot load — or, far worse if a loader is ever
52
+ * lenient, one that loads unverified.
53
+ *
54
+ * {@link signPluginTrust} therefore never returns a bare string. It answers
55
+ * `{ signed: false, reason }` when nothing is registered, when the
56
+ * deployment has no key, or when signing throws, and the caller MUST refuse
57
+ * the grant on it rather than writing a partial one.
58
+ */ import { definePluginServiceContract, registerPluginService, resolvePluginService } from "./plugin-services.js";
59
+ /**
60
+ * One implementation: the app that holds the key. A second would be a second
61
+ * signing authority, and the loaders verify against ONE public key.
62
+ */ export const PLUGIN_TRUST_SIGNER = definePluginServiceContract('core.plugin-trust-signer', {
63
+ multiple: false
64
+ });
65
+ /** Installs the app's signer. Called once, at server boot. */ export function registerPluginTrustSigner(signer, options) {
66
+ if (typeof (signer == null ? void 0 : signer.sign) !== 'function') {
67
+ throw new Error('a plugin trust signer needs a sign function');
68
+ }
69
+ registerPluginService(PLUGIN_TRUST_SIGNER, signer, _extends({}, (options == null ? void 0 : options.pluginId) ? {
70
+ pluginId: options.pluginId
71
+ } : {}));
72
+ }
73
+ /** Whether this deployment can sign at all — for a surface that says so. */ export function hasPluginTrustSigner() {
74
+ return Boolean(resolvePluginService(PLUGIN_TRUST_SIGNER));
75
+ }
76
+ /**
77
+ * Signs `sha256`, or says why it could not.
78
+ *
79
+ * NEVER THROWS, and never answers a signature it did not make. Read
80
+ * `signed` before reading `signature`; see the module docblock for why a
81
+ * caller must refuse rather than continue.
82
+ */ export async function signPluginTrust(sha256) {
83
+ const digest = sha256 == null ? void 0 : sha256.trim();
84
+ if (!digest) {
85
+ return {
86
+ signed: false,
87
+ reason: 'No version hash to sign.'
88
+ };
89
+ }
90
+ const signer = resolvePluginService(PLUGIN_TRUST_SIGNER);
91
+ if (!signer) {
92
+ console.error('[trust-signer] no implementation is installed, so realm trust cannot ' + 'be granted. The app registers this at boot.');
93
+ return {
94
+ signed: false,
95
+ reason: 'Trust signing is not available on this deployment.'
96
+ };
97
+ }
98
+ try {
99
+ const result = await signer.sign(digest);
100
+ // A signer that answered `signed` with nothing to show is a bug in the
101
+ // signer, and granting on it would write the trust flag with no
102
+ // signature — the one state the loaders cannot distinguish from tampering.
103
+ if (result.signed && !result.signature) {
104
+ return {
105
+ signed: false,
106
+ reason: 'Trust signing returned no signature.'
107
+ };
108
+ }
109
+ return result;
110
+ } catch (error) {
111
+ console.error('[trust-signer] signing failed', error);
112
+ return {
113
+ signed: false,
114
+ reason: 'Trust signing failed.'
115
+ };
116
+ }
117
+ }
118
+
119
+ //# sourceMappingURL=plugin-trust-signing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-trust-signing.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 * WHO HOLDS THE PLATFORM'S TRUST KEY (AGL-3080).\n *\n * Realm trust is the strongest grant in the platform: it drops a marketplace\n * bundle into the app realm, where neither the sandbox iframe nor the\n * manifest CSP stands between it and user data. A grant is an Ed25519\n * signature, made with `PLUGIN_TRUST_PRIVATE_KEY`, over the reviewed\n * version's sha256 — and every loader verifies it before a byte executes.\n *\n * ## Why this is a seam rather than a move\n *\n * The queue around it is the MARKETPLACE's: `marketplaceListings` is its\n * collection, \"a version must have passed review\" is its rule, and the\n * fields the grant writes are its documents. All of that belongs with the\n * plugin, the way the review queue and the report queue already do.\n *\n * The KEY does not. It is deployed to the console and nowhere else — never\n * to a tenant runtime — and this package is published to npm, where a\n * self-hoster generates their own pair. A plugin that read the key directly\n * would put the platform's signing authority inside a package anyone can\n * install, to no benefit: the plugin has nothing to do with it beyond\n * wanting one string signed.\n *\n * So the shell signs, and the marketplace decides what is worth signing.\n * The sibling of `core.site-cache`, and the same direction: the shell\n * offering a capability down.\n *\n * ## ⛔ AN ABSENT SIGNER REFUSES. IT DOES NOT GRANT.\n *\n * This is the opposite failure direction from every other contract here, and\n * it is the one that matters. `core.site-cache` answers `complete: false`\n * and the caller carries on, because a stale cache is survivable. An\n * unsigned realm grant is not survivable: the loaders fail closed on a\n * missing signature, so a \"grant\" without one produces a version that is\n * marked trusted and cannot load — or, far worse if a loader is ever\n * lenient, one that loads unverified.\n *\n * {@link signPluginTrust} therefore never returns a bare string. It answers\n * `{ signed: false, reason }` when nothing is registered, when the\n * deployment has no key, or when signing throws, and the caller MUST refuse\n * the grant on it rather than writing a partial one.\n */\n\nimport {\n definePluginServiceContract,\n registerPluginService,\n resolvePluginService,\n} from './plugin-services'\n\nexport interface PluginTrustSignature {\n /** Base64 Ed25519 signature over the sha256 hex. Absent unless `signed`. */\n signature?: string\n /**\n * Whether a signature was produced. FALSE when nothing is registered, the\n * deployment holds no key, or signing failed — and a caller must refuse\n * the grant, never write one without a signature.\n */\n signed: boolean\n /** Why not, in the operator's terms, when `signed` is false. */\n reason?: string\n}\n\nexport interface PluginTrustSigner {\n /** Signs a version's sha256 hex with the platform's Ed25519 key. */\n sign(sha256: string): Promise<PluginTrustSignature>\n}\n\n/**\n * One implementation: the app that holds the key. A second would be a second\n * signing authority, and the loaders verify against ONE public key.\n */\nexport const PLUGIN_TRUST_SIGNER =\n definePluginServiceContract<PluginTrustSigner>('core.plugin-trust-signer', {\n multiple: false,\n })\n\n/** Installs the app's signer. Called once, at server boot. */\nexport function registerPluginTrustSigner(\n signer: PluginTrustSigner,\n options?: { pluginId?: string },\n): void {\n if (typeof signer?.sign !== 'function') {\n throw new Error('a plugin trust signer needs a sign function')\n }\n registerPluginService(PLUGIN_TRUST_SIGNER, signer, {\n ...(options?.pluginId ? { pluginId: options.pluginId } : {}),\n })\n}\n\n/** Whether this deployment can sign at all — for a surface that says so. */\nexport function hasPluginTrustSigner(): boolean {\n return Boolean(resolvePluginService(PLUGIN_TRUST_SIGNER))\n}\n\n/**\n * Signs `sha256`, or says why it could not.\n *\n * NEVER THROWS, and never answers a signature it did not make. Read\n * `signed` before reading `signature`; see the module docblock for why a\n * caller must refuse rather than continue.\n */\nexport async function signPluginTrust(\n sha256: string,\n): Promise<PluginTrustSignature> {\n const digest = sha256?.trim()\n if (!digest) {\n return { signed: false, reason: 'No version hash to sign.' }\n }\n const signer = resolvePluginService(PLUGIN_TRUST_SIGNER)\n if (!signer) {\n console.error(\n '[trust-signer] no implementation is installed, so realm trust cannot ' +\n 'be granted. The app registers this at boot.',\n )\n return {\n signed: false,\n reason: 'Trust signing is not available on this deployment.',\n }\n }\n try {\n const result = await signer.sign(digest)\n // A signer that answered `signed` with nothing to show is a bug in the\n // signer, and granting on it would write the trust flag with no\n // signature — the one state the loaders cannot distinguish from tampering.\n if (result.signed && !result.signature) {\n return {\n signed: false,\n reason: 'Trust signing returned no signature.',\n }\n }\n return result\n } catch (error) {\n console.error('[trust-signer] signing failed', error)\n return { signed: false, reason: 'Trust signing failed.' }\n }\n}\n"],"names":["definePluginServiceContract","registerPluginService","resolvePluginService","PLUGIN_TRUST_SIGNER","multiple","registerPluginTrustSigner","signer","options","sign","Error","pluginId","hasPluginTrustSigner","Boolean","signPluginTrust","sha256","digest","trim","signed","reason","console","error","result","signature"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyCC,GAED,SACEA,2BAA2B,EAC3BC,qBAAqB,EACrBC,oBAAoB,QACf,uBAAmB;AAoB1B;;;CAGC,GACD,OAAO,MAAMC,sBACXH,4BAA+C,4BAA4B;IACzEI,UAAU;AACZ,GAAE;AAEJ,4DAA4D,GAC5D,OAAO,SAASC,0BACdC,MAAyB,EACzBC,OAA+B;IAE/B,IAAI,QAAOD,0BAAAA,OAAQE,IAAI,MAAK,YAAY;QACtC,MAAM,IAAIC,MAAM;IAClB;IACAR,sBAAsBE,qBAAqBG,QAAQ,aAC7CC,CAAAA,2BAAAA,QAASG,QAAQ,IAAG;QAAEA,UAAUH,QAAQG,QAAQ;IAAC,IAAI,CAAC;AAE9D;AAEA,0EAA0E,GAC1E,OAAO,SAASC;IACd,OAAOC,QAAQV,qBAAqBC;AACtC;AAEA;;;;;;CAMC,GACD,OAAO,eAAeU,gBACpBC,MAAc;IAEd,MAAMC,SAASD,0BAAAA,OAAQE,IAAI;IAC3B,IAAI,CAACD,QAAQ;QACX,OAAO;YAAEE,QAAQ;YAAOC,QAAQ;QAA2B;IAC7D;IACA,MAAMZ,SAASJ,qBAAqBC;IACpC,IAAI,CAACG,QAAQ;QACXa,QAAQC,KAAK,CACX,0EACE;QAEJ,OAAO;YACLH,QAAQ;YACRC,QAAQ;QACV;IACF;IACA,IAAI;QACF,MAAMG,SAAS,MAAMf,OAAOE,IAAI,CAACO;QACjC,uEAAuE;QACvE,gEAAgE;QAChE,2EAA2E;QAC3E,IAAIM,OAAOJ,MAAM,IAAI,CAACI,OAAOC,SAAS,EAAE;YACtC,OAAO;gBACLL,QAAQ;gBACRC,QAAQ;YACV;QACF;QACA,OAAOG;IACT,EAAE,OAAOD,OAAO;QACdD,QAAQC,KAAK,CAAC,iCAAiCA;QAC/C,OAAO;YAAEH,QAAQ;YAAOC,QAAQ;QAAwB;IAC1D;AACF"}