/** * AutoRefreshCoordinator, automatic token refresh with in-flight request queuing. * * Prevents user-visible 401s by: * 1. Pre-flight leeway check: if the token expires within refreshLeewayMs, * trigger an automatic refresh before the request is dispatched. * 2. Reactive 401 retry: if a request returns 401 and the token wasn't * already known expired, trigger a refresh then retry the request once. * 3. In-flight queuing: while a refresh is in progress, subsequent refresh * attempts queue on the same promise, one refresh call for all waiters. * * When no refresh endpoint is available (the coordinator has no `refresh` * function), the pre-flight check is a graceful no-op and the reactive path * triggers a token re-read (which may succeed if the store was updated externally). * * Error messages use this three-part format: * [what happened] · [why] · [what to do] */ import type { GoodVibesTokenStore } from './types.js'; import type { SDKObserver } from '../observer/index.js'; export interface AutoRefreshOptions { /** * Enable or disable automatic token refresh. Default: `true`. * When `false`, 401 responses propagate immediately without retry. */ readonly autoRefresh?: boolean | undefined; /** * Milliseconds before token expiry to trigger an automatic refresh. * Default: 60_000 (1 minute). */ readonly refreshLeewayMs?: number | undefined; /** * Clock used for the expiry comparisons. Defaults to `Date.now`. * * Exists so a caller, in practice a test, can drive the leeway and * expired-token branches deterministically instead of constructing tokens * whose real timestamps happen to straddle the window. Not a behaviour knob: * anything other than a monotonic wall clock makes expiry meaningless. */ readonly now?: (() => number) | undefined; /** * Consumer-provided callback invoked to obtain a new token when the current * token is near expiry (pre-flight leeway check) or a 401 is received * (reactive retry path). * * The callback must return the new token string and, optionally, its expiry * timestamp in Unix milliseconds. When provided, the coordinator calls this * to perform the actual token refresh and persists the result via * `setTokenEntry` (or `setToken` on stores that don't implement * `setTokenEntry`). * * When absent, the coordinator's pre-flight check is a graceful no-op and * the reactive 401 path re-reads the token store without making a network * call (useful when an external party updates the store). * * @example * const store = createMemoryTokenStore(initialToken); * const sdk = createGoodVibesSdk({ * baseUrl: 'https://daemon.example.com', * tokenStore: store, * autoRefresh: { * refresh: async () => { * const res = await fetch('/api/auth/refresh', { method: 'POST' }); * const { token, expiresAt } = await res.json(); * return { token, expiresAt }; * }, * }, * }); */ readonly refresh?: (() => Promise<{ token: string; expiresAt?: number; }>) | undefined; } export interface AutoRefreshCoordinatorOptions { readonly tokenStore: GoodVibesTokenStore; readonly autoRefresh: boolean; readonly refreshLeewayMs: number; /** Clock for expiry comparisons; defaults to `Date.now`. See AutoRefreshOptions.now. */ readonly now?: (() => number) | undefined; /** * Optional refresh function. Called to acquire a new token when the current * one is near expiry or a 401 was received. * * If undefined, the coordinator performs a graceful no-op (does not * error), in-flight queuing and leeway checks are still respected, but * no network call is made. Reactive 401 retry still re-reads the token * store in case an external party updated it. */ readonly refresh?: (() => Promise<{ token: string; expiresAt?: number | undefined; }>) | undefined; readonly observer?: SDKObserver | undefined; } export declare class AutoRefreshCoordinator { #private; constructor(options: AutoRefreshCoordinatorOptions); /** * Call before dispatching a request. If the token is near expiry (within * `refreshLeewayMs`), refreshes before the request goes out. * * If `autoRefresh` is disabled, this is a no-op. */ ensureFreshToken(): Promise; /** * Execute `fn` and retry once on 401 after triggering a refresh. * * If `autoRefresh` is false, the first 401 is rethrown immediately. * If the retry also returns 401, throws `GoodVibesSdkError{kind:'auth'}`. * * @param fn - The request function to execute. Must be side-effect-safe to * call twice (called at most twice). */ withRetryOn401(fn: () => Promise): Promise; /** * Refresh the token immediately and execute `fn` exactly once as the retry. * * Unlike `withRetryOn401`, this method does NOT call `fn` before refreshing, * it assumes the caller already received a 401 on the initial attempt. It * refreshes the token (serialised via the shared promise, as with * `withRetryOn401`) and then calls `fn` a single time. * * If `fn` throws a 401 on retry, a terminal `GoodVibesSdkError{kind:'auth'}` * is thrown with the standard three-part message format. * * Used by `createAutoRefreshMiddleware` to avoid making an extra HTTP call * when the middleware already observed the initial 401 from `next()`. * * @param fn - The retry request to execute after the refresh completes. */ refreshAndRetryOnce(fn: () => Promise): Promise; } export declare function is401Error(error: unknown): boolean; //# sourceMappingURL=auto-refresh.d.ts.map