import { createHash, timingSafeEqual } from "node:crypto" // 邀请码注册门禁。 // // 为什么必须有:每个新账号都能调用 Evolink 生成图片/视频,那是真金白银。线上不能让 // 陌生人自助注册,所以「能不能建号」由服务端的邀请码说话,不靠前端隐藏字段。 // // 配置:.env 里的 INVITE_CODES,逗号分隔可配多个(发给不同的人,便于以后单独作废)。 // 校验是「服务端唯一入口」——见 lib/auth.ts 的 databaseHooks.user.create.before: // 任何路径(密码注册 / 验证码 / 社交登录)新建用户都要过这道关。 /** 归一化:忽略大小写、空格和连字符——手抄邀请码不该因为格式对不上而失败 */ function normalize(value: string) { return value.replace(/[\s-]+/g, "").toUpperCase() } /** 定长摘要,方便用 timingSafeEqual 做等时比较(直接 === 会按前缀提前返回) */ function digest(value: string) { return createHash("sha256").update(normalize(value)).digest() } /** .env 中配置的全部有效邀请码(原文,仅服务端可见) */ export function inviteCodes(): string[] { return (process.env.INVITE_CODES ?? "") .split(",") .map((code) => code.trim()) .filter(Boolean) } /** * 注册是否需要邀请码。 * - 配了码 → 需要; * - 生产环境即使没配也需要(fail closed:此时谁都注册不了,好过对全网敞开)。 * 只有本地开发且没配码时才放开,免得每次调试都要填码。 */ export function inviteRequired(): boolean { return inviteCodes().length > 0 || process.env.NODE_ENV === "production" } /** 校验用户填的邀请码;门禁关闭时一律通过 */ export function verifyInviteCode(input: unknown): boolean { if (!inviteRequired()) return true if (typeof input !== "string" || !input.trim()) return false const codes = inviteCodes() if (codes.length === 0) return false // 生产环境忘配 INVITE_CODES:拒绝所有注册 const given = digest(input) let matched = false // 不提前 return:比对次数与哪一个码命中无关 for (const code of codes) { if (timingSafeEqual(digest(code), given)) matched = true } return matched } /** 门禁拒绝时给用户看的话(区分「码不对」和「服务端没配码」两种情况) */ export function inviteRejectionMessage(): string { return inviteCodes().length === 0 ? "本站暂未开放注册" : "邀请码不正确,请向邀请人确认" }