index.js 5.97 KB
/**
 * `@drama/auth` 公共面 —— 身份认证域唯一对外导出口(任务文档 §3 契约)。
 *
 * 设计纪律(架构 §9 红线 + §1 边界):
 *   - 包内不读 `process.env`、不在模块顶层建任何连接(db 由 `createUserRepo(db)` 注入,
 *     密钥/JWKS/OAuth client 全由注入的 Port 在 composition root 持有)。
 *   - 绝不自研密码学:JWT 签发/验证走 `tokenSigner`,OAuth 验签走 `oauth`,
 *     密码哈希走 `hasher`。本包只编排这些 Port + 把会话/凭据/验证码状态落库。
 *   - 唯一对外错误类型是 `AuthError`(结构化 `code`/`retryable`)。
 *
 * ----- 注入式 Port 协议(实现在 composition root,本包只消费契约)-----
 *
 * @typedef {Object} TokenSigner   薄封装 jose。
 * @property {(claims: object, opts: { ttlMs: number }) => string|Promise<string>} sign
 * @property {(token: string) => object|Promise<object>} verify  验签 + 过期校验,失败抛错。
 *
 * @typedef {Object} Hasher        薄封装 argon2 / bcrypt。
 * @property {(plain: string) => string|Promise<string>} hash
 * @property {(plain: string, hash: string) => boolean|Promise<boolean>} verify
 *
 * @typedef {Object} OauthExchangeResult
 * @property {string} provider_user_id
 * @property {string} email
 * @property {boolean} email_verified
 *
 * @typedef {Object} OauthPort     薄封装各家 SDK(含验签)。
 * @property {(args: { provider: 'google'|'apple', code: string, oauth_flow: 'access_token'|'authorization_code', redirect_uri?: string }) => OauthExchangeResult|Promise<OauthExchangeResult>} exchange
 *
 * 来自 @drama/email 的投递端口(验证码投递)。
 * @typedef {Object} EmailPort
 * @property {(args: { to: string, code: string, purpose: string, locale?: string }) => void|Promise<void>} sendCode
 *
 * ----- 领域 DTO(与 @drama/contracts 的 User/Session 对齐)-----
 *
 * @typedef {Object} User
 * @property {string} id
 * @property {string} email
 * @property {string} [password_hash]
 * @property {Array<{ provider: string, provider_user_id: string }>} [oauth_providers]
 * @property {string} [locale]
 * @property {Date} [email_verified_at]
 *
 * @typedef {Object} Session
 * @property {string} sid
 * @property {string} user_id
 * @property {string} refresh_hash
 * @property {string} family
 * @property {Date} expires_at    UTC
 * @property {boolean} [used]
 * @property {boolean} [revoked]
 *
 * @typedef {Object} CodeRecord
 * @property {string} email
 * @property {string} purpose
 * @property {string} code_hash
 * @property {Date} expires_at    UTC
 * @property {number} attempts
 *
 * ----- UserRepo 形态(createUserRepo(db) 返回,本包内消费)-----
 *
 * @typedef {Object} UserRepo
 * @property {(email: string) => Promise<User|null>} findByEmail
 * @property {(id: string) => Promise<User|null>} findById
 * @property {(args: { email: string, password_hash?: string, oauth_providers?: Array<{provider:string,provider_user_id:string}>, locale?: string, email_verified_at?: Date }) => Promise<User>} create
 * @property {(args: { user_id: string, provider: string, provider_user_id: string }) => Promise<User>} linkOauth
 * @property {(args: { user_id: string, password_hash: string }) => Promise<void>} updatePasswordHash
 * @property {(args: { user_id: string, sid: string, refresh_hash: string, family: string, expires_at: Date }) => Promise<void>} saveSession
 * @property {(args: { sid: string }) => Promise<Session|null>} findSession
 * @property {(args: { sid: string }) => Promise<void>} markRefreshUsed
 * @property {(args: { family: string }) => Promise<void>} revokeFamily
 * @property {(args: { user_id: string }) => Promise<void>} revokeUserSessions
 * @property {(args: { key: string, ip?: string, at: Date }) => Promise<void>} recordAttempt
 * @property {(args: { key: string, ip?: string, windowMs: number }) => Promise<number>} countRecentAttempts
 * @property {(args: { email: string, purpose: string, code_hash: string, expires_at: Date }) => Promise<void>} saveCode
 * @property {(args: { email: string, purpose: string }) => Promise<CodeRecord|null>} consumeCode
 * @property {(args: { email: string, purpose: string, attempts: number }) => Promise<void>} [bumpCodeAttempts]
 *
 * ----- Auth 形态(createAuth(...) 返回,全部返回 Promise)-----
 *
 * @typedef {Object} AuthTokens
 * @property {User} user
 * @property {string} access_token
 * @property {string} refresh_token
 *
 * @typedef {Object} Auth
 * @property {(args: { email: string, password: string, locale?: string, email_verified_at?: Date }) => Promise<AuthTokens>} register
 * @property {(args: { email: string, password: string, ip?: string, device_id?: string }) => Promise<AuthTokens>} login
 * @property {(args: { provider: 'google'|'apple', code: string, redirect_uri?: string, device_id?: string }) => Promise<AuthTokens & { is_new_user: boolean }>} oauthLogin
 * @property {(args: { refresh_token: string }) => Promise<{ user: User|{id:string}, access_token: string, refresh_token: string }>} refresh
 * @property {(args: { email: string, code: string, purpose: 'register'|'login'|'reset' }) => Promise<{ ok: true }>} verifyCode
 * @property {(args: { email: string, purpose: 'register'|'login'|'reset', locale?: string, ip?: string }) => Promise<{ ok: true }>} sendCode
 * @property {(args: { email: string, locale?: string, ip?: string }) => Promise<{ ok: true }>} requestPasswordReset
 * @property {(args: { email: string, password: string }) => Promise<{ ok: true }>} resetPassword
 * @property {(args: { user_id: string }) => Promise<User|null>} getUser
 * @property {(token: string) => Promise<object>} verifyAccessToken
 * @property {(args: { refresh_token: string }) => Promise<{ ok: true }>} logout
 * @property {(args: { user_id: string }) => Promise<{ ok: true }>} revokeAllSessions
 */

export { createAuth } from './auth.js'
export { createUserRepo } from './repo.js'
export { AuthError } from './errors.js'
export { createFakeUserRepo } from './fake-repo.js'
export { default as authDbSchema } from './db/tables/index.cjs'