/** * 会话与 token 签发的共享编排 —— register / login / oauth / refresh 共用。 * * 一次签发产出一对 access + refresh:access 是无状态 JWT(claims 含 sub/sid), * refresh 是不透明随机串,其哈希连同会话族(family)落库以支持轮转与 token-reuse 检测。 * 签名/哈希全走注入 Port,本文件不碰密码学,只生成随机标识与组织 claims。 */ const DEFAULT_ACCESS_TTL_MS = 15 * 60 * 1000 // 15 分钟 const DEFAULT_REFRESH_TTL_MS = 30 * 24 * 60 * 60 * 1000 // 30 天 /** 生成不透明随机标识(CSPRNG,非自研)。 @returns {string} */ export function randomId() { return globalThis.crypto.randomUUID().replace(/-/g, '') } /** * 为某 user + 会话族签发新 access+refresh,并把会话落库。 * 用于首次签发(family 新建)与轮转(沿用旧 family)。 * * @param {Object} deps * @param {import('./index.js').UserRepo} deps.userRepo * @param {import('./index.js').TokenSigner} deps.tokenSigner * @param {import('./index.js').Hasher} deps.hasher * @param {number} [deps.accessTtlMs] * @param {number} [deps.refreshTtlMs] * @param {Object} args * @param {string} args.user_id * @param {string} [args.family] 轮转时沿用旧族;缺省新建。 * @returns {Promise<{ access_token: string, refresh_token: string, sid: string, family: string }>} */ export async function issueSession( { userRepo, tokenSigner, hasher, accessTtlMs = DEFAULT_ACCESS_TTL_MS, refreshTtlMs = DEFAULT_REFRESH_TTL_MS }, { user_id, family }, ) { const sid = randomId() const fam = family ?? randomId() const refreshSecret = randomId() + randomId() const refresh_hash = await hasher.hash(refreshSecret) const expires_at = new Date(Date.now() + refreshTtlMs) await userRepo.saveSession({ user_id, sid, refresh_hash, family: fam, expires_at }) const access_token = await tokenSigner.sign( { sub: user_id, sid, fam }, { ttlMs: accessTtlMs }, ) // refresh 客户端态需携带 sid 以便 refresh 时定位会话(不暴露 hash)。 return { access_token, refresh_token: `${sid}.${refreshSecret}`, sid, family: fam } } /** * 拆包客户端 refresh token → { sid, secret }。格式非法返 null。 * @param {string} token * @returns {{ sid: string, secret: string }|null} */ export function parseRefreshToken(token) { if (typeof token !== 'string') return null const idx = token.indexOf('.') if (idx <= 0 || idx === token.length - 1) return null return { sid: token.slice(0, idx), secret: token.slice(idx + 1) } } export { DEFAULT_ACCESS_TTL_MS, DEFAULT_REFRESH_TTL_MS }