@krizaka/auth

中文文档
0.13.2发布于 2026/11/01Apache-2.0TypeScriptES Module构建来源已验证

Krizaka产品的帐户-名称,语言,主题,头像,通过带有验证标志的链接更改电子邮件,密码,与其他用户签出的会话,数据导出,删除-在端口后面,使用PostgreSQL适配器及其迁移。每一个名单

中文使用文档
文档目录4 节
  1. 每个范围的角色
  2. 联合登录(b谷歌,苹果,Facebook,原生PKCE)
  3. 签到的访客永远看不到公共的一面
  4. 会议,密码,地址和政府的锁

@krizaka /身份验证

一个Krizaka产品的账户,在一个端口后面:显示名称、语言、主题和头像种子;电子邮件更改通过链接发送到 新 地址(验证标志是由该行为设置的);密码(在任何敏感内容之前检查,其他会话在更改时退出;通过发送到帐户地址的链接重置-无论该地址是否有帐户,都是相同的答案,默认情况下30分钟内使用一次链接) passwordResetTtlMinutes(5至1440,由产品从其设置中读取并在邮件中说-每次会话都签出);其他人签到的现场会议;数据导出;删除。每种产品都保持其 自己的 accounts——这个包在产品的PostgreSQL和它需要的迁移上提供了一个适配器,没有什么核心。

import { emitMigration } from "@krizaka/db";
import { accountsSchema, dbAccounts } from "@krizaka/auth";

emitMigration(accountsSchema({ backfillVerified: true }), 1, { tables: { accounts: "accounts" } }); // commit the SQL once, in your migrations (idempotent)
const accounts = dbAccounts({ query, transaction, mail, baseUrl: "https://www.akinaza.com", product: "Akinaza", tokenPrefix: "ae", passwords: { hash, verify }, tokenHash, passwordResetTtlMinutes: 30 /* optional, 5–1440 */ }); // `transaction`: the work on one connection (BEGIN … COMMIT); the password reset needs it under concurrent requests
await accounts.updateProfile(id, { name: "Éric", theme: "dark" });
await accounts.sessions(id, new Date(), currentSessionId);
await accounts.requestPasswordReset(email, new Date()); // mails `<baseUrl>/<locale>/reset-password?token=…`
await accounts.resetPassword(token, newPassword, new Date()); // "CHANGED" | "INVALID"

模式。 这些表格是 @krizaka/db 模块(auth),版本1字节-与此包之前发布的SQL相同,因此具有它们的产品采用没有DDL更改的SQL: adoptMigration(accountsSchema(), 1, context) 只写标记, emitMigration(…) 为没有SQL的数据库编写SQL。原来的迁移函数不变, @deprecated,为一个未成年人。角色就是模块 auth_roles (rolesSchema(roles, { scoped })、表 owner 和 scope)和联邦链接 auth_identity_links (identityLinksSchema)。 邮件所携带的链接 mail.link (kind, url, minutes 或者 hours旁边) subject 和 text:发送带有商标的HTML (@krizaka/email: passwordReset, emailChange)从数据中呈现。

产品保留自己的东西:会话cookie和令牌哈希(tokenHash),它的密码哈希,它的邮件,它的速率限制和它的HTTP应答(AccountError 携带状态和稳定代码)。时间在流逝。now);这里没有表。

每个范围的角色

榜样就是数据: defineRoles({ roles, permissions, manages }) 说明存在哪些角色,每个角色可以做什么以及谁可以将哪个角色交给谁。 createRoles 保留成员关系(一个人、几个范围、每个角色)和邀请(带角色邀请的电子邮件、使用过一次的链接、只保留其散列,从不降级)、拒绝删除或降级最后一个所有者,以及答案 authorize(account, scope, "invoices") 与角色或稳定的拒绝(NOT_A_MEMBER 404年, FORBIDDEN 403)。 rolesMigration(roles, { scopeTable }) 制作表格。

联合登录(b谷歌,苹果,Facebook,原生PKCE)

