@h-ai/cli 0.1.0-alpha.67 → 0.1.0-alpha.68

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/README.en.md +40 -4
  2. package/README.ja.md +40 -4
  3. package/README.md +40 -4
  4. package/dist/{src-2V6VP4I5.js → chunk-FI56GONY.js} +32 -3103
  5. package/dist/chunk-FI56GONY.js.map +1 -0
  6. package/dist/chunk-MBPB37FR.js +1348 -0
  7. package/dist/chunk-MBPB37FR.js.map +1 -0
  8. package/dist/{chunk-7DSURTJ4.js → chunk-ZFNBUF77.js} +327 -199
  9. package/dist/chunk-ZFNBUF77.js.map +1 -0
  10. package/dist/cli-config-templates-W2LLPMB6.js +5 -0
  11. package/dist/{cli-config-templates-6AHNUGBN.js.map → cli-config-templates-W2LLPMB6.js.map} +1 -1
  12. package/dist/{cli-skill-templates-FC3QRKDO.js → cli-skill-templates-US5CCAJ4.js} +3 -3
  13. package/dist/{cli-skill-templates-FC3QRKDO.js.map → cli-skill-templates-US5CCAJ4.js.map} +1 -1
  14. package/dist/index.js +920 -336
  15. package/dist/index.js.map +1 -1
  16. package/dist/metafile-esm.json +1 -0
  17. package/dist/src-DSJJXM2F.js +3849 -0
  18. package/dist/src-DSJJXM2F.js.map +1 -0
  19. package/package.json +9 -8
  20. package/templates/apps/admin/messages/ja-JP.json +38 -0
  21. package/templates/apps/admin/src/routes/+layout.svelte.hbs +16 -6
  22. package/templates/apps/admin/src/routes/admin/+layout.svelte.hbs +3 -3
  23. package/templates/apps/api/apps/{{projectName}}-contract/src/index.ts.hbs +2 -0
  24. package/templates/apps/api/apps/{{projectName}}-contract/src/{{projectName}}-contract.ts.hbs +4 -1
  25. package/templates/apps/api/apps/{{projectName}}-service/README.md.hbs +5 -1
  26. package/templates/apps/api/apps/{{projectName}}-service/config/_core.yml.hbs +0 -10
  27. package/templates/apps/api/apps/{{projectName}}-service/config/_serv.yml.hbs +2 -1
  28. package/templates/apps/api/apps/{{projectName}}-service/src/app.ts.hbs +24 -11
  29. package/templates/apps/api/apps/{{projectName}}-service/src/index.ts.hbs +8 -1
  30. package/templates/apps/api/apps/{{projectName}}-service/src/lib/server/init.ts.hbs +68 -11
  31. package/templates/apps/api/apps/{{projectName}}-service/src/server/procedures/index.ts.hbs +10 -2
  32. package/templates/apps/api/e2e/api-service.spec.ts.hbs +2 -2
  33. package/templates/apps/api/package.json.hbs +1 -1
  34. package/templates/apps/api/playwright.config.ts.hbs +15 -7
  35. package/templates/apps/fullstack/apps/{{projectName}}-app/README.md.hbs +1 -1
  36. package/templates/apps/fullstack/apps/{{projectName}}-app/messages/ja-JP.json.hbs +7 -0
  37. package/templates/apps/fullstack/apps/{{projectName}}-app/src/App.svelte.hbs +7 -1
  38. package/templates/apps/fullstack/apps/{{projectName}}-app/vite.config.ts.hbs +1 -1
  39. package/templates/apps/fullstack/apps/{{projectName}}-desktop/README.md.hbs +1 -1
  40. package/templates/apps/fullstack/apps/{{projectName}}-desktop/messages/ja-JP.json.hbs +7 -0
  41. package/templates/apps/fullstack/apps/{{projectName}}-desktop/vite.config.ts.hbs +1 -1
  42. package/templates/apps/fullstack/apps/{{projectName}}-miniapp/messages/ja-JP.json.hbs +12 -0
  43. package/templates/apps/fullstack/apps/{{projectName}}-web/README.md.hbs +1 -1
  44. package/templates/apps/fullstack/apps/{{projectName}}-web/messages/ja-JP.json.hbs +7 -0
  45. package/templates/apps/fullstack/apps/{{projectName}}-web/vite.config.ts.hbs +1 -1
  46. package/templates/apps/fullstack/e2e/fullstack.spec.ts.hbs +19 -8
  47. package/templates/apps/fullstack/package.json.hbs +1 -1
  48. package/templates/apps/fullstack/packages/{{projectName}}-contract/src/admin-schemas.ts.hbs +1 -1
  49. package/templates/apps/fullstack/packages/{{projectName}}-contract/src/index.ts.hbs +2 -0
  50. package/templates/apps/fullstack/packages/{{projectName}}-contract/src/{{projectName}}-contract.ts.hbs +3 -0
  51. package/templates/apps/fullstack/packages/{{projectName}}-serv/README.md.hbs +5 -2
  52. package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_core.yml.hbs +0 -6
  53. package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_reach.yml.hbs +8 -0
  54. package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_serv.yml.hbs +2 -1
  55. package/templates/apps/fullstack/packages/{{projectName}}-serv/package.json.hbs +6 -0
  56. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/app-server.ts.hbs +9 -5
  57. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/admin-service.ts.hbs +3 -2
  58. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/messages.ts.hbs +16 -1
  59. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/procedures/admin-procedures.ts.hbs +7 -3
  60. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/procedures/index.ts.hbs +3 -0
  61. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/runtime.ts.hbs +58 -7
  62. package/templates/apps/fullstack/packages/{{projectName}}-serv/tests/app-server.test.ts.hbs +13 -3
  63. package/templates/apps/fullstack/packages/{{projectName}}-shared/README.md.hbs +9 -4
  64. package/templates/apps/fullstack/packages/{{projectName}}-shared/messages/ja-JP.json.hbs +105 -0
  65. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/AdminWorkspace.svelte.hbs +3 -3
  66. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/AppShell.svelte.hbs +5 -5
  67. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/pages/audit.svelte.hbs +7 -7
  68. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/pages/settings.svelte.hbs +1 -1
  69. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/pages/users.svelte.hbs +8 -8
  70. package/templates/apps/fullstack/packages/{{projectName}}-shared/tests/api-client.test.ts.hbs +11 -12
  71. package/templates/apps/fullstack/playwright.config.ts.hbs +15 -6
  72. package/templates/apps/h5/messages/ja-JP.json +40 -0
  73. package/templates/apps/h5/src/routes/+layout.svelte.hbs +14 -3
  74. package/templates/apps/mobile-app/messages/ja-JP.json +36 -0
  75. package/templates/apps/mobile-app/src/App.svelte.hbs +13 -3
  76. package/templates/apps/mobile-app/src/lib/api.ts.hbs +173 -14
  77. package/templates/apps/website/messages/ja-JP.json +32 -0
  78. package/templates/apps/website/src/routes/+layout.svelte.hbs +10 -2
  79. package/templates/base/e2e/app.spec.ts.hbs +13 -8
  80. package/templates/base/package.json.hbs +4 -1
  81. package/templates/base/playwright.config.ts +15 -4
  82. package/templates/base/src/hooks.server.ts.hbs +15 -6
  83. package/templates/base/src/lib/server/init.ts.hbs +139 -30
  84. package/templates/base/vite.config.ts.hbs +2 -2
  85. package/templates/features/iam/routes-shared/api/auth/logout/+server.ts +11 -6
  86. package/templates/features/payment/routes/api/v1/payment/create/+server.ts +18 -25
  87. package/templates/features/payment/routes/api/v1/payment/notify/[provider]/+server.ts +4 -0
  88. package/templates/i18n/project.inlang/settings.json +1 -1
  89. package/templates/shared/e2e-env.ts +98 -0
  90. package/templates/shared/run-e2e.mjs +51 -12
  91. package/templates/skills/AGENTS.md +4 -0
  92. package/templates/skills/bridges/admin/AGENTS.md +5 -1
  93. package/templates/skills/bridges/api/AGENTS.md +2 -0
  94. package/templates/skills/bridges/fullstack/AGENTS.md.hbs +4 -0
  95. package/templates/skills/bridges/h5/AGENTS.md +5 -1
  96. package/templates/skills/bridges/mobile-app/AGENTS.md +5 -1
  97. package/templates/skills/bridges/website/AGENTS.md +5 -1
  98. package/templates/skills/hai-ai/SKILL.md +8 -2
  99. package/templates/skills/hai-ai/guide.md +134 -60
  100. package/templates/skills/hai-ai/reference.md +51 -11
  101. package/templates/skills/hai-api-client/SKILL.md +2 -0
  102. package/templates/skills/hai-api-client/reference.md +17 -5
  103. package/templates/skills/hai-api-contract/SKILL.md +1 -0
  104. package/templates/skills/hai-api-contract/reference.md +4 -2
  105. package/templates/skills/hai-app-create/SKILL.md +4 -0
  106. package/templates/skills/hai-app-create/development.md +20 -4
  107. package/templates/skills/hai-app-create/reference.md +2 -1
  108. package/templates/skills/hai-app-review/SKILL.md +6 -2
  109. package/templates/skills/hai-app-review/differential.md +34 -0
  110. package/templates/skills/hai-app-tests/SKILL.md +3 -1
  111. package/templates/skills/hai-app-tests/reference.md +4 -4
  112. package/templates/skills/hai-app-tests/validation.md +10 -3
  113. package/templates/skills/hai-audit/SKILL.md +10 -8
  114. package/templates/skills/hai-audit/reference.md +3 -0
  115. package/templates/skills/hai-build/reference.md +6 -4
  116. package/templates/skills/hai-cache/SKILL.md +9 -3
  117. package/templates/skills/hai-cache/reference.md +53 -11
  118. package/templates/skills/hai-capacitor/SKILL.md +3 -1
  119. package/templates/skills/hai-capacitor/reference.md +9 -5
  120. package/templates/skills/hai-core/SKILL.md +4 -0
  121. package/templates/skills/hai-core/reference.md +25 -5
  122. package/templates/skills/hai-crypto/SKILL.md +10 -2
  123. package/templates/skills/hai-crypto/reference.md +64 -10
  124. package/templates/skills/hai-datapipe/SKILL.md +2 -0
  125. package/templates/skills/hai-datapipe/reference.md +3 -1
  126. package/templates/skills/hai-deploy/SKILL.md +9 -4
  127. package/templates/skills/hai-deploy/middleware.md +51 -37
  128. package/templates/skills/hai-iam/SKILL.md +24 -8
  129. package/templates/skills/hai-iam/reference.md +181 -154
  130. package/templates/skills/hai-kit/SKILL.md +23 -7
  131. package/templates/skills/hai-kit/reference.md +42 -24
  132. package/templates/skills/hai-payment/SKILL.md +13 -11
  133. package/templates/skills/hai-payment/reference.md +80 -41
  134. package/templates/skills/hai-pr-review/SKILL.md +21 -88
  135. package/templates/skills/hai-reach/SKILL.md +5 -2
  136. package/templates/skills/hai-reach/reference.md +83 -22
  137. package/templates/skills/hai-reldb/SKILL.md +5 -2
  138. package/templates/skills/hai-reldb/reference.md +68 -25
  139. package/templates/skills/hai-scheduler/SKILL.md +8 -5
  140. package/templates/skills/hai-scheduler/reference.md +38 -24
  141. package/templates/skills/hai-serv/SKILL.md +23 -8
  142. package/templates/skills/hai-serv/reference.md +123 -87
  143. package/templates/skills/hai-spec-implement/SKILL.md +4 -0
  144. package/templates/skills/hai-spec-implement/evaluation.md +18 -0
  145. package/templates/skills/hai-spec-implement/handoff.md +47 -0
  146. package/templates/skills/hai-spec-implement/templates.md +5 -0
  147. package/templates/skills/hai-spec-implement/workflow.md +2 -0
  148. package/templates/skills/hai-spec-review/SKILL.md +4 -0
  149. package/templates/skills/hai-storage/SKILL.md +8 -2
  150. package/templates/skills/hai-storage/reference.md +64 -25
  151. package/templates/skills/hai-ui/SKILL.md +16 -2
  152. package/templates/skills/hai-ui/guide.md +3 -3
  153. package/templates/skills/hai-ui/reference.md +17 -6
  154. package/templates/skills/hai-ui-design/SKILL.md +31 -0
  155. package/templates/skills/hai-ui-design/patterns.md +26 -0
  156. package/templates/skills/hai-ui-review/SKILL.md +30 -0
  157. package/templates/skills/hai-ui-review/checklist.md +22 -0
  158. package/templates/skills/hai-vecdb/SKILL.md +12 -8
  159. package/templates/skills/hai-vecdb/reference.md +33 -3
  160. package/dist/chunk-7DSURTJ4.js.map +0 -1
  161. package/dist/chunk-AOQ4N657.js +0 -640
  162. package/dist/chunk-AOQ4N657.js.map +0 -1
  163. package/dist/cli-config-templates-6AHNUGBN.js +0 -5
  164. package/dist/src-2V6VP4I5.js.map +0 -1
