@ethisyscore/extension-runtime 1.6.0
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/README.md +42 -0
- package/bin/mock-host.cjs +14 -0
- package/dist/host/index.cjs +600 -0
- package/dist/host/index.cjs.map +1 -0
- package/dist/host/index.d.cts +260 -0
- package/dist/host/index.d.ts +260 -0
- package/dist/host/index.js +577 -0
- package/dist/host/index.js.map +1 -0
- package/dist/index.cjs +16 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/mock-host/cli.cjs +800 -0
- package/dist/mock-host/cli.cjs.map +1 -0
- package/dist/mock-host/cli.d.cts +155 -0
- package/dist/mock-host/cli.d.ts +155 -0
- package/dist/mock-host/cli.js +770 -0
- package/dist/mock-host/cli.js.map +1 -0
- package/dist/mock-host/index.cjs +74 -0
- package/dist/mock-host/index.cjs.map +1 -0
- package/dist/mock-host/index.d.cts +95 -0
- package/dist/mock-host/index.d.ts +95 -0
- package/dist/mock-host/index.js +71 -0
- package/dist/mock-host/index.js.map +1 -0
- package/dist/plugin/index.cjs +113 -0
- package/dist/plugin/index.cjs.map +1 -0
- package/dist/plugin/index.d.cts +120 -0
- package/dist/plugin/index.d.ts +120 -0
- package/dist/plugin/index.js +107 -0
- package/dist/plugin/index.js.map +1 -0
- package/dist/registry-DpCx_LxF.d.cts +25 -0
- package/dist/registry-DpCx_LxF.d.ts +25 -0
- package/dist/transport-73otePiw.d.cts +307 -0
- package/dist/transport-73otePiw.d.ts +307 -0
- package/dist/transport-DVn2GVZh.d.cts +32 -0
- package/dist/transport-DVn2GVZh.d.ts +32 -0
- package/package.json +78 -0
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker-side host transport for `renderMode: remote-runtime` extensions
|
|
3
|
+
* (Contract B). Owns the lifecycle of:
|
|
4
|
+
*
|
|
5
|
+
* 1. A path-pinned module `Worker` spawned from a host-issued bootstrap URL.
|
|
6
|
+
* 2. A `MessagePort`-based RPC channel (handed to the worker on spawn).
|
|
7
|
+
* 3. A Shopify Remote DOM root receiver — wired here so the worker's UI
|
|
8
|
+
* tree mounts into a host React tree.
|
|
9
|
+
* 4. Event coalescing (default ≤16ms, one rAF) for high-frequency input
|
|
10
|
+
* events before they cross the port.
|
|
11
|
+
* 5. MCP tool/resource calls forwarded through the port — but **the
|
|
12
|
+
* capability token never crosses the boundary**. The token is bound
|
|
13
|
+
* to the worker scope on the HOST side: the host transport intercepts
|
|
14
|
+
* the plugin's MCP request, attaches the token to the outbound HTTP
|
|
15
|
+
* call, and forwards ONLY the response payload back through the port.
|
|
16
|
+
*
|
|
17
|
+
* This module is intentionally framework-thin: callers wire `mount(root)`
|
|
18
|
+
* onto a React tree (typically inside a `<WorkerSurfaceMount>`), and the
|
|
19
|
+
* Remote DOM receiver is responsible for translating worker-side
|
|
20
|
+
* RemoteRoot mutations into host-side component renders.
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* Structural subset of the DOM `Worker` interface that the transport needs.
|
|
24
|
+
* Tests inject a fake; production callers pass the global `Worker`.
|
|
25
|
+
*/
|
|
26
|
+
interface WorkerLike {
|
|
27
|
+
onmessage: ((ev: MessageEvent) => void) | null;
|
|
28
|
+
onerror: ((ev: ErrorEvent) => void) | null;
|
|
29
|
+
onmessageerror: ((ev: MessageEvent) => void) | null;
|
|
30
|
+
postMessage(message: unknown, transfer?: Transferable[]): void;
|
|
31
|
+
terminate(): void;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Constructor signature compatible with the DOM `Worker` constructor. The
|
|
35
|
+
* `workerCtor` option exists exclusively so tests can swap in a fake — in
|
|
36
|
+
* production the caller passes the global `Worker`.
|
|
37
|
+
*/
|
|
38
|
+
type WorkerCtor = new (url: string | URL, options?: WorkerOptions) => WorkerLike;
|
|
39
|
+
/**
|
|
40
|
+
* Discriminated union of MCP fetch shapes the host transport issues. The
|
|
41
|
+
* `capabilityToken` is attached on the host side ONLY — it is never present
|
|
42
|
+
* in any message that crosses the worker port.
|
|
43
|
+
*/
|
|
44
|
+
type McpHttpRequest = {
|
|
45
|
+
kind: "invokeTool";
|
|
46
|
+
name: string;
|
|
47
|
+
args: unknown;
|
|
48
|
+
capabilityToken: string;
|
|
49
|
+
signal?: AbortSignal;
|
|
50
|
+
} | {
|
|
51
|
+
kind: "getResource";
|
|
52
|
+
uri: string;
|
|
53
|
+
capabilityToken: string;
|
|
54
|
+
signal?: AbortSignal;
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Host-side HTTP client for MCP calls. Implementations are responsible for
|
|
58
|
+
* carrying the supplied `capabilityToken` on the outbound request (typically
|
|
59
|
+
* via `Authorization: Bearer`).
|
|
60
|
+
*/
|
|
61
|
+
interface McpHttpClient {
|
|
62
|
+
fetch(request: McpHttpRequest): Promise<{
|
|
63
|
+
ok: boolean;
|
|
64
|
+
data?: unknown;
|
|
65
|
+
error?: string;
|
|
66
|
+
}>;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Construction options for {@link WorkerRemoteDomTransport}.
|
|
70
|
+
*/
|
|
71
|
+
interface WorkerRemoteDomTransportOptions {
|
|
72
|
+
/**
|
|
73
|
+
* Canonical host-origin URL for the worker bootstrap script. This MUST
|
|
74
|
+
* be supplied by the host — never built from user input or extension
|
|
75
|
+
* manifest values — so the worker spawn is path-pinned.
|
|
76
|
+
*/
|
|
77
|
+
bootstrapUrl: string;
|
|
78
|
+
/**
|
|
79
|
+
* Mints (or fetches a cached) capability token. The host transport
|
|
80
|
+
* resolves this for each outbound MCP HTTP call. The token NEVER leaves
|
|
81
|
+
* host scope — plugin code (the worker side) cannot read it.
|
|
82
|
+
*/
|
|
83
|
+
capabilityToken: () => Promise<string>;
|
|
84
|
+
/**
|
|
85
|
+
* Host-side MCP HTTP client. The transport bridges port-side plugin
|
|
86
|
+
* MCP requests to this client and returns only the response payload
|
|
87
|
+
* back through the port.
|
|
88
|
+
*/
|
|
89
|
+
mcpClient: McpHttpClient;
|
|
90
|
+
/**
|
|
91
|
+
* Injectable Worker constructor (tests use a fake). Defaults to the
|
|
92
|
+
* global `Worker`.
|
|
93
|
+
*/
|
|
94
|
+
workerCtor?: WorkerCtor;
|
|
95
|
+
/**
|
|
96
|
+
* Coalescing window in milliseconds for high-frequency events. Defaults
|
|
97
|
+
* to 16ms (~one animation frame). Trailing-edge: the most recent payload
|
|
98
|
+
* within the window is delivered after the window elapses.
|
|
99
|
+
*/
|
|
100
|
+
coalesceMs?: number;
|
|
101
|
+
/**
|
|
102
|
+
* Maximum number of MCP requests (combined `invokeTool` + `getResource`)
|
|
103
|
+
* the transport will forward to {@link mcpClient} concurrently. Defaults
|
|
104
|
+
* to 8. Requests above the cap are rejected with a deterministic error
|
|
105
|
+
* reply over the port — the worker MUST handle that response shape.
|
|
106
|
+
*
|
|
107
|
+
* Bounded concurrency is a back-pressure boundary, not a quota: it stops
|
|
108
|
+
* a compromised or buggy worker from saturating the host's HTTP / token
|
|
109
|
+
* pool. Production hosts may want to lower this in multi-tenant pools
|
|
110
|
+
* where many extensions share one host process.
|
|
111
|
+
*/
|
|
112
|
+
maxConcurrentMcpRequests?: number;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Wire shape of a host-sourced input event forwarded to the worker over the
|
|
116
|
+
* port. Pointer / wheel / keyboard payloads share a single envelope type so
|
|
117
|
+
* the worker has one inbound dispatch site. The transport is structurally
|
|
118
|
+
* agnostic to the payload — it forwards verbatim.
|
|
119
|
+
*
|
|
120
|
+
* `kind` discriminates the event family. Coordinates are CSS pixels relative
|
|
121
|
+
* to the host canvas; coalescing happens host-side before the call to
|
|
122
|
+
* {@link WorkerRemoteDomTransport.postInputEvent}.
|
|
123
|
+
*/
|
|
124
|
+
type InputEventPayload = {
|
|
125
|
+
kind: "pointermove" | "pointerdown" | "pointerup" | "pointercancel";
|
|
126
|
+
surfaceId: string;
|
|
127
|
+
x: number;
|
|
128
|
+
y: number;
|
|
129
|
+
buttons: number;
|
|
130
|
+
pointerType: string;
|
|
131
|
+
} | {
|
|
132
|
+
kind: "wheel";
|
|
133
|
+
surfaceId: string;
|
|
134
|
+
deltaX: number;
|
|
135
|
+
deltaY: number;
|
|
136
|
+
deltaMode: number;
|
|
137
|
+
} | {
|
|
138
|
+
kind: "keydown" | "keyup";
|
|
139
|
+
surfaceId: string;
|
|
140
|
+
key: string;
|
|
141
|
+
code: string;
|
|
142
|
+
ctrlKey: boolean;
|
|
143
|
+
shiftKey: boolean;
|
|
144
|
+
altKey: boolean;
|
|
145
|
+
metaKey: boolean;
|
|
146
|
+
};
|
|
147
|
+
/**
|
|
148
|
+
* Documented wire protocol identifier embedded in handshake / message
|
|
149
|
+
* envelopes. Bumped when the worker ↔ host message shape changes in a
|
|
150
|
+
* backward-incompatible way.
|
|
151
|
+
*/
|
|
152
|
+
declare const WORKER_TRANSPORT_PROTOCOL = "ethisys.worker.remotedom.v1";
|
|
153
|
+
/**
|
|
154
|
+
* Wire shape of the handshake message the host posts to the worker on
|
|
155
|
+
* {@link WorkerRemoteDomTransport.connect}. Locked here so the SDK side and the
|
|
156
|
+
* API-hosted bootstrap script (`WorkerBootstrapScriptProvider`) read from the
|
|
157
|
+
* same field names — see the API tests pinning `event.data.moduleUrl` and
|
|
158
|
+
* `event.data.importMap`.
|
|
159
|
+
*
|
|
160
|
+
* The handshake is the SOLE message that ever carries `moduleUrl` or
|
|
161
|
+
* `importMap`; subsequent port traffic uses the discriminated message types
|
|
162
|
+
* declared below. The capability token NEVER appears in this payload (or any
|
|
163
|
+
* other postMessage payload) — it is bound on the host side via
|
|
164
|
+
* {@link WorkerRemoteDomTransportOptions.capabilityToken}.
|
|
165
|
+
*/
|
|
166
|
+
interface WorkerHandshakePayload {
|
|
167
|
+
readonly type: "ethisys:worker:handshake";
|
|
168
|
+
readonly protocol: string;
|
|
169
|
+
/** Host-origin URL of the plugin's worker bundle entry. */
|
|
170
|
+
readonly moduleUrl: string;
|
|
171
|
+
/**
|
|
172
|
+
* Frozen bare-specifier → host-origin URL map that the bootstrap installs
|
|
173
|
+
* before importing {@link moduleUrl}. Pulled from the plugin's
|
|
174
|
+
* `worker-bundle.import-map.json` at runtime by the host mount.
|
|
175
|
+
*/
|
|
176
|
+
readonly importMap: Readonly<Record<string, string>>;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Worker-side host transport for Contract B extensions.
|
|
180
|
+
*
|
|
181
|
+
* Construction immediately spawns the worker and transfers one end of a
|
|
182
|
+
* fresh `MessageChannel`; the host retains `port1`, the worker receives
|
|
183
|
+
* `port2`. Both sides exchange messages via their port and the worker port
|
|
184
|
+
* is the *only* channel for plugin ↔ host traffic after handshake.
|
|
185
|
+
*/
|
|
186
|
+
/**
|
|
187
|
+
* Default cap for in-flight MCP requests forwarded by the transport. Chosen to
|
|
188
|
+
* cover typical interactive UI traffic (dashboards, list views, detail panels)
|
|
189
|
+
* without letting a runaway worker saturate the host's HTTP / token pool.
|
|
190
|
+
*/
|
|
191
|
+
declare const DEFAULT_MAX_CONCURRENT_MCP_REQUESTS = 8;
|
|
192
|
+
declare class WorkerRemoteDomTransport {
|
|
193
|
+
private readonly worker;
|
|
194
|
+
private readonly hostPort;
|
|
195
|
+
private readonly workerPort;
|
|
196
|
+
private readonly mcpClient;
|
|
197
|
+
private readonly capabilityTokenProvider;
|
|
198
|
+
private readonly coalesceMs;
|
|
199
|
+
private readonly maxConcurrentMcpRequests;
|
|
200
|
+
private readonly abortController;
|
|
201
|
+
private inFlightMcpRequests;
|
|
202
|
+
private remoteDomConsumer;
|
|
203
|
+
private disposed;
|
|
204
|
+
private connected;
|
|
205
|
+
constructor(options: WorkerRemoteDomTransportOptions);
|
|
206
|
+
/**
|
|
207
|
+
* Post the handshake to the worker. Idempotent — only the FIRST call
|
|
208
|
+
* transfers the {@link MessagePort} and posts the handshake payload;
|
|
209
|
+
* subsequent calls are silent no-ops. Splitting this off from the
|
|
210
|
+
* constructor lets the host mount fetch the per-plugin
|
|
211
|
+
* `worker-bundle.import-map.json` (and resolve the bundle's module URL)
|
|
212
|
+
* before the bootstrap script consumes them.
|
|
213
|
+
*
|
|
214
|
+
* Wire shape: {@link WorkerHandshakePayload}. The capability token NEVER
|
|
215
|
+
* appears in the payload — it's bound on the host side via the
|
|
216
|
+
* {@link WorkerRemoteDomTransportOptions.capabilityToken} provider.
|
|
217
|
+
*
|
|
218
|
+
* The `moduleUrl` and `importMap` are forwarded to the worker so the
|
|
219
|
+
* bootstrap script (served from the host-pinned
|
|
220
|
+
* `/extensions/runtime/worker-bootstrap.js`) can:
|
|
221
|
+
* 1. Compose the frozen, same-origin-validated `IMPORT_MAP` from the
|
|
222
|
+
* handshake payload (rejecting any cross-origin entries).
|
|
223
|
+
* 2. `safeImport(moduleUrl)` the plugin's entry module.
|
|
224
|
+
*
|
|
225
|
+
* Same-origin enforcement is the bootstrap's job — this transport is
|
|
226
|
+
* structurally agnostic to the host origin and only forwards what it's
|
|
227
|
+
* handed.
|
|
228
|
+
*/
|
|
229
|
+
connect(moduleUrl: string, importMap: Record<string, string>): void;
|
|
230
|
+
/**
|
|
231
|
+
* Bind a consumer for Remote DOM mutation payloads emitted by the worker.
|
|
232
|
+
* The Remote DOM receiver wiring is owned by the caller (typically a host
|
|
233
|
+
* React component); this method exposes the raw stream so the receiver
|
|
234
|
+
* can integrate cleanly without a circular package import.
|
|
235
|
+
*/
|
|
236
|
+
onRemoteDom(consumer: (payload: unknown) => void): void;
|
|
237
|
+
/**
|
|
238
|
+
* Transfer control of a host-owned `<canvas>` to the worker so plugin code
|
|
239
|
+
* can render via `OffscreenCanvas`. The host retains the `<canvas>` for
|
|
240
|
+
* layout / accessibility purposes only — every pixel is produced inside the
|
|
241
|
+
* worker, so the main thread is never blocked per frame.
|
|
242
|
+
*
|
|
243
|
+
* The transfer rides the established MessagePort (not the worker global
|
|
244
|
+
* `postMessage`) so it is multiplexed with the rest of the host ↔ worker
|
|
245
|
+
* traffic on the same channel. The `OffscreenCanvas` handle is the sole
|
|
246
|
+
* `Transferable` in the envelope; no capability token, MCP context, or
|
|
247
|
+
* other host-only data crosses the boundary.
|
|
248
|
+
*
|
|
249
|
+
* Throws if the environment lacks `transferControlToOffscreen()` or if the
|
|
250
|
+
* canvas has already been transferred — see {@link createOffscreenCanvasTransfer}
|
|
251
|
+
* for the exact diagnostics.
|
|
252
|
+
*/
|
|
253
|
+
transferCanvas(canvas: HTMLCanvasElement, options: {
|
|
254
|
+
surfaceId: string;
|
|
255
|
+
}): {
|
|
256
|
+
offscreen: OffscreenCanvas;
|
|
257
|
+
};
|
|
258
|
+
/**
|
|
259
|
+
* Forward a host-sourced input event to the worker over the port. The
|
|
260
|
+
* payload shape is opaque to the transport — pointer / wheel / keyboard
|
|
261
|
+
* envelopes share the same wire type. Callers are expected to wrap
|
|
262
|
+
* high-frequency (pointer-move, wheel, scroll) events in
|
|
263
|
+
* {@link createCoalescer} or {@link createInputEventCoalescer} BEFORE
|
|
264
|
+
* calling this method so the port never sees the un-coalesced flood.
|
|
265
|
+
*
|
|
266
|
+
* Keyboard / pointer-down / pointer-up events are discrete and should be
|
|
267
|
+
* delivered without coalescing — pass them straight through.
|
|
268
|
+
*/
|
|
269
|
+
postInputEvent(payload: InputEventPayload): void;
|
|
270
|
+
/**
|
|
271
|
+
* Build a trailing-edge coalescer keyed to {@link WorkerRemoteDomTransportOptions.coalesceMs}.
|
|
272
|
+
*
|
|
273
|
+
* Use it to wrap pointer-move / scroll / resize callbacks **before**
|
|
274
|
+
* they cross the port. The last payload in any coalescing window is the
|
|
275
|
+
* one that wins.
|
|
276
|
+
*/
|
|
277
|
+
createCoalescer<T>(consumer: (payload: T) => void): (payload: T) => void;
|
|
278
|
+
/**
|
|
279
|
+
* Tear down the worker and port. Idempotent.
|
|
280
|
+
*
|
|
281
|
+
* Order matters: we abort BEFORE closing the port so any in-flight
|
|
282
|
+
* `mcpClient.fetch` that observes the signal short-circuits and the
|
|
283
|
+
* subsequent attempt to post a reply lands in the disposed-guard branch
|
|
284
|
+
* (which silently drops the post) instead of throwing on a closed port.
|
|
285
|
+
*/
|
|
286
|
+
dispose(): void;
|
|
287
|
+
/**
|
|
288
|
+
* Best-effort wrapper around `hostPort.postMessage` that tolerates posts
|
|
289
|
+
* arriving after {@link dispose}. The browser throws on a closed port and
|
|
290
|
+
* the async handlers below can race dispose, so any post initiated by an
|
|
291
|
+
* awaited continuation must be guarded.
|
|
292
|
+
*/
|
|
293
|
+
private safePostMessage;
|
|
294
|
+
private handlePortMessage;
|
|
295
|
+
/**
|
|
296
|
+
* Gate inbound MCP requests through the bounded concurrency window,
|
|
297
|
+
* reject with a deterministic error reply when the cap is exceeded, and
|
|
298
|
+
* decrement the counter unconditionally when the underlying handler
|
|
299
|
+
* settles (regardless of resolution/rejection shape).
|
|
300
|
+
*/
|
|
301
|
+
private dispatchMcp;
|
|
302
|
+
private handleInvokeTool;
|
|
303
|
+
private handleGetResource;
|
|
304
|
+
private replyError;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export { DEFAULT_MAX_CONCURRENT_MCP_REQUESTS as D, type InputEventPayload as I, type McpHttpClient as M, WORKER_TRANSPORT_PROTOCOL as W, type McpHttpRequest as a, type WorkerCtor as b, type WorkerHandshakePayload as c, type WorkerLike as d, WorkerRemoteDomTransport as e, type WorkerRemoteDomTransportOptions as f };
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker-side host transport for `renderMode: remote-runtime` extensions
|
|
3
|
+
* (Contract B). Owns the lifecycle of:
|
|
4
|
+
*
|
|
5
|
+
* 1. A path-pinned module `Worker` spawned from a host-issued bootstrap URL.
|
|
6
|
+
* 2. A `MessagePort`-based RPC channel (handed to the worker on spawn).
|
|
7
|
+
* 3. A Shopify Remote DOM root receiver — wired here so the worker's UI
|
|
8
|
+
* tree mounts into a host React tree.
|
|
9
|
+
* 4. Event coalescing (default ≤16ms, one rAF) for high-frequency input
|
|
10
|
+
* events before they cross the port.
|
|
11
|
+
* 5. MCP tool/resource calls forwarded through the port — but **the
|
|
12
|
+
* capability token never crosses the boundary**. The token is bound
|
|
13
|
+
* to the worker scope on the HOST side: the host transport intercepts
|
|
14
|
+
* the plugin's MCP request, attaches the token to the outbound HTTP
|
|
15
|
+
* call, and forwards ONLY the response payload back through the port.
|
|
16
|
+
*
|
|
17
|
+
* This module is intentionally framework-thin: callers wire `mount(root)`
|
|
18
|
+
* onto a React tree (typically inside a `<WorkerSurfaceMount>`), and the
|
|
19
|
+
* Remote DOM receiver is responsible for translating worker-side
|
|
20
|
+
* RemoteRoot mutations into host-side component renders.
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* Structural subset of the DOM `Worker` interface that the transport needs.
|
|
24
|
+
* Tests inject a fake; production callers pass the global `Worker`.
|
|
25
|
+
*/
|
|
26
|
+
interface WorkerLike {
|
|
27
|
+
onmessage: ((ev: MessageEvent) => void) | null;
|
|
28
|
+
onerror: ((ev: ErrorEvent) => void) | null;
|
|
29
|
+
onmessageerror: ((ev: MessageEvent) => void) | null;
|
|
30
|
+
postMessage(message: unknown, transfer?: Transferable[]): void;
|
|
31
|
+
terminate(): void;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Constructor signature compatible with the DOM `Worker` constructor. The
|
|
35
|
+
* `workerCtor` option exists exclusively so tests can swap in a fake — in
|
|
36
|
+
* production the caller passes the global `Worker`.
|
|
37
|
+
*/
|
|
38
|
+
type WorkerCtor = new (url: string | URL, options?: WorkerOptions) => WorkerLike;
|
|
39
|
+
/**
|
|
40
|
+
* Discriminated union of MCP fetch shapes the host transport issues. The
|
|
41
|
+
* `capabilityToken` is attached on the host side ONLY — it is never present
|
|
42
|
+
* in any message that crosses the worker port.
|
|
43
|
+
*/
|
|
44
|
+
type McpHttpRequest = {
|
|
45
|
+
kind: "invokeTool";
|
|
46
|
+
name: string;
|
|
47
|
+
args: unknown;
|
|
48
|
+
capabilityToken: string;
|
|
49
|
+
signal?: AbortSignal;
|
|
50
|
+
} | {
|
|
51
|
+
kind: "getResource";
|
|
52
|
+
uri: string;
|
|
53
|
+
capabilityToken: string;
|
|
54
|
+
signal?: AbortSignal;
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Host-side HTTP client for MCP calls. Implementations are responsible for
|
|
58
|
+
* carrying the supplied `capabilityToken` on the outbound request (typically
|
|
59
|
+
* via `Authorization: Bearer`).
|
|
60
|
+
*/
|
|
61
|
+
interface McpHttpClient {
|
|
62
|
+
fetch(request: McpHttpRequest): Promise<{
|
|
63
|
+
ok: boolean;
|
|
64
|
+
data?: unknown;
|
|
65
|
+
error?: string;
|
|
66
|
+
}>;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Construction options for {@link WorkerRemoteDomTransport}.
|
|
70
|
+
*/
|
|
71
|
+
interface WorkerRemoteDomTransportOptions {
|
|
72
|
+
/**
|
|
73
|
+
* Canonical host-origin URL for the worker bootstrap script. This MUST
|
|
74
|
+
* be supplied by the host — never built from user input or extension
|
|
75
|
+
* manifest values — so the worker spawn is path-pinned.
|
|
76
|
+
*/
|
|
77
|
+
bootstrapUrl: string;
|
|
78
|
+
/**
|
|
79
|
+
* Mints (or fetches a cached) capability token. The host transport
|
|
80
|
+
* resolves this for each outbound MCP HTTP call. The token NEVER leaves
|
|
81
|
+
* host scope — plugin code (the worker side) cannot read it.
|
|
82
|
+
*/
|
|
83
|
+
capabilityToken: () => Promise<string>;
|
|
84
|
+
/**
|
|
85
|
+
* Host-side MCP HTTP client. The transport bridges port-side plugin
|
|
86
|
+
* MCP requests to this client and returns only the response payload
|
|
87
|
+
* back through the port.
|
|
88
|
+
*/
|
|
89
|
+
mcpClient: McpHttpClient;
|
|
90
|
+
/**
|
|
91
|
+
* Injectable Worker constructor (tests use a fake). Defaults to the
|
|
92
|
+
* global `Worker`.
|
|
93
|
+
*/
|
|
94
|
+
workerCtor?: WorkerCtor;
|
|
95
|
+
/**
|
|
96
|
+
* Coalescing window in milliseconds for high-frequency events. Defaults
|
|
97
|
+
* to 16ms (~one animation frame). Trailing-edge: the most recent payload
|
|
98
|
+
* within the window is delivered after the window elapses.
|
|
99
|
+
*/
|
|
100
|
+
coalesceMs?: number;
|
|
101
|
+
/**
|
|
102
|
+
* Maximum number of MCP requests (combined `invokeTool` + `getResource`)
|
|
103
|
+
* the transport will forward to {@link mcpClient} concurrently. Defaults
|
|
104
|
+
* to 8. Requests above the cap are rejected with a deterministic error
|
|
105
|
+
* reply over the port — the worker MUST handle that response shape.
|
|
106
|
+
*
|
|
107
|
+
* Bounded concurrency is a back-pressure boundary, not a quota: it stops
|
|
108
|
+
* a compromised or buggy worker from saturating the host's HTTP / token
|
|
109
|
+
* pool. Production hosts may want to lower this in multi-tenant pools
|
|
110
|
+
* where many extensions share one host process.
|
|
111
|
+
*/
|
|
112
|
+
maxConcurrentMcpRequests?: number;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Wire shape of a host-sourced input event forwarded to the worker over the
|
|
116
|
+
* port. Pointer / wheel / keyboard payloads share a single envelope type so
|
|
117
|
+
* the worker has one inbound dispatch site. The transport is structurally
|
|
118
|
+
* agnostic to the payload — it forwards verbatim.
|
|
119
|
+
*
|
|
120
|
+
* `kind` discriminates the event family. Coordinates are CSS pixels relative
|
|
121
|
+
* to the host canvas; coalescing happens host-side before the call to
|
|
122
|
+
* {@link WorkerRemoteDomTransport.postInputEvent}.
|
|
123
|
+
*/
|
|
124
|
+
type InputEventPayload = {
|
|
125
|
+
kind: "pointermove" | "pointerdown" | "pointerup" | "pointercancel";
|
|
126
|
+
surfaceId: string;
|
|
127
|
+
x: number;
|
|
128
|
+
y: number;
|
|
129
|
+
buttons: number;
|
|
130
|
+
pointerType: string;
|
|
131
|
+
} | {
|
|
132
|
+
kind: "wheel";
|
|
133
|
+
surfaceId: string;
|
|
134
|
+
deltaX: number;
|
|
135
|
+
deltaY: number;
|
|
136
|
+
deltaMode: number;
|
|
137
|
+
} | {
|
|
138
|
+
kind: "keydown" | "keyup";
|
|
139
|
+
surfaceId: string;
|
|
140
|
+
key: string;
|
|
141
|
+
code: string;
|
|
142
|
+
ctrlKey: boolean;
|
|
143
|
+
shiftKey: boolean;
|
|
144
|
+
altKey: boolean;
|
|
145
|
+
metaKey: boolean;
|
|
146
|
+
};
|
|
147
|
+
/**
|
|
148
|
+
* Documented wire protocol identifier embedded in handshake / message
|
|
149
|
+
* envelopes. Bumped when the worker ↔ host message shape changes in a
|
|
150
|
+
* backward-incompatible way.
|
|
151
|
+
*/
|
|
152
|
+
declare const WORKER_TRANSPORT_PROTOCOL = "ethisys.worker.remotedom.v1";
|
|
153
|
+
/**
|
|
154
|
+
* Wire shape of the handshake message the host posts to the worker on
|
|
155
|
+
* {@link WorkerRemoteDomTransport.connect}. Locked here so the SDK side and the
|
|
156
|
+
* API-hosted bootstrap script (`WorkerBootstrapScriptProvider`) read from the
|
|
157
|
+
* same field names — see the API tests pinning `event.data.moduleUrl` and
|
|
158
|
+
* `event.data.importMap`.
|
|
159
|
+
*
|
|
160
|
+
* The handshake is the SOLE message that ever carries `moduleUrl` or
|
|
161
|
+
* `importMap`; subsequent port traffic uses the discriminated message types
|
|
162
|
+
* declared below. The capability token NEVER appears in this payload (or any
|
|
163
|
+
* other postMessage payload) — it is bound on the host side via
|
|
164
|
+
* {@link WorkerRemoteDomTransportOptions.capabilityToken}.
|
|
165
|
+
*/
|
|
166
|
+
interface WorkerHandshakePayload {
|
|
167
|
+
readonly type: "ethisys:worker:handshake";
|
|
168
|
+
readonly protocol: string;
|
|
169
|
+
/** Host-origin URL of the plugin's worker bundle entry. */
|
|
170
|
+
readonly moduleUrl: string;
|
|
171
|
+
/**
|
|
172
|
+
* Frozen bare-specifier → host-origin URL map that the bootstrap installs
|
|
173
|
+
* before importing {@link moduleUrl}. Pulled from the plugin's
|
|
174
|
+
* `worker-bundle.import-map.json` at runtime by the host mount.
|
|
175
|
+
*/
|
|
176
|
+
readonly importMap: Readonly<Record<string, string>>;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Worker-side host transport for Contract B extensions.
|
|
180
|
+
*
|
|
181
|
+
* Construction immediately spawns the worker and transfers one end of a
|
|
182
|
+
* fresh `MessageChannel`; the host retains `port1`, the worker receives
|
|
183
|
+
* `port2`. Both sides exchange messages via their port and the worker port
|
|
184
|
+
* is the *only* channel for plugin ↔ host traffic after handshake.
|
|
185
|
+
*/
|
|
186
|
+
/**
|
|
187
|
+
* Default cap for in-flight MCP requests forwarded by the transport. Chosen to
|
|
188
|
+
* cover typical interactive UI traffic (dashboards, list views, detail panels)
|
|
189
|
+
* without letting a runaway worker saturate the host's HTTP / token pool.
|
|
190
|
+
*/
|
|
191
|
+
declare const DEFAULT_MAX_CONCURRENT_MCP_REQUESTS = 8;
|
|
192
|
+
declare class WorkerRemoteDomTransport {
|
|
193
|
+
private readonly worker;
|
|
194
|
+
private readonly hostPort;
|
|
195
|
+
private readonly workerPort;
|
|
196
|
+
private readonly mcpClient;
|
|
197
|
+
private readonly capabilityTokenProvider;
|
|
198
|
+
private readonly coalesceMs;
|
|
199
|
+
private readonly maxConcurrentMcpRequests;
|
|
200
|
+
private readonly abortController;
|
|
201
|
+
private inFlightMcpRequests;
|
|
202
|
+
private remoteDomConsumer;
|
|
203
|
+
private disposed;
|
|
204
|
+
private connected;
|
|
205
|
+
constructor(options: WorkerRemoteDomTransportOptions);
|
|
206
|
+
/**
|
|
207
|
+
* Post the handshake to the worker. Idempotent — only the FIRST call
|
|
208
|
+
* transfers the {@link MessagePort} and posts the handshake payload;
|
|
209
|
+
* subsequent calls are silent no-ops. Splitting this off from the
|
|
210
|
+
* constructor lets the host mount fetch the per-plugin
|
|
211
|
+
* `worker-bundle.import-map.json` (and resolve the bundle's module URL)
|
|
212
|
+
* before the bootstrap script consumes them.
|
|
213
|
+
*
|
|
214
|
+
* Wire shape: {@link WorkerHandshakePayload}. The capability token NEVER
|
|
215
|
+
* appears in the payload — it's bound on the host side via the
|
|
216
|
+
* {@link WorkerRemoteDomTransportOptions.capabilityToken} provider.
|
|
217
|
+
*
|
|
218
|
+
* The `moduleUrl` and `importMap` are forwarded to the worker so the
|
|
219
|
+
* bootstrap script (served from the host-pinned
|
|
220
|
+
* `/extensions/runtime/worker-bootstrap.js`) can:
|
|
221
|
+
* 1. Compose the frozen, same-origin-validated `IMPORT_MAP` from the
|
|
222
|
+
* handshake payload (rejecting any cross-origin entries).
|
|
223
|
+
* 2. `safeImport(moduleUrl)` the plugin's entry module.
|
|
224
|
+
*
|
|
225
|
+
* Same-origin enforcement is the bootstrap's job — this transport is
|
|
226
|
+
* structurally agnostic to the host origin and only forwards what it's
|
|
227
|
+
* handed.
|
|
228
|
+
*/
|
|
229
|
+
connect(moduleUrl: string, importMap: Record<string, string>): void;
|
|
230
|
+
/**
|
|
231
|
+
* Bind a consumer for Remote DOM mutation payloads emitted by the worker.
|
|
232
|
+
* The Remote DOM receiver wiring is owned by the caller (typically a host
|
|
233
|
+
* React component); this method exposes the raw stream so the receiver
|
|
234
|
+
* can integrate cleanly without a circular package import.
|
|
235
|
+
*/
|
|
236
|
+
onRemoteDom(consumer: (payload: unknown) => void): void;
|
|
237
|
+
/**
|
|
238
|
+
* Transfer control of a host-owned `<canvas>` to the worker so plugin code
|
|
239
|
+
* can render via `OffscreenCanvas`. The host retains the `<canvas>` for
|
|
240
|
+
* layout / accessibility purposes only — every pixel is produced inside the
|
|
241
|
+
* worker, so the main thread is never blocked per frame.
|
|
242
|
+
*
|
|
243
|
+
* The transfer rides the established MessagePort (not the worker global
|
|
244
|
+
* `postMessage`) so it is multiplexed with the rest of the host ↔ worker
|
|
245
|
+
* traffic on the same channel. The `OffscreenCanvas` handle is the sole
|
|
246
|
+
* `Transferable` in the envelope; no capability token, MCP context, or
|
|
247
|
+
* other host-only data crosses the boundary.
|
|
248
|
+
*
|
|
249
|
+
* Throws if the environment lacks `transferControlToOffscreen()` or if the
|
|
250
|
+
* canvas has already been transferred — see {@link createOffscreenCanvasTransfer}
|
|
251
|
+
* for the exact diagnostics.
|
|
252
|
+
*/
|
|
253
|
+
transferCanvas(canvas: HTMLCanvasElement, options: {
|
|
254
|
+
surfaceId: string;
|
|
255
|
+
}): {
|
|
256
|
+
offscreen: OffscreenCanvas;
|
|
257
|
+
};
|
|
258
|
+
/**
|
|
259
|
+
* Forward a host-sourced input event to the worker over the port. The
|
|
260
|
+
* payload shape is opaque to the transport — pointer / wheel / keyboard
|
|
261
|
+
* envelopes share the same wire type. Callers are expected to wrap
|
|
262
|
+
* high-frequency (pointer-move, wheel, scroll) events in
|
|
263
|
+
* {@link createCoalescer} or {@link createInputEventCoalescer} BEFORE
|
|
264
|
+
* calling this method so the port never sees the un-coalesced flood.
|
|
265
|
+
*
|
|
266
|
+
* Keyboard / pointer-down / pointer-up events are discrete and should be
|
|
267
|
+
* delivered without coalescing — pass them straight through.
|
|
268
|
+
*/
|
|
269
|
+
postInputEvent(payload: InputEventPayload): void;
|
|
270
|
+
/**
|
|
271
|
+
* Build a trailing-edge coalescer keyed to {@link WorkerRemoteDomTransportOptions.coalesceMs}.
|
|
272
|
+
*
|
|
273
|
+
* Use it to wrap pointer-move / scroll / resize callbacks **before**
|
|
274
|
+
* they cross the port. The last payload in any coalescing window is the
|
|
275
|
+
* one that wins.
|
|
276
|
+
*/
|
|
277
|
+
createCoalescer<T>(consumer: (payload: T) => void): (payload: T) => void;
|
|
278
|
+
/**
|
|
279
|
+
* Tear down the worker and port. Idempotent.
|
|
280
|
+
*
|
|
281
|
+
* Order matters: we abort BEFORE closing the port so any in-flight
|
|
282
|
+
* `mcpClient.fetch` that observes the signal short-circuits and the
|
|
283
|
+
* subsequent attempt to post a reply lands in the disposed-guard branch
|
|
284
|
+
* (which silently drops the post) instead of throwing on a closed port.
|
|
285
|
+
*/
|
|
286
|
+
dispose(): void;
|
|
287
|
+
/**
|
|
288
|
+
* Best-effort wrapper around `hostPort.postMessage` that tolerates posts
|
|
289
|
+
* arriving after {@link dispose}. The browser throws on a closed port and
|
|
290
|
+
* the async handlers below can race dispose, so any post initiated by an
|
|
291
|
+
* awaited continuation must be guarded.
|
|
292
|
+
*/
|
|
293
|
+
private safePostMessage;
|
|
294
|
+
private handlePortMessage;
|
|
295
|
+
/**
|
|
296
|
+
* Gate inbound MCP requests through the bounded concurrency window,
|
|
297
|
+
* reject with a deterministic error reply when the cap is exceeded, and
|
|
298
|
+
* decrement the counter unconditionally when the underlying handler
|
|
299
|
+
* settles (regardless of resolution/rejection shape).
|
|
300
|
+
*/
|
|
301
|
+
private dispatchMcp;
|
|
302
|
+
private handleInvokeTool;
|
|
303
|
+
private handleGetResource;
|
|
304
|
+
private replyError;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export { DEFAULT_MAX_CONCURRENT_MCP_REQUESTS as D, type InputEventPayload as I, type McpHttpClient as M, WORKER_TRANSPORT_PROTOCOL as W, type McpHttpRequest as a, type WorkerCtor as b, type WorkerHandshakePayload as c, type WorkerLike as d, WorkerRemoteDomTransport as e, type WorkerRemoteDomTransportOptions as f };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transport contract used by the plugin-side React hooks
|
|
3
|
+
* (`useMcpResource`, `useMcpTool`) to talk to the host.
|
|
4
|
+
*
|
|
5
|
+
* The hooks are deliberately transport-agnostic: the concrete implementation
|
|
6
|
+
* (postMessage bridge, in-process direct call, fetch-based, …) is injected
|
|
7
|
+
* via {@link ExtensionRuntimeProvider} or the per-hook `transport` option.
|
|
8
|
+
* This keeps the React surface stable across Contract A (host-rendered) and
|
|
9
|
+
* Contract B (worker remote-runtime) execution modes.
|
|
10
|
+
*
|
|
11
|
+
* Implementations must honour the supplied {@link AbortSignal} for
|
|
12
|
+
* cancellation — hooks rely on it to tear down in-flight calls on unmount
|
|
13
|
+
* or argument changes.
|
|
14
|
+
*/
|
|
15
|
+
interface McpTransport {
|
|
16
|
+
/**
|
|
17
|
+
* Fetch a resource by URI. Implementations should reject with an `Error`
|
|
18
|
+
* when the host returns a failure, and should observe `signal` to abort
|
|
19
|
+
* any in-flight work.
|
|
20
|
+
*/
|
|
21
|
+
getResource<T>(uri: string, signal?: AbortSignal): Promise<{
|
|
22
|
+
uri: string;
|
|
23
|
+
data: T;
|
|
24
|
+
}>;
|
|
25
|
+
/**
|
|
26
|
+
* Invoke a tool by name. The request object is opaque to the transport.
|
|
27
|
+
* Implementations should observe `signal` to abort in-flight work.
|
|
28
|
+
*/
|
|
29
|
+
invokeTool<TReq, TRes>(name: string, args: TReq, signal?: AbortSignal): Promise<TRes>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type { McpTransport as M };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transport contract used by the plugin-side React hooks
|
|
3
|
+
* (`useMcpResource`, `useMcpTool`) to talk to the host.
|
|
4
|
+
*
|
|
5
|
+
* The hooks are deliberately transport-agnostic: the concrete implementation
|
|
6
|
+
* (postMessage bridge, in-process direct call, fetch-based, …) is injected
|
|
7
|
+
* via {@link ExtensionRuntimeProvider} or the per-hook `transport` option.
|
|
8
|
+
* This keeps the React surface stable across Contract A (host-rendered) and
|
|
9
|
+
* Contract B (worker remote-runtime) execution modes.
|
|
10
|
+
*
|
|
11
|
+
* Implementations must honour the supplied {@link AbortSignal} for
|
|
12
|
+
* cancellation — hooks rely on it to tear down in-flight calls on unmount
|
|
13
|
+
* or argument changes.
|
|
14
|
+
*/
|
|
15
|
+
interface McpTransport {
|
|
16
|
+
/**
|
|
17
|
+
* Fetch a resource by URI. Implementations should reject with an `Error`
|
|
18
|
+
* when the host returns a failure, and should observe `signal` to abort
|
|
19
|
+
* any in-flight work.
|
|
20
|
+
*/
|
|
21
|
+
getResource<T>(uri: string, signal?: AbortSignal): Promise<{
|
|
22
|
+
uri: string;
|
|
23
|
+
data: T;
|
|
24
|
+
}>;
|
|
25
|
+
/**
|
|
26
|
+
* Invoke a tool by name. The request object is opaque to the transport.
|
|
27
|
+
* Implementations should observe `signal` to abort in-flight work.
|
|
28
|
+
*/
|
|
29
|
+
invokeTool<TReq, TRes>(name: string, args: TReq, signal?: AbortSignal): Promise<TRes>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type { McpTransport as M };
|