@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.
Files changed (39) hide show
  1. package/README.md +42 -0
  2. package/bin/mock-host.cjs +14 -0
  3. package/dist/host/index.cjs +600 -0
  4. package/dist/host/index.cjs.map +1 -0
  5. package/dist/host/index.d.cts +260 -0
  6. package/dist/host/index.d.ts +260 -0
  7. package/dist/host/index.js +577 -0
  8. package/dist/host/index.js.map +1 -0
  9. package/dist/index.cjs +16 -0
  10. package/dist/index.cjs.map +1 -0
  11. package/dist/index.d.cts +6 -0
  12. package/dist/index.d.ts +6 -0
  13. package/dist/index.js +10 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/mock-host/cli.cjs +800 -0
  16. package/dist/mock-host/cli.cjs.map +1 -0
  17. package/dist/mock-host/cli.d.cts +155 -0
  18. package/dist/mock-host/cli.d.ts +155 -0
  19. package/dist/mock-host/cli.js +770 -0
  20. package/dist/mock-host/cli.js.map +1 -0
  21. package/dist/mock-host/index.cjs +74 -0
  22. package/dist/mock-host/index.cjs.map +1 -0
  23. package/dist/mock-host/index.d.cts +95 -0
  24. package/dist/mock-host/index.d.ts +95 -0
  25. package/dist/mock-host/index.js +71 -0
  26. package/dist/mock-host/index.js.map +1 -0
  27. package/dist/plugin/index.cjs +113 -0
  28. package/dist/plugin/index.cjs.map +1 -0
  29. package/dist/plugin/index.d.cts +120 -0
  30. package/dist/plugin/index.d.ts +120 -0
  31. package/dist/plugin/index.js +107 -0
  32. package/dist/plugin/index.js.map +1 -0
  33. package/dist/registry-DpCx_LxF.d.cts +25 -0
  34. package/dist/registry-DpCx_LxF.d.ts +25 -0
  35. package/dist/transport-73otePiw.d.cts +307 -0
  36. package/dist/transport-73otePiw.d.ts +307 -0
  37. package/dist/transport-DVn2GVZh.d.cts +32 -0
  38. package/dist/transport-DVn2GVZh.d.ts +32 -0
  39. 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 };