You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Area: sdk — friction (DX) / gap · found via WaveHouse-Stats dogfooding
Expected: point the SDK at a WaveHouse fronted by a header-gated proxy (Cloudflare Access service token, mTLS sidecar, auth gateway) by attaching arbitrary per-request headers — or by supplying a custom fetch.
Actual:ClientConfig exposes only baseURL, auth (which sets just Authorization: Bearer …), and options.maxRetries. No headers option, no fetch injection point — so the SDK can't carry headers like CF-Access-Client-*.
Impact (Stats): initially blocked them (Worker writes went through a CF-Access-gated origin the SDK couldn't satisfy). They've since moved ingest to a public JWT-gated endpoint, so it no longer blocks them — but it still bites any deployment fronting WaveHouse behind a header-gated proxy. A defense-in-depth gate shouldn't force a consumer off the SDK.
Additional consumers named later: cookie-authenticated origins, where the blocker isn't a missing header but that fetch defaults to credentials: "same-origin", so a cross-origin browser request sends no cookie at all.
Related: SDK epic #194; SSE-auth migration #203; base path in baseURL#428 (a deployment behind a BFF generally needs both).
From WaveHouse-Stats WAVEHOUSE-FEEDBACK.md dogfooding (dev 71c2dc4), 2026-06-04.
Rescoped 2026-08-12. #456 already implements the fetch override; headers and fetchOptions are being folded into that same PR so the HTTP-customization surface lands as one coherent API rather than in three increments.
options.fetch — supply the HTTP implementation. Exported as FetchLike, deliberately narrower than typeof fetch (a string URL is all the SDK passes), which accepts strictly more implementations than the wide type does. REST path only, by construction. (implemented in feat(sdk)!: add options.headers, fetchOptions, and fetch #456)
headers — arbitrary headers on every request. The ergonomic answer for CF-Access and friends; a fetch override is a power tool that shouldn't be the only route to a static header.
fetchOptions — a RequestInit merged into every request. Covers credentials: "include" for the cookie case above, plus undici dispatcher, mode, cache, etc., without forcing a full fetch wrapper.
fetchOptions is the established name for "extra RequestInit on every request" — OpenAI's own documented undici-proxy example is new OpenAI({ fetch, fetchOptions: { dispatcher: proxyAgent } }). Adopting it is cheaper than inventing a credentials knob that solves one case.
Open design questions
Naming/nesting. Supabase nests under global: {}; Anthropic/OpenAI keep options flat. WaveHouse currently nests transport config under options: {} (maxRetries, fetch). Do headers / fetchOptions sit under options for consistency, or at the top of ClientConfig next to auth?
Precedence. When auth, headers, and fetchOptions.headers all set Authorization, which wins? Needs to be defined and tested, not emergent. Anthropic's convention is that per-request options override client defaults, and an explicit null removes a default header.
Whether headers reaches SSE. Native EventSource cannot set headers at all — see below.
Area: sdk — friction (DX) / gap · found via WaveHouse-Stats dogfooding
Expected: point the SDK at a WaveHouse fronted by a header-gated proxy (Cloudflare Access service token, mTLS sidecar, auth gateway) by attaching arbitrary per-request headers — or by supplying a custom
fetch.Actual:
ClientConfigexposes onlybaseURL,auth(which sets justAuthorization: Bearer …), andoptions.maxRetries. Noheadersoption, nofetchinjection point — so the SDK can't carry headers likeCF-Access-Client-*.Impact (Stats): initially blocked them (Worker writes went through a CF-Access-gated origin the SDK couldn't satisfy). They've since moved ingest to a public JWT-gated endpoint, so it no longer blocks them — but it still bites any deployment fronting WaveHouse behind a header-gated proxy. A defense-in-depth gate shouldn't force a consumer off the SDK.
Additional consumers named later: cookie-authenticated origins, where the blocker isn't a missing header but that
fetchdefaults tocredentials: "same-origin", so a cross-origin browser request sends no cookie at all.Related: SDK epic #194; SSE-auth migration #203; base path in
baseURL#428 (a deployment behind a BFF generally needs both).From WaveHouse-Stats
WAVEHOUSE-FEEDBACK.mddogfooding (dev71c2dc4), 2026-06-04.Scope — all three knobs land together in #456
Rescoped 2026-08-12. #456 already implements the
fetchoverride;headersandfetchOptionsare being folded into that same PR so the HTTP-customization surface lands as one coherent API rather than in three increments.options.fetch— supply the HTTP implementation. Exported asFetchLike, deliberately narrower thantypeof fetch(astringURL is all the SDK passes), which accepts strictly more implementations than the wide type does. REST path only, by construction. (implemented in feat(sdk)!: add options.headers, fetchOptions, and fetch #456)headers— arbitrary headers on every request. The ergonomic answer for CF-Access and friends; afetchoverride is a power tool that shouldn't be the only route to a static header.fetchOptions— aRequestInitmerged into every request. Coverscredentials: "include"for the cookie case above, plus undicidispatcher,mode,cache, etc., without forcing a full fetch wrapper.Prior art driving the shape
global.fetch,global.headerstype Fetch = typeof fetchfetch,fetchOptions,defaultHeaders,defaultQuery(input: string | URL | Request, init?) => Promise<Response>fetch,fetchOptionsfetchOptionsis the established name for "extraRequestIniton every request" — OpenAI's own documented undici-proxy example isnew OpenAI({ fetch, fetchOptions: { dispatcher: proxyAgent } }). Adopting it is cheaper than inventing acredentialsknob that solves one case.Open design questions
global: {}; Anthropic/OpenAI keep options flat. WaveHouse currently nests transport config underoptions: {}(maxRetries,fetch). Doheaders/fetchOptionssit underoptionsfor consistency, or at the top ofClientConfignext toauth?Record<string, string>alone can't carry a rotating service token. A() => Record<string,string> | Promise<…>form can, but inherits the per-call cost question in feat(sdk): define the auth() callback contract — per-request cost, caching, retry staleness #458.auth,headers, andfetchOptions.headersall setAuthorization, which wins? Needs to be defined and tested, not emergent. Anthropic's convention is that per-request options override client defaults, and an explicitnullremoves a default header.headersreaches SSE. NativeEventSourcecannot set headers at all — see below.Out of scope — split out
stream/sse.tsusesnew EventSource(url), which can set no headers and takes no fetch, which is why the JWT rides in?token=today. None of the three knobs above can reach it without a transport change; feat(sdk): migrate SSE auth from ?token=JWT to Fetch EventSource #203 owns that decision and now carries the full analysis (polyfill fetch-override vs. fetch-based transport vs. hybrid, and theLast-Event-ID/ auto-reconnect cost). Until feat(sdk): migrate SSE auth from ?token=JWT to Fetch EventSource #203 lands, this issue's fix is REST-only, and that limit should be documented rather than left implicit.auth()per-request cost and caching → feat(sdk): define the auth() callback contract — per-request cost, caching, retry staleness #458. Note that the original comment here claimedauth()also runs on every retry attempt; that is not accurate — it is called once perrequest(), before the retry loop (http.ts:48vs:57). feat(sdk): define the auth() callback contract — per-request cost, caching, retry staleness #458 records the correction.