Repository navigation
feat(sdk): migrate SSE auth from ?token=JWT to Fetch EventSource #203
Description
Activity
- addedenhancementNew feature or requestNew feature or requestarea/apiHTTP handlers, routing, middlewareHTTP handlers, routing, middlewarearea/sdkTypeScript SDK (clients/ts/)TypeScript SDK (clients/ts/)securitySecurity-sensitive issue or fixSecurity-sensitive issue or fix
on Jun 2, 2026 - addedarea/docsDocumentation, site/, READMEDocumentation, site/, README
on Jun 2, 2026 - added a parent issue
on Jun 2, 2026 - addedarea/observabilityMetrics, logs, traces, health, profilingMetrics, logs, traces, health, profilingbreaking-changeBreaking change to public API, CLI, or configBreaking change to public API, CLI, or config
on Jun 2, 2026 - removedarea/observabilityMetrics, logs, traces, health, profilingMetrics, logs, traces, health, profiling
on Jun 3, 2026 - marked security(streaming): SSE ?token= bearer transits the upstream proxy/CDN in the URL (logs/referrers) #324 as a duplicate of this issue
on Jun 10, 2026 - addedarea/streamingSSE / live-query delivery path (/v1/stream)SSE / live-query delivery path (/v1/stream)area/authAuthentication: tokens, JWT/JWKS, keys, token expiry/revocationAuthentication: tokens, JWT/JWKS, keys, token expiry/revocation
on Aug 3, 2026 Context from #456 (
options.fetch) — findings that bear on this issue#456 adds
options.fetchtoClientOptions, letting a consumer supply the HTTP implementation. It is REST-only by construction, so it does not touch this issue's surface — but the two overlap enough that it is worth writing down what was learned, so whoever picks this up can build on it rather than rediscover it.I have no strong opinion on which direction this issue should take — the SSE-side tradeoffs (reconnect semantics, browser support policy,
?token=deprecation timing) likely have context I don't have. What follows is findings and options, not a recommendation.What #456 establishes
ClientOptions.fetch?: FetchLike, threaded throughHttpContextto the single REST chokepoint (request()inhttp.ts). Every namespace inherits it; retries go through it.- The exported type is deliberately narrower than
typeof fetch:A string URL is all the SDK ever passes. This accepts strictly more implementations thanexport type FetchLike = (url: string, init?: RequestInit) => Promise<Response>;
typeof fetchdoes — the globalfetchstill assigns, and so does hand-written(url: string, init?: RequestInit) => Promise<Response>middleware, whichtypeof fetchrejects on parameter contravariance. - SSE is explicitly documented as exempt.
stream/sse.tsconstructsnew EventSource(url.toString())(sse.ts:80) and never touchesfetch, so the override cannot reach it..liveQuery()'s initial backfill is an ordinary REST call and does go through the override; only the live connection is exempt. This asymmetry is documented on the SDK page.
Whatever lands here decides whether that carve-out shrinks or stays.
Finding: the
eventsourcepolyfill already supports a fetch overrideThe
eventsourcenpm package added afetchoption in v4.1.0, which both attaches arbitrary headers and lets a caller inject a fetch implementation:new EventSource(url, { fetch: (input, init) => fetch(input, { ...init, headers: { ...init.headers, Authorization: 'Bearer …' } }), })
It also exposes
Symbol.for('eventsource.supports-fetch-override')so a consumer can feature-detect support before relying on it.Relevant detail: this repo already depends on
eventsource@4.1.0(tests/e2e/sdk/polyfills.ts, resolved ateventsource@4.1.0in the lockfile), and the SDK docs already instruct Node users to polyfill with it.The catch: this only helps where the polyfill is in play. Browsers use native
EventSource, which supports neither custom headers nor a fetch override — onlywithCredentials. That is the whole reason?token=exists today (sse.ts:63-68).Two shapes this could take
Option A — use the polyfill's
fetchoverride.
Pass a fetch (and/or headers) into theEventSourceconstructor when the implementation supports it, detected via the symbol above.- Cheap; no transport rewrite.
- Keeps native
EventSource's auto-reconnect andLast-Event-IDresumption for free. - Node/polyfill only. Browsers keep
?token=, so the query-param deprecation in this issue's task list could not complete for the browser path, and feat(sdk): allow custom request headers (or a fetch override) for header-gated origins #269's header-gated-origin case stays unsolved for browser streaming.
Option B — implement SSE over
fetch+ReadableStream(the@microsoft/fetch-event-sourceapproach).
Stop usingEventSourceentirely; parse the event stream off afetchresponse body.- Works in browsers and Node alike, so
Authorizationheaders work everywhere and?token=can actually be retired. - Composes with feat(sdk)!: add options.headers, fetchOptions, and fetch #456: streams would flow through
options.fetchlike everything else, closing the documented carve-out. - You inherit what native
EventSourcewas doing for free, and this is the part worth scoping carefully:- Auto-reconnect with backoff.
sse.ts:16currently notes "Native EventSource auto-reconnects", and status transitions are derived fromreadyState(sse.ts:106-110) — all of that would need reimplementing. Last-Event-IDresumption. The server prefers theLast-Event-IDheader for gap-fill (internal/api/stream.go:86-89), and nativeEventSourcesets it automatically on reconnect. A fetch-based transport must track the last event id and resend it itself, or resumption silently regresses.- This interacts with SSE: emit periodic keepalive heartbeats so idle streams survive proxy timeouts #226's keepalive fix — see
docs/.../reverse-proxy.mdx:124, where quiet-stream drops were masked in browsers precisely becauseEventSourceauto-reconnected and gap-filled viaLast-Event-ID. Losing that would surface those drops to browser consumers.
- Auto-reconnect with backoff.
Option C — a hybrid: native
EventSourcewhen no header/fetch customization is requested, fetch-based transport only when it is. Preserves today's battle-tested browser path by default, at the cost of two code paths to maintain and test.If a fetch-based transport is chosen
options.fetchwill not apply to streams automatically — the new transport has to threadctx.options.fetchthrough explicitly (it is onHttpContext.options.fetchalready). It is a small change once the transport is fetch-based, but it is a deliberate one. If that happens, the SSE carve-out currently documented indocs/src/content/docs/sdk/index.mdxand in theClientOptions.fetchTSDoc needs updating in the same PR.Ecosystem note
The seam is not unusual: Supabase configures its realtime transport separately (
realtime?: RealtimeClientOptions) and itsglobal.fetchdoes not cover it either. So "the fetch override doesn't reach the streaming transport" is the norm in this space, not a WaveHouse wart — worth knowing when weighing how much Option B's cost is worth.Sequencing
#456 is open and does not block this. #269 is being rescoped so that headers /
fetchOptions/fetchland on the REST path there, and the streaming half is tracked here — so this issue owns the answer to "can a header-gated or cookie-authenticated origin be streamed from?"Both #456 and this will touch the streaming config surface, so whichever merges second wants a
git merge origin/mainfirst.— Claude Opus 5, via Claude Code
- moved this from Backlog to In progress in WaveHouse Task Board
on Aug 13, 2026
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsDone
Move SSE auth off the
?token={JWT}query param to a Fetch-basedEventSourceimplementation that attaches anAuthorizationheader.Tasks
Authorization: Bearer …?token={jwt}path (warn now, remove in a later major)(WebSocket removal already shipped in #190.)
Part of #194