1import type { Redis } from "ioredis"; 2 3/** 4 * TASK-0129 — the Redis key layout for OAuth codes / refresh tokens / 5 * families / the access-token JTI denylist. ONE source of truth shared by 6 * `oauth.service.ts` (issue/rotate/revoke) and the global `auth.guard.ts` 7 * (JTI denylist check on every `type:"oauth"` bearer). 8 * 9 * All OAuth credentials are REDIS-ONLY (no DB tables) — codes, refresh 10 * tokens, and the JTI denylist. Redis is the system of record here because 11 * these are short-lived, high-churn, and need atomic single-use (GETDEL) + 12 * TTL; PostgreSQL is not the right fit (see C-13.7 / task "Redis only"). 13 */ 14export const OAUTH_CODE_PREFIX = "oauth:code:"; 15export const OAUTH_RT_PREFIX = "oauth:rt:"; 16export const OAUTH_FAMILY_PREFIX = "oauth:family:"; 17export const OAUTH_JTI_PREFIX = "oauth:jti:"; 18 19/** Single-use authorization code (SETEX 600, consumed via GETDEL). */ 20export const codeKey = (code: string): string => `${OAUTH_CODE_PREFIX}${code}`; 21/** Rotating refresh token, keyed by sha256 (the raw token never lands in Redis). */ 22export const rtKey = (tokenSha256: string): string => `${OAUTH_RT_PREFIX}${tokenSha256}`; 23/** The reuse-kill switch for a token family ("revoked"). */ 24export const familyKey = (familyId: string): string => `${OAUTH_FAMILY_PREFIX}${familyId}`; 25/** The access-token JTI denylist ("1"). */ 26export const jtiKey = (jti: string): string => `${OAUTH_JTI_PREFIX}${jti}`; 27/** All single-use authorization codes issued to a client (drained on app delete). */ 28export const clientCodesKey = (clientId: string): string => `oauth:client-codes:${clientId}`; 29/** All refresh-token hashes currently known for a client (drained on app delete). */ 30export const clientRtSetKey = (clientId: string): string => `oauth:client-rtset:${clientId}`; 31 32/** 33 * Is this access-token JTI on the denylist? Fail-OPEN on a Redis error 34 * (mirrors the API-key auth-failure throttle and the rate-limit guard): a 35 * Redis blip must not lock every OAuth token out — the access token is 36 * short-lived (15 min) so a worst case is a briefly-live revoked token. 37 */ 38export async function isJtiRevoked(redis: Redis, jti: string | undefined): Promise<boolean> { 39 if (!jti) return false; 40 try { 41 return (await redis.get(jtiKey(jti))) === "1"; 42 } catch { 43 return false; 44 } 45} 46