@@ -7,16 +7,18 @@ description: "使用 @h-ai/iam 实现登录、注册、会话、OTP/LDAP/API Key
7
7
 
8
8
  ## 能力契约
9
9
 
10
- | 项目 | 契约 |
11
- | --- | --- |
12
- | 能力 | 使用 @h-ai/iam 实现登录、注册、会话、OTP/LDAP/API Key 和 RBAC |
13
- | 适用场景 | 应用身份认证、会话失效或角色权限管理 |
14
- | 输入 | IamConfigInput、已初始化依赖、凭据、用户/角色/权限 ID |
15
- | 输出 | 认证、会话与授权操作的 HaiResult |
16
- | 限制 | 仅服务端,先初始化 reldb/cache(crypto 未初始化时由 IAM 初始化);公开认证入口是 iam.auth。客户端经 contract/API 调用,凭证只用于认证和安全存储,不进入日志或公开配置。 |
10
+ | 项目 | 契约 |
11
+ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
12
+ | 能力 | 使用 @h-ai/iam 实现登录、注册、会话、OTP/LDAP/API Key 和 RBAC |
13
+ | 适用场景 | 应用身份认证、会话失效或角色权限管理 |
14
+ | 输入 | IamConfigInput、已初始化依赖、凭据、用户/角色/权限 ID |
15
+ | 输出 | 认证、会话与授权操作的 HaiResult |
16
+ | 限制 | 仅服务端,先初始化 reldb/cache(crypto 未初始化时由 IAM 初始化);公开认证入口是 iam.auth。客户端经 contract/API 调用,凭证只用于认证和安全存储,不进入日志或公开配置。 |
17
17
 
18
18
  ## 使用路径
19
19
 
20
+ - 密码登录只接收 identifier/password;AuthPanel 将 LoginForm 的 username 映射为 identifier。会话期限来自服务端 session.maxAge/refreshTokenMaxAge,不增加未实现的登录偏好字段或持久会话选择框。
21
+
20
22
  - `_iam.yml` 只存 IamConfigSettingsInput,运行时回调单独传给 iam.init;不把 reldb/cache 句柄混入配置。
21
23
  - 角色资料与 permissionIds 一次保存:未传保持、空数组清空、未知 ID 失败;人数查询失败不能显示为 0。
22
24
  - IAM 服务端是 iam.auth;typed HTTP client 的路径是 apiClient.iam.auth。
@@ -28,7 +30,7 @@ description: "使用 @h-ai/iam 实现登录、注册、会话、OTP/LDAP/API Key
28
30
  - LDAP/Reach 均可选:基础认证无需安装;默认 LDAP 关闭,选用默认客户端才安装 ldapts 并设置 login.ldap + ldap。OTP 邮件/短信由 otp.email/sms 控制,选择 Reach 标准模板或自定义发送回调;具体签名与失败处理见参考中的“可选 LDAP 与通知”。
29
31
  - 多节点共享 DB + Redis,Memory 只用于单进程。重置的策略失败计入 maxAttempts;OTP 消费不解除 resendInterval。管理员 createUser 不受自助注册开关限制。
30
32
 
31
- - 受保护请求使用 `iam.auth.verifyToken`:当前用户状态 + 角色 JOIN + 批量权限读取,失败即拒绝;`iam.session.verifyToken` 仅验证会话快照。serv 内置 IAM 注入默认使用当前授权入口,无需手动补查。
33
+ - 受保护请求使用 `iam.auth.verifyToken`:先核验会话绑定的持久凭据代,再复核当前用户启用状态与凭据代并读取当前角色/权限;任一持久读取失败即拒绝。`iam.session.get/verifyToken` 也读取持久用户并校验启用状态与 `credentialVersion`,但返回的角色/权限仍是会话快照,不能替代当前授权。serv 内置 IAM 注入默认使用当前授权入口,无需手动补查。
32
34
  - 用户批量读取用 `iam.user.getUsers(ids, { include: ['roles'] })`(最多 1000,去重保留输入顺序,缺失省略);角色筛选用 `listUsers({ roleCode })`,在分页前执行,避免 N+1 和分页后过滤。
33
35
 
34
36
  ## 按需参考
@@ -45,3 +47,17 @@ description: "使用 @h-ai/iam 实现登录、注册、会话、OTP/LDAP/API Key
45
47
  - 错误码 — `HaiIamError`
46
48
  - 常见模式
47
49
  - 相关 Skills
