@h-ai/iam 0.1.0-alpha5

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.
package/README.md ADDED
@@ -0,0 +1,513 @@
1
+ # @h-ai/iam
2
+
3
+ 身份与访问管理模块,提供统一的 `iam` 对象实现认证、会话、授权与用户管理。
4
+
5
+ ## 功能特性
6
+
7
+ | 功能 | 说明 |
8
+ | -------------- | ------------------------------------------------------ |
9
+ | **认证** | 密码、OTP(邮箱/短信验证码)、LDAP、API Key 多策略认证 |
10
+ | **会话** | 有状态会话(随机访问令牌 + 缓存,滑动续期可选) |
11
+ | **授权** | RBAC 角色与权限管理(DB + 缓存,通配符权限匹配) |
12
+ | **用户管理** | 注册、查询、更新、密码重置、管理员重置密码 |
13
+ | **API Key** | API Key 创建、吊销、验证,支持 scope 与过期时间 |
14
+ | **前端客户端** | HTTP API 契约定义,支持独立前端使用 |
15
+
16
+ ## 安装
17
+
18
+ ```bash
19
+ pnpm add @h-ai/iam
20
+ ```
21
+
22
+ ## 依赖
23
+
24
+ - `@h-ai/reldb` — 数据库(用户/角色/权限持久化),**需在 iam.init() 前初始化**
25
+ - `@h-ai/cache` — 缓存(会话/OTP/重置令牌/权限缓存),**需在 iam.init() 前初始化**
26
+ - `@h-ai/crypto` — 密码哈希(内部使用,自动初始化)
27
+ - `@h-ai/audit` — 审计日志(RBAC 关键操作记录)
28
+
29
+ ## 快速开始
30
+
31
+ ```ts
32
+ import { cache } from '@h-ai/cache'
33
+ import { iam } from '@h-ai/iam'
34
+ import { reldb } from '@h-ai/reldb'
35
+
36
+ // 1. 初始化依赖
37
+ await reldb.init({ type: 'sqlite', database: './data.db' })
38
+ await cache.init({ type: 'memory' })
39
+
40
+ // 2. 初始化 IAM(自动使用已初始化的 reldb 和 cache 单例)
41
+ await iam.init({
42
+ session: { maxAge: 86400, sliding: true },
43
+ // OTP 回调(启用 OTP 登录时注入发送逻辑)
44
+ onOtpSendEmail: async (email, code) => {
45
+ await sendEmail(email, `验证码: ${code}`)
46
+ },
47
+ // 密码重置回调(启用密码重置时注入通知逻辑)
48
+ onPasswordResetRequest: async (user, token, expiresAt) => {
49
+ await sendEmail(user.email!, `重置链接: https://example.com/reset?token=${token}`)
50
+ },
51
+ })
52
+
53
+ // 2. 注册
54
+ const userResult = await iam.user.register({
55
+ username: 'admin',
56
+ email: 'admin@example.com',
57
+ password: 'Password123',
58
+ })
59
+
60
+ // 3. 登录
61
+ const loginResult = await iam.auth.login({
62
+ identifier: 'admin',
63
+ password: 'Password123',
64
+ })
65
+ if (loginResult.success) {
66
+ const { user, tokens } = loginResult.data
67
+ }
68
+
69
+ // 4. 验证令牌
70
+ const session = await iam.auth.verifyToken(loginResult.data.tokens.accessToken)
71
+
72
+ // 5. OTP 验证码登录
73
+ await iam.auth.sendOtp('user@example.com')
74
+ const otpResult = await iam.auth.loginWithOtp({
75
+ identifier: 'user@example.com',
76
+ code: '123456',
77
+ })
78
+
79
+ // 6. 检查权限
80
+ const hasPermission = await iam.authz.checkPermission(
81
+ loginResult.data.user.id,
82
+ 'user:read',
83
+ )
84
+
85
+ // 7. 关闭
86
+ await iam.close()
87
+ ```
88
+
89
+ ## 设计说明
90
+
91
+ ### 架构概览
92
+
93
+ IAM 是**生命周期单例**模块,通过 `iam.init()` / `iam.close()` 管理运行时状态。内部由 5 个子模块构成,按依赖顺序初始化:
94
+
95
+ ```
96
+ iam.init()
97
+ ├─ session (缓存会话管理,无 DB 依赖)
98
+ ├─ authz (RBAC 角色/权限,DB + 缓存)
99
+ ├─ authn (认证策略,依赖 session + authz)
100
+ ├─ user (用户管理,依赖 session + authz + passwordStrategy)
101
+ └─ seed (种子数据,依赖 authz)
102
+ ```
103
+
104
+ 所有子模块使用**工厂 + 闭包**模式创建(如 `createSessionOperations(deps)`),不使用 class。通过 getter 暴露给调用方(`iam.auth` / `iam.user` / `iam.authz` / `iam.session` / `iam.apiKey`)。
105
+
106
+ 初始化前访问任何子模块方法,均返回 `NOT_INITIALIZED` 错误(基于 `NotInitializedKit` Proxy)。
107
+
108
+ ### 数据存储分布
109
+
110
+ | 数据类型 | 存储位置 | 说明 |
111
+ | ------------------------------------------------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------- |
112
+ | 用户、角色、权限、关联关系、API Key | DB(6 张表) | `hai_iam_users`、`hai_iam_roles`、`hai_iam_permissions`、`hai_iam_role_permissions`、`hai_iam_user_roles`、`hai_iam_api_keys` |
113
+ | 会话(accessToken → Session) | 缓存 | 有 TTL,滑动续期可选 |
114
+ | Token 映射(refreshToken → userId + accessToken) | 缓存 | 独立 key,refresh 后旧 token 立即失效 |
115
+ | OTP 验证码 | 缓存 | 有 TTL + 尝试次数限制 |
116
+ | 密码重置令牌 | 缓存 | 有 TTL + 最大验证次数限制 |
117
+ | API Key | DB | `hai_iam_api_keys`,明文不落库,仅存 hash;前缀用于候选检索 |
118
+
119
+ ---
120
+
121
+ ### 子模块:认证(authn)— `iam.auth`
122
+
123
+ #### 关键对象
124
+
125
+ - **`AuthnOperations`** — 认证操作接口,包含 `login` / `loginWithOtp` / `loginWithLdap` / `loginWithApiKey` / `logout` / `verifyToken` / `sendOtp` / `registerAndLogin`
126
+ - **`AuthStrategy`** — 认证策略接口(`type` + `authenticate` + `challenge?`),4 种实现:密码、OTP、LDAP、API Key
127
+ - **`AuthResult`** — 登录结果,含用户信息、`TokenPair`(accessToken + refreshToken)、角色/权限列表
128
+ - **`Credentials`** — 统一凭证联合类型(`{ type: 'password' | 'otp' | 'ldap' | 'apikey' } & 具体凭证`)
129
+
130
+ #### 关键流程
131
+
132
+ **密码登录流程:**
133
+
134
+ ```
135
+ login(credentials)
136
+ → 检查密码登录已启用
137
+ → 按 identifier 查找用户(username / email / phone 三字段匹配)
138
+ → 用户不存在时执行 dummy hash(防 timing attack,固定耗时)
139
+ → 检查账户是否锁定(lockoutDuration 内 loginFailedCount >= maxLoginAttempts)
140
+ → 密码哈希验证(@h-ai/crypto)
141
+ → 验证失败:记录失败次数,达上限则锁定账户
142
+ → 验证成功:重置失败计数
143
+ → 检查密码是否过期(expirationDays > 0 时检查 passwordUpdatedAt)
144
+ → 查询用户角色 + 权限
145
+ → 创建会话(session.create → 生成 TokenPair)
146
+ → 返回 AuthResult { user, tokens, roles, permissions, agreements? }
147
+ ```
148
+
149
+ **Token 验证流程:**
150
+
151
+ ```
152
+ verifyToken(accessToken)
153
+ → session.get(accessToken) → 缓存查询
154
+ → 滑动续期模式下自动延长 TTL
155
+ → 返回 Session 对象(含 userId、roles、permissions)
156
+ ```
157
+
158
+ #### 使用方式
159
+
160
+ ```ts
161
+ // 密码登录
162
+ const result = await iam.auth.login({ identifier: 'admin', password: 'Password123' })
163
+
164
+ // OTP 登录
165
+ await iam.auth.sendOtp('user@example.com')
166
+ const result = await iam.auth.loginWithOtp({ identifier: 'user@example.com', code: '123456' })
167
+
168
+ // LDAP 登录(需在 init 时提供 ldapClientFactory)
169
+ const result = await iam.auth.loginWithLdap({ username: 'jdoe', password: 'pass' })
170
+
171
+ // API Key 登录(需启用 login.apikey: true)
172
+ const result = await iam.auth.loginWithApiKey({ key: 'hai_xxxx...' })
173
+
174
+ // 验证令牌
175
+ const session = await iam.auth.verifyToken(accessToken)
176
+
177
+ // 注册并登录(一站式)
178
+ const result = await iam.auth.registerAndLogin({ username: 'new', password: 'Pass123' })
179
+ ```
180
+
181
+ #### 特别关注
182
+
183
+ - **Timing attack 防护**:用户不存在时执行 dummy password hash,保持响应时间一致
184
+ - **账户锁定**:连续登录失败 `maxLoginAttempts` 次后锁定 `lockoutDuration` 秒
185
+ - **认证策略可独立启用/禁用**:通过 `login.password` / `login.otp` / `login.ldap` / `login.apikey` 控制
186
+ - **OTP 安全**:rejection sampling 消除模偏差,常量时间比较防 timing attack,频率限制(`resendInterval`)
187
+
188
+ ---
189
+
190
+ ### 子模块:会话(session)— `iam.session`
191
+
192
+ #### 关键对象
193
+
194
+ - **`SessionOperations`** — 会话操作接口:`create` / `get` / `verifyToken` / `update` / `delete` / `deleteByUserId` / `refresh` / `revokeRefresh` / `patchUserSessions`
195
+ - **`Session`** — 会话实体,含 userId、roles、permissions、accessToken、过期时间等
196
+ - **`TokenPair`** — 令牌对:`{ accessToken, refreshToken, expiresIn, tokenType: 'Bearer' }`
197
+
198
+ #### 关键流程
199
+
200
+ **Token 刷新(Rotation 策略):**
201
+
202
+ ```
203
+ refresh(refreshToken)
204
+ → 从缓存读取 refreshToken → { userId, accessToken } 映射
205
+ → 获取旧 session(通过 accessToken)
206
+ → 删除旧 refreshToken(缓存 key 立即失效 → 防重放攻击)
207
+ → 删除旧 accessToken session
208
+ → 创建全新 session(复用旧 session 的用户上下文)
209
+ → 返回新的 TokenPair
210
+ ```
211
+
212
+ **单设备登录:**
213
+
214
+ ```
215
+ create(options) [singleDevice=true]
216
+ → 清除该用户所有已有令牌(遍历 user→tokens 映射)
217
+ → 生成新的 accessToken + refreshToken
218
+ → 存储 session 到缓存
219
+ ```
220
+
221
+ #### 使用方式
222
+
223
+ ```ts
224
+ // 创建会话(通常由 login 内部调用)
225
+ const session = await iam.session.create({
226
+ userId: 'user-id',
227
+ username: 'admin',
228
+ roles: ['admin'],
229
+ permissions: ['user:read', 'user:write'],
230
+ })
231
+
232
+ // 验证令牌
233
+ const session = await iam.session.verifyToken(accessToken)
234
+
235
+ // 刷新令牌
236
+ const newTokens = await iam.session.refresh(refreshToken)
237
+
238
+ // 强制下线
239
+ await iam.session.deleteByUserId(userId)
240
+ ```
241
+
242
+ #### 特别关注
243
+
244
+ - **纯缓存存储**:不落库,重启后所有会话失效(符合安全最佳实践)
245
+ - **滑动续期**:`sliding: true` 时每次 `get()` 自动延长 TTL
246
+ - **Refresh Rotation**:旧 refreshToken 单次使用后立即失效,检测到重放即说明 token 泄漏
247
+ - **Token 强度**:256-bit crypto random(`crypto.getRandomValues(new Uint8Array(32))`),base64url 编码
248
+
249
+ ---
250
+
251
+ ### 子模块:授权(authz)— `iam.authz`
252
+
253
+ #### 关键对象
254
+
255
+ - **`AuthzOperations`** — 授权操作接口,覆盖角色 CRUD、权限 CRUD、角色-权限/用户-角色分配、权限检查
256
+ - **`Role`** — 角色实体:`{ id, code, name, description, isSystem }`
257
+ - **`Permission`** — 权限实体:`{ id, code, name, type, resource, action }`,type 可为 `menu` / `api` / `button`
258
+ - **`PermissionType`** — 权限类型枚举
259
+
260
+ #### 关键流程
261
+
262
+ **权限检查(`checkPermission`):**
263
+
264
+ ```
265
+ checkPermission(userId, permission)
266
+ → RBAC 未启用 → 直接返回 true
267
+ → 查询用户角色列表(DB)
268
+ → 检查是否包含超管角色(缓存 superAdminRoleId)→ 是则直接返回 true
269
+ → 逐角色查询权限代码列表(缓存优先,miss 时查 DB 并写入缓存)
270
+ → 逐权限匹配:精确匹配 或 通配符匹配(`admin:*` 匹配 `admin:read`)
271
+ → 返回 true / false
272
+ ```
273
+
274
+ **角色/权限变更后的会话同步:**
275
+
276
+ ```
277
+ assignPermissionToRole(roleId, permId)
278
+ → DB 写入关联关系
279
+ → 清除角色权限缓存
280
+ → 查询该角色下所有用户 → 逐用户重新解析权限 → 更新活跃 session
281
+ (best-effort:同步失败仅 log.error,不影响权限分配结果)
282
+ ```
283
+
284
+ #### 使用方式
285
+
286
+ ```ts
287
+ // 角色管理
288
+ const role = await iam.authz.createRole({ code: 'editor', name: '编辑' })
289
+ const roles = await iam.authz.getAllRoles({ page: 1, pageSize: 20 })
290
+
291
+ // 权限管理
292
+ const perm = await iam.authz.createPermission({
293
+ code: 'article:publish',
294
+ name: '发布文章',
295
+ type: 'api',
296
+ resource: 'article',
297
+ action: 'publish',
298
+ })
299
+
300
+ // 分配与检查
301
+ await iam.authz.assignRole(userId, role.data.id)
302
+ await iam.authz.assignPermissionToRole(role.data.id, perm.data.id)
303
+ const allowed = await iam.authz.checkPermission(userId, 'article:publish')
304
+
305
+ // 批量同步角色
306
+ await iam.authz.syncRoles(userId, [roleId1, roleId2])
307
+ ```
308
+
309
+ #### 特别关注
310
+
311
+ - **通配符匹配**:`admin:*` 匹配 `admin:read`、`admin:write:detail` 等(单层 `*` 匹配冒号后全部)
312
+ - **超管角色**:配置的 `rbac.superAdminRole`(默认 `super_admin`)自动拥有所有权限
313
+ - **事务保护**:`deleteRole` / `deletePermission` 使用 DB 事务,先清关联再删实体
314
+ - **会话同步延迟**:权限变更后活跃 session 中的权限列表会被同步更新,但为 best-effort(大量用户场景可能有延迟)
315
+ - **种子数据**:`seedDefaultData: true` 时自动创建 admin/user/guest 三个默认角色及基础权限,幂等执行
316
+
317
+ ---
318
+
319
+ ### 子模块:用户管理(user)— `iam.user`
320
+
321
+ #### 关键对象
322
+
323
+ - **`UserOperations`** — 用户操作接口:注册、查询、更新、删除、密码修改与重置、密码强度验证
324
+ - **`User`** — 用户实体(公开字段):`{ id, username, email, phone, displayName, avatarUrl, enabled, roles? }`
325
+ - **`StoredUser`** — 内部存储用户(含 `passwordHash`、`loginFailedCount`、`lockedUntil` 等敏感字段)
326
+ - **`RegisterOptions`** / **`RegisterResult`** — 注册输入与输出
327
+ - **`UpdateCurrentUserInput`** — 当前用户可修改的白名单字段(`displayName` / `avatarUrl` / `phone` / `metadata`)
328
+
329
+ #### 关键流程
330
+
331
+ **用户注册流程:**
332
+
333
+ ```
334
+ register(options)
335
+ → 检查注册是否启用
336
+ → 密码强度验证(Zod schema + 自定义规则)
337
+ → 密码哈希(@h-ai/crypto)
338
+ → DB 事务:创建用户 + 查询创建结果
339
+ → 分配默认角色(rbac.defaultRole,事务外执行,失败仅 log)
340
+ → 返回 RegisterResult { user, agreements? }
341
+ ```
342
+
343
+ **密码重置流程:**
344
+
345
+ ```
346
+ requestPasswordReset(email)
347
+ → 按邮箱查找用户(不存在时静默返回 ok → 防枚举攻击)
348
+ → 生成 256-bit 随机令牌
349
+ → 缓存存储 SHA-256(token) → userId 映射(有 TTL + maxAttempts)
350
+ → 调用 onPasswordResetRequest 回调(业务层负责发送邮件/短信)
351
+
352
+ confirmPasswordReset(token, newPassword)
353
+ → 缓存查询 SHA-256(token) → 校验有效期和尝试次数
354
+ → 密码强度验证 → 哈希 → 更新 DB
355
+ → 清除该用户所有活跃会话(强制重新登录)
356
+ → 删除已使用的重置令牌
357
+ ```
358
+
359
+ #### 使用方式
360
+
361
+ ```ts
362
+ // 注册
363
+ const result = await iam.user.register({
364
+ username: 'alice',
365
+ email: 'alice@example.com',
366
+ password: 'Password123',
367
+ })
368
+
369
+ // 当前用户操作
370
+ const me = await iam.user.getCurrentUser(accessToken)
371
+ await iam.user.updateCurrentUser(accessToken, { displayName: '新名称' })
372
+ await iam.user.changeCurrentUserPassword(accessToken, 'oldPass', 'newPass')
373
+
374
+ // 管理操作
375
+ const users = await iam.user.listUsers({ page: 1, pageSize: 20, search: 'alice', include: ['roles'] })
376
+ await iam.user.updateUser(userId, { enabled: false }) // 禁用用户 → 自动清除会话
377
+ await iam.user.deleteUser(userId) // 删除用户 → 事务清除角色关联
378
+
379
+ // 密码重置
380
+ await iam.user.requestPasswordReset('alice@example.com')
381
+ await iam.user.confirmPasswordReset(token, 'NewPassword456')
382
+ await iam.user.adminResetPassword(userId, 'TempPassword123')
383
+
384
+ // 密码强度验证(同步方法)
385
+ const valid = iam.user.validatePassword('weak')
386
+ ```
387
+
388
+ #### 特别关注
389
+
390
+ - **`toUser()` 字段脱敏**:所有对外返回的用户对象均剥离 `passwordHash`、`loginFailedCount` 等敏感字段
391
+ - **会话联动**:`deleteUser` / `updateUser({ enabled: false })` / `changePassword` / `confirmPasswordReset` 自动清除受影响用户的所有活跃会话
392
+ - **防枚举攻击**:`requestPasswordReset` 对不存在的邮箱返回 `ok(undefined)` 而非错误
393
+ - **validatePassword 是同步方法**:这是 `UserOperations` 中唯一的同步方法,其余均为异步
394
+ - **搜索模糊匹配**:`listUsers` 支持按用户名、邮箱、手机号、显示名称模糊搜索,LIKE 通配符已转义
395
+
396
+ ---
397
+
398
+ ### 子模块:API Key — `iam.apiKey`
399
+
400
+ #### 关键对象
401
+
402
+ - **`ApiKeyOperations`** — API Key 操作接口:`createApiKey` / `listApiKeys` / `getApiKey` / `revokeApiKey` / `verifyApiKey`
403
+ - **`ApiKey`** — API Key 实体(公开字段):`{ id, userId, name, keyPrefix, enabled, expiresAt, scopes }`
404
+ - **`CreateApiKeyResult`** — 创建结果:`{ apiKey, rawKey }`(明文密钥仅返回一次)
405
+
406
+ #### 关键流程
407
+
408
+ **API Key 验证:**
409
+
410
+ ```
411
+ verifyApiKey(rawKey)
412
+ → 从 rawKey 提取前缀(prefix + 前 8 字符)
413
+ → 按前缀从 DB 检索候选 API Key 列表(缩小范围)
414
+ → 逐候选项进行 hash 验证(同步操作)
415
+ → 验证通过 → 检查是否过期/禁用
416
+ → 异步更新 lastUsedAt(不阻塞返回)
417
+ → 返回 ApiKey 实体
418
+ ```
419
+
420
+ #### 使用方式
421
+
422
+ ```ts
423
+ // 创建 API Key
424
+ const result = await iam.apiKey.createApiKey(userId, {
425
+ name: 'CI/CD',
426
+ expirationDays: 90,
427
+ scopes: ['api:read'],
428
+ })
429
+ // result.data.rawKey → 明文密钥(仅此一次展示)
430
+
431
+ // 列出用户的 API Key
432
+ const keys = await iam.apiKey.listApiKeys(userId)
433
+
434
+ // 吊销
435
+ await iam.apiKey.revokeApiKey(keyId)
436
+ ```
437
+
438
+ #### 特别关注
439
+
440
+ - **明文不落库**:数据库仅存储 hash,创建时返回的 `rawKey` 是唯一获取机会
441
+ - **前缀检索**:使用 `keyPrefix` 缩小候选集,避免全表扫描
442
+ - **数量限制**:单用户最多 `maxKeysPerUser`(默认 10)个 API Key
443
+ - **需显式启用**:`login.apikey: true` 才会初始化 API Key 子功能
444
+
445
+ ---
446
+
447
+ ## 更多用法
448
+
449
+ 详细 API 参数、错误码及集成模式请参考 Skill 模板(`packages/cli/templates/skills/hai-iam/SKILL.md`)。
450
+
451
+ ## 前端 API 契约
452
+
453
+ 前端通过 `@h-ai/iam/api` 导出的契约定义(`iamEndpoints`)与 `@h-ai/api-client` 配合调用:
454
+
455
+ ```ts
456
+ import { api } from '@h-ai/api-client'
457
+ import { iamEndpoints } from '@h-ai/iam/api'
458
+
459
+ await api.init({ baseUrl: '/api/iam' })
460
+
461
+ // 登录
462
+ const result = await api.call(iamEndpoints.login, {
463
+ identifier: 'admin',
464
+ password: 'Password123',
465
+ })
466
+
467
+ // 获取当前用户
468
+ const user = await api.call(iamEndpoints.currentUser, {})
469
+
470
+ // 修改密码
471
+ await api.call(iamEndpoints.changePassword, {
472
+ oldPassword: 'Password123',
473
+ newPassword: 'NewPassword456',
474
+ })
475
+ ```
476
+
477
+ ## 密码重置
478
+
479
+ ```ts
480
+ // 请求重置(即使用户不存在也返回 ok,防止枚举)
481
+ await iam.user.requestPasswordReset('admin@example.com')
482
+
483
+ // 确认重置(校验令牌有效期和尝试次数 → 更新密码 → 清除所有会话)
484
+ const result = await iam.user.confirmPasswordReset(token, 'NewPassword456')
485
+
486
+ // 管理员重置(无需旧密码)
487
+ await iam.user.adminResetPassword(userId, 'TempPassword123')
488
+ ```
489
+
490
+ ## 错误处理
491
+
492
+ 所有操作返回 `HaiResult<T>`,通过 `HaiIamError` 做分支判断:
493
+
494
+ ```ts
495
+ import { HaiIamError, iam } from '@h-ai/iam'
496
+
497
+ const result = await iam.auth.login({ identifier: 'admin', password: 'wrong' })
498
+ if (!result.success) {
499
+ if (result.error.code === HaiIamError.INVALID_CREDENTIALS.code) {
500
+ // 用户名或密码错误
501
+ }
502
+ }
503
+ ```
504
+
505
+ ## 测试
506
+
507
+ ```bash
508
+ pnpm test
509
+ ```
510
+
511
+ ## 许可证
512
+
513
+ Apache-2.0