release(v4.6.1): bind globalThis.fetch in browser-receiver-sensitive call sites

Browsers' Window.fetch is a WebIDL bound operation; storing it as
this.fetchImpl / this.fetchFn and calling via the instance receiver
threw "Illegal invocation" on the first request. Bind once at
construction in InboxClient, LongPollBridge, and SseBridge. Reported
by Prism (multi-device E2EE terminal), blocking every browser
consumer of the v4.6 transport stack on inbox.start() / bridge.connect().

WsBridge unaffected (uses WebSocket). Node/Bun fetch tolerates a free
receiver, so the bug never surfaced server-side — added regression
tests that install a strict-receiver globalThis.fetch to catch the
issue without an actual browser harness.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-07 23:00:58 +02:00
parent 2c400d7094
commit 8746571d2a
31 changed files with 243 additions and 28 deletions

View File

@@ -51,7 +51,10 @@ export class LongPollBridge implements BridgeTransport {
private loopPromise: Promise<void> | null = null;
constructor(private readonly options: LongPollBridgeOptions) {
this.fetchFn = options.fetch ?? globalThis.fetch;
// Bind to globalThis: browser `fetch` is a WebIDL bound operation
// and throws "Illegal invocation" when called as `this.fetchFn(...)`.
const f = options.fetch ?? globalThis.fetch;
this.fetchFn = f.bind(globalThis);
this.cursor = options.startCursor ?? 0;
}

View File

@@ -58,7 +58,10 @@ export class SseBridge implements BridgeTransport {
console.warn('[shade-bridge:sse]', err.message);
constructor(private readonly options: SseBridgeOptions) {
this.fetchFn = options.fetch ?? globalThis.fetch;
// Bind to globalThis: browser `fetch` is a WebIDL bound operation
// and throws "Illegal invocation" when called as `this.fetchFn(...)`.
const f = options.fetch ?? globalThis.fetch;
this.fetchFn = f.bind(globalThis);
this.cursor = options.startCursor ?? 0;
}