Skip to content

feat(sdk): define the auth() callback contract — per-request cost, caching, retry staleness #458

Description

@EricAndrechek

Area: sdk — performance / DX · split out of #269

ClientConfig.auth is a callback the SDK awaits to obtain a bearer token. It is invoked once per request, with no caching, no in-flight dedupe, and no documented expectation about cost.

// clients/ts/src/http.ts:47-52
if (ctx.auth) {
  const token = await ctx.auth();
  if (token) headers.Authorization = `Bearer ${token}`;
}

That is free when the callback returns a value already in memory — the shape most examples show (auth: () => localStorage.getItem('token')). It is a network round trip on every single request for anyone whose callback mints or refreshes a token, which is a natural pattern for exactly the proxied / BFF / gateway deployments in #269 and #428. Nothing in the docs says which of those the SDK expects, so the expensive shape looks supported and is silently slow.

Two directions, not mutually exclusive:

  1. Document the contract. State plainly that auth is called per request and must be cheap — cache and refresh on your side. Lowest cost, and arguably sufficient.
  2. Dedupe in the SDK. Collapse concurrent in-flight calls (so a burst of parallel queries mints one token, not N), optionally with a short TTL. More helpful, but it needs a defined invalidation story — a cached token that outlives its expiry is worse than no cache, so it likely needs either a TTL the caller sets or a 401-triggered refresh.

Correction to the original #269 note

The #269 comment that raised this said auth() runs "again on every retry attempt". That part is not accurate — worth recording so it doesn't get designed around. auth() is called at http.ts:48, before the retry loop opens at http.ts:57, so a request that retries 3 times still calls auth() exactly once. Retries reuse the header computed up front.

That has its own (separate, lower-priority) consequence worth deciding on: if a token expires during a retry-and-backoff sequence, the retries keep sending the stale one, and the 503 Retry-After path can wait 30s by default. Whether to re-mint on retry is a real question, but it is the opposite trade from caching, and both should be settled together.

Not in scope

options.fetch (#456) is a workaround for the determined — a wrapper can cache tokens itself — but it is not an answer for the default path, and it does not apply to the SSE transport, which calls auth() separately (stream/sse.ts:65-68).

— Filed by Claude Opus 5, via Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/authAuthentication: tokens, JWT/JWKS, keys, token expiry/revocationarea/sdkTypeScript SDK (clients/ts/)enhancementNew feature or request

    Type

    No type

    Projects

    Status
    Backlog

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions