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:
56
CHANGELOG.md
56
CHANGELOG.md
@@ -5,6 +5,62 @@ All notable changes to Shade are documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [4.6.1] — 2026-05-07 — Browser `fetch` receiver lost in `Inbox` and HTTP bridges
|
||||
|
||||
Every browser consumer of the v4.6.0 transport stack crashed on the
|
||||
*first* network call with:
|
||||
|
||||
```
|
||||
Failed to execute 'fetch' on 'Window': Illegal invocation
|
||||
```
|
||||
|
||||
`@shade/inbox`, `@shade/transport-bridge` (`SseBridge`, `LongPollBridge`)
|
||||
each cached the default `globalThis.fetch` reference as a class property
|
||||
and later invoked it as `this.fetchImpl(url, …)` / `this.fetchFn(url, …)`.
|
||||
The browser's `fetch` is a WebIDL bound operation: calling it as a
|
||||
method on any object other than the `Window` rejects with the error
|
||||
above. Node/Bun `fetch` tolerates a free receiver, so the bug only
|
||||
manifested in actual browsers and slipped through the SDK test suite.
|
||||
|
||||
Reported by Prism (multi-device E2EE terminal) — `inbox.start()` →
|
||||
`register()` → `client.register()` → `this.fetchImpl(url, …)` threw on
|
||||
the first `/v1/inbox/register` POST, so `transport.start()` never sent
|
||||
the pair handshake and the web side timed out after 30s with "PC did
|
||||
not reply".
|
||||
|
||||
### Fixed
|
||||
|
||||
#### `@shade/inbox` — `InboxClient` constructor
|
||||
`fetchImpl` is now `(options.fetch ?? globalThis.fetch).bind(globalThis)`.
|
||||
A consumer-supplied `options.fetch` is bound too — a custom fetch with
|
||||
its own receiver requirements must bind itself; binding to `globalThis`
|
||||
is otherwise a no-op for free functions.
|
||||
|
||||
#### `@shade/transport-bridge` — `LongPollBridge` and `SseBridge` constructors
|
||||
Same binding fix in both. `WsBridge` was unaffected (uses `WebSocket`).
|
||||
|
||||
### Tests
|
||||
- `packages/shade-inbox/tests/client.test.ts` — installs a strict-receiver
|
||||
`globalThis.fetch` that mimics the WebIDL "Illegal invocation" check,
|
||||
constructs `InboxClient` with no `fetch` override, runs `register()`,
|
||||
and asserts the strict fetch saw `globalThis` as `this`. Pre-fix this
|
||||
throws; post-fix it passes.
|
||||
- `packages/shade-transport-bridge/tests/bridge.test.ts` — same regression
|
||||
for both `LongPollBridge.connect()` (probe call) and `SseBridge.connect()`
|
||||
(open-once call).
|
||||
|
||||
### Migration
|
||||
|
||||
None. Existing `options.fetch` overrides keep working unchanged. Apps
|
||||
shipping a workaround like
|
||||
|
||||
```ts
|
||||
new Inbox({ ..., fetch: globalThis.fetch.bind(globalThis) });
|
||||
```
|
||||
|
||||
can drop the `.bind(globalThis)` and the redundant `fetch:` option once
|
||||
they're on `4.6.1`.
|
||||
|
||||
## [4.6.0] — 2026-05-07 — Broadcast channels (Signal sender-keys for one-to-many fan-out)
|
||||
|
||||
Prism's PC desktop is the *sender* in a one-to-many fan-out — one PTY
|
||||
|
||||
Reference in New Issue
Block a user