import { U as UploadDocumentMeta, d as BridgePushThemeEnvelope, c as BridgePushLocaleEnvelope, b as BridgePushDensityEnvelope, a as BridgePushA11yEnvelope, B as BridgeNavPushEnvelope } from './bridge-envelopes-DA6vxbyb.cjs'; /** * Version-skew detection for host-side MCP calls. * * A mounted plugin surface holds a capability token minted against the extension version that * was active when the page loaded. When the install is upgraded underneath it, the kernel stops * honouring that token: it rejects with `401` and * `X-Capability-Token-Rejection-Reason: ExtensionVersionMismatch`. Every other rejection reason is * terminal for the surface, but this one is recoverable — remounting against the new version fixes * it — which is why it is worth telling apart from an ordinary auth failure. * * Two signals are handled here, and the difference matters: * * - **Reactive**, from a rejection: the call already failed. Recovery costs the user a round trip * and whatever in-page state the remount discards. * - **Proactive**, from the version stamped on a *successful* response: the surface learns the * install moved while it is still working, and can remount at a moment of its choosing. Reads * keep succeeding until the token's own version check catches up, so this is the path that * avoids a visible failure altogether. * * The interpretation lives here rather than in each host application so that "which reason means * remount" is decided once, in code that can be tested, instead of being re-derived by every * embedder from HTTP trivia. */ /** Response header naming the extension version a request was served as. */ declare const EXTENSION_VERSION_HEADER = "x-extension-version"; /** Response header naming the version a rejected surface must remount at. */ declare const EXTENSION_ACTIVE_VERSION_HEADER = "x-extension-active-version"; /** Response header carrying the kernel's structured capability-token rejection reason. */ declare const CAPABILITY_REJECTION_REASON_HEADER = "x-capability-token-rejection-reason"; /** The one rejection reason a surface can resolve by remounting. */ declare const VERSION_MISMATCH_REASON = "extensionversionmismatch"; /** * What the host observed about an MCP response, beyond its payload. Both fields are optional * because the {@link McpHttpClient} contract predates them: an implementation that does not supply * them simply never reports skew, which is the previous behaviour rather than a new failure. */ interface McpResponseMetadata { /** HTTP status code, when the implementation surfaces it. */ readonly status?: number; /** Response headers. Looked up case-insensitively, so any casing is fine. */ readonly headers?: Readonly>; } /** * How skew came to light. * * - `"rejection"` — a call was refused because the surface's token is stale. Recovery costs a round * trip and whatever in-page state the remount discards. * - `"response-stamp"` — a call succeeded, but the install has moved on. The surface can remount * before anything fails, so this is the path worth preferring. */ type VersionSkewSource = "rejection" | "response-stamp"; /** * The result of one host-side MCP call: the payload the plugin asked for, plus whatever the host * observed about the response itself. * * Named rather than inlined at the {@link McpHttpClient} declaration so hosts have a type to * implement against, and so the skew detector and the transports agree on one shape instead of * three structurally-compatible copies drifting apart. */ interface McpHttpResponse extends McpResponseMetadata { /** Whether the call succeeded. */ readonly ok: boolean; /** The response payload, shaped by the request kind. */ readonly data?: unknown; /** Failure message when {@link ok} is false. */ readonly error?: string; /** * Optional machine-readable classification for a failure, mirroring the `mcpErrorCode` a client * can attach to a thrown error. Set it when the client knows why the call failed more precisely * than the status says - it takes precedence over {@link status} when the bridge classifies the * reply. Values outside the published vocabulary are ignored rather than forwarded. */ readonly mcpErrorCode?: string; } /** A detected mismatch between the version a surface is running and the version now installed. */ interface VersionSkew { /** * The version to remount at, when the kernel named it. Undefined means skew is certain but the * target is not — the surface should re-read the extension descriptor rather than guess. */ readonly activeVersion?: string; /** Which signal detected the skew. */ readonly detectedFrom: VersionSkewSource; } /** * Decides whether an MCP response means the surface is running against a version that is no longer * installed. * * @param response What the host observed: whether the call succeeded, plus any status and headers. * @param mountedExtensionVersion The version this surface mounted at. Required for proactive * detection — without it a response stamp cannot be compared to anything, and only rejections are * detectable. * @returns The skew, or `null` when the response says nothing about versions. */ declare function detectVersionSkew(response: McpHttpResponse, mountedExtensionVersion?: string): VersionSkew | null; /** * Worker-side host transport for `renderMode: remote-runtime` extensions * (Contract B). Owns the lifecycle of: * * 1. A path-pinned module `Worker` spawned from a host-issued bootstrap URL. * 2. A `MessagePort`-based RPC channel (handed to the worker on spawn). * 3. A Shopify Remote DOM root receiver — wired here so the worker's UI * tree mounts into a host React tree. * 4. Event coalescing (default ≤16ms, one rAF) for high-frequency input * events before they cross the port. * 5. MCP tool/resource calls forwarded through the port — but **the * capability token never crosses the boundary**. The token is bound * to the worker scope on the HOST side: the host transport intercepts * the plugin's MCP request, attaches the token to the outbound HTTP * call, and forwards ONLY the response payload back through the port. * * This module is intentionally framework-thin: callers wire `mount(root)` * onto a React tree (typically inside a ``), and the * Remote DOM receiver is responsible for translating worker-side * RemoteRoot mutations into host-side component renders. */ /** * Structural subset of the DOM `Worker` interface that the transport needs. * Tests inject a fake; production callers pass the global `Worker`. */ interface WorkerLike { onmessage: ((ev: MessageEvent) => void) | null; onerror: ((ev: ErrorEvent) => void) | null; onmessageerror: ((ev: MessageEvent) => void) | null; postMessage(message: unknown, transfer?: Transferable[]): void; terminate(): void; } /** * Constructor signature compatible with the DOM `Worker` constructor. The * `workerCtor` option exists exclusively so tests can swap in a fake — in * production the caller passes the global `Worker`. */ type WorkerCtor = new (url: string | URL, options?: WorkerOptions) => WorkerLike; /** * Discriminated union of MCP fetch shapes the host transport issues. The * `capabilityToken` is attached on the host side ONLY — it is never present * in any message that crosses the worker port. */ type McpHttpRequest = { kind: "invokeTool"; name: string; args: unknown; capabilityToken: string; signal?: AbortSignal; } | { kind: "getResource"; uri: string; capabilityToken: string; signal?: AbortSignal; } | McpUploadRequest; /** * Host-side document-upload request (a `kind` of {@link McpHttpRequest}). * Carries the transferred bytes + advisory metadata plus the host-minted * capability token. As with the other request kinds, the token is attached on * the HOST side only and never crosses the worker port. Implementations POST * the bytes as `application/octet-stream` (metadata in `x-cc-*` headers) to the * kernel's capability-token-authed document-upload route; `data` on success is * an {@link UploadDocumentResult}. */ interface McpUploadRequest { kind: "uploadDocument"; meta: UploadDocumentMeta; buffer: ArrayBuffer; capabilityToken: string; signal?: AbortSignal; } /** * Host-side HTTP client for MCP calls. Implementations are responsible for * carrying the supplied `capabilityToken` on the outbound request (typically * via `Authorization: Bearer` / `PluginCap`). The `uploadDocument` kind is * additive — implementations that predate it fall through to their unknown-kind * branch until updated (the FE upload hook is inert until the host handles it). */ interface McpHttpClient { /** * Performs the call. The response's `status` and `headers` are optional and additive: supplying * them is what enables version-skew detection (see {@link detectVersionSkew}), and an * implementation that omits them behaves exactly as before rather than failing. */ fetch(request: McpHttpRequest): Promise; } /** * Construction options for {@link WorkerRemoteDomTransport}. */ interface WorkerRemoteDomTransportOptions { /** * Canonical host-origin URL for the worker bootstrap script. This MUST * be supplied by the host — never built from user input or extension * manifest values — so the worker spawn is path-pinned. */ bootstrapUrl: string; /** * Mints (or fetches a cached) capability token. The host transport * resolves this for each outbound MCP HTTP call. The token NEVER leaves * host scope — plugin code (the worker side) cannot read it. */ capabilityToken: () => Promise; /** * Host-side MCP HTTP client. The transport bridges port-side plugin * MCP requests to this client and returns only the response payload * back through the port. */ mcpClient: McpHttpClient; /** * The extension version this surface mounted at. Supplying it enables *proactive* skew * detection: a successful response stamped with a different version means the install moved * while the surface is still working, so it can remount before anything fails. Without it only * outright rejections are detectable, which is a worse experience but not a broken one. */ mountedExtensionVersion?: string; /** * Called at most once, when this surface is found to be running against a version that is no * longer installed. The host is expected to remount the surface (which re-mints a capability * token against the new version); the transport itself does not retry, because the pending call * belongs to a page that is about to be replaced. */ onVersionSkew?: (skew: VersionSkew) => void; /** * Injectable Worker constructor (tests use a fake). Defaults to the * global `Worker`. */ workerCtor?: WorkerCtor; /** * Coalescing window in milliseconds for high-frequency events. Defaults * to 16ms (~one animation frame). Trailing-edge: the most recent payload * within the window is delivered after the window elapses. */ coalesceMs?: number; /** * Maximum number of MCP requests (combined `invokeTool` + `getResource`) * the transport will forward to {@link mcpClient} concurrently. Defaults * to 8. Requests above the cap are rejected with a deterministic error * reply over the port — the worker MUST handle that response shape. * * Bounded concurrency is a back-pressure boundary, not a quota: it stops * a compromised or buggy worker from saturating the host's HTTP / token * pool. Production hosts may want to lower this in multi-tenant pools * where many extensions share one host process. */ maxConcurrentMcpRequests?: number; } /** * Wire shape of a host-sourced input event forwarded to the worker over the * port. Pointer / wheel / keyboard payloads share a single envelope type so * the worker has one inbound dispatch site. The transport is structurally * agnostic to the payload — it forwards verbatim. * * `kind` discriminates the event family. Coordinates are CSS pixels relative * to the host canvas; coalescing happens host-side before the call to * {@link WorkerRemoteDomTransport.postInputEvent}. */ type InputEventPayload = { kind: "pointermove" | "pointerdown" | "pointerup" | "pointercancel"; surfaceId: string; x: number; y: number; buttons: number; pointerType: string; } | { kind: "wheel"; surfaceId: string; deltaX: number; deltaY: number; deltaMode: number; } | { kind: "keydown" | "keyup"; surfaceId: string; key: string; code: string; ctrlKey: boolean; shiftKey: boolean; altKey: boolean; metaKey: boolean; }; /** * Documented wire protocol identifier embedded in handshake / message * envelopes. Bumped when the worker ↔ host message shape changes in a * backward-incompatible way. */ declare const WORKER_TRANSPORT_PROTOCOL = "ethisys.worker.remotedom.v1"; /** * Wire shape of the handshake message the host posts to the worker on * {@link WorkerRemoteDomTransport.connect}. Locked here so the SDK side and the * API-hosted bootstrap script (`WorkerBootstrapScriptProvider`) read from the * same field names — see the API tests pinning `event.data.moduleUrl` and * `event.data.importMap`. * * The handshake is the SOLE message that ever carries `moduleUrl` or * `importMap`; subsequent port traffic uses the discriminated message types * declared below. The capability token NEVER appears in this payload (or any * other postMessage payload) — it is bound on the host side via * {@link WorkerRemoteDomTransportOptions.capabilityToken}. */ interface WorkerHandshakePayload { readonly type: "ethisys:worker:handshake"; readonly protocol: string; /** Host-origin URL of the plugin's worker bundle entry. */ readonly moduleUrl: string; /** * Frozen bare-specifier → host-origin URL map that the bootstrap installs * before importing {@link moduleUrl}. Pulled from the plugin's * `worker-bundle.import-map.json` at runtime by the host mount. */ readonly importMap: Readonly>; } /** * Worker-side host transport for Contract B extensions. * * Construction immediately spawns the worker and transfers one end of a * fresh `MessageChannel`; the host retains `port1`, the worker receives * `port2`. Both sides exchange messages via their port and the worker port * is the *only* channel for plugin ↔ host traffic after handshake. */ /** * Default cap for in-flight MCP requests forwarded by the transport. Chosen to * cover typical interactive UI traffic (dashboards, list views, detail panels) * without letting a runaway worker saturate the host's HTTP / token pool. */ declare const DEFAULT_MAX_CONCURRENT_MCP_REQUESTS = 8; declare class WorkerRemoteDomTransport { private readonly worker; private readonly hostPort; private readonly workerPort; private readonly mcpClient; private readonly capabilityTokenProvider; private readonly mountedExtensionVersion?; private readonly onVersionSkew?; private versionSkewReported; private readonly coalesceMs; private readonly maxConcurrentMcpRequests; private readonly abortController; private readonly inFlightAborts; private inFlightMcpRequests; private remoteDomConsumer; private bridgeMessageConsumer; private disposed; private connected; constructor(options: WorkerRemoteDomTransportOptions); /** * Post the handshake to the worker. Idempotent — only the FIRST call * transfers the {@link MessagePort} and posts the handshake payload; * subsequent calls are silent no-ops. Splitting this off from the * constructor lets the host mount fetch the per-plugin * `worker-bundle.import-map.json` (and resolve the bundle's module URL) * before the bootstrap script consumes them. * * Wire shape: {@link WorkerHandshakePayload}. The capability token NEVER * appears in the payload — it's bound on the host side via the * {@link WorkerRemoteDomTransportOptions.capabilityToken} provider. * * The `moduleUrl` and `importMap` are forwarded to the worker so the * bootstrap script (served from the host-pinned * `/extensions/runtime/worker-bootstrap.js`) can: * 1. Compose the frozen, same-origin-validated `IMPORT_MAP` from the * handshake payload (rejecting any cross-origin entries). * 2. `safeImport(moduleUrl)` the plugin's entry module. * * Same-origin enforcement is the bootstrap's job — this transport is * structurally agnostic to the host origin and only forwards what it's * handed. */ connect(moduleUrl: string, importMap: Record): void; /** * Bind a consumer for Remote DOM mutation payloads emitted by the worker. * The Remote DOM receiver wiring is owned by the caller (typically a host * React component); this method exposes the raw stream so the receiver * can integrate cleanly without a circular package import. */ onRemoteDom(consumer: (payload: unknown) => void): void; /** * Transfer control of a host-owned `` to the worker so plugin code * can render via `OffscreenCanvas`. The host retains the `` for * layout / accessibility purposes only — every pixel is produced inside the * worker, so the main thread is never blocked per frame. * * The transfer rides the established MessagePort (not the worker global * `postMessage`) so it is multiplexed with the rest of the host ↔ worker * traffic on the same channel. The `OffscreenCanvas` handle is the sole * `Transferable` in the envelope; no capability token, MCP context, or * other host-only data crosses the boundary. * * Throws if the environment lacks `transferControlToOffscreen()` or if the * canvas has already been transferred — see {@link createOffscreenCanvasTransfer} * for the exact diagnostics. */ transferCanvas(canvas: HTMLCanvasElement, options: { surfaceId: string; }): { offscreen: OffscreenCanvas; }; /** * Forward a host-sourced input event to the worker over the port. The * payload shape is opaque to the transport — pointer / wheel / keyboard * envelopes share the same wire type. Callers are expected to wrap * high-frequency (pointer-move, wheel, scroll) events in * {@link createCoalescer} or {@link createInputEventCoalescer} BEFORE * calling this method so the port never sees the un-coalesced flood. * * Keyboard / pointer-down / pointer-up events are discrete and should be * delivered without coalescing — pass them straight through. */ postInputEvent(payload: InputEventPayload): void; /** * Build a trailing-edge coalescer keyed to {@link WorkerRemoteDomTransportOptions.coalesceMs}. * * Use it to wrap pointer-move / scroll / resize callbacks **before** * they cross the port. The last payload in any coalescing window is the * one that wins. */ createCoalescer(consumer: (payload: T) => void): (payload: T) => void; /** * Register a consumer for inbound plugin→host bridge messages * (chrome requests, a11y announce, lifecycle events). Only the most recently * registered consumer is kept — the host mount wires exactly one. */ onBridgeMessage(consumer: (msg: unknown) => void): void; /** * Push the current host theme to the plugin worker. Safe to call on every * host theme-context change — the transport coalesces nothing here; rate * limiting at the call site is the host's responsibility. */ pushTheme(payload: Omit): void; /** Push the current host locale and text direction to the plugin worker. */ pushLocale(payload: Omit): void; /** Push the current UI density preference to the plugin worker. */ pushDensity(payload: Omit): void; /** Push updated accessibility preferences to the plugin worker. */ pushA11y(payload: Omit): void; /** Push the current SPA navigation state to the plugin worker. */ pushNav(payload: Omit): void; /** * Tear down the worker and port. Idempotent. * * Order matters: we abort BEFORE closing the port so any in-flight * `mcpClient.fetch` that observes the signal short-circuits and the * subsequent attempt to post a reply lands in the disposed-guard branch * (which silently drops the post) instead of throwing on a closed port. */ dispose(): void; /** * Best-effort wrapper around `hostPort.postMessage` that tolerates posts * arriving after {@link dispose}. The browser throws on a closed port and * the async handlers below can race dispose, so any post initiated by an * awaited continuation must be guarded. */ private safePostMessage; private handlePortMessage; /** * Gate inbound MCP requests through the bounded concurrency window, * reject with a deterministic error reply when the cap is exceeded, and * decrement the counter unconditionally when the underlying handler * settles (regardless of resolution/rejection shape). */ private dispatchMcp; /** * Create a per-request AbortController and register it under the request id * so an inbound `ethisys:mcp:abort` can cancel just this call. If the * transport is already disposed the controller starts aborted so the * handler's `mcpClient.fetch` short-circuits immediately. */ private registerRequestAbort; /** Drop a settled request's AbortController. */ private releaseRequestAbort; /** * The signal to hand to `mcpClient.fetch` for a given request: the * per-request controller when registered (client cancel path), falling back * to the dispose-level controller. */ private requestSignal; /** * Raises the skew callback the first time this surface is seen to be running against a version * that is no longer installed. * * Latched deliberately. A page typically has several MCP calls in flight, and after an upgrade * every one of them reports the same skew — firing per response would ask the host to remount * the same surface repeatedly, and the second remount would tear down the first. One signal per * transport is all a remount needs, and the transport is discarded along with the surface. * * The callback is host code, so it is assumed to be able to throw. Two consequences are guarded * here: the throw must not escape into the MCP handler's catch (which would reply `ok: false` * for a call that actually succeeded), and it must not consume the latch (which would leave the * surface with no response AND no further chance of being told to remount). */ private reportVersionSkew; private handleInvokeTool; private handleGetResource; private handleUploadDocument; private replyError; } export { CAPABILITY_REJECTION_REASON_HEADER as C, DEFAULT_MAX_CONCURRENT_MCP_REQUESTS as D, EXTENSION_ACTIVE_VERSION_HEADER as E, type InputEventPayload as I, type McpHttpClient as M, VERSION_MISMATCH_REASON as V, WORKER_TRANSPORT_PROTOCOL as W, EXTENSION_VERSION_HEADER as a, type McpHttpRequest as b, type McpHttpResponse as c, type McpResponseMetadata as d, type McpUploadRequest as e, type VersionSkew as f, type VersionSkewSource as g, type WorkerCtor as h, type WorkerHandshakePayload as i, type WorkerLike as j, WorkerRemoteDomTransport as k, type WorkerRemoteDomTransportOptions as l, detectVersionSkew as m };