50
+
51
+ 会话与 refresh 消费记录统一保存在 `hai:iam:sessions:{SHA256(userId)}` 版本化文档,不再使用独立 token 映射/索引。Bearer token 携带不可作为凭据的分区提示和 256 位随机值;客户端必须视为不透明字符串。`Session.version` 用于轮换提交校验。Memory 仅用于单进程测试,生产所有节点共享 Redis(含 Cluster)。并发冲突从最新文档重试,耗尽返回 CONFLICT;旧 refresh 不会产生第二个后继。若响应丢失后结果不明,调用方按失败关闭并重新登录,不假定旧凭据仍有效;全部会话仍受同一用户状态索引管理。
52
+
53
+ `iam.session.update(token, patch, expectedVersion)` 必须传入读取到的 Session.version。旧版本返回 CONFLICT,已撤销 token 返回 SESSION_NOT_FOUND;不会将旧内容重新写回或覆盖新会话。仅滑动时间戳更新不改变内容版本,允许正常并行请求。批量权限更新绑定首次读取的 token/版本集合,不将迟到数据应用到后续登录;遇到较新内容版本失败,调用方重新查询授权后再提交。
54
+
55
+ `create({ maxAge })` 可覆盖配置默认时长(正安全整数秒),该值持久化为 `Session.maxAge`;签发和刷新返回的 `expiresIn` 均使用它。滑动续期按该时长延长 access,返回已成功提交的 `lastActiveAt/expiresAt`;迟到请求只延长时间,不缩短较新续期。显式 `update` 的有效日期补丁完整生效并产生新内容版本。客户端根据本次返回的 `expiresIn` 安排刷新,不能硬编码配置默认值。
56
+
57
+ access 与 refresh 独立到期:用户分区保留至所有 access/refresh 的最晚有效期,但 access 认证只检查自己的 `expiresAt`。access 过期仍可凭未过期、未撤销的 refresh 轮换;每次轮换从新签发时刻重新计算 `refreshTokenMaxAge`,旧 access/refresh 同时失效。删除 access、logoutAll、单设备替换均撤销待刷新记录;仅 revokeRefresh 保留仍有效的 access。刷新路由只校验 refresh,不能先要求旧 access 认证成功。
58
+
59
+ OTP 发布/消费用单身份 CAS;密码重置最终 SQL 同时核验 resetVersion、credentialVersion、有效期、用户状态并改密/消费令牌。认证与刷新绑定数据库凭据代际;通知第三参数提供 expiresAt/idempotencyKey,接收端持久化幂等,异常是投递未知且不自动重发。
60
+
61
+ `iam.auth.logout(accessToken)` 直接按完整不透明令牌在用户分区执行一次 CAS,原子撤销 access 与关联 refresh;access 已到期也不能跳过撤销。非法/不存在的令牌幂等成功,缓存读取或提交故障返回失败,恢复后可用同一令牌重试。HTTP 登出只允许撤销所持令牌,不以 userId 定位他人会话,也不要求该 access 仍通过认证。调用方必须检查 `success`;本地 Cookie/安全存储清理不代表服务端撤销已经确认,失败应显示提示。
62
+
63
+ 配置示例用 `IamConfigInput` 表达可省略默认值的输入;需要完整策略快照时用对应 Schema.parse 得到输出类型。应用仅从 `@h-ai/iam` 取公开函数/类型,不深导内部策略与仓储。源码内部示例明确标注上下文,文档示例类型检查不替代真实共享存储和通知集成验证。
@@ -10,7 +10,7 @@
10
10
 
11
11
  管理界面自动创建角色时使用 `role_${core.id.generate()}` 生成 code,显示名称独立保存;不要通过移除中文、表情或折叠大小写生成标识。重命名不重建 code。
12
12
 
13
- > `@h-ai/iam` 是统一的身份与访问管理模块,支持多种认证策略(密码/OTP/LDAP/API Key)、Bearer Token 管理和 RBAC 授权。依赖 `@h-ai/reldb`(持久化)、`@h-ai/cache`(会话 / OTP / 重置令牌 / 一次性票据 / 共享锁)、`@h-ai/crypto`(密码哈希)、`@h-ai/audit`(审计日志)。
13
+ > `@h-ai/iam` 是统一的身份与访问管理模块,支持多种认证策略(密码/OTP/LDAP/API Key)、Bearer Token 管理和 RBAC 授权。依赖 `@h-ai/reldb`(持久化)、`@h-ai/cache`(会话 / OTP / 一次性票据)、`@h-ai/crypto`(密码哈希)、`@h-ai/audit`(审计日志)。
14
14
 
15
15
  ## 运行环境
16
16
 
@@ -18,12 +18,12 @@
18
18
 
19
19
  ## 依赖
20
20
 
21
- | 模块 | 用途 | 是否必需 | 初始化要求 |
22
- | --- | --- | --- | --- |
23
- | `@h-ai/reldb` | 数据库(用户/角色/权限持久化) | **必需** | 需在 `iam.init()` 前初始化 |
24
- | `@h-ai/cache` | 缓存(会话/OTP/重置令牌/一次性票据/共享锁) | **必需** | 需在 `iam.init()` 前初始化 |
25
- | `@h-ai/crypto` | 密码哈希 | 内部使用 | 自动初始化 |
26
- | `@h-ai/audit` | 审计日志(RBAC 关键操作记录) | 内部使用 | 已初始化时自动写入 |
21
+ | 模块 | 用途 | 是否必需 | 初始化要求 |
22
+ | -------------- | ------------------------------ | -------- | -------------------------- |
23
+ | `@h-ai/reldb` | 数据库(用户/角色/权限持久化) | **必需** | 需在 `iam.init()` 前初始化 |
24
+ | `@h-ai/cache` | 缓存(会话/OTP/一次性票据) | **必需** | 需在 `iam.init()` 前初始化 |
25
+ | `@h-ai/crypto` | 密码哈希 | 内部使用 | 自动初始化 |
26
+ | `@h-ai/audit` | 审计日志(RBAC 关键操作记录) | 内部使用 | 已初始化时自动写入 |
27
27
 
28
28
  ## 适用场景
29
29
 
@@ -79,11 +79,14 @@ import { reldb } from '@h-ai/reldb'
79
79
  // 位于应用启动函数内;依赖初始化失败时立即返回,不能继续启动 IAM。
80
80
  core.init({ configDir: './config' })
81
81
  const db = await reldb.init({ type: 'sqlite', database: './data.db' })
82
- if (!db.success) return db
82
+ if (!db.success)
83
+ return db
83
84
  const cached = await cache.init({ type: 'memory' })
84
- if (!cached.success) return cached
85
+ if (!cached.success)
86
+ return cached
85
87
  const initialized = await iam.init(core.config.getOrThrow<IamConfigSettingsInput>('iam'))
86
- if (!initialized.success) return initialized
88
+ if (!initialized.success)
89
+ return initialized
87
90
  // 关闭时按 iam → cache → reldb 的反向依赖顺序执行。
88
91
  ```
89
92
 
@@ -91,7 +94,7 @@ if (!initialized.success) return initialized
91
94
 
92
95
  基础初始化创建用户/RBAC 五张表;启用 API Key 登录时另创建 `hai_iam_api_keys`。
93
96
 
94
- Token 和 OTP 存储在 cache 中(不落库)。refreshToken 使用独立的 cache key 存储。
97
+ Token 和 OTP 存储在 cache 中(不落库)。session/refresh/index 位于同一用户分区版本文档,通过单键 CAS 提交。
95
98
 
96
99
  ### 可选 LDAP 与通知
97
100
 
@@ -99,25 +102,27 @@ LDAP 默认关闭,Reach 不属于 IAM 的启动前置依赖。`ldapts` 和 `@h
99
102
 
100
103
  - 默认 LDAP 客户端:在服务端 workspace 执行 `pnpm add ldapts`,配置 `login.ldap: true` 与 `ldap.url/bindDn/bindPassword/searchBase`。`searchFilter` 默认 `(uid={{username}})`;AD 可使用 `(sAMAccountName={{username}})` 与对应 `usernameAttribute`。`ldaps://` 使用 TLS,`ldap://` + `useTls: true` 使用 StartTLS。`ldapClientFactory` 可注入返回 `bind/search/unbind` 和 HaiResult 的客户端,此时无需安装 ldapts。
101
104
  - Reach 自动发送:应用安装、初始化 `@h-ai/reach`,在 `iam.init({ ...settings, reach })` 中注入一次。`otp_email/otp_sms` 模板使用 `{code}`;`password_reset/password_reset_sms` 使用 `{token}/{expiresAt}/{resetUrl}`。模板需在 IAM 初始化前配置,Provider 绑定实际邮件/短信通道。`passwordReset.mode` 选择 `email/phone`。
102
- - 自定义发送:直接传 `onOtpSendEmail(email, code)`、`onOtpSendSms(phone, code)`、`onPasswordResetRequest(user, token, expiresAt)`,均返回 `Promise<void>`。可以只实现选用通道或与 Reach 混用,显式回调优先。检查发送服务结果,投递失败在回调内抛错;IAM 捕获并返回业务错误,不要吞掉发送失败。
105
+ - 自定义发送:直接传 `onOtpSendEmail(email, code, context)`、`onOtpSendSms(phone, code, context)`、`onPasswordResetRequest(user, token, context)`,均返回 `Promise<void>`;context 包含 expiresAt/idempotencyKey,发送服务按该稳定键持久化去重,异常表示结果未知,不自动重发。可以只实现选用通道或与 Reach 混用,显式回调优先。检查发送服务结果,投递失败在回调内抛错;IAM 捕获并返回业务错误,不要吞掉发送失败。
103
106
  - `otp` 必须有配置并开启所需通道;缺少回调或标准模板时 `iam.publicConfig` 不公布该入口。Reach 由应用管理生命周期,IAM 不负责初始化或关闭。Console 仅记录元数据。
