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
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;
|
|
}
|
|
|