每个产品都有自己的账户; krizaka-users 是端口背后的联合提供商,因此产品没有自己的OAuth应用程序。 IdentityProvider 有两个呼叫: start(provider, returnTo, { codeChallenge? }) 给出收件人的地址, complete(code, { codeVerifier? }) 交换一次性代码(一次性使用)为一个验证 ExternalIdentity (issuer, subject, email, emailVerified). 返回地址保留在产品上(路径);未知提供者被拒绝。

import { createFederatedSignIn, dbIdentityLinks, identityLinksSchema, usersIdentityProvider } from "@krizaka/auth";

emitMigration(identityLinksSchema, 1, { tables: { accounts: "accounts" } }); // once, in your migrations (idempotent): (issuer, subject) → account
const federation = usersIdentityProvider({ baseUrl: "https://auth.dev.akinaza.com", appUrl: "https://dev.akinaza.com", fetch });
const signIn = createFederatedSignIn({ links: dbIdentityLinks(query), accounts: { findByEmail, create }, clock });

const { url } = await federation.start("google", "/fr/account"); // redirect there
const result = await signIn.signIn(await federation.complete(code)); // { status: "signed-in", accountId, via } | { status: "refused", code }

外部标识附加到现有帐户 只有在双方都验证过地址的情况下 (由供应商和产品决定);否则将被拒绝(EMAIL_NOT_VERIFIED, ACCOUNT_EMAIL_UNVERIFIED),并且它绝不是第二个用户表。 create 是可选的:忽略它,没有账户的人将被拒绝(NO_ACCOUNT). 适配器运行的一致性工具包 @krizaka/auth/testkit (identityProviderContract, identityLinkStoreContract)。

postgresAccounts 名字是 dbAccounts;它仍然是 @deprecated 别名一个小调,离开下一个大调。

签到的访客永远看不到公共的一面

在产品的 proxy.ts (网络 Request/Response,无框架): landingRedirect(request, { cookie, landing: ["/", "/fr", "/en"], home, origin }) (origin:应用程序声明的公共来源,从不从请求中读取;绝对位置在上面,相对没有)回答307到家里有 Cache-Control: private, no-store 和 Vary: Cookie 当会话cookie存在于着陆路由上时,则 null. sessionHint(response, request, { cookie, secure }) 保持可读性 kz-session=1 与cookie同步标记(而不是令牌),因此静态页面对每个人都保持相同 NavBarSession 的 @krizaka/ui 在其第一次绘制上绘制已登录占位符。两者都只读取cookie作为提示:页面和API仍然解析会话。

会议,密码,地址和政府的锁