104
107
 
105
108
  协议以 Modal 显示,关闭后保留输入。URL 方式必须为 HTTP(S) 且页面允许 iframe 嵌入;脚本被隔离禁用。`LoginForm/RegisterForm` 提供 `header/extraFields/footer`;`AuthPanel` 提供 `loginHeader/loginFields/loginFooter`、`registerHeader/registerFields/registerFooter`。额外字段由应用绑定状态,在自己的动作中处理,IAM 不接收未经定义的字段。
106
109
 
110
+ 密码登录只接收 identifier/password;AuthPanel 将 LoginForm 的 username 映射为 identifier。会话期限来自服务端 session.maxAge/refreshTokenMaxAge,不增加未实现的登录偏好字段或持久会话选择框。
111
+
107
112
  ## 核心 API
108
113
 
109
114
  ### 认证 — `iam.auth`
110
115
 
111
- | 方法 | 签名 | 说明 |
112
- | ------------------ | ----------------------------------------------------------- | ---------------------------------- |
113
- | `login` | `({ identifier, password }) => Promise<HaiResult<AuthResult>>` | 密码登录(用户名/邮箱/手机号) |
114
- | `loginWithOtp` | `({ identifier, code }) => Promise<HaiResult<AuthResult>>` | OTP 验证码登录 |
115
- | `loginWithLdap` | `({ username, password }) => Promise<HaiResult<AuthResult>>` | LDAP 登录 |
116
- | `loginWithApiKey` | `({ key }) => Promise<HaiResult<AuthResult>>` | API Key 登录 |
117
- | `sendOtp` | `(identifier) => Promise<HaiResult<{ expiresAt }>>` | 发送 OTP 验证码 |
116
+ | 方法 | 签名 | 说明 |
117
+ | ------------------ | -------------------------------------------------------------- | ------------------------------------------------ |
118
+ | `login` | `({ identifier, password }) => Promise<HaiResult<AuthResult>>` | 密码登录(用户名/邮箱/手机号) |
119
+ | `loginWithOtp` | `({ identifier, code }) => Promise<HaiResult<AuthResult>>` | OTP 验证码登录 |
120
+ | `loginWithLdap` | `({ username, password }) => Promise<HaiResult<AuthResult>>` | LDAP 登录 |
121
+ | `loginWithApiKey` | `({ key }) => Promise<HaiResult<AuthResult>>` | API Key 登录 |
122
+ | `sendOtp` | `(identifier) => Promise<HaiResult<{ expiresAt }>>` | 发送 OTP 验证码 |
118
123
  | `verifyToken` | `(token) => Promise<HaiResult<Session>>` | 验证会话并读取当前用户状态及授权;数据库失败拒绝 |
119
- | `logout` | `(token) => Promise<HaiResult<void>>` | 登出 |
120
- | `registerAndLogin` | `(options: RegisterOptions) => Promise<HaiResult<AuthResult>>` | 注册并登录(一站式) |
124
+ | `logout` | `(token) => Promise<HaiResult<void>>` | 登出 |
125
+ | `registerAndLogin` | `(options: RegisterOptions) => Promise<HaiResult<AuthResult>>` | 注册并登录(一站式) |
121
126
 
122
127
  **AuthResult**(已改造为 TokenPair):
123
128
 
