Skip to content
markpaper

src/rest/authToken.ts

v0.2.1 · 2.4 KB

Download file
// 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 };
}
All files