@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.
- package/package.json +10 -10
- package/src/lib/app-utils/console-routes.d.ts +0 -6
- package/src/lib/app-utils/console-routes.js +0 -2
- package/src/lib/app-utils/console-routes.js.map +1 -1
- package/src/lib/app-utils/docs-help.generated.d.ts +12 -0
- package/src/lib/app-utils/docs-help.generated.js +27 -0
- package/src/lib/app-utils/docs-help.generated.js.map +1 -1
- package/src/lib/app-utils/docs-index.generated.js +5 -5
- package/src/lib/app-utils/docs-index.generated.js.map +1 -1
- package/src/lib/app-utils/index.d.ts +1 -0
- package/src/lib/app-utils/index.js +1 -0
- package/src/lib/app-utils/index.js.map +1 -1
- package/src/lib/app-utils/notifications.d.ts +11 -0
- package/src/lib/app-utils/notifications.js +11 -1
- package/src/lib/app-utils/notifications.js.map +1 -1
- package/src/lib/app-utils/staff-role-gate.d.ts +56 -0
- package/src/lib/app-utils/staff-role-gate.js +46 -0
- package/src/lib/app-utils/staff-role-gate.js.map +1 -0
- package/src/lib/plugin-manager/feature-plugins.d.ts +61 -0
- package/src/lib/plugin-manager/feature-plugins.js.map +1 -1
- package/src/lib/plugin-manager/plugin-permissions.d.ts +7 -0
- package/src/lib/plugin-manager/plugin-permissions.js +30 -0
- package/src/lib/plugin-manager/plugin-permissions.js.map +1 -1
- package/src/lib/plugin-manager/plugin-site-cache.d.ts +78 -0
- package/src/lib/plugin-manager/plugin-site-cache.js +125 -0
- package/src/lib/plugin-manager/plugin-site-cache.js.map +1 -0
- package/src/lib/plugin-manager/plugin-trust-signing.d.ts +51 -0
- package/src/lib/plugin-manager/plugin-trust-signing.js +119 -0
- 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"}
|