src/rest/authToken.ts
v0.2.1 · 2.4 KB
// Auth-token cache for private reads (knowledge base: instances-and-api.md §2.4).
//
// The signer issues a token via `create_auth_token_with_expiry(DEFAULT_10_MIN_AUTH_EXPIRY)`: it lives
// 10 minutes. Recommended: cache it for 5 (half the lifetime) and fetch a new one on 401/403.
// The header is `authorization: <token>` without a `Bearer` prefix (this form was verified live; the
// prefixed form was not tried).
/** Token lifetime granted by the SDK's default expiry. */
export const AUTH_TOKEN_LIFETIME_MS = 10 * 60_000;
/** Recommended cache TTL: half the lifetime. */
export const DEFAULT_AUTH_TOKEN_TTL_MS = 5 * 60_000;
export interface AuthTokenCacheOptions {
/** Fetches a fresh token (e.g. `signer.authToken()`); must throw on failure. */
getToken: () => Promise<string>;
/** Default {@link DEFAULT_AUTH_TOKEN_TTL_MS}. */
ttlMs?: number;
/** Clock, for tests. */
now?: () => number;
}
export interface AuthTokenCache {
/** Cached token, or a fresh one when missing/expired. Concurrent callers share one fetch. */
get(): Promise<string>;
/** Drops the cached token (call on 401/403). */
invalidate(): void;
/** Cached token and its expiry without fetching; `null` when empty. */
peek(): { token: string; expiresAt: number } | null;
}
export function createAuthTokenCache(opts: AuthTokenCacheOptions): AuthTokenCache {
const ttl = opts.ttlMs ?? DEFAULT_AUTH_TOKEN_TTL_MS;
const clock = opts.now ?? Date.now;
let cached: { token: string; expiresAt: number } | null = null;
let inflight: Promise<string> | null = null;
return {
get() {
const now = clock();
if (cached && now < cached.expiresAt) return Promise.resolve(cached.token);
if (inflight) return inflight;
inflight = opts
.getToken()
.then((token) => {
if (typeof token !== 'string' || token === '') throw new Error('auth token provider returned an empty token');
cached = { token, expiresAt: clock() + ttl };
return token;
})
.finally(() => {
inflight = null;
});
return inflight;
},
invalidate() {
cached = null;
},
peek() {
return cached ? { ...cached } : null;
},
};
}
/** Header for private reads: `authorization: <token>` (no `Bearer`). */
export function authHeader(token: string): Record<string, string> {
return { authorization: token };
}