You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

55 lines
1.6 KiB

import { randomUUID, timingSafeEqual } from "node:crypto";
const DEFAULT_TTL_MS = 180_000;
type Entry = { answerBuf: Buffer; expiresAt: number };
const store = new Map<string, Entry>();
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;
}