/** * createAuth —— 唯一入口工厂,只做依赖装配 + 各能力编排。 * * 纪律:本文件不碰任何密码学(全在注入 Port);缺依赖在装配时即 assert 抛错 * (不拖到运行某行);每条业务流程在独立文件(register/login/oauth/refresh/code)。 * verifyAccessToken / logout / revokeAllSessions 是薄方法,直接落本文件。 */ import { AuthError } from './errors.js' import { makeRegister } from './register.js' import { makeLogin } from './login.js' import { makeOauthLogin } from './oauth.js' import { makeRefresh } from './refresh.js' import { makeSendCode, makeVerifyCode } from './code.js' import { makeResetPassword } from './reset-password.js' import { makeRequestPasswordReset } from './request-password-reset.js' import { parseRefreshToken } from './session.js' /** * @param {Object} [deps] * @param {import('./index.js').UserRepo} [deps.userRepo] * @param {import('./index.js').EmailPort} [deps.email] * @param {import('./index.js').OauthPort} [deps.oauth] * @param {import('./index.js').TokenSigner} [deps.tokenSigner] * @param {import('./index.js').Hasher} [deps.hasher] * @param {Object} [opts] 可选的业务规则覆写(TTL / 阈值 / 窗口)。 * @returns {import('./index.js').Auth} */ export function createAuth({ userRepo, email, oauth, tokenSigner, hasher } = {}, opts = {}) { assertDep('userRepo', userRepo, ['findByEmail', 'create', 'updatePasswordHash', 'saveSession', 'findSession']) assertDep('email', email, ['sendCode']) assertDep('oauth', oauth, ['exchange']) assertDep('tokenSigner', tokenSigner, ['sign', 'verify']) assertDep('hasher', hasher, ['hash', 'verify']) const deps = /** @type {{ userRepo: import('./index.js').UserRepo, email: import('./index.js').EmailPort, oauth: import('./index.js').OauthPort, tokenSigner: import('./index.js').TokenSigner, hasher: import('./index.js').Hasher, maxAttempts?: number, windowMs?: number, accessTtlMs?: number, refreshTtlMs?: number, ttlMs?: number, rand?: () => number }} */ ( { userRepo, email, oauth, tokenSigner, hasher, ...opts } ) const register = makeRegister(deps) const login = makeLogin(deps) const oauthLogin = makeOauthLogin(deps) const refresh = makeRefresh(deps) const sendCode = makeSendCode(deps) const requestPasswordReset = makeRequestPasswordReset({ userRepo: deps.userRepo, sendCode }) const verifyCode = makeVerifyCode(deps) const resetPassword = makeResetPassword(deps) /** @type {import('./index.js').Auth['verifyAccessToken']} */ async function verifyAccessToken(token) { if (!token || typeof token !== 'string') { throw new AuthError({ code: 'TOKEN_INVALID' }) } try { const claims = await deps.tokenSigner.verify(token) await assertAccessSessionActive(claims) return claims } catch (/** @type {any} */ err) { if (err instanceof AuthError) throw err // jose 用 'ERR_JWT_EXPIRED'(code 或 name)区分过期与无效签名。 const tag = (err && (err.code || err.name)) || '' const expired = /EXPIRED/i.test(String(tag)) throw new AuthError({ code: expired ? 'TOKEN_EXPIRED' : 'TOKEN_INVALID', cause: err }) } } /** * @param {Record} claims */ async function assertAccessSessionActive(claims) { const sid = String(claims?.sid || '') if (!sid) throw new AuthError({ code: 'TOKEN_INVALID' }) const session = await deps.userRepo.findSession({ sid }) if (!session || session.revoked) throw new AuthError({ code: 'TOKEN_INVALID' }) if (Date.now() > session.expires_at.getTime()) { throw new AuthError({ code: 'TOKEN_EXPIRED' }) } } /** @type {import('./index.js').Auth['logout']} */ async function logout({ refresh_token }) { const parsed = parseRefreshToken(refresh_token) if (parsed) { const session = await deps.userRepo.findSession({ sid: parsed.sid }) if (session) await deps.userRepo.revokeFamily({ family: session.family }) } return { ok: true } } /** @type {import('./index.js').Auth['revokeAllSessions']} */ async function revokeAllSessions({ user_id }) { if (!user_id) throw new AuthError({ code: 'USER_NOT_FOUND' }) await deps.userRepo.revokeUserSessions({ user_id }) return { ok: true } } /** * @param {{ user_id: string }} args */ async function getUser({ user_id }) { if (!user_id || typeof deps.userRepo.findById !== 'function') return null return deps.userRepo.findById(user_id) } return { register, login, oauthLogin, refresh, verifyCode, sendCode, requestPasswordReset, resetPassword, getUser, verifyAccessToken, logout, revokeAllSessions, } } /** * 装配时依赖自检:缺整个依赖或缺关键方法都立刻抛 AuthError。 * @param {string} name * @param {any} dep * @param {string[]} methods */ function assertDep(name, dep, methods) { if (!dep || typeof dep !== 'object') { throw new AuthError({ code: 'TOKEN_INVALID', message: `createAuth: missing dependency "${name}"` }) } for (const m of methods) { if (typeof dep[m] !== 'function') { throw new AuthError({ code: 'TOKEN_INVALID', message: `createAuth: dependency "${name}" missing method "${m}"`, }) } } }