Liking cljdoc? Tell your friends :D

com.blockether.vis.internal.provider.oauth

Single-flight OAuth token refresh, shared by every provider.

A token endpoint that ROTATES the refresh_token (Anthropic, OpenAI Codex, MCP servers) answers HTTP 400 invalid_grant when two exchanges spend one refresh token. A 401 storm (each iteration's retry plus the limits poll) caused exactly that race and killed whole turns. So refresh is serialized PER CREDENTIAL STORE, never globally: each make-file-refresher owns its lock, and an optional OS file lock serializes processes. Under the lock, a caller reuses a result that another caller just saved, so a burst of N 401s runs ONE exchange.

USE: a file-backed rotating store calls make-file-refresher. It returns a 0/1-arg fn for :provider/get-token-fn / :provider/refresh-token-fn. A store with its own cache (GitHub Copilot) calls single-flight! and log-refresh! itself.

Single-flight OAuth token refresh, shared by every provider.

A token endpoint that ROTATES the refresh_token (Anthropic, OpenAI Codex, MCP
servers) answers HTTP 400 `invalid_grant` when two exchanges spend one refresh
token. A 401 storm (each iteration's retry plus the limits poll) caused exactly
that race and killed whole turns. So refresh is serialized PER CREDENTIAL STORE,
never globally: each `make-file-refresher` owns its lock, and an optional OS file
lock serializes processes. Under the lock, a caller reuses a result that another
caller just saved, so a burst of N 401s runs ONE exchange.

USE: a file-backed rotating store calls `make-file-refresher`. It returns a 0/1-arg
fn for `:provider/get-token-fn` / `:provider/refresh-token-fn`. A store with its own
cache (GitHub Copilot) calls `single-flight!` and `log-refresh!` itself.
raw docstring

auth-json-strclj

(auth-json-str auth-state)

Encode provider auth state with the canonical total JSON codec.

Encode provider auth state with the canonical total JSON codec.
sourceraw docstring

call-with-file-lockclj

(call-with-file-lock lock-path f)
(call-with-file-lock lock-path timeout-ms f)

Run f while holding an OS advisory lock on lock-path, serializing the critical section ACROSS processes (e.g. two vis JVMs — a source-runtime client and its gateway daemon — sharing one credential file). The caller must already hold the in-process monitor, since a single JVM cannot hold two overlapping locks on the same file. f gets true when it holds the lock, false when it runs unlocked, and nil for a blank lock-path.

f runs EXACTLY ONCE, always, and its exception propagates untouched — a retry here would re-run a ROTATING token exchange with an already-spent refresh token. Any locking failure (unsupported filesystem, IO error, overlapping lock), or a peer still holding the lock after timeout-ms, degrades to running f UNLOCKED rather than blocking authentication.

The single exception is an INTERRUPT while waiting for a peer: the caller is being cancelled, so f does NOT run and InterruptedException propagates with the flag restored. The lock file handle is closed on EVERY exit path, including that one — an fd left to the GC would also leave the advisory lock held for an unbounded time.

Run `f` while holding an OS advisory lock on `lock-path`, serializing the
critical section ACROSS processes (e.g. two vis JVMs — a source-runtime client and
its gateway daemon — sharing one credential file). The caller must already
hold the in-process monitor, since a single JVM cannot hold two overlapping
locks on the same file. `f` gets true when it holds the lock, false when it
runs unlocked, and nil for a blank `lock-path`.

`f` runs EXACTLY ONCE, always, and its exception propagates untouched — a
retry here would re-run a ROTATING token exchange with an already-spent
refresh token. Any locking failure (unsupported filesystem, IO error,
overlapping lock), or a peer still holding the lock after `timeout-ms`,
degrades to running `f` UNLOCKED rather than blocking authentication.

The single exception is an INTERRUPT while waiting for a peer: the caller is
being cancelled, so `f` does NOT run and `InterruptedException` propagates
with the flag restored. The lock file handle is closed on EVERY exit path,
including that one — an fd left to the GC would also leave the advisory lock
held for an unbounded time.
sourceraw docstring

default-file-lock-timeout-msclj

How long to wait for the CROSS-PROCESS advisory lock before giving up and running unlocked. A peer's own exchange takes a second or two, so this is generous; the point is that a peer that STALLED (or died holding the lock on a filesystem that leaks it) must never freeze provider auth in this JVM forever — the caller reaches here holding the in-process monitor, so a wedged acquisition blocks EVERY refresh here, not just this one.

How long to wait for the CROSS-PROCESS advisory lock before giving up and
running unlocked. A peer's own exchange takes a second or two, so this is
generous; the point is that a peer that STALLED (or died holding the lock on a
filesystem that leaks it) must never freeze provider auth in this JVM
forever — the caller reaches here holding the in-process monitor, so a wedged
acquisition blocks EVERY refresh here, not just this one.
sourceraw docstring

default-reuse-window-msclj

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.
sourceraw docstring

fresh-within?clj

(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.
sourceraw docstring

log-refresh!clj

(log-refresh! {:keys [store outcome trigger started-ms error refresh-token
                      next-refresh-token locked]})

Log ONE refresh exchange of credential store store as ::token-refresh. outcome is :refreshed, :superseded (refused, but another writer replaced the credential), :rejected (a sign-in is needed), :failed, :persist-failed (the rotated token is lost) or :no-refresh-token. locked is false when the exchange ran without its file lock. The record holds no token, body or error message: only the HTTP status, the error type and class, and token fingerprints.

Log ONE refresh exchange of credential store `store` as `::token-refresh`.
`outcome` is :refreshed, :superseded (refused, but another writer replaced the
credential), :rejected (a sign-in is needed), :failed, :persist-failed (the
rotated token is lost) or :no-refresh-token. `locked` is false when the exchange
ran without its file lock. The record holds no token, body or error message:
only the HTTP status, the error type and class, and token fingerprints.
sourceraw docstring

make-file-refresherclj

(make-file-refresher {:keys [store load saved-at usable? refresh-token exchange!
                             persist! on-rejected! ->token no-token!
                             reuse-window-ms lock-path]})

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, MCP servers). Owns its own lock; reuses usable saved creds 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. Every exchange is logged under :store through log-refresh!; a reuse is not.

opts — all required unless noted: :store keyword or string the store name in the log :load (fn [] -> creds-map|nil) read persisted creds :saved-at (fn [creds] -> epoch-ms|nil) persist timestamp; required only without :usable? :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 :usable? (optional) (fn [creds] -> bool) reuse test; default: saved within :reuse-window-ms :on-rejected! (optional) (fn [creds]) after a refused grant, before the sign-in error; default: persist :reauth-required true :reuse-window-ms (optional) override the default window :lock-path (optional) file path for an OS advisory lock that serializes the rotating exchange ACROSS processes (two vis JVMs sharing one credential file). Without it, only in-process callers serialize and two processes can still race the rotation into HTTP 400 invalid_grant.

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, MCP servers). Owns its own lock; reuses usable saved creds
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. Every exchange is logged under `:store` through
[[log-refresh!]]; a reuse is not.

opts — all required unless noted:
  :store         keyword or string              the store name in the log
  :load          (fn [] -> creds-map|nil)       read persisted creds
  :saved-at      (fn [creds] -> epoch-ms|nil)   persist timestamp; required
                                                only without `:usable?`
  :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
  :usable?       (optional) (fn [creds] -> bool) reuse test; default: saved
                 within `:reuse-window-ms`
  :on-rejected!  (optional) (fn [creds]) after a refused grant, before the
                 sign-in error; default: persist `:reauth-required true`
  :reuse-window-ms (optional) override the default window
  :lock-path       (optional) file path for an OS advisory lock that
                   serializes the rotating exchange ACROSS processes
                   (two vis JVMs sharing one credential file). Without it,
                   only in-process callers serialize and two processes can
                   still race the rotation into HTTP 400 `invalid_grant`.

Returns the provider-token map produced by `:->token`.
sourceraw docstring

new-lockclj

(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.
sourceraw docstring

reauthentication-error?clj

(reauthentication-error? error)

True when a refresh grant requires a new sign-in.

True when a refresh grant requires a new sign-in.
sourceraw docstring

reauthentication-statusclj

(reauthentication-status credentials)

Return the saved rejection verdict, or nil when sign-in is not required.

Return the saved rejection verdict, or nil when sign-in is not required.
sourceraw docstring

require-usable-credentials!clj

(require-usable-credentials! credentials)

Reject a saved invalid grant before token reuse or another refresh exchange.

Reject a saved invalid grant before token reuse or another refresh exchange.
sourceraw docstring

single-flight!clj

(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.

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.
sourceraw docstring

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
←Move to previous article
→Move to next article
Ctrl+/Jump to the search field
× close