import { randomUUID, timingSafeEqual } from "node:crypto"; const DEFAULT_TTL_MS = 180_000; type Entry = { answerBuf: Buffer; expiresAt: number }; const store = new Map(); function sweepExpired(now: number) { for (const [id, e] of store) { if (now > e.expiresAt) { store.delete(id); } } } /** `answerNormalized` 须为规范形式(小写、无首尾空格) */ export function captchaCreate( answerNormalized: string, options?: { ttlMs?: number }, ): { captchaId: string } { const now = Date.now(); sweepExpired(now); const ttlMs = options?.ttlMs ?? DEFAULT_TTL_MS; const captchaId = randomUUID(); store.set(captchaId, { answerBuf: Buffer.from(answerNormalized, "utf8"), expiresAt: now + ttlMs, }); return { captchaId }; } /** 成功或失败均删除 challenge(含答案错误、过期) */ export function captchaConsume(captchaId: unknown, rawAnswer: unknown): boolean { if (typeof captchaId !== "string" || typeof rawAnswer !== "string") { return false; } const now = Date.now(); sweepExpired(now); const entry = store.get(captchaId); if (!entry || now > entry.expiresAt) { if (entry) { store.delete(captchaId); } return false; } const guessBuf = Buffer.from(rawAnswer.trim().toLowerCase(), "utf8"); if (guessBuf.length !== entry.answerBuf.length) { store.delete(captchaId); return false; } const ok = timingSafeEqual(guessBuf, entry.answerBuf); store.delete(captchaId); return ok; }