@web-ts-toolkit/oidc-vault-dpop-client
Framework-agnostic browser sessions and scoped API fetch for Express OIDC Vault. The installed package ships a self-contained README with body/cookie quickstarts, API types, recovery tables and upgrade notes.
Install and import
pnpm add @web-ts-toolkit/oidc-vault-dpop-client
jose and idb are included dependencies. Browser code needs neither Express
nor the backend package or Node ambient types. Strict DOM-only Bundler/NodeNext
consumers can use types: [] and skipLibCheck: false; ESM/CJS declaration
conditions are separate. Use named root imports, with no default/deep imports.
Node tooling/import/require needs >=22.12.0 for CJS require(esm) of jose.
Sessions require a secure context, Web Crypto, sessionStorage and IndexedDB
CryptoKey persistence. Cookie transport also needs Web Locks + BroadcastChannel.
Importing is safe before browser startup; construct one session per tab/mount
when browser features are available. Match the vault's public origin/basePath
and the APIs' replay namespaces; configure credentialed CORS exposing
DPoP-Nonce and WWW-Authenticate.
Session and gateway example
import {
createOidcVaultDpopSession,
fetchWithDpop,
OidcVaultDpopClientError,
type DpopApi,
} from '@web-ts-toolkit/oidc-vault-dpop-client';
const origin = 'https://gateway.example.com';
const session = createOidcVaultDpopSession({
backendOrigin: origin,
basePath: '/auth/oidc',
sessionTransport: 'body',
requestTimeoutMs: 15_000,
maxResponseBytes: 1_048_576,
});
const apis: readonly DpopApi[] = [
{ origin, pathPrefix: '/orders', replayNamespace: 'orders-api' },
{ origin, pathPrefix: '/billing', replayNamespace: 'billing-api' },
];
export const signIn = (): Promise<void> => session.login('/callback');
export const signOut = (): Promise<void> => session.logout();
export async function bootstrap(): Promise<'ready' | 'sign-in'> {
try {
const url = new URL(location.href);
const code = url.searchParams.get('code');
if (code !== null) {
url.searchParams.delete('code');
history.replaceState(null, '', url.href);
await session.exchange(code);
} else await session.refresh();
return 'ready';
} catch (error) {
if (error instanceof OidcVaultDpopClientError && error.requiresLogin) return 'sign-in';
throw error; // Retain recovery state; no automatic mutation retry.
}
}
export function loadOrders(signal: AbortSignal): Promise<Response> {
return fetchWithDpop({ session, apis }, `${origin}/orders?limit=20`, { signal });
}
// Explicit recovery action for an unusable persisted key, followed by fresh login.
export async function recoverBrowserKey(): Promise<void> {
await session.resetKey();
await signIn();
}
Select cookie and the matching mount on both sides for cookie transport.
Login/exchange always include credentials for the temporary transaction cookie;
refresh/logout need no access JWT or ath. Access JWTs stay memory-only, body
handles/pending login are tab-local sessionStorage, and cookie handles remain
backend HttpOnly cookies. Keys are scoped by frontend/backend/normalized mount.
pathPrefix selects the longest normalized segment match: /orders excludes
/orders-evil. Omission or / permits the entire origin. Duplicate normalized
scopes reject before auth work. Outbound paths and proofs normalize unreserved
escapes/dot segments, retain reserved separators, and preserve query data in
the request. Distinct namespaces isolate gateway services' nonces.
Recovery and limits
- Credentials: exact
DPoP, canonical compact signed JWT, exactcnf: { jkt }, safe mandatoryexpseconds and safe lifetime arithmetic. Browser decoding checks the response contract; APIs authenticate JWTs and same-key proofs. Generic server unbound/opaque compatibility remains.expiresIn: 0retains rotation results but is not a live API/peer credential; API use stops with non-terminalDPOP_ACCESS_TOKEN_EXPIRED. - Lifecycle: both transports use FIFO mutations; refresh sharing stops at
intervening mutations. Cookie peers use same-origin locks and a 180 ms token
wait. Superseded/disposed operations cannot restore old auth or clear newer
login;
DPOP_OPERATION_SUPERSEDED/DPOP_SESSION_DISPOSEDare non-terminal. - Cleanup:
clearinvalidates local auth/owned HTTP and keeps the key;disposecloses the context and preserves committed reload state;resetKeydeletes only scoped key/metadata without backend work. Await clear/reset before teardown when persistence must finish.logoutrequests server revocation. A corrupt key needs explicit reset-then-login; no implicit replacement occurs. Old sessions/JWTs remain until revocation/expiry and other users of that key need fresh login. The historicaloidc-vault-dpop-example-v1DB is retained. - HTTP: vault attempts default to 15,000 ms through headers/body/JSON and 1 MiB of streamed bytes. Queue/storage/crypto/recognition/other locks are outside that deadline; timers cannot preempt synchronous work or suspended tabs. API/proof signals stop waits, not already-started crypto or shared refresh. API bodies remain caller-owned. Abort is not rollback of server mutation or Set-Cookie; a lost rotation response can require login. No network/timeout retry.
- Nonces: learn valid success/final/single-attempt headers into a 64-tuple
oldest-remembered cache, with no inferred TTL. A later explicit mutation may
use that nonce. Vaults retry once then throw
DPOP_NONCE_RETRY_EXHAUSTED; retry-enabled APIs allow one nonce retry and oneinvalid_tokenrefresh budget. Every attempt needs a fresh proof/JTI; mutations remain single-attempt unless the server operation has an explicit idempotency contract. Final API responses belong to the caller. - Errors:
requiresLoginidentifies terminal session/key/browser-binding/ enrolled recognition errors, not every 401. Proof/nonce/transient failures keep auth; unknown server codes become fixedOIDC_REQUEST_FAILED. Recognition is optional change detection, never proof of possession or an API sender constraint.
Same-origin browser coordination is not a distributed refresh lease. Recorded Chromium/Firefox evidence uses same-site loopback; Safari/WebKit and arbitrary cross-site HTTPS/third-party-cookie policy need deployment-specific checks. See the shipped upgrade notes and runnable TypeScript SPA for full contracts and commands.