@@ -140,16 +145,16 @@ interface TokenPair {
140
145
 
141
146
  ### 会话管理 — `iam.session`
142
147
 
143
- | 方法 | 签名 | 说明 |
144
- | ------------------- | -------------------------------------------------------------------- | --------------------------------------- |
145
- | `create` | `({ userId, username, roles, source? }) => Promise<HaiResult<Session>>` | 创建会话(返回含 TokenPair 的 session) |
146
- | `get` | `(token) => Promise<HaiResult<Session \| null>>` | 获取会话(滑动续期自动延长) |
147
- | `verifyToken` | `(token) => Promise<HaiResult<Session>>` | 验证令牌 |
148
- | `update` | `(token, data) => Promise<HaiResult<void>>` | 更新会话字段(浅合并 data) |
149
- | `delete` | `(token) => Promise<HaiResult<void>>` | 删除会话 |
150
- | `deleteByUserId` | `(userId) => Promise<HaiResult<number>>` | 删除用户所有会话(返回删除数量) |
151
- | `refresh` | `(refreshToken) => Promise<HaiResult<TokenPair>>` | 刷新 Token(rotation:旧 Token 失效) |
152
- | `revokeRefresh` | `(refreshToken) => Promise<HaiResult<void>>` | 吊销 refreshToken |
148
+ | 方法 | 签名 | 说明 |
149
+ | ------------------- | ----------------------------------------------------------------------- | ---------------------------------------- |
150
+ | `create` | `({ userId, username, roles, source? }) => Promise<HaiResult<Session>>` | 创建会话(返回含 TokenPair 的 session) |
151
+ | `get` | `(token) => Promise<HaiResult<Session \| null>>` | 获取会话(滑动续期自动延长) |
152
+ | `verifyToken` | `(token) => Promise<HaiResult<Session>>` | 验证令牌 |
153
+ | `update` | `(token, data, expectedVersion) => Promise<HaiResult<void>>` | 更新会话字段(浅合并 data) |
154
+ | `delete` | `(token) => Promise<HaiResult<void>>` | 删除会话 |
155
+ | `deleteByUserId` | `(userId) => Promise<HaiResult<number>>` | 删除用户所有会话(返回删除数量) |
156
+ | `refresh` | `(refreshToken) => Promise<HaiResult<TokenPair>>` | 刷新 Token(rotation:旧 Token 失效) |
157
+ | `revokeRefresh` | `(refreshToken) => Promise<HaiResult<void>>` | 吊销 refreshToken |
153
158
  | `patchUserSessions` | `(userId, updates: SessionFieldUpdates) => Promise<HaiResult<void>>` | 批量更新用户所有会话的 roles/permissions |
154
159
 
155
160
  **Token 刷新(rotation 策略)**:
@@ -162,10 +167,10 @@ interface TokenPair {
162
167
 
163
168
  短期、一次性能力票据,用于无法携带 Bearer Token 的通道(如 WebSocket URL)。密码学安全随机、TTL、原子单次消费、用途绑定。
164
169
 
165
- | 方法 | 签名 | 说明 |
166
- | ---------- | -------------------------------------------------------------------------------------- | ------------------------------------- |
167
- | `issue` | `({ subjectId, purpose, grant?, ttlMs? }) => Promise<HaiResult<{ ticket, expiresAt }>>` | 签发(默认 30s) |
168
- | `consume` | `(ticket, { purpose? }?) => Promise<HaiResult<{ subjectId, purpose, grant }>>` | 原子消费(单次有效,用途不符拒绝) |
170
+ | 方法 | 签名 | 说明 |
171
+ | --------- | --------------------------------------------------------------------------------------- | ---------------------------------- |
172
+ | `issue` | `({ subjectId, purpose, grant?, ttlMs? }) => Promise<HaiResult<{ ticket, expiresAt }>>` | 签发(默认 30s) |
173
+ | `consume` | `(ticket, { purpose? }?) => Promise<HaiResult<{ subjectId, purpose, grant }>>` | 原子消费(单次有效,用途不符拒绝) |
169
174
 
170
175
  ```ts
171
176
  const issued = await iam.ticket.issue({ subjectId, purpose: 'ai-audio', grant: { operation: 'transcribe' } })
@@ -176,50 +181,50 @@ const consumed = await iam.ticket.consume(ticket, { purpose: 'ai-audio' }) //
176
181
 
177
182
  ### 用户管理 — `iam.user`
178
183
 
179
- | 方法 | 签名 | 说明 |
180
- | -------------------------- | ----------------------------------------------------------------------- | ------------------------------------ |
181
- | `register` | `(input) => Promise<HaiResult<RegisterResult>>` | 用户注册 |
182
- | `getUser` | `(id, options?) => Promise<HaiResult<User \| null>>` | 获取用户(可选 include: ['roles']) |
183
- | `getCurrentUser` | `(token) => Promise<HaiResult<User>>` | 通过令牌获取当前用户 |
184
- | `updateCurrentUser` | `(token, data: UpdateCurrentUserInput) => Promise<HaiResult<User>>` | 当前用户修改个人资料(白名单字段) |
185
- | `getUsers` | `(ids, options?) => Promise<HaiResult<User[]>>` | 批量公开用户,上限 1000;去重输入顺序、缺失省略、可 include roles |
186
- | `listUsers` | `(options?: ListUsersOptions) => Promise<HaiResult<PaginatedResult<User>>>` | 用户列表(分页 + 搜索 + roleCode 分页前筛选 + 白名单排序) |
187
- | `updateUser` | `(id, data) => Promise<HaiResult<User>>` | 更新用户信息 |
188
- | `deleteUser` | `(id) => Promise<HaiResult<void>>` | 删除用户(自动清除会话) |
189
- | `changePassword` | `(id, old, new) => Promise<HaiResult<void>>` | 修改密码(自动清除会话) |
190
- | `changeCurrentUserPassword`| `(token, old, new) => Promise<HaiResult<void>>` | 当前用户修改密码(通过令牌识别) |
191
- | `requestPasswordReset` | `(email) => Promise<HaiResult<void>>` | 请求密码重置(防枚举) |
192
- | `confirmPasswordReset` | `(token, newPassword) => Promise<HaiResult<void>>` | 确认密码重置(自动清除会话) |
193
- | `adminResetPassword` | `(userId, newPassword) => Promise<HaiResult<void>>` | 管理员直接重置密码 |
194
- | `validatePassword` | `(password) => HaiResult<void>` | 验证密码强度(**同步方法**) |
184
+ | 方法 | 签名 | 说明 |
185
+ | --------------------------- | --------------------------------------------------------------------------- | ----------------------------------------------------------------- |
186
+ | `register` | `(input) => Promise<HaiResult<RegisterResult>>` | 用户注册 |
187
+ | `getUser` | `(id, options?) => Promise<HaiResult<User \| null>>` | 获取用户(可选 include: ['roles']) |
188
+ | `getCurrentUser` | `(token) => Promise<HaiResult<User>>` | 通过令牌获取当前用户 |
189
+ | `updateCurrentUser` | `(token, data: UpdateCurrentUserInput) => Promise<HaiResult<User>>` | 当前用户修改个人资料(白名单字段) |
190
+ | `getUsers` | `(ids, options?) => Promise<HaiResult<User[]>>` | 批量公开用户,上限 1000;去重输入顺序、缺失省略、可 include roles |
191
+ | `listUsers` | `(options?: ListUsersOptions) => Promise<HaiResult<PaginatedResult<User>>>` | 用户列表(分页 + 搜索 + roleCode 分页前筛选 + 白名单排序) |
192
+ | `updateUser` | `(id, data) => Promise<HaiResult<User>>` | 更新用户信息 |
193
+ | `deleteUser` | `(id) => Promise<HaiResult<void>>` | 删除用户(自动清除会话) |
194
+ | `changePassword` | `(id, old, new) => Promise<HaiResult<void>>` | 修改密码(自动清除会话) |
195
+ | `changeCurrentUserPassword` | `(token, old, new) => Promise<HaiResult<void>>` | 当前用户修改密码(通过令牌识别) |
196
+ | `requestPasswordReset` | `(email) => Promise<HaiResult<void>>` | 请求密码重置(防枚举) |
197
+ | `confirmPasswordReset` | `(token, newPassword) => Promise<HaiResult<void>>` | 确认密码重置(自动清除会话) |
198
+ | `adminResetPassword` | `(userId, newPassword) => Promise<HaiResult<void>>` | 管理员直接重置密码 |
199
+ | `validatePassword` | `(password) => HaiResult<void>` | 验证密码强度(**同步方法**) |
195
200
 
196
201
  ### RBAC 授权 — `iam.authz`
197
202
 
198
- | 方法 | 签名 | 说明 |
199
- | -------------------------- | -------------------------------------------------------------------------------- | -------------------------------------------- |
200
- | `checkPermission` | `(userId, permission) => Promise<HaiResult<boolean>>` | 检查权限(超管自动通过,支持通配符) |
201
- | `getUserAuthorization` | `(userId) => Promise<HaiResult<{roles: string[], permissions: string[]}>>` | 当前角色及权限代码,角色 JOIN + 批量权限查询;失败不回退缓存 |
202
- | `getUserPermissions` | `(userId) => Promise<HaiResult<Permission[]>>` | 获取用户权限列表(通过角色聚合去重) |
203
- | `getUserRoles` | `(userId) => Promise<HaiResult<Role[]>>` | 获取用户角色列表 |
204
- | `getUserRolesForMany` | `(userIds) => Promise<HaiResult<Map<string, Role[]>>>` | 批量获取多用户角色(避免 N+1) |
205
- | `assignRole` | `(userId, roleId, tx?) => Promise<HaiResult<void>>` | 分配角色给用户 |
206
- | `removeRole` | `(userId, roleId, tx?) => Promise<HaiResult<void>>` | 移除用户角色 |
207
- | `syncRoles` | `(userId, roleIds, tx?) => Promise<HaiResult<void>>` | 同步用户角色(替换为目标列表) |
208
- | `createRole` | `({ code, name, description? }, tx?) => Promise<HaiResult<Role>>` | 创建角色 |
209
- | `getRole` | `(roleId) => Promise<HaiResult<Role \| null>>` | 获取角色 |
210
- | `getRoleByCode` | `(code) => Promise<HaiResult<Role \| null>>` | 根据代码获取角色 |
211
- | `getAllRoles` | `(options?) => Promise<HaiResult<PaginatedResult<Role>>>` | 获取所有角色(分页) |
212
- | `updateRole` | `(roleId, data, tx?) => Promise<HaiResult<Role>>` | 更新角色 |
213
- | `deleteRole` | `(roleId, tx?) => Promise<HaiResult<void>>` | 删除角色(级联清理关联 + 会话同步) |
214
- | `createPermission` | `({ code, name, type?, resource?, action? }, tx?) => Promise<HaiResult<Permission>>`| 创建权限 |
215
- | `getPermission` | `(permissionId) => Promise<HaiResult<Permission \| null>>` | 获取权限 |
216
- | `getPermissionByCode` | `(code) => Promise<HaiResult<Permission \| null>>` | 根据代码获取权限 |
217
- | `getAllPermissions` | `(options?: PermissionQueryOptions) => Promise<HaiResult<PaginatedResult<Permission>>>` | 获取所有权限(分页 + 类型/关键字筛选) |
218
- | `deletePermission` | `(permissionId, tx?) => Promise<HaiResult<void>>` | 删除权限(级联清理关联 + 会话同步) |
219
- | `assignPermissionToRole` | `(roleId, permId, tx?) => Promise<HaiResult<void>>` | 分配权限给角色 |
220
- | `removePermissionFromRole` | `(roleId, permId, tx?) => Promise<HaiResult<void>>` | 移除角色权限 |
221
- | `getRolePermissions` | `(roleId) => Promise<HaiResult<Permission[]>>` | 获取角色的权限列表 |
222
- | `getRolePermissionsForMany`| `(roleIds) => Promise<HaiResult<Map<string, Permission[]>>>` | 批量获取多角色权限(避免 N+1) |
203
+ | 方法 | 签名 | 说明 |
204
+ | --------------------------- | --------------------------------------------------------------------------------------- | ------------------------------------------------------------ |
205
+ | `checkPermission` | `(userId, permission) => Promise<HaiResult<boolean>>` | 检查权限(超管自动通过,支持通配符) |
206
+ | `getUserAuthorization` | `(userId) => Promise<HaiResult<{roles: string[], permissions: string[]}>>` | 当前角色及权限代码,角色 JOIN + 批量权限查询;失败不回退缓存 |
207
+ | `getUserPermissions` | `(userId) => Promise<HaiResult<Permission[]>>` | 获取用户权限列表(通过角色聚合去重) |
208
+ | `getUserRoles` | `(userId) => Promise<HaiResult<Role[]>>` | 获取用户角色列表 |
209
+ | `getUserRolesForMany` | `(userIds) => Promise<HaiResult<Map<string, Role[]>>>` | 批量获取多用户角色(避免 N+1) |
210
+ | `assignRole` | `(userId, roleId, tx?) => Promise<HaiResult<void>>` | 分配角色给用户 |
211
+ | `removeRole` | `(userId, roleId, tx?) => Promise<HaiResult<void>>` | 移除用户角色 |
212
+ | `syncRoles` | `(userId, roleIds, tx?) => Promise<HaiResult<void>>` | 同步用户角色(替换为目标列表) |
213
+ | `createRole` | `({ code, name, description? }, tx?) => Promise<HaiResult<Role>>` | 创建角色 |
214
+ | `getRole` | `(roleId) => Promise<HaiResult<Role \| null>>` | 获取角色 |
215
+ | `getRoleByCode` | `(code) => Promise<HaiResult<Role \| null>>` | 根据代码获取角色 |
216
+ | `getAllRoles` | `(options?) => Promise<HaiResult<PaginatedResult<Role>>>` | 获取所有角色(分页) |
217
+ | `updateRole` | `(roleId, data, tx?) => Promise<HaiResult<Role>>` | 更新角色 |
218
+ | `deleteRole` | `(roleId, tx?) => Promise<HaiResult<void>>` | 删除角色(级联清理关联 + 会话同步) |
219
+ | `createPermission` | `({ code, name, type?, resource?, action? }, tx?) => Promise<HaiResult<Permission>>` | 创建权限 |
220
+ | `getPermission` | `(permissionId) => Promise<HaiResult<Permission \| null>>` | 获取权限 |
221
+ | `getPermissionByCode` | `(code) => Promise<HaiResult<Permission \| null>>` | 根据代码获取权限 |
222
+ | `getAllPermissions` | `(options?: PermissionQueryOptions) => Promise<HaiResult<PaginatedResult<Permission>>>` | 获取所有权限(分页 + 类型/关键字筛选) |
223
+ | `deletePermission` | `(permissionId, tx?) => Promise<HaiResult<void>>` | 删除权限(级联清理关联 + 会话同步) |
224
+ | `assignPermissionToRole` | `(roleId, permId, tx?) => Promise<HaiResult<void>>` | 分配权限给角色 |
225
+ | `removePermissionFromRole` | `(roleId, permId, tx?) => Promise<HaiResult<void>>` | 移除角色权限 |
226
+ | `getRolePermissions` | `(roleId) => Promise<HaiResult<Permission[]>>` | 获取角色的权限列表 |
227
+ | `getRolePermissionsForMany` | `(roleIds) => Promise<HaiResult<Map<string, Permission[]>>>` | 批量获取多角色权限(避免 N+1) |
223
228
 
224
229
  **通配符规则**:`admin:*` 匹配 `admin:read`、`admin:write` 等。超管角色自动拥有所有权限。
225
230
 
@@ -238,14 +243,14 @@ apikey:
238
243
 
239
244
  假设主体拥有 `record:read`、`record:write`、`user:read`,不同 scopes 的结果如下(主体授权改变后重新计算):
240
245
 
241
- | scopes | 有效权限 | roles |
242
- | --- | --- | --- |
243
- | 未传或 `[]` | 无操作权限,仅可识别身份 | `[]` |
244
- | `['record:read']` | `record:read` | `[]` |
245
- | `['record:*']` | `record:read`、`record:write` | `[]` |
246
- | `['record:read', 'user:read']` | 两个指定权限 | `[]` |
247
- | `['missing:read']` | 主体未拥有,因此无权限 | `[]` |
248
- | `['*']` | 主体当前拥有的全部权限 | 保留主体角色 |
246
+ | scopes | 有效权限 | roles |
247
+ | ------------------------------ | ----------------------------- | ------------ |
248
+ | 未传或 `[]` | 无操作权限,仅可识别身份 | `[]` |
249
+ | `['record:read']` | `record:read` | `[]` |
250
+ | `['record:*']` | `record:read`、`record:write` | `[]` |
251
+ | `['record:read', 'user:read']` | 两个指定权限 | `[]` |
252
+ | `['missing:read']` | 主体未拥有,因此无权限 | `[]` |
253
+ | `['*']` | 主体当前拥有的全部权限 | 保留主体角色 |
249
254
 
250
255
  `record:*` 匹配冒号后的全部层级,不支持任意正则或 `record:r*` 等中间通配符。主体仅拥有 `record:read` 时,`['record:*']` 仍只得到读取权限;主体持有通配符时,交集保留较窄范围。受限 Key 没有角色入口,使用 permission 守卫;显式 `*` 是宽授权,按实际需要选择。
251
256
 
@@ -258,7 +263,9 @@ import { iam } from '@h-ai/iam'
258
263
  async function createReadKey(userId: string) {
259
264
  // 调用前校验管理权限或本人归属;scopes 使用权限 code。
260
265
  const created = await iam.apiKey.createApiKey(userId, {
261
- name: 'Read client', expirationDays: 30, scopes: ['record:read'],
266
+ name: 'Read client',
267
+ expirationDays: 30,
268
+ scopes: ['record:read'],
262
269
  })
263
270
  if (!created.success)
264
271
  return created
@@ -288,7 +295,10 @@ async function requireRecordRead(accessToken: string, deniedMessage: string) {
288
295
 
289
296
  async function listMyKeys(userId: string) {
290
297
  return iam.apiKey.listApiKeys(userId, {
291
- page: 1, pageSize: 20, search: 'Read', enabled: true,
298
+ page: 1,
299
+ pageSize: 20,
300
+ search: 'Read',
301
+ enabled: true,
292
302
  })
293
303
  }
294
304
 
@@ -304,13 +314,13 @@ Key 登录会话绑定 Key ID。刷新使用普通 `iam.session.refresh`,不
304
314
 
305
315
  `expirationDays: 0` 表示不设过期时间,省略时使用 `apikey.defaultExpirationDays`;需要限时访问时显式设置。明文 Key 和 Token 不进入 URL、日志、公开配置或普通 localStorage。
306
316
 
307
- | 方法 | 签名 | 说明 |
308
- | -------------- | ----------------------------------------------------------------------- | -------------------------------------- |
309
- | `createApiKey` | `(userId, options: CreateApiKeyOptions) => Promise<HaiResult<CreateApiKeyResult>>` | 创建 API Key(明文密钥仅返回一次) |
310
- | `listApiKeys` | `(userId, options?) => Promise<HaiResult<PaginatedResult<ApiKey>>>` | 分页列出用户的 API Key |
311
- | `getApiKey` | `(keyId) => Promise<HaiResult<ApiKey \| null>>` | 获取 API Key 详情 |
312
- | `revokeApiKey` | `(keyId) => Promise<HaiResult<void>>` | 吊销/删除 API Key |
313
- | `verifyApiKey` | `(rawKey) => Promise<HaiResult<ApiKey>>` | 验证 API Key 并返回实体(含用户 ID) |
317
+ | 方法 | 签名 | 说明 |
318
+ | -------------- | ---------------------------------------------------------------------------------- | ------------------------------------ |
319
+ | `createApiKey` | `(userId, options: CreateApiKeyOptions) => Promise<HaiResult<CreateApiKeyResult>>` | 创建 API Key(明文密钥仅返回一次) |
320
+ | `listApiKeys` | `(userId, options?) => Promise<HaiResult<PaginatedResult<ApiKey>>>` | 分页列出用户的 API Key |
321
+ | `getApiKey` | `(keyId) => Promise<HaiResult<ApiKey \| null>>` | 获取 API Key 详情 |
322
+ | `revokeApiKey` | `(keyId) => Promise<HaiResult<void>>` | 吊销/删除 API Key |
323
+ | `verifyApiKey` | `(rawKey) => Promise<HaiResult<ApiKey>>` | 验证 API Key 并返回实体(含用户 ID) |
314
324
 
315
325
  ## HTTP API 契约
316
326
 
@@ -324,52 +334,52 @@ const result = await apiClient.iam.auth.login({ identifier: username, password }
324
334
 
325
335
  ## 错误码 — `HaiIamError`
326
336
 
327
- | 错误码 | code | 说明 |
328
- |--------|------|------|
329
- | `HaiIamError.AUTH_FAILED` | `hai:iam:001` | 认证失败 |
330
- | `HaiIamError.INVALID_CREDENTIALS` | `hai:iam:002` | 凭证无效 |
331
- | `HaiIamError.USER_NOT_FOUND` | `hai:iam:003` | 用户不存在 |
332
- | `HaiIamError.USER_DISABLED` | `hai:iam:004` | 用户已禁用 |
333
- | `HaiIamError.USER_LOCKED` | `hai:iam:005` | 用户已锁定 |
334
- | `HaiIamError.USER_ALREADY_EXISTS` | `hai:iam:006` | 用户已存在 |
335
- | `HaiIamError.PASSWORD_EXPIRED` | `hai:iam:007` | 密码已过期 |
336
- | `HaiIamError.PASSWORD_POLICY_VIOLATION` | `hai:iam:008` | 密码不符合策略 |
337
- | `HaiIamError.OTP_INVALID` | `hai:iam:009` | 验证码无效 |
338
- | `HaiIamError.NOT_INITIALIZED` | `hai:iam:010` | 未初始化 |
339
- | `HaiIamError.OTP_RESEND_TOO_FAST` | `hai:iam:011` | 发送过于频繁 |
340
- | `HaiIamError.LOGIN_DISABLED` | `hai:iam:012` | 登录方式已禁用 |
341
- | `HaiIamError.REGISTER_DISABLED` | `hai:iam:013` | 注册已禁用 |
342
- | `HaiIamError.STRATEGY_NOT_SUPPORTED` | `hai:iam:014` | 认证策略不支持 |
343
- | `HaiIamError.APIKEY_INVALID` | `hai:iam:015` | API Key 无效 |
344
- | `HaiIamError.APIKEY_EXPIRED` | `hai:iam:016` | API Key 已过期 |
345
- | `HaiIamError.APIKEY_DISABLED` | `hai:iam:017` | API Key 已禁用 |
346
- | `HaiIamError.APIKEY_NOT_FOUND` | `hai:iam:018` | API Key 不存在 |
347
- | `HaiIamError.RESET_TOKEN_INVALID` | `hai:iam:019` | 重置令牌无效 |
348
- | `HaiIamError.RESET_TOKEN_EXPIRED` | `hai:iam:020` | 重置令牌已过期 |
349
- | `HaiIamError.RESET_TOKEN_MAX_ATTEMPTS` | `hai:iam:021` | 重置验证次数超限 |
350
- | `HaiIamError.OTP_EXPIRED` | `hai:iam:022` | 验证码已过期 |
351
- | `HaiIamError.SESSION_NOT_FOUND` | `hai:iam:101` | 会话不存在 |
352
- | `HaiIamError.SESSION_EXPIRED` | `hai:iam:102` | 会话已过期 |
353
- | `HaiIamError.SESSION_INVALID` | `hai:iam:103` | 会话无效 |
354
- | `HaiIamError.SESSION_CREATE_FAILED` | `hai:iam:104` | 会话创建失败 |
355
- | `HaiIamError.TOKEN_EXPIRED` | `hai:iam:105` | 令牌已过期 |
356
- | `HaiIamError.TOKEN_INVALID` | `hai:iam:106` | 令牌无效 |
357
- | `HaiIamError.TOKEN_REFRESH_FAILED` | `hai:iam:107` | 令牌刷新失败 |
358
- | `HaiIamError.PERMISSION_DENIED` | `hai:iam:201` | 权限不足 |
359
- | `HaiIamError.ROLE_NOT_FOUND` | `hai:iam:202` | 角色不存在 |
360
- | `HaiIamError.PERMISSION_NOT_FOUND` | `hai:iam:203` | 权限不存在 |
361
- | `HaiIamError.ROLE_ALREADY_EXISTS` | `hai:iam:204` | 角色已存在 |
362
- | `HaiIamError.PERMISSION_ALREADY_EXISTS` | `hai:iam:205` | 权限已存在 |
363
- | `HaiIamError.LDAP_CONNECTION_FAILED` | `hai:iam:301` | LDAP 连接失败 |
364
- | `HaiIamError.LDAP_BIND_FAILED` | `hai:iam:302` | LDAP 绑定失败 |
365
- | `HaiIamError.LDAP_SEARCH_FAILED` | `hai:iam:303` | LDAP 搜索失败 |
366
- | `HaiIamError.REPOSITORY_ERROR` | `hai:iam:401` | 存储层操作错误 |
367
- | `HaiIamError.NOT_FOUND` | `hai:iam:402` | 资源不存在 |
368
- | `HaiIamError.CONFLICT` | `hai:iam:403` | 资源冲突 |
369
- | `HaiIamError.FORBIDDEN` | `hai:iam:501` | 禁止访问 |
370
- | `HaiIamError.INVALID_ARGUMENT` | `hai:iam:502` | 参数无效 |
371
- | `HaiIamError.CONFIG_ERROR` | `hai:iam:901` | 配置错误 |
372
- | `HaiIamError.INTERNAL_ERROR` | `hai:iam:999` | 内部错误 |
337
+ | 错误码 | code | 说明 |
338
+ | --------------------------------------- | ------------- | ---------------- |
339
+ | `HaiIamError.AUTH_FAILED` | `hai:iam:001` | 认证失败 |
340
+ | `HaiIamError.INVALID_CREDENTIALS` | `hai:iam:002` | 凭证无效 |
341
+ | `HaiIamError.USER_NOT_FOUND` | `hai:iam:003` | 用户不存在 |
342
+ | `HaiIamError.USER_DISABLED` | `hai:iam:004` | 用户已禁用 |
343
+ | `HaiIamError.USER_LOCKED` | `hai:iam:005` | 用户已锁定 |
344
+ | `HaiIamError.USER_ALREADY_EXISTS` | `hai:iam:006` | 用户已存在 |
345
+ | `HaiIamError.PASSWORD_EXPIRED` | `hai:iam:007` | 密码已过期 |
346
+ | `HaiIamError.PASSWORD_POLICY_VIOLATION` | `hai:iam:008` | 密码不符合策略 |
347
+ | `HaiIamError.OTP_INVALID` | `hai:iam:009` | 验证码无效 |
348
+ | `HaiIamError.NOT_INITIALIZED` | `hai:iam:010` | 未初始化 |
349
+ | `HaiIamError.OTP_RESEND_TOO_FAST` | `hai:iam:011` | 发送过于频繁 |
350
+ | `HaiIamError.LOGIN_DISABLED` | `hai:iam:012` | 登录方式已禁用 |
351
+ | `HaiIamError.REGISTER_DISABLED` | `hai:iam:013` | 注册已禁用 |
352
+ | `HaiIamError.STRATEGY_NOT_SUPPORTED` | `hai:iam:014` | 认证策略不支持 |
353
+ | `HaiIamError.APIKEY_INVALID` | `hai:iam:015` | API Key 无效 |
354
+ | `HaiIamError.APIKEY_EXPIRED` | `hai:iam:016` | API Key 已过期 |
355
+ | `HaiIamError.APIKEY_DISABLED` | `hai:iam:017` | API Key 已禁用 |
356
+ | `HaiIamError.APIKEY_NOT_FOUND` | `hai:iam:018` | API Key 不存在 |
357
+ | `HaiIamError.RESET_TOKEN_INVALID` | `hai:iam:019` | 重置令牌无效 |
358
+ | `HaiIamError.RESET_TOKEN_EXPIRED` | `hai:iam:020` | 重置令牌已过期 |
359
+ | `HaiIamError.RESET_TOKEN_MAX_ATTEMPTS` | `hai:iam:021` | 重置验证次数超限 |
360
+ | `HaiIamError.OTP_EXPIRED` | `hai:iam:022` | 验证码已过期 |
361
+ | `HaiIamError.SESSION_NOT_FOUND` | `hai:iam:101` | 会话不存在 |
362
+ | `HaiIamError.SESSION_EXPIRED` | `hai:iam:102` | 会话已过期 |
363
+ | `HaiIamError.SESSION_INVALID` | `hai:iam:103` | 会话无效 |
364
+ | `HaiIamError.SESSION_CREATE_FAILED` | `hai:iam:104` | 会话创建失败 |
365
+ | `HaiIamError.TOKEN_EXPIRED` | `hai:iam:105` | 令牌已过期 |
366
+ | `HaiIamError.TOKEN_INVALID` | `hai:iam:106` | 令牌无效 |
367
+ | `HaiIamError.TOKEN_REFRESH_FAILED` | `hai:iam:107` | 令牌刷新失败 |
368
+ | `HaiIamError.PERMISSION_DENIED` | `hai:iam:201` | 权限不足 |
369
+ | `HaiIamError.ROLE_NOT_FOUND` | `hai:iam:202` | 角色不存在 |
370
+ | `HaiIamError.PERMISSION_NOT_FOUND` | `hai:iam:203` | 权限不存在 |
371
+ | `HaiIamError.ROLE_ALREADY_EXISTS` | `hai:iam:204` | 角色已存在 |
372
+ | `HaiIamError.PERMISSION_ALREADY_EXISTS` | `hai:iam:205` | 权限已存在 |
373
+ | `HaiIamError.LDAP_CONNECTION_FAILED` | `hai:iam:301` | LDAP 连接失败 |
374
+ | `HaiIamError.LDAP_BIND_FAILED` | `hai:iam:302` | LDAP 绑定失败 |
375
+ | `HaiIamError.LDAP_SEARCH_FAILED` | `hai:iam:303` | LDAP 搜索失败 |
376
+ | `HaiIamError.REPOSITORY_ERROR` | `hai:iam:401` | 存储层操作错误 |
377
+ | `HaiIamError.NOT_FOUND` | `hai:iam:402` | 资源不存在 |
378
+ | `HaiIamError.CONFLICT` | `hai:iam:403` | 资源冲突 |
379
+ | `HaiIamError.FORBIDDEN` | `hai:iam:501` | 禁止访问 |
380
+ | `HaiIamError.INVALID_ARGUMENT` | `hai:iam:502` | 参数无效 |
381
+ | `HaiIamError.CONFIG_ERROR` | `hai:iam:901` | 配置错误 |
382
+ | `HaiIamError.INTERNAL_ERROR` | `hai:iam:999` | 内部错误 |
373
383
 
374
384
  ## 常见模式
375
385
 
@@ -414,9 +424,12 @@ if (me.success) {
414
424
  // Token 过期时,api-client 自动调用 refreshPath 刷新
415
425
  // 刷新失败时,清除 Token 并跳转登录页
416
426
 
417
- // 登出
418
- await apiClient.iam.auth.logout({})
419
- await apiClient.auth.clear()
427
+ // 登出:本地清理与服务端撤销确认分别处理,调用方继续检查返回结果。
428
+ async function signOut() {
429
+ const result = await apiClient.iam.auth.logout({})
430
+ await apiClient.auth.clear()
431
+ return result
432
+ }
420
433
  ```
421
434
 
422
435
  ### 自动会话失效
@@ -451,18 +464,16 @@ agreements:
451
464
  # privacyPolicyUrl: https://example.com/privacy
452
465
  ```
453
466
 
454
-
455
-
456
467
  服务端通过 `iam.publicConfig` 提供白名单配置,或通过客户端的 `client.iam.auth.publicConfig()` 标准 HTTP contract 获取。只展示已开启且具备依赖的登录方式;LDAP 为可选集成,见上方配置与安装说明。
457
468
 
458
- | 配置 | 用途 |
459
- | --- | --- |
460
- | `login.password / otp / ldap` | 独立启用密码、验证码、企业账号 |
461
- | `otp.email / sms` | 独立启用邮件、短信 |
462
- | `register.enabled / defaultEnabled` | 自助注册及新账户状态 |
463
- | `passwordReset.enabled / mode / resetUrl` | 启用重置、选择 email/phone 渠道、配置重置页面绝对地址 |
464
- | `agreements.showOnRegister / showOnLogin` | 注册/登录协议弹窗显示;链接只接受 HTTP(S) |
465
- | `ui.variant / title / subtitle / brandTitle / brandText / logoUrl / illustrationUrl` | 卡片/分栏布局、标题、品牌、Logo 和配图 |
469
+ | 配置 | 用途 |
470
+ | ------------------------------------------------------------------------------------ | ----------------------------------------------------- |
471
+ | `login.password / otp / ldap` | 独立启用密码、验证码、企业账号 |
472
+ | `otp.email / sms` | 独立启用邮件、短信 |
473
+ | `register.enabled / defaultEnabled` | 自助注册及新账户状态 |
474
+ | `passwordReset.enabled / mode / resetUrl` | 启用重置、选择 email/phone 渠道、配置重置页面绝对地址 |
475
+ | `agreements.showOnRegister / showOnLogin` | 注册/登录协议弹窗显示;链接只接受 HTTP(S) |
476
+ | `ui.variant / title / subtitle / brandTitle / brandText / logoUrl / illustrationUrl` | 卡片/分栏布局、标题、品牌、Logo 和配图 |
466
477
 
467
478
  选择 Reach 时,在 reldb/cache/Reach 初始化成功后使用 `iam.init({ ...settings, reach })` 自动接入 Reach 的 `otp_email`、`otp_sms`、`password_reset`、`password_reset_sms` 模板。模板绑定对应邮件/短信 Provider;显式发送回调可覆盖默认行为。Console Provider 只记录元数据,实际收信需配置 SMTP、短信或 HTTP 通知服务。发送失败返回错误,不会伪报已发送。
468
479
 
@@ -472,7 +483,7 @@ SvelteKit 使用 `kit.auth.createApi(iam)` 导出标准 handler,匿名端点
472
483
 
473
484
  ### 分布式安全与边界
474
485
 
475
- 所有节点必须共享同一数据库和 Redis;Memory 仅用于单进程。登录 `maxLoginAttempts / lockoutDuration` 使用数据库原子更新,到期后的失败重新计数。OTP `maxAttempts / resendInterval` 使用共享锁和独立发送间隔状态,耗尽或消费验证码不能绕过重发限制,并发只消费一次。重置 `tokenExpiresIn / maxAttempts` 使用共享缓存 TTL 和每用户共享锁;每次有效令牌请求(包括密码策略失败)计数,最新令牌有效且只成功消费一次。重置后撤销旧会话、解除登录锁定;禁用用户不能通过密码、LDAP、OTP 登录。密码策略以服务端为准。
486
+ 所有节点共享同一数据库和 Redis;Memory 仅用于单进程测试。OTP 的验证码、尝试次数和冷却期保存在同一身份 key,通过 CAS 发布和消费;冲突拒绝旧代,消费/耗尽保留冷却期。密码重置的摘要、有效期、尝试次数和 resetVersion 保存在用户数据库行,策略失败计次,最终一条条件 SQL 同时改密、消费令牌并推进 credentialVersion;新令牌、后续尝试、停用或到期都会阻止迟到提交。发送重置请求只推进 resetVersion,不撤销现有会话;成功改密才推进 credentialVersion。认证策略保存已验证的凭据代际,建立会话/认证/刷新均校验数据库当前代际;缓存批量清理失败也不能使旧会话有效。通知只在成功发布后调用,第三参数为 { expiresAt, idempotencyKey },应用及 Reach Provider 必须用该稳定键持久化去重。异常/超时可能已送达,返回结果未知,不自动重发或回滚已发布凭据;没有接收端幂等保障时不能承诺外部副作用恰好一次。
476
487
 
477
488
  可选字段(如 `ldap`)若尚未写入 YAML,请先声明该字段,或通过 `HAI_IAM` 的 JSON 值整体覆盖模块配置;已有字段支持 `HAI_IAM_LOGIN_OTP` 等环境变量覆盖。
478
489
 
@@ -484,6 +495,22 @@ SvelteKit 使用 `kit.auth.createApi(iam)` 导出标准 handler,匿名端点
484
495
  - `hai-kit`:SvelteKit 集成(Bearer Token + 契约处理)
485
496
  - `hai-api-client`:客户端契约调用
486
497
  - `hai-reldb`:底层数据存储
487
- - `hai-cache`:会话、OTP/重置状态、一次性票据和共享锁
498
+ - `hai-cache`:会话、OTP 状态和一次性票据
488
499
  - `hai-crypto`:密码哈希(内部依赖)
489
500
  - `hai-ui`:IAM 场景组件(LoginForm/RegisterForm 等)
501
+
502
+ 会话与 refresh 消费记录统一保存在 `hai:iam:sessions:{SHA256(userId)}` 版本化文档,不再使用独立 token 映射/索引。Bearer token 携带不可作为凭据的分区提示和 256 位随机值;客户端必须视为不透明字符串。`Session.version` 用于轮换提交校验。Memory 仅用于单进程测试,生产所有节点共享 Redis(含 Cluster)。并发冲突从最新文档重试,耗尽返回 CONFLICT;旧 refresh 不会产生第二个后继。若响应丢失后结果不明,调用方按失败关闭并重新登录,不假定旧凭据仍有效;全部会话仍受同一用户状态索引管理。
503
+
504
+ `iam.session.update(token, patch, expectedVersion)` 必须传入读取到的 Session.version。旧版本返回 CONFLICT,已撤销 token 返回 SESSION_NOT_FOUND;不会将旧内容重新写回或覆盖新会话。仅滑动时间戳更新不改变内容版本,允许正常并行请求。批量权限更新绑定首次读取的 token/版本集合,不将迟到数据应用到后续登录;遇到较新内容版本失败,调用方重新查询授权后再提交。
505
+
506
+ `create({ maxAge })` 可覆盖配置默认时长(正安全整数秒),该值持久化为 `Session.maxAge`;签发和刷新返回的 `expiresIn` 均使用它。滑动续期按该时长延长 access,返回已成功提交的 `lastActiveAt/expiresAt`;迟到请求只延长时间,不缩短较新续期。显式 `update` 的有效日期补丁完整生效并产生新内容版本。客户端根据本次返回的 `expiresIn` 安排刷新,不能硬编码配置默认值。
507
+
508
+ access 与 refresh 独立到期:用户分区保留至所有 access/refresh 的最晚有效期,但 access 认证只检查自己的 `expiresAt`。access 过期仍可凭未过期、未撤销的 refresh 轮换;每次轮换从新签发时刻重新计算 `refreshTokenMaxAge`,旧 access/refresh 同时失效。删除 access、logoutAll、单设备替换均撤销待刷新记录;仅 revokeRefresh 保留仍有效的 access。刷新路由只校验 refresh,不能先要求旧 access 认证成功。
509
+
510
+ 会话仓库将缓存读取/提交失败返回为失败 HaiResult,不解释为会话不存在或过期,也不执行清理补偿。用户会话及索引共用一个文档,批量授权更新不再逐 token 调用 get/ttl 或维护第二份索引;只按已确认的绝对期限清理。故障恢复后可重新读取并重试更新/退出,logoutAll 仍覆盖原有全部记录。结构损坏同样失败关闭并保留原值,交由运维处理,禁止用空文档覆盖未知授权状态。
511
+
512
+ LDAP 的 ldapSyncUser: false 要求已有启用的本地用户以绑定数据库 credentialVersion;没有本地记录返回 USER_NOT_FOUND,不再创建虚拟 DN 身份。
513
+
514
+ Session.credentialVersion 绑定创建时的数据库凭据代际;CreateSessionOptions.expectedCredentialVersion 仅供已验证策略传入,旧代拒绝。省略它的受信低层 create 仍读取真实、启用的本地用户并绑定当前代,不能伪造缺少数据库记录的身份。
515
+
516
+ `iam.auth.logout(accessToken)` 直接按完整不透明令牌在用户分区执行一次 CAS,原子撤销 access 与关联 refresh;access 已到期也不能跳过撤销。非法/不存在的令牌幂等成功,缓存读取或提交故障返回失败,恢复后可用同一令牌重试。HTTP 登出只允许撤销所持令牌,不以 userId 定位他人会话,也不要求该 access 仍通过认证。调用方必须检查 `success`;本地 Cookie/安全存储清理不代表服务端撤销已经确认,失败应显示提示。