# auth-client _(no submodules)_ ## Purpose `RackspaceAuthClient` wraps Rackspace identity v2 (`POST /v2.0/tokens`) with the three behaviours every consumer of the storage / CDN endpoints needs: a cached token reused across requests, a proactive refresh that fires before the documented expiry, and a service-catalog lookup that resolves the per-region `cloudFiles` and `cloudFilesCDN` endpoints in one round-trip. The adapter wires this client behind every public call so application code never thinks about token state or endpoint discovery. ## Features - Single-shot auth — POSTs `apiKeyCredentials` and caches the result. - Token cache reuses an in-memory `CachedToken` across calls. - Proactive refresh five minutes before the documented expiry — no caller is blocked waiting for an in-flight re-auth on a near-expired token. - Concurrent-call deduplication: parallel `getToken` calls share one auth request. - Region-aware service-catalog lookup; rejects with `REGION_NOT_IN_CATALOG` when the response lacks an entry for the configured region. - `invalidate()` hook so an outer 401 handler can drop the cache before re-trying. - AbortSignal threaded into the auth fetch. ## Usage: Basic ```ts import { RackspaceAuthClient } from "@suaveplan/storage-cdn-rackspace"; const auth = new RackspaceAuthClient({ username: "me", apiKey: "secret" }); const { tokenId, storageEndpoint, cdnEndpoint } = await auth.getToken(); ``` ## Usage: Advanced ```ts import { RackspaceAuthClient } from "@suaveplan/storage-cdn-rackspace"; const auth = new RackspaceAuthClient({ authUrl: "https://lon.identity.api.rackspacecloud.com/v2.0", username: process.env["RAX_USER"]!, apiKey: process.env["RAX_KEY"]!, region: "LON", fetch: customFetch, now: () => clock.now(), }); async function withFreshToken(op: (tokenId: string) => Promise): Promise { try { const { tokenId } = await auth.getToken(); return await op(tokenId); } catch (err) { auth.invalidate(); const { tokenId } = await auth.getToken(); return op(tokenId); // single retry } } ``` ## API Reference ### `class RackspaceAuthClient` - `constructor(options)` — `username`, `apiKey` required; `authUrl`, `region`, `fetch`, `now` optional with sensible defaults (`DEFAULT_AUTH_URL`, `"LON"`, `globalThis.fetch`, `Date.now`). - `getToken(signal?): Promise` — returns the cached token when still fresh; otherwise authenticates. - `invalidate(): void` — drops the cache; the next `getToken` re-auths. ### `CachedToken` `{ tokenId, expiresMs, storageEndpoint, cdnEndpoint, region }` — both endpoints carry a trailing slash so consumers can compose URLs via `new URL(key, endpoint)` safely. ## Implementation Notes - Concurrent dedup uses a single `#pending` promise; the first caller initiates the request, subsequent callers await the same promise. The promise is cleared in `finally` so a rejected auth doesn't poison future calls. - Proactive refresh is calculated against `now()` rather than wall-clock time so tests can pin the boundary precisely with an injected `now`. - The trailing-slash invariant on endpoints is enforced inside `resolveEndpoints` — the Rackspace catalog historically returns URLs without trailing slashes in some regions, and downstream `new URL(rel, base)` would otherwise truncate the last path segment. - `invalidate()` does not affect any in-flight `#pending` request — it only clears `#cache`. This matches the documented "re-auth once, retry once" pattern from the adapter's 401 recovery path. - The auth request body uses the documented `RAX-KSKEY:apiKeyCredentials` shape — Rackspace's API-key variant of identity v2. Username/password is supported by identity v2 but is not the recommended Rackspace path.