Unified OAuth token-refresh facade, shared by every provider.
WHY THIS EXISTS — Providers whose token endpoint ROTATES the
refresh_token on every exchange (Anthropic, OpenAI Codex) must never run
two refresh exchanges at once: the second reuses an already-rotated
refresh token and the server answers HTTP 400 invalid_grant. Under a
401 "storm" — the turn loop's per-iteration retry PLUS a usage/limits
poll, all sharing one credential — that race killed whole turns (~10 refreshes/min: one lost the
rotation race and the turn died). Providers that mint a short-lived token from a STABLE
credential (GitHub Copilot) don't 400, but still benefit: concurrent
401s otherwise stampede the exchange endpoint with redundant calls.
THE MODEL — refresh is serialized PER CREDENTIAL STORE, never globally.
Each make-file-refresher / refresher call mints its OWN lock, so a
refresh to Anthropic and a refresh to Codex run fully in parallel; only
two refreshes to the SAME store (e.g. two sessions both hitting
Anthropic) serialize — which is the whole point. Once the lock is held,
a caller REUSES a result another thread just produced (creds persisted
within default-reuse-window-ms, or an already-valid cache) so a burst
of N concurrent 401s collapses into ONE exchange.
USE — file-backed rotating stores (Anthropic, Codex): make-file-refresher.
Cache-backed / bespoke stores (Copilot): refresher with custom
reuse/refresh fns. Both return a 0-arg fn yielding the provider-token
map, owning their own lock; drop them straight into
:provider/get-token-fn / :provider/refresh-token-fn.
Unified OAuth token-refresh facade, shared by every provider. WHY THIS EXISTS — Providers whose token endpoint ROTATES the refresh_token on every exchange (Anthropic, OpenAI Codex) must never run two refresh exchanges at once: the second reuses an already-rotated refresh token and the server answers HTTP 400 `invalid_grant`. Under a 401 "storm" — the turn loop's per-iteration retry PLUS a usage/limits poll, all sharing one credential — that race killed whole turns (~10 refreshes/min: one lost the rotation race and the turn died). Providers that mint a short-lived token from a STABLE credential (GitHub Copilot) don't 400, but still benefit: concurrent 401s otherwise stampede the exchange endpoint with redundant calls. THE MODEL — refresh is serialized PER CREDENTIAL STORE, never globally. Each `make-file-refresher` / `refresher` call mints its OWN lock, so a refresh to Anthropic and a refresh to Codex run fully in parallel; only two refreshes to the SAME store (e.g. two sessions both hitting Anthropic) serialize — which is the whole point. Once the lock is held, a caller REUSES a result another thread just produced (creds persisted within `default-reuse-window-ms`, or an already-valid cache) so a burst of N concurrent 401s collapses into ONE exchange. USE — file-backed rotating stores (Anthropic, Codex): `make-file-refresher`. Cache-backed / bespoke stores (Copilot): `refresher` with custom reuse/refresh fns. Both return a 0-arg fn yielding the provider-token map, owning their own lock; drop them straight into `:provider/get-token-fn` / `:provider/refresh-token-fn`.
A refresh persisted/produced within this window is REUSED instead of re-run. Wide enough to absorb a 401 burst (every iteration's retry + the limits poll), short enough that a genuinely-stale token still refreshes promptly.
A refresh persisted/produced within this window is REUSED instead of re-run. Wide enough to absorb a 401 burst (every iteration's retry + the limits poll), short enough that a genuinely-stale token still refreshes promptly.
(fresh-within? saved-at-ms)(fresh-within? saved-at-ms window-ms)True when saved-at-ms (epoch millis, or nil) is within window-ms of
now — i.e. another caller persisted these creds this recently, so they
are safe to reuse rather than running another (rotating) exchange.
True when `saved-at-ms` (epoch millis, or nil) is within `window-ms` of now — i.e. another caller persisted these creds this recently, so they are safe to reuse rather than running another (rotating) exchange.
(make-file-refresher {:keys [load saved-at refresh-token exchange! persist!
->token no-token! reuse-window-ms]})Build a 0/1-arg single-flight refresh fn for a FILE-backed credential store whose token endpoint ROTATES the refresh_token (Anthropic, OpenAI Codex). Owns its own lock; reuses creds persisted within the reuse window instead of running a second (rotating) exchange — UNLESS the reusable token equals the one just rejected (pass that token when calling the returned fn on 401 recovery), so a server-rotated but locally-fresh token is re-exchanged rather than handed back dead.
opts — all required unless noted:
:load (fn [] -> creds-map|nil) read persisted creds
:saved-at (fn [creds] -> epoch-ms|nil) persist timestamp
(usually the keyword
:saved-at-ms/:saved-at)
:refresh-token (fn [creds] -> string|nil) the rotating refresh token
:exchange! (fn [refresh-token] -> creds) the rotating HTTP call
:persist! (fn [creds] -> creds) save + STAMP saved-at
:->token (fn [creds] -> token-map) runtime provider-token shape
:no-token! (fn [] -> throws) when no refresh token on file
:reuse-window-ms (optional) override the default window
Returns the provider-token map produced by :->token.
Build a 0/1-arg single-flight refresh fn for a FILE-backed credential
store whose token endpoint ROTATES the refresh_token (Anthropic,
OpenAI Codex). Owns its own lock; reuses creds persisted within the
reuse window instead of running a second (rotating) exchange — UNLESS the
reusable token equals the one just rejected (pass that token when calling the
returned fn on 401 recovery), so a server-rotated but locally-fresh token is
re-exchanged rather than handed back dead.
opts — all required unless noted:
:load (fn [] -> creds-map|nil) read persisted creds
:saved-at (fn [creds] -> epoch-ms|nil) persist timestamp
(usually the keyword
`:saved-at-ms`/`:saved-at`)
:refresh-token (fn [creds] -> string|nil) the rotating refresh token
:exchange! (fn [refresh-token] -> creds) the rotating HTTP call
:persist! (fn [creds] -> creds) save + STAMP saved-at
:->token (fn [creds] -> token-map) runtime provider-token shape
:no-token! (fn [] -> throws) when no refresh token on file
:reuse-window-ms (optional) override the default window
Returns the provider-token map produced by `:->token`.(new-lock)A fresh monitor object. One per credential store, owned by the refresher built around it — never share a lock across providers.
A fresh monitor object. One per credential store, owned by the refresher built around it — never share a lock across providers.
(refresher reuse refresh!)Build a single-flight refresh fn that OWNS a fresh lock. refresh! is as in
single-flight!; reuse takes ONE arg — the REJECTED token (the access
token the server just 401'd, or nil) — and returns the reusable token map or
nil. For stores that aren't a plain rotating file — e.g. an in-memory cache
(GitHub Copilot).
The returned fn is 0/1-arity: call it WITH the rejected token on 401 recovery
so reuse never hands back the very token that was just rejected; the 0-arity
form (nil rejected) keeps plain freshness reuse.
Build a single-flight refresh fn that OWNS a fresh lock. `refresh!` is as in `single-flight!`; `reuse` takes ONE arg — the REJECTED token (the access token the server just 401'd, or nil) — and returns the reusable token map or nil. For stores that aren't a plain rotating file — e.g. an in-memory cache (GitHub Copilot). The returned fn is 0/1-arity: call it WITH the rejected token on 401 recovery so `reuse` never hands back the very token that was just rejected; the 0-arity form (nil rejected) keeps plain freshness reuse.
(single-flight! lock reuse refresh!)Serialize refresh! on lock. Once the lock is held, call reuse
first: if it returns a non-nil result, another caller already refreshed
while this thread waited for the lock, so return that WITHOUT a second
exchange; otherwise run refresh! and return its result. reuse and
refresh! are 0-arg fns; reuse returns the reusable token map or nil.
Low-level core — prefer make-file-refresher / refresher.
Serialize `refresh!` on `lock`. Once the lock is held, call `reuse` first: if it returns a non-nil result, another caller already refreshed while this thread waited for the lock, so return that WITHOUT a second exchange; otherwise run `refresh!` and return its result. `reuse` and `refresh!` are 0-arg fns; `reuse` returns the reusable token map or nil. Low-level core — prefer `make-file-refresher` / `refresher`.
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |