/** * OAuth 登录:经注入 oauth Port 换码 + 验签取邮箱 → 首登建号 / 已存在合并 → 签发会话。 * * 红线:包内不自写 JWKS 拉取 / RS256 验签 / id_token 解码——全在 oauth.exchange 内完成 * (Google 走 google-auth-library 校验 id_token、Apple 走其 JWKS)。验签失败统一 * OAUTH_VERIFY_FAILED。本文件只做 provider 校验 + 建号/合并的账户状态机。 */ import { AuthError } from './errors.js' import { issueSession } from './session.js' const SUPPORTED_PROVIDERS = new Set(['google', 'apple']) /** * @param {Object} deps * @param {import('./index.js').UserRepo} deps.userRepo * @param {import('./index.js').OauthPort} deps.oauth * @param {import('./index.js').TokenSigner} deps.tokenSigner * @param {import('./index.js').Hasher} deps.hasher * @param {number} [deps.accessTtlMs] * @param {number} [deps.refreshTtlMs] * @returns {(args: { provider: string, code: string, oauth_flow?: 'access_token'|'authorization_code', redirect_uri?: string, device_id?: string }) => Promise} */ export function makeOauthLogin(deps) { const { userRepo, oauth } = deps return async function oauthLogin({ provider, code, oauth_flow = 'access_token', redirect_uri }) { if (!SUPPORTED_PROVIDERS.has(provider)) { throw new AuthError({ code: 'UNSUPPORTED_PROVIDER', message: `provider: ${provider}` }) } let identity try { identity = await oauth.exchange({ provider: /** @type {'google'|'apple'} */ (provider), code, oauth_flow, redirect_uri, }) } catch (err) { throw new AuthError({ code: 'OAUTH_VERIFY_FAILED', cause: err }) } if (!identity || !identity.email || !identity.provider_user_id) { throw new AuthError({ code: 'OAUTH_VERIFY_FAILED' }) } const email = String(identity.email).trim().toLowerCase() let user = await userRepo.findByEmail(email) let is_new_user = false if (!user) { // 首登:自动建号并绑定 provider 身份。 user = await userRepo.create({ email, oauth_providers: [{ provider, provider_user_id: identity.provider_user_id }], ...(identity.email_verified ? { email_verified_at: new Date() } : {}), }) is_new_user = true } else { // 已存在:合并到同一 user(绑定该 provider,若未绑)。 const linked = (user.oauth_providers ?? []).some( (p) => p.provider === provider && p.provider_user_id === identity.provider_user_id, ) if (!linked) { user = await userRepo.linkOauth({ user_id: user.id, provider, provider_user_id: identity.provider_user_id, }) } } const issued = await issueSession(deps, { user_id: user.id }) return { user, access_token: issued.access_token, refresh_token: issued.refresh_token, is_new_user, } } }