import { randomUUID } from 'crypto'; export type OAuthState = { providerName: string; redirectUri: string; createdAt: number; userId?: number; isBinding: boolean; }; const STATE_TTL_MS = 5 * 60 * 1000; // 5 minutes export class OAuthStateStore { private store = new Map(); private cleanupInterval: NodeJS.Timeout | null = null; constructor() { this.startCleanup(); } generate( providerName: string, redirectUri: string, userId?: number ): { state: string; oauthState: OAuthState } { const state = randomUUID(); const oauthState: OAuthState = { providerName, redirectUri, createdAt: Date.now(), userId, isBinding: userId !== undefined, }; this.store.set(state, oauthState); return { state, oauthState }; } validate(state: string): OAuthState | null { const oauthState = this.store.get(state); if (!oauthState) { return null; } if (Date.now() - oauthState.createdAt > STATE_TTL_MS) { this.store.delete(state); return null; } return oauthState; } consume(state: string): OAuthState | null { const oauthState = this.validate(state); if (oauthState) { this.store.delete(state); } return oauthState; } private startCleanup() { this.cleanupInterval = setInterval(() => { const now = Date.now(); for (const [state, data] of this.store.entries()) { if (now - data.createdAt > STATE_TTL_MS) { this.store.delete(state); } } }, 60_000); } destroy() { if (this.cleanupInterval) { clearInterval(this.cleanupInterval); } } } export const oauthStateStore = new OAuthStateStore();