Yomizaka和Akinaza各自复制了什么(password.ts, client-ip.ts, session.ts的管理大门 access.ts他来过一次。每个部件都是其输入的一个函数:产品读取其环境和框架,并传递值。

import { allowedIps, clientIp, createSessions, dbSessionStore, hashPassword, requireAdmin, sessionCookie, sessionTokenOf, verifyPassword } from "@krizaka/auth";

const sessions = createSessions({
  store: dbSessionStore(query, { columns: ["id", "email", "role", "locale"], toAccount: (row) => ({ id: String(row.id), email: String(row.email), role: row.role as "USER" | "ADMIN", locale: row.locale as "en" | "fr" }), active: { column: "status", equals: "ACTIVE" } }),
  clock: { now: () => new Date() },
  tokenPrefix: "ys",                       // tokens are `ys1.<43 characters>`
  secret: () => env.AUTH_SESSION_SECRET,   // keys the stored hash (HMAC-SHA256)
  sessionDays: () => settings.get("auth.sessionDays"), // the setting, read for each new session
});

const ip = clientIp(request.headers, { header: env.ABUSE_CLIENT_IP_HEADER, trustedProxyHops: env.ABUSE_TRUSTED_PROXY_HOPS });
const { token, expires } = await sessions.create(account.id, { ip, userAgent: request.headers.get("user-agent") });
cookies().set(sessionCookie(token, expires, { name: "yomizaka_session", secure: production }));

const session = await sessions.resolve(sessionTokenOf(request.headers, { cookie: "yomizaka_session" })); // bearer of a native app, else the cookie
const verdict = requireAdmin({ session, ip, allowedIps: allowedIps(env.AUTH_ALLOWED_IPS, { production }), surface: "api" }); // { ok: true } | { ok: false, status, code }
  • 密码: hashPassword 和 verifyPassword scrypt (N=2^15, r=8, p=1),存储 scrypt$N$r$p$salt$hash,副本的格式:存储在这里的产品验证一个哈希(一个测试pin)。 passwordProblem (12 ~ 256个字符)和 decoyHash (当地址没有账户时对其进行验证:相同的工作,相同的时间)。把它们交给 dbAccounts({ passwords: { hash: hashPassword, verify: verifyPassword } }).
  • 客户地址: clientIp(headers, { header, trustedProxyHops }) 读取边缘设置的标题,否则读取条目 trustedProxyHops 从…的右边 X-Forwarded-For (从来没有左:客户端写),否则 X-Real-IP,否则为null。
  • 届: 一个 SessionStore 港口与 dbSessionStore (产品的 sessions 表,声明为参数:检查表名和帐户列,挂起的帐户一次不签名); memorySessionStore 用于测试,以及 sessionsSchema 对于一个没有表的产品。标记只显示一次;只有它的HMAC被存储,与副本的公式相同,所以已经存储的会话继续工作。一个会话持续 sessionDays 24小时的时间。
  • 政府的锁: requireAdmin({ session, ip, allowedIps, surface }) 决定三个锁(一个会话、ADMIN角色、列表中的一个地址),并回答API 401或403上的状态和代码 UNAUTHENTICATED, FORBIDDEN 或者 IP_NOT_ALLOWED;在404页上显示每个失败的锁,因为管理没有宣布自己。 allowedIps(list, { production }) 制作一个空列表,在生产中是“nobody”,在开发中是“anyone”。
  • @krizaka/auth/contract (网络安全需求 zod): GET /api/me 答案 200 { signedIn, signupOpen, account? } 与 Cache-Control: private, no-store 和 Vary: Cookie (krizaka-ui # 551)。登出不是错误。 meResponse({ signupOpen, account }) 构建答案并与之核对 meSchema;沙洲岛 @krizaka/ui 读取相同的文件。
  • @krizaka/auth/settings (需要 @krizaka/settings):注册表片段 authSettings: auth.sessionDays (14,1至90)和 auth.passwordResetMinutes (30,5至1440)。
  • 忘记密码: dbAccounts 已经是复位端口: requestPasswordReset(email, now) (同样的答案,无论地址是否有帐户,链接以该人的语言发送)和 resetPassword(token, next, now) (令牌以散列方式存储,使用一次,过期 auth.passwordResetMinutes,每个会话结束)。将产品的限位器放在两条路线的前面(@krizaka/rate-limit)。

sessionStoreContract (从 @krizaka/auth/testkit这是合同的全部内容 SessionStore 适配器运行,内存和数据库都一样。

版本与发布记录

@krizaka/auth 共发布 19 个版本,首次发布于 2026/10/23;过去 12 个月发布了 19 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.13.2latest2026/11/011 天前
0.13.12026/10/304 天前
0.13.02026/10/295 天前
0.12.02026/10/295 天前
0.11.22026/10/295 天前
0.11.12026/10/295 天前

关于 @krizaka/auth 的常见问题

@krizaka/auth 是什么?

Krizaka产品的帐户-名称,语言,主题,头像,通过带有验证标志的链接更改电子邮件,密码,与其他用户签出的会话,数据导出,删除-在端口后面,使用PostgreSQL适配器及其迁移。每一个名单

如何安装 @krizaka/auth?

在项目目录运行 npm install @krizaka/auth,或使用 pnpm add / yarn add / bun add。

@krizaka/auth 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@krizaka/auth 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@krizaka/auth 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=22。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@krizaka/auth 可以商用吗?

@krizaka/auth 使用 Apache-2.0 许可。宽松许可:允许商业使用、修改和再分发,并附带明确的专利授权;分发时需保留许可与 NOTICE 文件,修改过的文件要注明改动。以上为概括,不构成法律意见,请以许可原文为准。

@krizaka/auth 还在维护吗?

最新版本 0.13.2 发布于 2026/11/01(1 天前);至今共发布 19 个版本,其中过去 12 个月发布了 19 个;首次发布于 2026/10/23。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。