@jini-ai/agent-plugins 0.4.7 → 0.4.8
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/CHANGELOG.md +21 -0
- package/README.md +46 -0
- package/dist/admin-surfaces/action-dispatcher.d.ts +37 -0
- package/dist/admin-surfaces/action-dispatcher.js +69 -0
- package/dist/admin-surfaces/action-schema.d.ts +56 -0
- package/dist/admin-surfaces/action-schema.js +167 -0
- package/dist/admin-surfaces/action-tools.d.ts +40 -0
- package/dist/admin-surfaces/action-tools.js +52 -0
- package/dist/admin-surfaces/callback-field-inbox.d.ts +35 -0
- package/dist/admin-surfaces/callback-field-inbox.js +53 -0
- package/dist/admin-surfaces/capability-declarations.d.ts +41 -0
- package/dist/admin-surfaces/capability-declarations.js +102 -0
- package/dist/admin-surfaces/contribution.d.ts +49 -0
- package/dist/admin-surfaces/contribution.js +30 -0
- package/dist/admin-surfaces/delivery-bindings.d.ts +76 -0
- package/dist/admin-surfaces/delivery-bindings.js +99 -0
- package/dist/admin-surfaces/descriptor.d.ts +69 -0
- package/dist/admin-surfaces/descriptor.js +196 -0
- package/dist/admin-surfaces/external-links.d.ts +45 -0
- package/dist/admin-surfaces/external-links.js +70 -0
- package/dist/admin-surfaces/host-validator.d.ts +62 -0
- package/dist/admin-surfaces/host-validator.js +87 -0
- package/dist/admin-surfaces/index.d.ts +23 -0
- package/dist/admin-surfaces/index.js +21 -0
- package/dist/admin-surfaces/protocol.d.ts +103 -0
- package/dist/admin-surfaces/protocol.js +25 -0
- package/dist/admin-surfaces/sandbox.d.ts +78 -0
- package/dist/admin-surfaces/sandbox.js +92 -0
- package/dist/admin-surfaces/settings-value.d.ts +28 -0
- package/dist/admin-surfaces/settings-value.js +45 -0
- package/dist/admin-surfaces/surface-capabilities.d.ts +167 -0
- package/dist/admin-surfaces/surface-capabilities.js +215 -0
- package/dist/admin-surfaces/surface-client.d.ts +116 -0
- package/dist/admin-surfaces/surface-client.js +219 -0
- package/dist/admin-surfaces/surface-handlers.d.ts +111 -0
- package/dist/admin-surfaces/surface-handlers.js +172 -0
- package/dist/admin-surfaces/theme.d.ts +59 -0
- package/dist/admin-surfaces/theme.js +57 -0
- package/dist/folder-zip-node.js +59 -49
- package/dist/folder-zip.js +9 -7
- package/dist/lifecycle/automatic-use.d.ts +79 -0
- package/dist/lifecycle/automatic-use.js +165 -0
- package/dist/lifecycle/compatibility-metadata.d.ts +19 -0
- package/dist/lifecycle/compatibility-metadata.js +45 -0
- package/dist/lifecycle/compatibility.d.ts +52 -0
- package/dist/lifecycle/compatibility.js +60 -0
- package/dist/lifecycle/fetch-archive.js +26 -24
- package/dist/lifecycle/index.d.ts +14 -2
- package/dist/lifecycle/index.js +8 -1
- package/dist/lifecycle/install.js +7 -0
- package/dist/lifecycle/installed-package-index.js +41 -29
- package/dist/lifecycle/installed-package-roots.js +28 -20
- package/dist/lifecycle/manifest.d.ts +13 -0
- package/dist/lifecycle/manifest.js +115 -101
- package/dist/lifecycle/package-paths.js +3 -3
- package/dist/lifecycle/ports.d.ts +1 -1
- package/dist/lifecycle/provider-contribution.d.ts +65 -0
- package/dist/lifecycle/provider-contribution.js +93 -0
- package/dist/lifecycle/resolve-agent-plugin-refs.js +15 -2
- package/dist/lifecycle/retire-bundled.js +20 -18
- package/dist/lifecycle/search.d.ts +6 -1
- package/dist/lifecycle/search.js +45 -7
- package/dist/lifecycle/set-enabled.d.ts +2 -1
- package/dist/lifecycle/set-enabled.js +52 -8
- package/dist/lifecycle/skill-file-reader.js +85 -71
- package/dist/lifecycle/tools/capabilities.d.ts +199 -0
- package/dist/lifecycle/tools/capabilities.js +213 -0
- package/dist/lifecycle/tools/definition.d.ts +32 -0
- package/dist/lifecycle/tools/definition.js +33 -0
- package/dist/lifecycle/tools/index.d.ts +10 -0
- package/dist/lifecycle/tools/index.js +10 -0
- package/dist/lifecycle/tools/manifest.d.ts +33 -0
- package/dist/lifecycle/tools/manifest.js +89 -0
- package/dist/lifecycle/tools/projection.d.ts +90 -0
- package/dist/lifecycle/tools/projection.js +283 -0
- package/dist/lifecycle/types.d.ts +2 -0
- package/dist/lifecycle/uninstall.d.ts +3 -1
- package/dist/lifecycle/uninstall.js +51 -27
- package/dist/lifecycle/update-plan.d.ts +18 -0
- package/dist/lifecycle/update-plan.js +22 -0
- package/dist/lifecycle/update-store.d.ts +71 -0
- package/dist/lifecycle/update-store.js +150 -0
- package/dist/lifecycle/update.d.ts +55 -0
- package/dist/lifecycle/update.js +321 -0
- package/package.json +38 -6
- package/ui-ux-design/plugin.json +7 -2
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The surface-side half of the admin-surface message protocol, for a plugin's own UI.
|
|
3
|
+
*
|
|
4
|
+
* Self-contained on purpose (no imports): a plugin's surface is plain static files, so it may import
|
|
5
|
+
* this module from the package or copy the compiled file next to its entry. The constants repeat
|
|
6
|
+
* `./protocol.ts`; the vertical-slice test proves the two still agree.
|
|
7
|
+
*
|
|
8
|
+
* Trust rules it applies: it accepts init only from its parent window (once), learns the host's
|
|
9
|
+
* origin from that init and posts only to that origin (never "*"), and accepts a response only from
|
|
10
|
+
* the parent, from that origin, for this mount, for a request it still has pending, in a valid shape.
|
|
11
|
+
*
|
|
12
|
+
* Theme: the host may send its design tokens with init and again in `theme` messages (`./theme.ts`).
|
|
13
|
+
* The client applies the `--jini-*` ones with a safe value as custom properties on the surface's root
|
|
14
|
+
* element (CSSOM `setProperty`, which a CSP without 'unsafe-inline' allows), plus `color-scheme` and
|
|
15
|
+
* `data-jini-scheme`, so the surface's own stylesheet can follow the admin's look and light/dark mode.
|
|
16
|
+
*/
|
|
17
|
+
const PROTOCOL = 'jini.admin-surface';
|
|
18
|
+
const VERSION = 1;
|
|
19
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
20
|
+
const THEME_TOKEN = /^--jini-[a-z0-9-]{1,60}$/;
|
|
21
|
+
const UNSAFE_THEME_VALUE = /url\(|image\(|image-set\(|[;{}<>\\@]/i;
|
|
22
|
+
const MAX_THEME_TOKENS = 100;
|
|
23
|
+
/** Rejection value of `call` when the host refused the action or the request timed out. */
|
|
24
|
+
export class AdminSurfaceCallError extends Error {
|
|
25
|
+
code;
|
|
26
|
+
retryable;
|
|
27
|
+
constructor(required) {
|
|
28
|
+
super(required.message);
|
|
29
|
+
this.name = 'AdminSurfaceCallError';
|
|
30
|
+
this.code = required.code;
|
|
31
|
+
this.retryable = required.retryable;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function isMessage(data, kind) {
|
|
35
|
+
if (typeof data !== 'object' || data === null)
|
|
36
|
+
return false;
|
|
37
|
+
const message = data;
|
|
38
|
+
return message.protocol === PROTOCOL && message.version === VERSION && message.kind === kind;
|
|
39
|
+
}
|
|
40
|
+
function isFailure(value) {
|
|
41
|
+
if (typeof value !== 'object' || value === null)
|
|
42
|
+
return false;
|
|
43
|
+
const error = value;
|
|
44
|
+
return typeof error.code === 'string' && typeof error.message === 'string' && typeof error.retryable === 'boolean';
|
|
45
|
+
}
|
|
46
|
+
/** The host's theme with every unknown name or unsafe value dropped; undefined when it is not a theme. */
|
|
47
|
+
function acceptedTheme(value) {
|
|
48
|
+
if (typeof value !== 'object' || value === null)
|
|
49
|
+
return undefined;
|
|
50
|
+
const { scheme, tokens } = value;
|
|
51
|
+
if ((scheme !== 'light' && scheme !== 'dark') || typeof tokens !== 'object' || tokens === null)
|
|
52
|
+
return undefined;
|
|
53
|
+
const safe = Object.entries(tokens).slice(0, MAX_THEME_TOKENS).filter(([name, token]) => THEME_TOKEN.test(name)
|
|
54
|
+
&& typeof token === 'string' && token.length > 0 && token.length <= 200 && !UNSAFE_THEME_VALUE.test(token));
|
|
55
|
+
return { scheme, tokens: Object.fromEntries(safe) };
|
|
56
|
+
}
|
|
57
|
+
/** The browser root when there is a document (none in a server-side test). */
|
|
58
|
+
function documentRoot(scope) {
|
|
59
|
+
return scope.document?.documentElement ?? null;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The browser's size source: the body's height including what overflows it (an open toggletip), re-read
|
|
63
|
+
* on resize, DOM changes, hover and focus (hover/focus can show a CSS-only tooltip). Null without a DOM.
|
|
64
|
+
*/
|
|
65
|
+
function documentSize(scope) {
|
|
66
|
+
const body = scope.document?.body;
|
|
67
|
+
if (!body || !scope.ResizeObserver || !scope.MutationObserver || !scope.getComputedStyle)
|
|
68
|
+
return null;
|
|
69
|
+
const { ResizeObserver: Resize, MutationObserver: Mutation, getComputedStyle: computedStyle } = scope;
|
|
70
|
+
return (onHeight) => {
|
|
71
|
+
const measure = () => {
|
|
72
|
+
const style = computedStyle(body);
|
|
73
|
+
// scrollHeight is rounded to whole pixels; the box height keeps the fraction, which would
|
|
74
|
+
// otherwise leave a 1px inner scrollbar.
|
|
75
|
+
const content = Math.max(body.scrollHeight, body.getBoundingClientRect().height);
|
|
76
|
+
onHeight(Math.ceil(content + parseFloat(style.marginTop || '0') + parseFloat(style.marginBottom || '0')));
|
|
77
|
+
};
|
|
78
|
+
const resize = new Resize(measure);
|
|
79
|
+
const mutation = new Mutation(measure);
|
|
80
|
+
resize.observe(body);
|
|
81
|
+
mutation.observe(body, { subtree: true, childList: true, attributes: true, characterData: true });
|
|
82
|
+
body.addEventListener('mouseover', measure);
|
|
83
|
+
body.addEventListener('focusin', measure);
|
|
84
|
+
measure();
|
|
85
|
+
return () => { resize.disconnect(); mutation.disconnect(); body.removeEventListener('mouseover', measure); body.removeEventListener('focusin', measure); };
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/** A response has `ok: true` with a result, or `ok: false` with a well-formed failure. */
|
|
89
|
+
function isWellFormedResponse(message) {
|
|
90
|
+
return message.ok === true ? Object.hasOwn(message, 'result') : message.ok === false && isFailure(message.error);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Connects a surface to its host. In a browser:
|
|
94
|
+
* `createAdminSurfaceClient({ parent: window.parent, listen: (h) => { addEventListener('message', h); return () => removeEventListener('message', h); } })`.
|
|
95
|
+
* @param optional.timeoutMs - How long `call` waits for a response before rejecting with `timeout`.
|
|
96
|
+
* @complexity O(1) per message.
|
|
97
|
+
*/
|
|
98
|
+
export function createAdminSurfaceClient(required, optional = {}) {
|
|
99
|
+
const timeoutMs = optional.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
100
|
+
const nowMs = optional.nowMs ?? Date.now;
|
|
101
|
+
const pending = new Map();
|
|
102
|
+
let init;
|
|
103
|
+
let nextRequest = 0;
|
|
104
|
+
// The Promise executor runs synchronously, so `resolveReady` is assigned before anything can call it.
|
|
105
|
+
let resolveReady;
|
|
106
|
+
const ready = new Promise((resolve) => { resolveReady = resolve; });
|
|
107
|
+
// The DOM's own observer/element types are wider than the structural subset this client reads.
|
|
108
|
+
const browser = optional.browser ?? globalThis;
|
|
109
|
+
const themeRoot = optional.themeRoot === undefined ? documentRoot(browser) : optional.themeRoot;
|
|
110
|
+
let currentTheme;
|
|
111
|
+
const size = optional.size === undefined ? documentSize(browser) : optional.size;
|
|
112
|
+
let stopSize;
|
|
113
|
+
let lastHeight = -1;
|
|
114
|
+
/** Tells the host this surface's height (only when it changed), so the frame fits its content. */
|
|
115
|
+
function reportHeight(height) {
|
|
116
|
+
if (init === undefined || !Number.isFinite(height) || height === lastHeight)
|
|
117
|
+
return;
|
|
118
|
+
lastHeight = height;
|
|
119
|
+
required.parent.postMessage({ protocol: PROTOCOL, version: VERSION, kind: 'resize', mountId: init.mountId, height }, init.origin);
|
|
120
|
+
}
|
|
121
|
+
/** Replaces the applied tokens: a token the new theme no longer sends falls back to the surface's CSS. */
|
|
122
|
+
function applyTheme(value) {
|
|
123
|
+
const next = acceptedTheme(value);
|
|
124
|
+
if (next === undefined)
|
|
125
|
+
return;
|
|
126
|
+
if (themeRoot !== null) {
|
|
127
|
+
for (const name of Object.keys(currentTheme?.tokens ?? {}))
|
|
128
|
+
if (!(name in next.tokens))
|
|
129
|
+
themeRoot.style.removeProperty(name);
|
|
130
|
+
for (const [name, token] of Object.entries(next.tokens))
|
|
131
|
+
themeRoot.style.setProperty(name, token);
|
|
132
|
+
themeRoot.style.setProperty('color-scheme', next.scheme);
|
|
133
|
+
themeRoot.setAttribute('data-jini-scheme', next.scheme);
|
|
134
|
+
}
|
|
135
|
+
currentTheme = next;
|
|
136
|
+
}
|
|
137
|
+
function onInit(event, data) {
|
|
138
|
+
if (init !== undefined || typeof data.mountId !== 'string')
|
|
139
|
+
return;
|
|
140
|
+
init = { mountId: data.mountId, origin: event.origin, bootstrap: data.bootstrap };
|
|
141
|
+
applyTheme(data.theme);
|
|
142
|
+
stopSize = size?.(reportHeight);
|
|
143
|
+
resolveReady();
|
|
144
|
+
}
|
|
145
|
+
function onResponse(event, data) {
|
|
146
|
+
if (event.origin !== init?.origin || data.mountId !== init.mountId || typeof data.requestId !== 'string')
|
|
147
|
+
return;
|
|
148
|
+
const waiting = pending.get(data.requestId);
|
|
149
|
+
if (waiting === undefined || !isWellFormedResponse(data))
|
|
150
|
+
return;
|
|
151
|
+
pending.delete(data.requestId);
|
|
152
|
+
clearTimeout(waiting.timer);
|
|
153
|
+
if (data.ok === true)
|
|
154
|
+
waiting.resolve(data.result);
|
|
155
|
+
else
|
|
156
|
+
waiting.reject(new AdminSurfaceCallError(data.error));
|
|
157
|
+
}
|
|
158
|
+
const unsubscribe = required.listen((event) => {
|
|
159
|
+
if (event.source !== required.parent)
|
|
160
|
+
return;
|
|
161
|
+
if (isMessage(event.data, 'init'))
|
|
162
|
+
onInit(event, event.data);
|
|
163
|
+
else if (isMessage(event.data, 'theme')) {
|
|
164
|
+
if (event.origin === init?.origin && event.data.mountId === init.mountId)
|
|
165
|
+
applyTheme(event.data.theme);
|
|
166
|
+
}
|
|
167
|
+
else if (isMessage(event.data, 'action-response'))
|
|
168
|
+
onResponse(event, event.data);
|
|
169
|
+
});
|
|
170
|
+
/** Posts one request of `kind` to the host origin and waits for its `action-response`. */
|
|
171
|
+
async function send(kind, fields) {
|
|
172
|
+
if (init === undefined)
|
|
173
|
+
await ready;
|
|
174
|
+
const { mountId, origin } = init;
|
|
175
|
+
nextRequest += 1;
|
|
176
|
+
const requestId = `r${nextRequest}`;
|
|
177
|
+
return new Promise((resolve, reject) => {
|
|
178
|
+
const timer = setTimeout(() => {
|
|
179
|
+
pending.delete(requestId);
|
|
180
|
+
reject(new AdminSurfaceCallError({ code: 'timeout', message: 'The host did not answer in time. Try again.', retryable: true }));
|
|
181
|
+
}, timeoutMs);
|
|
182
|
+
pending.set(requestId, { resolve, reject, timer });
|
|
183
|
+
required.parent.postMessage({ protocol: PROTOCOL, version: VERSION, kind, mountId, requestId, ...fields }, origin);
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
/** Asks the host to run one declared action; resolves with its result. */
|
|
187
|
+
function call(input) {
|
|
188
|
+
return send('action-request', { action: input.action, payload: input.payload });
|
|
189
|
+
}
|
|
190
|
+
/** Asks the host to open `url` outside the sandbox; only https links on the surface's declared
|
|
191
|
+
* `linkOrigins` open, and the host decides how (new tab, system browser). Rejects with the host's refusal. */
|
|
192
|
+
async function openLink(input) {
|
|
193
|
+
await send('open-link', { url: input.url });
|
|
194
|
+
}
|
|
195
|
+
/** The bootstrap `data` the host sent with init, or `undefined` once it has expired (or none was sent). */
|
|
196
|
+
function bootstrap() {
|
|
197
|
+
const envelope = init?.bootstrap;
|
|
198
|
+
if (typeof envelope !== 'object' || envelope === null)
|
|
199
|
+
return undefined;
|
|
200
|
+
const { expiresAt, data } = envelope;
|
|
201
|
+
return typeof expiresAt === 'number' && nowMs() < expiresAt ? data : undefined;
|
|
202
|
+
}
|
|
203
|
+
/** The host's theme as applied, or `undefined` when the host sent none. */
|
|
204
|
+
function theme() {
|
|
205
|
+
return currentTheme;
|
|
206
|
+
}
|
|
207
|
+
/** Stops listening and rejects every pending call. */
|
|
208
|
+
function close() {
|
|
209
|
+
unsubscribe();
|
|
210
|
+
stopSize?.();
|
|
211
|
+
for (const [requestId, waiting] of pending) {
|
|
212
|
+
clearTimeout(waiting.timer);
|
|
213
|
+
waiting.reject(new AdminSurfaceCallError({ code: 'closed', message: 'The surface was closed before the host answered.', retryable: false }));
|
|
214
|
+
pending.delete(requestId);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return { ready, call, openLink, bootstrap, theme, close };
|
|
218
|
+
}
|
|
219
|
+
//# sourceMappingURL=surface-client.js.map
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Plugin-supplied server-side handlers for admin surfaces: a surface may name a contained
|
|
3
|
+
* `.mjs` `handlerModule` that answers its declared actions and may provide its bootstrap data.
|
|
4
|
+
*
|
|
5
|
+
* Why it exists: without it a host can only answer a surface's actions with its own code, so a
|
|
6
|
+
* provider plugin cannot ship a working settings or resource-chooser screen. Why it is safe to run plugin code
|
|
7
|
+
* here: the module loads only through the lifecycle's one executable loader
|
|
8
|
+
* (`defineExecutablePluginContribution`: active plugin, shipped digest, on-disk containment), it
|
|
9
|
+
* receives host capability ports (never a raw secret) through the admin-surface capability session
|
|
10
|
+
* (`./surface-capabilities.ts`: only the surface's declared connections and config, revoked on disable,
|
|
11
|
+
* results and errors scanned for tokens the call obtained, the same gate as executable tools), and the
|
|
12
|
+
* dispatcher still checks the declared input and result schemas around it.
|
|
13
|
+
* Bootstrap data is the one answer allowed to carry an access token (a surface may need a short-lived
|
|
14
|
+
* one); it must match the surface's declared `bootstrap` schema and is never cached by the host.
|
|
15
|
+
*/
|
|
16
|
+
import { type LoadedPluginContribution, type PluginContributionDefinition } from '../lifecycle/contribution-loader.js';
|
|
17
|
+
import type { IssuedTokenMemories } from '../lifecycle/tools/capabilities.js';
|
|
18
|
+
import { type AdminSurfaceCapabilities, type AdminSurfaceCapabilitiesPort } from './surface-capabilities.js';
|
|
19
|
+
import { type AdminSurfaceActionHandler } from './action-dispatcher.js';
|
|
20
|
+
import { type AdminSurfaceDescriptor } from './descriptor.js';
|
|
21
|
+
/** Who a call is for. The host route establishes it; plugin and surface ids are rebound to the
|
|
22
|
+
* loaded module's own before a handler sees them. */
|
|
23
|
+
export interface AdminSurfaceHandlerContext {
|
|
24
|
+
readonly workspaceId: string;
|
|
25
|
+
readonly principalId?: string;
|
|
26
|
+
readonly pluginId: string;
|
|
27
|
+
readonly surfaceId: string;
|
|
28
|
+
}
|
|
29
|
+
/** Which kind of host is running the surface, so a plugin picks its flow (for example a sign-in in
|
|
30
|
+
* the system browser on desktop) without inferring it from config. */
|
|
31
|
+
export type AdminSurfaceHostKind = 'hosted' | 'desktop';
|
|
32
|
+
/** The context a handler sees: the route's context plus the plugin's own declared connection ids
|
|
33
|
+
* (ids, never refs: the host resolves the ref, so a plugin can never name another plugin's connection)
|
|
34
|
+
* and the host kind the host was built with (never a caller's or the plugin's value). */
|
|
35
|
+
export interface AdminSurfacePluginContext extends AdminSurfaceHandlerContext {
|
|
36
|
+
readonly connectionIds: readonly string[];
|
|
37
|
+
readonly hostKind: AdminSurfaceHostKind;
|
|
38
|
+
}
|
|
39
|
+
/** What a bootstrap provider receives. */
|
|
40
|
+
export interface AdminSurfaceBootstrapCall {
|
|
41
|
+
readonly capabilities: AdminSurfaceCapabilities;
|
|
42
|
+
readonly context: AdminSurfacePluginContext;
|
|
43
|
+
}
|
|
44
|
+
/** What an action handler receives. `input` already matched the action's declared input schema. */
|
|
45
|
+
export interface AdminSurfaceHandlerCall extends AdminSurfaceBootstrapCall {
|
|
46
|
+
readonly input: unknown;
|
|
47
|
+
/** Builds a surface-safe failure to throw: `code` (lowercase hyphenated) and `message` reach the frame. */
|
|
48
|
+
readonly fail: (required: {
|
|
49
|
+
readonly code: string;
|
|
50
|
+
readonly message: string;
|
|
51
|
+
}, optional?: {
|
|
52
|
+
readonly retryable?: boolean;
|
|
53
|
+
}) => Error;
|
|
54
|
+
}
|
|
55
|
+
/** What a handler module's default export must be. */
|
|
56
|
+
export interface AdminSurfaceHandlerModule {
|
|
57
|
+
/** Keyed by declared action name; a declared action without one answers `action-unavailable`. */
|
|
58
|
+
readonly actions: Readonly<Record<string, (required: AdminSurfaceHandlerCall, optional?: Record<string, never>) => unknown>>;
|
|
59
|
+
/** Data the surface starts with; requires a `bootstrap` schema on the surface. */
|
|
60
|
+
readonly bootstrap?: (required: AdminSurfaceBootstrapCall, optional?: Record<string, never>) => unknown;
|
|
61
|
+
}
|
|
62
|
+
type HandledSurface = AdminSurfaceDescriptor & {
|
|
63
|
+
readonly handlerModule: string;
|
|
64
|
+
};
|
|
65
|
+
export type LoadedAdminSurfaceHandlers = LoadedPluginContribution<HandledSurface, AdminSurfaceHandlerModule>;
|
|
66
|
+
export declare const ADMIN_SURFACE_PLUGIN_DISABLED_MESSAGE = "This plugin was disabled, so its admin surface cannot act. Re-enable the plugin and reopen the surface.";
|
|
67
|
+
/**
|
|
68
|
+
* The executable contribution for the host's surfaces file (the same file `defineAdminSurfaceContribution`
|
|
69
|
+
* reads): one item per surface that names a `handlerModule`.
|
|
70
|
+
* @param required.filename - The host-named surfaces file at the plugin root.
|
|
71
|
+
* @complexity O(1); each load parses the file and performs one contained import per handled surface.
|
|
72
|
+
*/
|
|
73
|
+
export declare function defineAdminSurfaceHandlerContribution(required: {
|
|
74
|
+
readonly filename: string;
|
|
75
|
+
}, _optional?: Record<string, never>): PluginContributionDefinition<HandledSurface, LoadedAdminSurfaceHandlers>;
|
|
76
|
+
/**
|
|
77
|
+
* Holds the admitted handler modules and answers for them. Call `sync` with every load of
|
|
78
|
+
* `defineAdminSurfaceHandlerContribution` (on boot and on every activation change); a surface missing
|
|
79
|
+
* from a load loses its handlers, and capabilities its module kept are revoked.
|
|
80
|
+
* @param required.capabilities - Host port building one call's admin-surface capabilities.
|
|
81
|
+
* @param required.issuedTokens - Per-plugin token memory shared with the plugin's tool projection.
|
|
82
|
+
* @param required.hostKind - The kind of host this process is; every handler and bootstrap call sees it.
|
|
83
|
+
* @throws TypeError when `hostKind` is neither `hosted` nor `desktop`.
|
|
84
|
+
* @returns `sync`, plus `handlersFor` and `bootstrapData` for the host's action dispatcher and bootstrap route.
|
|
85
|
+
* @complexity sync is O(s + a) surfaces and actions; lookups are O(1).
|
|
86
|
+
*/
|
|
87
|
+
export declare function createAdminSurfaceHandlerHost(required: {
|
|
88
|
+
readonly capabilities: AdminSurfaceCapabilitiesPort;
|
|
89
|
+
readonly issuedTokens: IssuedTokenMemories;
|
|
90
|
+
readonly hostKind: AdminSurfaceHostKind;
|
|
91
|
+
}, _optional?: Record<string, never>): {
|
|
92
|
+
/** Admits one load result. @returns The loader's refusals plus this seam's own, in order. */
|
|
93
|
+
sync(input: {
|
|
94
|
+
readonly loaded: readonly LoadedAdminSurfaceHandlers[];
|
|
95
|
+
readonly refusals: readonly string[];
|
|
96
|
+
}, _options?: Record<string, never>): {
|
|
97
|
+
readonly refusals: readonly string[];
|
|
98
|
+
};
|
|
99
|
+
/** The plugin's handlers for one surface (empty when none are admitted), for `createAdminSurfaceActionDispatcher`. */
|
|
100
|
+
handlersFor(input: {
|
|
101
|
+
readonly pluginId: string;
|
|
102
|
+
readonly surfaceId: string;
|
|
103
|
+
}, _options?: Record<string, never>): Readonly<Record<string, AdminSurfaceActionHandler<AdminSurfaceHandlerContext>>>;
|
|
104
|
+
/** The plugin's bootstrap data for `context`'s surface, or null when it provides none.
|
|
105
|
+
* @throws Error when the data does not match the declared schema (the message names a path, never a value). */
|
|
106
|
+
bootstrapData(input: {
|
|
107
|
+
readonly context: AdminSurfaceHandlerContext;
|
|
108
|
+
}, _options?: Record<string, never>): Promise<unknown>;
|
|
109
|
+
};
|
|
110
|
+
export {};
|
|
111
|
+
//# sourceMappingURL=surface-handlers.d.ts.map
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Plugin-supplied server-side handlers for admin surfaces: a surface may name a contained
|
|
3
|
+
* `.mjs` `handlerModule` that answers its declared actions and may provide its bootstrap data.
|
|
4
|
+
*
|
|
5
|
+
* Why it exists: without it a host can only answer a surface's actions with its own code, so a
|
|
6
|
+
* provider plugin cannot ship a working settings or resource-chooser screen. Why it is safe to run plugin code
|
|
7
|
+
* here: the module loads only through the lifecycle's one executable loader
|
|
8
|
+
* (`defineExecutablePluginContribution`: active plugin, shipped digest, on-disk containment), it
|
|
9
|
+
* receives host capability ports (never a raw secret) through the admin-surface capability session
|
|
10
|
+
* (`./surface-capabilities.ts`: only the surface's declared connections and config, revoked on disable,
|
|
11
|
+
* results and errors scanned for tokens the call obtained, the same gate as executable tools), and the
|
|
12
|
+
* dispatcher still checks the declared input and result schemas around it.
|
|
13
|
+
* Bootstrap data is the one answer allowed to carry an access token (a surface may need a short-lived
|
|
14
|
+
* one); it must match the surface's declared `bootstrap` schema and is never cached by the host.
|
|
15
|
+
*/
|
|
16
|
+
import { defineExecutablePluginContribution } from '../lifecycle/contribution-loader.js';
|
|
17
|
+
import { openAdminSurfaceCapabilitySession } from './surface-capabilities.js';
|
|
18
|
+
import { AdminSurfaceActionError } from './action-dispatcher.js';
|
|
19
|
+
import { validateAdminSurfaceActionValue } from './action-schema.js';
|
|
20
|
+
import { parseAdminSurfacesFile } from './descriptor.js';
|
|
21
|
+
const HOST_KINDS = ['hosted', 'desktop'];
|
|
22
|
+
export const ADMIN_SURFACE_PLUGIN_DISABLED_MESSAGE = 'This plugin was disabled, so its admin surface cannot act. Re-enable the plugin and reopen the surface.';
|
|
23
|
+
const FAILURE_CODE_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
24
|
+
const MAX_FAILURE_CODE_LENGTH = 64;
|
|
25
|
+
const MAX_FAILURE_MESSAGE_LENGTH = 300;
|
|
26
|
+
const isFunction = (value) => typeof value === 'function';
|
|
27
|
+
const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
28
|
+
/** The module's export as a handler module, or why not. Shape only; declarations are checked at sync. @complexity O(a) exported actions. */
|
|
29
|
+
function validateHandlerModule({ exported }) {
|
|
30
|
+
if (!isRecord(exported))
|
|
31
|
+
return "its module's default export is not an object";
|
|
32
|
+
if (!isRecord(exported.actions) || !Object.values(exported.actions).every(isFunction))
|
|
33
|
+
return "its module's default export needs an actions object of functions";
|
|
34
|
+
if (exported.bootstrap !== undefined && !isFunction(exported.bootstrap))
|
|
35
|
+
return 'bootstrap must be a function';
|
|
36
|
+
return exported;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The executable contribution for the host's surfaces file (the same file `defineAdminSurfaceContribution`
|
|
40
|
+
* reads): one item per surface that names a `handlerModule`.
|
|
41
|
+
* @param required.filename - The host-named surfaces file at the plugin root.
|
|
42
|
+
* @complexity O(1); each load parses the file and performs one contained import per handled surface.
|
|
43
|
+
*/
|
|
44
|
+
export function defineAdminSurfaceHandlerContribution(required, _optional = {}) {
|
|
45
|
+
return defineExecutablePluginContribution({
|
|
46
|
+
filename: required.filename, contribution: 'admin surface handlers',
|
|
47
|
+
parse: ({ raw }) => {
|
|
48
|
+
const parsed = parseAdminSurfacesFile({ raw });
|
|
49
|
+
return parsed.ok ? { ok: true, descriptors: parsed.descriptors.filter((descriptor) => descriptor.handlerModule !== undefined) } : parsed;
|
|
50
|
+
},
|
|
51
|
+
modulePath: ({ descriptor }) => descriptor.handlerModule, validate: validateHandlerModule,
|
|
52
|
+
refusal: ({ plugin, descriptor, reason }) => `admin surface handlers for '${descriptor.id}' from '${plugin.pluginId}' were not loaded: ${reason}`,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
/** A surface-safe failure, or a generic one when the plugin's code is not a valid id. @complexity O(1). */
|
|
56
|
+
function fail(required, optional = {}) {
|
|
57
|
+
const { code, message } = required;
|
|
58
|
+
if (typeof code !== 'string' || code.length > MAX_FAILURE_CODE_LENGTH || !FAILURE_CODE_PATTERN.test(code))
|
|
59
|
+
return new Error('a plugin failure used an invalid code');
|
|
60
|
+
return new AdminSurfaceActionError({ code, message: String(message).slice(0, MAX_FAILURE_MESSAGE_LENGTH) }, { retryable: optional.retryable === true });
|
|
61
|
+
}
|
|
62
|
+
/** Why a loaded module cannot answer for its surface, or null. @complexity O(a) exported actions. */
|
|
63
|
+
function admissionRefusal({ descriptor, module }) {
|
|
64
|
+
const undeclared = Object.keys(module.actions).find((name) => !descriptor.actions.some((action) => action.name === name));
|
|
65
|
+
if (undeclared !== undefined)
|
|
66
|
+
return `it exports a handler for undeclared action '${undeclared.slice(0, 64)}'`;
|
|
67
|
+
return module.bootstrap !== undefined && descriptor.bootstrap === undefined ? 'it exports bootstrap() but the surface declares no bootstrap schema' : null;
|
|
68
|
+
}
|
|
69
|
+
/** Plugin and surface ids cannot contain NUL, so the key is unambiguous. */
|
|
70
|
+
const keyOf = (pluginId, surfaceId) => `${pluginId}\u0000${surfaceId}`;
|
|
71
|
+
/**
|
|
72
|
+
* Holds the admitted handler modules and answers for them. Call `sync` with every load of
|
|
73
|
+
* `defineAdminSurfaceHandlerContribution` (on boot and on every activation change); a surface missing
|
|
74
|
+
* from a load loses its handlers, and capabilities its module kept are revoked.
|
|
75
|
+
* @param required.capabilities - Host port building one call's admin-surface capabilities.
|
|
76
|
+
* @param required.issuedTokens - Per-plugin token memory shared with the plugin's tool projection.
|
|
77
|
+
* @param required.hostKind - The kind of host this process is; every handler and bootstrap call sees it.
|
|
78
|
+
* @throws TypeError when `hostKind` is neither `hosted` nor `desktop`.
|
|
79
|
+
* @returns `sync`, plus `handlersFor` and `bootstrapData` for the host's action dispatcher and bootstrap route.
|
|
80
|
+
* @complexity sync is O(s + a) surfaces and actions; lookups are O(1).
|
|
81
|
+
*/
|
|
82
|
+
export function createAdminSurfaceHandlerHost(required, _optional = {}) {
|
|
83
|
+
// Checked at run time too: a JavaScript host may pass an unchecked config value, and a plugin would pick its flow from it.
|
|
84
|
+
if (!HOST_KINDS.includes(required.hostKind))
|
|
85
|
+
throw new TypeError("createAdminSurfaceHandlerHost: hostKind must be 'hosted' or 'desktop'.");
|
|
86
|
+
const { hostKind } = required;
|
|
87
|
+
let admitted = new Map();
|
|
88
|
+
/** One call's capability session, scoped to the loaded plugin and the route's workspace/principal. */
|
|
89
|
+
function sessionFor(self, context) {
|
|
90
|
+
const { pluginId, descriptor } = self.loaded;
|
|
91
|
+
const principal = context.principalId === undefined ? {} : { principalId: context.principalId };
|
|
92
|
+
const capabilities = required.capabilities({ pluginId, workspaceId: context.workspaceId, ...principal });
|
|
93
|
+
const session = openAdminSurfaceCapabilitySession({ capabilities, isLive: self.isLive, declared: descriptor, issued: self.issued }, { ...(principal.principalId !== undefined && { actorId: principal.principalId }) });
|
|
94
|
+
const connectionIds = (descriptor.connections ?? []).map((connection) => connection.id);
|
|
95
|
+
return { session, context: { workspaceId: context.workspaceId, ...principal, pluginId, surfaceId: descriptor.id, connectionIds, hostKind } };
|
|
96
|
+
}
|
|
97
|
+
function actionHandler(self, run) {
|
|
98
|
+
return async ({ payload, context: caller }) => {
|
|
99
|
+
if (!self.isLive())
|
|
100
|
+
throw new AdminSurfaceActionError({ code: 'plugin-disabled', message: ADMIN_SURFACE_PLUGIN_DISABLED_MESSAGE });
|
|
101
|
+
const { session, context } = sessionFor(self, caller);
|
|
102
|
+
try {
|
|
103
|
+
return session.release({ value: await run({ input: payload, capabilities: session.capabilities, context, fail }) });
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
throw session.sanitize({ error });
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
/** A generation for one surface; live only while `admitted` holds this exact object. */
|
|
111
|
+
function admit(key, loaded) {
|
|
112
|
+
const declared = loaded.descriptor.actions.filter((action) => Object.hasOwn(loaded.module.actions, action.name));
|
|
113
|
+
const self = { loaded, handlers: {}, isLive: () => admitted.get(key) === self, issued: required.issuedTokens.forPlugin({ pluginId: loaded.pluginId }) };
|
|
114
|
+
Object.assign(self.handlers, Object.fromEntries(declared.map((action) => [action.name, actionHandler(self, loaded.module.actions[action.name])])));
|
|
115
|
+
return Object.freeze(self);
|
|
116
|
+
}
|
|
117
|
+
/** The previous generation when module and declaration are unchanged, so re-syncing never revokes it. */
|
|
118
|
+
function unchanged(previous, loaded) {
|
|
119
|
+
const same = previous !== undefined && previous.loaded.module === loaded.module && JSON.stringify(previous.loaded.descriptor) === JSON.stringify(loaded.descriptor);
|
|
120
|
+
return same ? previous : undefined;
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
/** Admits one load result. @returns The loader's refusals plus this seam's own, in order. */
|
|
124
|
+
sync(input, _options = {}) {
|
|
125
|
+
const refusals = [...input.refusals];
|
|
126
|
+
const claims = new Map();
|
|
127
|
+
for (const entry of input.loaded)
|
|
128
|
+
claims.set(keyOf(entry.pluginId, entry.descriptor.id), (claims.get(keyOf(entry.pluginId, entry.descriptor.id)) ?? 0) + 1);
|
|
129
|
+
const next = new Map();
|
|
130
|
+
const previous = admitted;
|
|
131
|
+
for (const entry of input.loaded) {
|
|
132
|
+
const key = keyOf(entry.pluginId, entry.descriptor.id);
|
|
133
|
+
const reason = claims.get(key) > 1 ? 'more than one handler module claims this surface' : admissionRefusal(entry);
|
|
134
|
+
const line = `admin surface handlers for '${entry.descriptor.id}' from '${entry.pluginId}' were not loaded: ${reason}`;
|
|
135
|
+
if (reason !== null) {
|
|
136
|
+
if (!refusals.includes(line))
|
|
137
|
+
refusals.push(line);
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
next.set(key, unchanged(previous.get(key), entry) ?? admit(key, entry));
|
|
141
|
+
}
|
|
142
|
+
admitted = next;
|
|
143
|
+
return { refusals };
|
|
144
|
+
},
|
|
145
|
+
/** The plugin's handlers for one surface (empty when none are admitted), for `createAdminSurfaceActionDispatcher`. */
|
|
146
|
+
handlersFor(input, _options = {}) {
|
|
147
|
+
return admitted.get(keyOf(input.pluginId, input.surfaceId))?.handlers ?? {};
|
|
148
|
+
},
|
|
149
|
+
/** The plugin's bootstrap data for `context`'s surface, or null when it provides none.
|
|
150
|
+
* @throws Error when the data does not match the declared schema (the message names a path, never a value). */
|
|
151
|
+
async bootstrapData(input, _options = {}) {
|
|
152
|
+
const self = admitted.get(keyOf(input.context.pluginId, input.context.surfaceId));
|
|
153
|
+
const provide = self?.loaded.module.bootstrap;
|
|
154
|
+
if (self === undefined || provide === undefined)
|
|
155
|
+
return null;
|
|
156
|
+
const { session, context } = sessionFor(self, input.context);
|
|
157
|
+
let data;
|
|
158
|
+
try {
|
|
159
|
+
data = await provide({ capabilities: session.capabilities, context });
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
throw session.sanitize({ error });
|
|
163
|
+
}
|
|
164
|
+
// Admission guarantees a schema whenever the module provides bootstrap data.
|
|
165
|
+
const violation = validateAdminSurfaceActionValue({ schema: self.loaded.descriptor.bootstrap, value: data });
|
|
166
|
+
if (violation !== null)
|
|
167
|
+
throw new Error(`Admin surface '${context.surfaceId}' from '${context.pluginId}' returned bootstrap data outside its declared schema (${violation}); report it to the plugin author.`);
|
|
168
|
+
return data;
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
//# sourceMappingURL=surface-handlers.js.map
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The host's design tokens for plugin surfaces: which `--jini-*` custom properties a host
|
|
3
|
+
* shares with every surface frame, how it reads their current values, and the envelope that sends
|
|
4
|
+
* a change after init.
|
|
5
|
+
*
|
|
6
|
+
* Why: a surface is a sandboxed frame without `allow-same-origin`, so it cannot see the admin's
|
|
7
|
+
* stylesheets, and its CSP allows only its own files. Values posted in a message are the only way
|
|
8
|
+
* its screens can match the admin and follow a light/dark or admin-theme change. The surface client
|
|
9
|
+
* applies them as custom properties on its root (CSSOM, which the CSP allows; never a style
|
|
10
|
+
* attribute or a stylesheet), so a surface writes `var(--jini-primary, <fallback>)` and looks right
|
|
11
|
+
* with or without a host that sends tokens.
|
|
12
|
+
*
|
|
13
|
+
* Values are text a host computed from its own CSS; anything that could fetch (`url(`, `image-set(`)
|
|
14
|
+
* or leave a declaration (`;`, braces) is dropped here and again in the surface client.
|
|
15
|
+
*/
|
|
16
|
+
import { ADMIN_SURFACE_PROTOCOL, ADMIN_SURFACE_PROTOCOL_VERSION } from './protocol.js';
|
|
17
|
+
/** The shared vocabulary: colours, type, radius, spacing, depth and focus. A host may send fewer. */
|
|
18
|
+
export declare const ADMIN_SURFACE_THEME_TOKENS: readonly ["--jini-bg", "--jini-surface", "--jini-bg-subtle", "--jini-text", "--jini-text-muted", "--jini-border", "--jini-border-strong", "--jini-primary", "--jini-primary-ink", "--jini-link", "--jini-danger", "--jini-danger-bg", "--jini-success", "--jini-success-bg", "--jini-warning", "--jini-warning-bg", "--jini-font-ui", "--jini-font-mono", "--jini-font-size-xs", "--jini-font-size-sm", "--jini-font-size-base", "--jini-font-size-md", "--jini-radius-sm", "--jini-radius", "--jini-radius-md", "--jini-radius-pill", "--jini-space-1", "--jini-space-2", "--jini-space-3", "--jini-space-4", "--jini-shadow-xs", "--jini-shadow-sm", "--jini-control-bg", "--jini-control-border", "--jini-control-shadow-focus", "--jini-overlay-tooltip-bg", "--jini-overlay-tooltip-text", "--jini-overlay-tooltip-border"];
|
|
19
|
+
export type AdminSurfaceThemeToken = (typeof ADMIN_SURFACE_THEME_TOKENS)[number];
|
|
20
|
+
export type AdminSurfaceColorScheme = 'light' | 'dark';
|
|
21
|
+
/** What a surface receives: the scheme for native controls plus the token values. */
|
|
22
|
+
export interface AdminSurfaceTheme {
|
|
23
|
+
readonly scheme: AdminSurfaceColorScheme;
|
|
24
|
+
readonly tokens: Readonly<Partial<Record<AdminSurfaceThemeToken, string>>>;
|
|
25
|
+
}
|
|
26
|
+
/** Host page -> surface after init, whenever the admin's theme or colour scheme changes. */
|
|
27
|
+
export interface AdminSurfaceThemeMessage {
|
|
28
|
+
readonly protocol: typeof ADMIN_SURFACE_PROTOCOL;
|
|
29
|
+
readonly version: typeof ADMIN_SURFACE_PROTOCOL_VERSION;
|
|
30
|
+
readonly kind: 'theme';
|
|
31
|
+
readonly mountId: string;
|
|
32
|
+
readonly theme: AdminSurfaceTheme;
|
|
33
|
+
}
|
|
34
|
+
/** Whether `value` is plain declaration text that cannot fetch anything or end its declaration.
|
|
35
|
+
* @complexity O(n) in the value length. */
|
|
36
|
+
export declare function isSafeAdminSurfaceThemeValue(value: unknown): value is string;
|
|
37
|
+
/**
|
|
38
|
+
* Reads the shared tokens from the host's computed style.
|
|
39
|
+
* @param required.style - Usually `getComputedStyle(element)` for the element the admin theme is on.
|
|
40
|
+
* @param optional.sources - Where to read a token when the host draws with its own name for it
|
|
41
|
+
* (e.g. `{ '--jini-text': '--fg' }`); unnamed tokens are read under their own name.
|
|
42
|
+
* @returns Only the tokens with a safe, non-empty value.
|
|
43
|
+
* @complexity O(t) tokens.
|
|
44
|
+
*/
|
|
45
|
+
export declare function readAdminSurfaceTheme(required: {
|
|
46
|
+
readonly style: {
|
|
47
|
+
getPropertyValue(name: string): string;
|
|
48
|
+
};
|
|
49
|
+
readonly scheme: AdminSurfaceColorScheme;
|
|
50
|
+
}, optional?: {
|
|
51
|
+
readonly sources?: Readonly<Partial<Record<AdminSurfaceThemeToken, string>>>;
|
|
52
|
+
}): AdminSurfaceTheme;
|
|
53
|
+
/** Builds the envelope that sends a changed theme to one mount's frame.
|
|
54
|
+
* @complexity O(1). */
|
|
55
|
+
export declare function adminSurfaceThemeMessage(required: {
|
|
56
|
+
readonly mountId: string;
|
|
57
|
+
readonly theme: AdminSurfaceTheme;
|
|
58
|
+
}, _optional?: Record<string, never>): AdminSurfaceThemeMessage;
|
|
59
|
+
//# sourceMappingURL=theme.d.ts.map
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The host's design tokens for plugin surfaces: which `--jini-*` custom properties a host
|
|
3
|
+
* shares with every surface frame, how it reads their current values, and the envelope that sends
|
|
4
|
+
* a change after init.
|
|
5
|
+
*
|
|
6
|
+
* Why: a surface is a sandboxed frame without `allow-same-origin`, so it cannot see the admin's
|
|
7
|
+
* stylesheets, and its CSP allows only its own files. Values posted in a message are the only way
|
|
8
|
+
* its screens can match the admin and follow a light/dark or admin-theme change. The surface client
|
|
9
|
+
* applies them as custom properties on its root (CSSOM, which the CSP allows; never a style
|
|
10
|
+
* attribute or a stylesheet), so a surface writes `var(--jini-primary, <fallback>)` and looks right
|
|
11
|
+
* with or without a host that sends tokens.
|
|
12
|
+
*
|
|
13
|
+
* Values are text a host computed from its own CSS; anything that could fetch (`url(`, `image-set(`)
|
|
14
|
+
* or leave a declaration (`;`, braces) is dropped here and again in the surface client.
|
|
15
|
+
*/
|
|
16
|
+
import { ADMIN_SURFACE_PROTOCOL, ADMIN_SURFACE_PROTOCOL_VERSION } from './protocol.js';
|
|
17
|
+
/** The shared vocabulary: colours, type, radius, spacing, depth and focus. A host may send fewer. */
|
|
18
|
+
export const ADMIN_SURFACE_THEME_TOKENS = Object.freeze([
|
|
19
|
+
'--jini-bg', '--jini-surface', '--jini-bg-subtle', '--jini-text', '--jini-text-muted',
|
|
20
|
+
'--jini-border', '--jini-border-strong', '--jini-primary', '--jini-primary-ink', '--jini-link',
|
|
21
|
+
'--jini-danger', '--jini-danger-bg', '--jini-success', '--jini-success-bg', '--jini-warning', '--jini-warning-bg',
|
|
22
|
+
'--jini-font-ui', '--jini-font-mono', '--jini-font-size-xs', '--jini-font-size-sm', '--jini-font-size-base', '--jini-font-size-md',
|
|
23
|
+
'--jini-radius-sm', '--jini-radius', '--jini-radius-md', '--jini-radius-pill',
|
|
24
|
+
'--jini-space-1', '--jini-space-2', '--jini-space-3', '--jini-space-4',
|
|
25
|
+
'--jini-shadow-xs', '--jini-shadow-sm', '--jini-control-bg', '--jini-control-border', '--jini-control-shadow-focus',
|
|
26
|
+
'--jini-overlay-tooltip-bg', '--jini-overlay-tooltip-text', '--jini-overlay-tooltip-border',
|
|
27
|
+
]);
|
|
28
|
+
const MAX_VALUE_LENGTH = 200;
|
|
29
|
+
const UNSAFE_VALUE = /url\(|image\(|image-set\(|[;{}<>\\@]/i;
|
|
30
|
+
/** Whether `value` is plain declaration text that cannot fetch anything or end its declaration.
|
|
31
|
+
* @complexity O(n) in the value length. */
|
|
32
|
+
export function isSafeAdminSurfaceThemeValue(value) {
|
|
33
|
+
return typeof value === 'string' && value.length > 0 && value.length <= MAX_VALUE_LENGTH && !UNSAFE_VALUE.test(value);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Reads the shared tokens from the host's computed style.
|
|
37
|
+
* @param required.style - Usually `getComputedStyle(element)` for the element the admin theme is on.
|
|
38
|
+
* @param optional.sources - Where to read a token when the host draws with its own name for it
|
|
39
|
+
* (e.g. `{ '--jini-text': '--fg' }`); unnamed tokens are read under their own name.
|
|
40
|
+
* @returns Only the tokens with a safe, non-empty value.
|
|
41
|
+
* @complexity O(t) tokens.
|
|
42
|
+
*/
|
|
43
|
+
export function readAdminSurfaceTheme(required, optional = {}) {
|
|
44
|
+
const tokens = {};
|
|
45
|
+
for (const token of ADMIN_SURFACE_THEME_TOKENS) {
|
|
46
|
+
const value = required.style.getPropertyValue(optional.sources?.[token] ?? token).trim();
|
|
47
|
+
if (isSafeAdminSurfaceThemeValue(value))
|
|
48
|
+
tokens[token] = value;
|
|
49
|
+
}
|
|
50
|
+
return { scheme: required.scheme, tokens };
|
|
51
|
+
}
|
|
52
|
+
/** Builds the envelope that sends a changed theme to one mount's frame.
|
|
53
|
+
* @complexity O(1). */
|
|
54
|
+
export function adminSurfaceThemeMessage(required, _optional = {}) {
|
|
55
|
+
return { protocol: ADMIN_SURFACE_PROTOCOL, version: ADMIN_SURFACE_PROTOCOL_VERSION, kind: 'theme', mountId: required.mountId, theme: required.theme };
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=theme.js.map
|