@h-ai/iam 0.1.0-alpha.55 → 0.1.0-alpha.57

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.en.md ADDED
@@ -0,0 +1,556 @@
1
+ # @h-ai/iam
2
+
3
+ [简体中文](./README.md) | [English](./README.en.md) | [日本語](./README.ja.md)
4
+
5
+ [Framework home](../../README.en.md) · Related modules: [@h-ai/audit](../audit/README.en.md) · [@h-ai/cache](../cache/README.en.md) · [@h-ai/core](../core/README.en.md) · [@h-ai/crypto](../crypto/README.en.md) · [@h-ai/reldb](../reldb/README.en.md)
6
+
7
+ Identity and access management through a unified `iam` object for authentication, sessions, authorization, and user management.
8
+
9
+ ## Features
10
+
11
+ | Feature | Description |
12
+ | -------------------- | ---------------------------------------------------------------------------------------------------------------------- |
13
+ | **Authentication** | Password, OTP (email/SMS codes), LDAP, and API Key strategies |
14
+ | **Sessions** | Stateful sessions (random access tokens + cache, optional sliding renewal) |
15
+ | **Authorization** | RBAC roles and permissions (DB + cache, wildcard matching) |
16
+ | **User management** | Registration, queries, updates, password reset, and admin password reset |
17
+ | **API Key** | Create, revoke, and verify API keys with scopes and expiration |
18
+ | **One-time tickets** | Short-lived capability tickets (issue / atomic consume) for channels such as WebSocket that cannot carry Bearer tokens |
19
+ | **Frontend client** | HTTP API contracts for independent frontends |
20
+
21
+ ## Installation
22
+
23
+ ```bash
24
+ pnpm add @h-ai/iam
25
+ ```
26
+
27
+ ## Dependencies
28
+
29
+ - `@h-ai/reldb`: persistence for users/roles/permissions; **initialize before iam.init()**.
30
+ - `@h-ai/cache`: sessions/OTP/reset tokens/permission cache; **initialize before iam.init()**.
31
+ - `@h-ai/crypto`: password hashing, used internally and initialized automatically.
32
+ - `@h-ai/audit`: audit records for key RBAC operations.
33
+
34
+ ## Quick start
35
+
36
+ ```ts
37
+ import { cache } from '@h-ai/cache'
38
+ import { iam } from '@h-ai/iam'
39
+ import { reldb } from '@h-ai/reldb'
40
+
41
+ // 1. 初始化依赖
42
+ await reldb.init({ type: 'sqlite', database: './data.db' })
43
+ await cache.init({ type: 'memory' })
44
+
45
+ // 2. 初始化 IAM(自动使用已初始化的 reldb 和 cache 单例)
46
+ await iam.init({
47
+ session: { maxAge: 86400, sliding: true },
48
+ // OTP 回调(启用 OTP 登录时注入发送逻辑)
49
+ onOtpSendEmail: async (email, code) => {
50
+ await sendEmail(email, `验证码: ${code}`)
51
+ },
52
+ // 密码重置回调(启用密码重置时注入通知逻辑)
53
+ onPasswordResetRequest: async (user, token, expiresAt) => {
54
+ await sendEmail(user.email!, `重置链接: https://example.com/reset?token=${token}`)
55
+ },
56
+ })
57
+
58
+ // 2. 注册
59
+ const userResult = await iam.user.register({
60
+ username: 'admin',
61
+ email: 'admin@example.com',
62
+ password: 'Password123',
63
+ })
64
+
65
+ // 3. 登录
66
+ const loginResult = await iam.auth.login({
67
+ identifier: 'admin',
68
+ password: 'Password123',
69
+ })
70
+ if (loginResult.success) {
71
+ const { user, tokens } = loginResult.data
72
+ }
73
+
74
+ // 4. 验证令牌
75
+ const session = await iam.auth.verifyToken(loginResult.data.tokens.accessToken)
76
+
77
+ // 5. OTP 验证码登录
78
+ await iam.auth.sendOtp('user@example.com')
79
+ const otpResult = await iam.auth.loginWithOtp({
80
+ identifier: 'user@example.com',
81
+ code: '123456',
82
+ })
83
+
84
+ // 6. 检查权限
85
+ const hasPermission = await iam.authz.checkPermission(
86
+ loginResult.data.user.id,
87
+ 'user:read',
88
+ )
89
+
90
+ // 7. 关闭
91
+ await iam.close()
92
+ ```
93
+
94
+ `iam.config` returns a **redacted configuration snapshot**. Sensitive LDAP `url` / `bindPassword` and similar fields are hidden automatically. Use it to inspect state, not as the original configuration source.
95
+
96
+ ## Design
97
+
98
+ ### Architecture
99
+
100
+ IAM is a **lifecycle singleton** managed through `iam.init()` / `iam.close()`. Five internal subsystems initialize in dependency order:
101
+
102
+ ```
103
+ iam.init()
104
+ ├─ session (cached sessions; no DB dependency)
105
+ ├─ authz (RBAC roles/permissions; DB + cache)
106
+ ├─ authn (authentication; depends on session + authz)
107
+ ├─ user (users; depends on session + authz + passwordStrategy)
108
+ └─ seed (seed data; depends on authz)
109
+ ```
110
+
111
+ All subsystems use **factories + closures** (for example `createSessionOperations(deps)`), not classes. Getters expose them as `iam.auth` / `iam.user` / `iam.authz` / `iam.session` / `iam.apiKey`.
112
+
113
+ Before initialization, every subsystem method returns `NOT_INITIALIZED` through a `NotInitializedKit` proxy.
114
+
115
+ ### Data storage
116
+
117
+ | Data | Storage | Description |
118
+ | --------------------------------------------------- | ------------- | ----------------------------------------------------------------------------------------------------------------------------- |
119
+ | Users, roles, permissions, associations, API keys | DB (6 tables) | `hai_iam_users`、`hai_iam_roles`、`hai_iam_permissions`、`hai_iam_role_permissions`、`hai_iam_user_roles`、`hai_iam_api_keys` |
120
+ | Sessions (accessToken → Session) | Cache | TTL, optional sliding renewal |
121
+ | Token mapping (refreshToken → userId + accessToken) | Cache | Separate keys; old tokens expire immediately after refresh |
122
+ | OTP codes | Cache | TTL + attempt limit |
123
+ | Password reset tokens | Cache | TTL + maximum verification attempts |
124
+ | API Key | DB | `hai_iam_api_keys`; store hashes only, never plaintext; prefix narrows candidates |
125
+
126
+ ---
127
+
128
+ ### Authentication (authn) — `iam.auth`
129
+
130
+ #### Key types
131
+
132
+ - **`AuthnOperations`**: `login` / `loginWithOtp` / `loginWithLdap` / `loginWithApiKey` / `logout` / `verifyToken` / `sendOtp` / `registerAndLogin`.
133
+ - **`AuthStrategy`**: strategy interface (`type` + `authenticate` + `challenge?`), implemented for password, OTP, LDAP, and API Key.
134
+ - **`AuthResult`**: login result with user details, `TokenPair` (accessToken + refreshToken), and roles/permissions.
135
+ - **`Credentials`**: a credential union combining `{ type: 'password' | 'otp' | 'ldap' | 'apikey' }` with strategy-specific credentials.
136
+
137
+ #### Key workflows
138
+
139
+ **Password login:**
140
+
141
+ ```
142
+ login(credentials)
143
+ → Check password login is enabled
144
+ → Find user by identifier (username / email / phone)
145
+ → Dummy hash for missing users (fixed timing to mitigate timing attacks)
146
+ → Check account lock (loginFailedCount >= maxLoginAttempts within lockoutDuration)
147
+ → Verify password hash (@h-ai/crypto)
148
+ → Failure: record attempt; lock at the limit
149
+ → Success: reset failure count
150
+ → Check password expiration (passwordUpdatedAt when expirationDays > 0)
151
+ → Query user roles + permissions
152
+ → Create session (session.create → TokenPair)
153
+ → Return AuthResult { user, tokens, roles, permissions, agreements? }
154
+ ```
155
+
156
+ **Token verification:**
157
+
158
+ ```
159
+ verifyToken(accessToken)
160
+ → session.get(accessToken) → cache lookup
161
+ → Extend TTL automatically when sliding renewal is enabled
162
+ → Return Session (userId, roles, permissions)
163
+ ```
164
+
165
+ #### Usage
166
+
167
+ `authz.createRole({ ..., permissionIds })` and `authz.updateRole(id, { ..., permissionIds })` save role data and the complete permission set in one transaction; any failure rolls back. `permissionIds: []` clears permissions, omission preserves them, duplicate IDs are deduplicated, and unknown permissions fail. Concurrent updates use the last committed complete set without merging. Self-managed commits synchronize user sessions; with an external transaction, the caller must synchronize after commit. Admin Console uses this API directly, without split updates or compensating role deletion.
168
+
169
+ ```ts
170
+ // 密码登录
171
+ const result = await iam.auth.login({ identifier: 'admin', password: 'Password123' })
172
+
173
+ // OTP 登录
174
+ await iam.auth.sendOtp('user@example.com')
175
+ const result = await iam.auth.loginWithOtp({ identifier: 'user@example.com', code: '123456' })
176
+
177
+ // LDAP 登录(需在 init 时提供 ldapClientFactory)
178
+ const result = await iam.auth.loginWithLdap({ username: 'jdoe', password: 'pass' })
179
+
180
+ // API Key 登录(需启用 login.apikey: true)
181
+ const result = await iam.auth.loginWithApiKey({ key: 'hai_xxxx...' })
182
+
183
+ // 验证令牌
184
+ const session = await iam.auth.verifyToken(accessToken)
185
+
186
+ // 注册并登录(一站式)
187
+ const result = await iam.auth.registerAndLogin({ username: 'new', password: 'Pass123' })
188
+ ```
189
+
190
+ #### Considerations
191
+
192
+ - **Timing attack mitigation**: perform a dummy password hash for nonexistent users to keep response timing consistent.
193
+ - **Account lockout**: lock for `lockoutDuration` seconds after `maxLoginAttempts` consecutive failures.
194
+ - **Independent strategy toggles**: `login.password` / `login.otp` / `login.ldap` / `login.apikey`.
195
+ - **OTP security**: rejection sampling avoids modulo bias, constant-time comparison mitigates timing attacks, and `resendInterval` limits frequency.
196
+
197
+ ---
198
+
199
+ ### Sessions (session) — `iam.session`
200
+
201
+ #### Key types
202
+
203
+ - **`SessionOperations`**: `create` / `get` / `verifyToken` / `update` / `delete` / `deleteByUserId` / `refresh` / `revokeRefresh` / `patchUserSessions`.
204
+ - **`Session`**: userId, roles, permissions, accessToken, expiration, and other session data.
205
+ - **`TokenPair`**: `{ accessToken, refreshToken, expiresIn, tokenType: 'Bearer' }`.
206
+
207
+ #### Key workflows
208
+
209
+ **Token refresh (rotation):**
210
+
211
+ ```
212
+ refresh(refreshToken)
213
+ → Read refreshToken → { userId, accessToken } from cache
214
+ → Get old session via accessToken
215
+ → Delete old refreshToken (expire key immediately to prevent replay)
216
+ → Delete old accessToken session
217
+ → Create a new session with the old user context
218
+ → Return new TokenPair
219
+ ```
220
+
221
+ **Single-device login:**
222
+
223
+ ```
224
+ create(options) [singleDevice=true]
225
+ → Clear all existing user tokens via user→tokens mapping
226
+ → Generate new accessToken + refreshToken
227
+ → Store session in cache
228
+ ```
229
+
230
+ #### Usage
231
+
232
+ ```ts
233
+ // 创建会话(通常由 login 内部调用)
234
+ const session = await iam.session.create({
235
+ userId: 'user-id',
236
+ username: 'admin',
237
+ roles: ['admin'],
238
+ permissions: ['user:read', 'user:write'],
239
+ })
240
+
241
+ // 验证令牌
242
+ const session = await iam.session.verifyToken(accessToken)
243
+
244
+ // 刷新令牌
245
+ const newTokens = await iam.session.refresh(refreshToken)
246
+
247
+ // 强制下线
248
+ await iam.session.deleteByUserId(userId)
249
+ ```
250
+
251
+ #### Considerations
252
+
253
+ - **Cache-only storage**: no database persistence; all sessions expire after restart, following the security design.
254
+ - **Sliding renewal**: with `sliding: true`, each `get()` extends TTL automatically.
255
+ - **Refresh rotation**: old refreshToken expires after one use; replay detection indicates token leakage.
256
+ - **Token strength**: 256-bit cryptographic randomness (`crypto.getRandomValues(new Uint8Array(32))`), base64url encoded.
257
+
258
+ ---
259
+
260
+ ### Authorization (authz) — `iam.authz`
261
+
262
+ #### Key types
263
+
264
+ - **`AuthzOperations`**: role/permission CRUD, role-permission/user-role assignments, and permission checks.
265
+ - **`Role`**: `{ id, code, name, description, isSystem }`.
266
+ - **`Permission`**: `{ id, code, name, type, resource, action }`; type is `menu` / `api` / `button`.
267
+ - **`PermissionType`**: permission type enum.
268
+
269
+ #### Key workflows
270
+
271
+ **Permission checks (`checkPermission`):**
272
+
273
+ ```
274
+ checkPermission(userId, permission)
275
+ → Return true if RBAC is disabled
276
+ → Query user roles (DB)
277
+ → Check super-admin role (cached superAdminRoleId); return true if present
278
+ → Get each role's permission codes (cache first; DB on miss, then cache)
279
+ → Match each permission exactly or by wildcard (`admin:*` matches `admin:read`)
280
+ → Return true / false
281
+ ```
282
+
283
+ **Session synchronization after role/permission changes:**
284
+
285
+ ```
286
+ assignPermissionToRole(roleId, permId)
287
+ → Write association to DB
288
+ → Clear role permission cache
289
+ → Find role users → resolve permissions per user → update active sessions
290
+ (best-effort: failures only log.error; permission assignment still succeeds)
291
+ ```
292
+
293
+ #### Usage
294
+
295
+ ```ts
296
+ // 角色管理
297
+ const role = await iam.authz.createRole({ code: 'editor', name: '编辑' })
298
+ const roles = await iam.authz.getAllRoles({ page: 1, pageSize: 20 })
299
+
300
+ // 权限管理
301
+ const perm = await iam.authz.createPermission({
302
+ code: 'article:publish',
303
+ name: '发布文章',
304
+ type: 'api',
305
+ resource: 'article',
306
+ action: 'publish',
307
+ })
308
+
309
+ // 分配与检查
310
+ await iam.authz.assignRole(userId, role.data.id)
311
+ await iam.authz.assignPermissionToRole(role.data.id, perm.data.id)
312
+ const allowed = await iam.authz.checkPermission(userId, 'article:publish')
313
+
314
+ // 批量同步角色
315
+ await iam.authz.syncRoles(userId, [roleId1, roleId2])
316
+ ```
317
+
318
+ #### Considerations
319
+
320
+ - **Wildcard matching**: `admin:*` matches `admin:read`, `admin:write:detail`, etc.; one `*` matches everything after the colon.
321
+ - **Super-admin role**: `rbac.superAdminRole` (default `super_admin`) automatically grants all permissions.
322
+ - **Transactions**: `deleteRole` / `deletePermission` clear associations before deleting entities in a DB transaction.
323
+ - **Session synchronization latency**: permission changes update active session permissions on a best-effort basis; many users may cause delay.
324
+ - **Seed data**: `seedDefaultData: true` idempotently creates admin/user/guest roles and basic permissions.
325
+
326
+ ---
327
+
328
+ ### Users (user) — `iam.user`
329
+
330
+ #### Key types
331
+
332
+ - **`UserOperations`**: registration, queries, updates, deletion, password changes/resets, and password strength validation.
333
+ - **`User`** (public fields): `{ id, username, email, phone, displayName, avatarUrl, enabled, roles? }`.
334
+ - **`StoredUser`**: internal stored user, including sensitive `passwordHash`, `loginFailedCount`, `lockedUntil`, etc.
335
+ - **`RegisterOptions`** / **`RegisterResult`**: registration input/output.
336
+ - **`UpdateCurrentUserInput`**: fields the current user may change (`displayName` / `avatarUrl` / `phone` / `metadata`).
337
+
338
+ #### Key workflows
339
+
340
+ **User registration:**
341
+
342
+ ```
343
+ register(options)
344
+ → Check registration is enabled
345
+ → Validate password strength (Zod schema + custom rules)
346
+ → Hash password (@h-ai/crypto)
347
+ → DB transaction: create user + query result
348
+ → Assign default role (rbac.defaultRole, outside transaction; failures only log)
349
+ → Return RegisterResult { user, agreements? }
350
+ ```
351
+
352
+ **Password reset:**
353
+
354
+ ```
355
+ requestPasswordReset(email)
356
+ → Find user by email (missing users return ok silently to prevent enumeration)
357
+ → Generate a random 256-bit token
358
+ → Cache SHA-256(token) → userId (TTL + maxAttempts)
359
+ → Call onPasswordResetRequest (application sends email/SMS)
360
+
361
+ confirmPasswordReset(token, newPassword)
362
+ → Look up SHA-256(token) and verify expiration/attempts
363
+ → Validate password strength → hash → update DB
364
+ → Clear all active user sessions to force login
365
+ → Delete consumed reset token
366
+ ```
367
+
368
+ #### Usage
369
+
370
+ ```ts
371
+ // 注册
372
+ const result = await iam.user.register({
373
+ username: 'alice',
374
+ email: 'alice@example.com',
375
+ password: 'Password123',
376
+ })
377
+
378
+ // 当前用户操作
379
+ const me = await iam.user.getCurrentUser(accessToken)
380
+ await iam.user.updateCurrentUser(accessToken, { displayName: '新名称' })
381
+ await iam.user.changeCurrentUserPassword(accessToken, 'oldPass', 'newPass')
382
+
383
+ // 管理操作
384
+ const users = await iam.user.listUsers({
385
+ page: 1,
386
+ pageSize: 20,
387
+ search: 'alice',
388
+ sortBy: 'createdAt',
389
+ sortDirection: 'desc',
390
+ include: ['roles'],
391
+ })
392
+ await iam.user.updateUser(userId, { enabled: false }) // 禁用用户 → 自动清除会话
393
+ await iam.user.deleteUser(userId) // 删除用户 → 事务清除角色关联
394
+
395
+ // 密码重置
396
+ await iam.user.requestPasswordReset('alice@example.com')
397
+ await iam.user.confirmPasswordReset(token, 'NewPassword456')
398
+ await iam.user.adminResetPassword(userId, 'TempPassword123')
399
+
400
+ // 密码强度验证(同步方法)
401
+ const valid = iam.user.validatePassword('weak')
402
+ ```
403
+
404
+ #### Considerations
405
+
406
+ - **`toUser()` redaction**: public users omit sensitive fields such as `passwordHash` and `loginFailedCount`.
407
+ - **Session invalidation**: `deleteUser` / `updateUser({ enabled: false })` / `changePassword` / `confirmPasswordReset` clear all active sessions for affected users.
408
+ - **Enumeration protection**: `requestPasswordReset` returns `ok(undefined)` for nonexistent emails instead of an error.
409
+ - **Synchronous validatePassword**: the only synchronous method in `UserOperations`; all others are asynchronous.
410
+ - **Fuzzy search**: `listUsers` searches username, email, phone, and display name with escaped LIKE wildcards.
411
+ - **Server sorting**: `listUsers` allows only `username`, `email`, `displayName`, `enabled`, and `createdAt`, mapped through an allowlist; defaults to newest creation first.
412
+
413
+ ---
414
+
415
+ ### API keys — `iam.apiKey`
416
+
417
+ #### Key types
418
+
419
+ - **`ApiKeyOperations`**: `createApiKey` / `listApiKeys` / `getApiKey` / `revokeApiKey` / `verifyApiKey`.
420
+ - **`ApiKey`** (public fields): `{ id, userId, name, keyPrefix, enabled, expiresAt, scopes }`.
421
+ - **`CreateApiKeyResult`**: `{ apiKey, rawKey }`; the plaintext key is returned only once.
422
+
423
+ #### Key workflows
424
+
425
+ **API key verification:**
426
+
427
+ ```
428
+ verifyApiKey(rawKey)
429
+ → Extract prefix from rawKey (prefix + first 8 characters)
430
+ → Query candidate keys by prefix from DB
431
+ → Verify each hash synchronously
432
+ → If valid, check expiration/disabled state
433
+ → Update lastUsedAt asynchronously without blocking return
434
+ → Return ApiKey
435
+ ```
436
+
437
+ #### Usage
438
+
439
+ ```ts
440
+ // 创建 API Key
441
+ const result = await iam.apiKey.createApiKey(userId, {
442
+ name: 'CI/CD',
443
+ expirationDays: 90,
444
+ scopes: ['api:read'],
445
+ })
446
+ // result.data.rawKey → 明文密钥(仅此一次展示)
447
+
448
+ // 列出用户的 API Key(服务端分页)
449
+ const keys = await iam.apiKey.listApiKeys(userId, { page: 1, pageSize: 20, search: 'ci' })
450
+ // keys.data → { items, total, page, pageSize }
451
+
452
+ // 吊销
453
+ await iam.apiKey.revokeApiKey(keyId)
454
+ ```
455
+
456
+ #### Considerations
457
+
458
+ - **No plaintext persistence**: only hashes are stored; `rawKey` at creation is the sole opportunity to retrieve it.
459
+ - **Prefix lookup**: `keyPrefix` narrows candidates without a full table scan.
460
+ - **Count limit**: at most `maxKeysPerUser` (default 10) keys per user.
461
+ - **Explicit enablement**: API keys initialize only with `login.apikey: true`.
462
+
463
+ ---
464
+
465
+ ## More usage
466
+
467
+ See the skill template (Chinese) for detailed API parameters, codes, and integration patterns: `packages/cli/templates/skills/hai-iam/SKILL.md`.
468
+
469
+ ## Frontend API calls
470
+
471
+ Public HTTP APIs are defined by `@h-ai/api-contract` and called through the `@h-ai/api-client` typed client:
472
+
473
+ ```ts
474
+ import { apiClient } from '@h-ai/api-client'
475
+
476
+ await apiClient.init({ baseUrl: '/api/v1' })
477
+
478
+ // 登录
479
+ const result = await apiClient.iam.auth.login({
480
+ identifier: 'admin',
481
+ password: 'Password123',
482
+ })
483
+
484
+ // 获取当前用户
485
+ const user = await apiClient.iam.auth.currentUser()
486
+
487
+ // 修改密码
488
+ await apiClient.iam.auth.changePassword({
489
+ oldPassword: 'Password123',
490
+ newPassword: 'NewPassword456',
491
+ })
492
+ ```
493
+
494
+ ## Password reset
495
+
496
+ ```ts
497
+ // 请求重置(即使用户不存在也返回 ok,防止枚举)
498
+ await iam.user.requestPasswordReset('admin@example.com')
499
+
500
+ // 确认重置(校验令牌有效期和尝试次数 → 更新密码 → 清除所有会话)
501
+ const result = await iam.user.confirmPasswordReset(token, 'NewPassword456')
502
+
503
+ // 管理员重置(无需旧密码)
504
+ await iam.user.adminResetPassword(userId, 'TempPassword123')
505
+ ```
506
+
507
+ ## One-time tickets — `iam.ticket`
508
+
509
+ Short-lived capability tickets for channels that cannot carry Bearer tokens, such as WebSocket URLs. An authenticated HTTP request issues a ticket bound to a subject, purpose, and operation. The server **atomically consumes** it on first validation, making it valid only once.
510
+
511
+ ```ts
512
+ // 签发(已登录 HTTP 请求内):绑定主体、用途与本次操作,30 秒过期
513
+ const issued = await iam.ticket.issue({
514
+ subjectId: session.userId,
515
+ purpose: 'ai-audio',
516
+ grant: { operation: 'transcribe', model: 'whisper-1' },
517
+ ttlMs: 30_000,
518
+ })
519
+ // issued.data.ticket 交给客户端,用于建立 WebSocket 连接
520
+
521
+ // 消费(服务端建连时):原子单次,返回主体 + 用途 + grant
522
+ const consumed = await iam.ticket.consume(ticket, { purpose: 'ai-audio' })
523
+ if (consumed.success) {
524
+ const { subjectId, grant } = consumed.data
525
+ // 用 grant.operation / grant.model 交叉校验客户端请求
526
+ }
527
+ ```
528
+
529
+ Features: cryptographically secure randomness, TTL, atomic single use (only one concurrent consumer succeeds), and purpose binding to prevent cross-purpose replay.
530
+
531
+ ## Error handling
532
+
533
+ All operations return `HaiResult<T>`; branch on `HaiIamError`:
534
+
535
+ ```ts
536
+ import { HaiIamError, iam } from '@h-ai/iam'
537
+
538
+ const result = await iam.auth.login({ identifier: 'admin', password: 'wrong' })
539
+ if (!result.success) {
540
+ if (result.error.code === HaiIamError.INVALID_CREDENTIALS.code) {
541
+ // 用户名或密码错误
542
+ }
543
+ }
544
+ ```
545
+
546
+ ## Tests
547
+
548
+ ```bash
549
+ pnpm test
550
+ ```
551
+
552
+ ## License
553
+
554
+ Apache-2.0
555
+
556
+ Role lists aggregate actual member counts for the current page with one `iam.authz.getRoleUserCounts(roleIds)` call, returning `HaiResult<Map<string, number>>`. Unassigned roles get 0; failed queries show loading errors rather than fabricated zero counts.
package/README.ja.md ADDED
@@ -0,0 +1,556 @@
1
+ # @h-ai/iam
2
+
3
+ [简体中文](./README.md) | [English](./README.en.md) | [日本語](./README.ja.md)
4
+
5
+ [フレームワークのホーム](../../README.ja.md) · 関連モジュール:[@h-ai/audit](../audit/README.ja.md) · [@h-ai/cache](../cache/README.ja.md) · [@h-ai/core](../core/README.ja.md) · [@h-ai/crypto](../crypto/README.ja.md) · [@h-ai/reldb](../reldb/README.ja.md)
6
+
7
+ 共通の `iam` オブジェクトで認証、セッション、認可、ユーザー管理を提供する Identity and Access Management モジュールです。
8
+
9
+ ## 機能
10
+
11
+ | 機能 | 説明 |
12
+ | ------------------------------ | ----------------------------------------------------------------------------------------- |
13
+ | **認証** | パスワード、OTP(メール・SMS コード)、LDAP、API Key の認証方式 |
14
+ | **セッション** | 状態を持つセッション(ランダム access token + キャッシュ、任意の自動延長) |
15
+ | **認可** | RBAC のロール・権限管理(DB + キャッシュ、ワイルドカード照合) |
16
+ | **ユーザー管理** | 登録、検索、更新、パスワードリセット、管理者によるリセット |
17
+ | **API Key** | scope・期限付き API Key の作成、失効、検証 |
18
+ | **一度限りのチケット** | Bearer Token を付けられない WebSocket などのための短期チケット(発行 / アトミックな消費) |
19
+ | **フロントエンドクライアント** | 独立フロントエンド向けの HTTP API 契約 |
20
+
21
+ ## インストール
22
+
23
+ ```bash
24
+ pnpm add @h-ai/iam
25
+ ```
26
+
27
+ ## 依存関係
28
+
29
+ - `@h-ai/reldb`:ユーザー・ロール・権限の永続化。**iam.init() より先に初期化**。
30
+ - `@h-ai/cache`:セッション・OTP・リセット Token・権限のキャッシュ。**iam.init() より先に初期化**。
31
+ - `@h-ai/crypto`:内部で使用するパスワードハッシュ。自動初期化。
32
+ - `@h-ai/audit`:主要な RBAC 操作の監査記録。
33
+
34
+ ## クイックスタート
35
+
36
+ ```ts
37
+ import { cache } from '@h-ai/cache'
38
+ import { iam } from '@h-ai/iam'
39
+ import { reldb } from '@h-ai/reldb'
40
+
41
+ // 1. 初始化依赖
42
+ await reldb.init({ type: 'sqlite', database: './data.db' })
43
+ await cache.init({ type: 'memory' })
44
+
45
+ // 2. 初始化 IAM(自动使用已初始化的 reldb 和 cache 单例)
46
+ await iam.init({
47
+ session: { maxAge: 86400, sliding: true },
48
+ // OTP 回调(启用 OTP 登录时注入发送逻辑)
49
+ onOtpSendEmail: async (email, code) => {
50
+ await sendEmail(email, `验证码: ${code}`)
51
+ },
52
+ // 密码重置回调(启用密码重置时注入通知逻辑)
53
+ onPasswordResetRequest: async (user, token, expiresAt) => {
54
+ await sendEmail(user.email!, `重置链接: https://example.com/reset?token=${token}`)
55
+ },
56
+ })
57
+
58
+ // 2. 注册
59
+ const userResult = await iam.user.register({
60
+ username: 'admin',
61
+ email: 'admin@example.com',
62
+ password: 'Password123',
63
+ })
64
+
65
+ // 3. 登录
66
+ const loginResult = await iam.auth.login({
67
+ identifier: 'admin',
68
+ password: 'Password123',
69
+ })
70
+ if (loginResult.success) {
71
+ const { user, tokens } = loginResult.data
72
+ }
73
+
74
+ // 4. 验证令牌
75
+ const session = await iam.auth.verifyToken(loginResult.data.tokens.accessToken)
76
+
77
+ // 5. OTP 验证码登录
78
+ await iam.auth.sendOtp('user@example.com')
79
+ const otpResult = await iam.auth.loginWithOtp({
80
+ identifier: 'user@example.com',
81
+ code: '123456',
82
+ })
83
+
84
+ // 6. 检查权限
85
+ const hasPermission = await iam.authz.checkPermission(
86
+ loginResult.data.user.id,
87
+ 'user:read',
88
+ )
89
+
90
+ // 7. 关闭
91
+ await iam.close()
92
+ ```
93
+
94
+ `iam.config` は**機密情報をマスクした設定スナップショット**を返します。LDAP の `url` / `bindPassword` などは自動的に非表示になります。状態確認用で、元の設定の代わりにはなりません。
95
+
96
+ ## 設計
97
+
98
+ ### アーキテクチャ
99
+
100
+ IAM は `iam.init()` / `iam.close()` で状態を管理する**ライフサイクル付きシングルトン**です。内部の 5 つのサブモジュールを依存順に初期化します:
101
+
102
+ ```
103
+ iam.init()
104
+ ├─ session (キャッシュによるセッション、DB 依存なし)
105
+ ├─ authz (RBAC ロール・権限、DB + キャッシュ)
106
+ ├─ authn (認証、session + authz に依存)
107
+ ├─ user (ユーザー管理、session + authz + passwordStrategy に依存)
108
+ └─ seed (初期データ、authz に依存)
109
+ ```
110
+
111
+ 各サブモジュールは class ではなく**ファクトリー + クロージャー**(`createSessionOperations(deps)` など)で作成し、getter で `iam.auth` / `iam.user` / `iam.authz` / `iam.session` / `iam.apiKey` として公開します。
112
+
113
+ 初期化前のサブモジュール呼び出しは `NotInitializedKit` Proxy により `NOT_INITIALIZED` を返します。
114
+
115
+ ### データの保存先
116
+
117
+ | データ種別 | 保存先 | 説明 |
118
+ | ------------------------------------------------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------- |
119
+ | ユーザー、ロール、権限、関連付け、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` |
120
+ | セッション(accessToken → Session) | キャッシュ | TTL 付き、任意の自動延長 |
121
+ | Token 対応(refreshToken → userId + accessToken) | キャッシュ | 独立 key、更新後に旧 Token を即時無効化 |
122
+ | OTP コード | キャッシュ | TTL + 試行回数制限 |
123
+ | パスワードリセット Token | キャッシュ | TTL + 最大検証回数 |
124
+ | API Key | DB | `hai_iam_api_keys`。平文は保存せず hash のみ。接頭辞で候補検索 |
125
+
126
+ ---
127
+
128
+ ### 認証(authn)— `iam.auth`
129
+
130
+ #### 主要な型
131
+
132
+ - **`AuthnOperations`**:認証操作。`login` / `loginWithOtp` / `loginWithLdap` / `loginWithApiKey` / `logout` / `verifyToken` / `sendOtp` / `registerAndLogin`。
133
+ - **`AuthStrategy`**:認証方式のインターフェース(`type` + `authenticate` + `challenge?`)。パスワード・OTP・LDAP・API Key の 4 実装。
134
+ - **`AuthResult`**:ユーザー情報、`TokenPair`(accessToken + refreshToken)、ロール・権限を含むログイン結果。
135
+ - **`Credentials`**:`{ type: 'password' | 'otp' | 'ldap' | 'apikey' }` と各方式の認証情報を組み合わせた共通の共用体型。
136
+
137
+ #### 主要な処理
138
+
139
+ **パスワードによるログイン:**
140
+
141
+ ```
142
+ login(credentials)
143
+ → パスワードログインの有効化を確認
144
+ → identifier でユーザー検索(username / email / phone)
145
+ → 未存在なら dummy hash(固定時間でタイミング攻撃を防止)
146
+ → アカウントロックを確認(lockoutDuration 内で loginFailedCount >= maxLoginAttempts)
147
+ → パスワードハッシュ検証(@h-ai/crypto)
148
+ → 失敗:回数を記録し、上限でロック
149
+ → 成功:失敗回数をリセット
150
+ → 期限を確認(expirationDays > 0 なら passwordUpdatedAt)
151
+ → ロール + 権限を検索
152
+ → セッション作成(session.create → TokenPair)
153
+ → AuthResult { user, tokens, roles, permissions, agreements? } を返す
154
+ ```
155
+
156
+ **Token の検証:**
157
+
158
+ ```
159
+ verifyToken(accessToken)
160
+ → session.get(accessToken) → キャッシュ検索
161
+ → 自動延長モードなら TTL を延長
162
+ → Session を返す(userId、roles、permissions)
163
+ ```
164
+
165
+ #### 使用方法
166
+
167
+ `authz.createRole({ ..., permissionIds })` と `authz.updateRole(id, { ..., permissionIds })` は、同じトランザクションでロールと権限集合全体を保存し、途中の失敗はすべてロールバックします。`permissionIds: []` は消去、省略は維持、重複 ID は除去、不明な権限は失敗です。並行更新は最後にコミットした完全な集合を採用し、集合を合成しません。自動管理のコミット後はユーザーセッションを同期し、外部トランザクションでは呼び出し元がコミット後に同期します。Admin Console も直接この API を使い、分割更新や補償削除は行いません。
168
+
169
+ ```ts
170
+ // 密码登录
171
+ const result = await iam.auth.login({ identifier: 'admin', password: 'Password123' })
172
+
173
+ // OTP 登录
174
+ await iam.auth.sendOtp('user@example.com')
175
+ const result = await iam.auth.loginWithOtp({ identifier: 'user@example.com', code: '123456' })
176
+
177
+ // LDAP 登录(需在 init 时提供 ldapClientFactory)
178
+ const result = await iam.auth.loginWithLdap({ username: 'jdoe', password: 'pass' })
179
+
180
+ // API Key 登录(需启用 login.apikey: true)
181
+ const result = await iam.auth.loginWithApiKey({ key: 'hai_xxxx...' })
182
+
183
+ // 验证令牌
184
+ const session = await iam.auth.verifyToken(accessToken)
185
+
186
+ // 注册并登录(一站式)
187
+ const result = await iam.auth.registerAndLogin({ username: 'new', password: 'Pass123' })
188
+ ```
189
+
190
+ #### 注意点
191
+
192
+ - **タイミング攻撃対策**:未存在ユーザーにも dummy password hash を実行し、応答時間をそろえます。
193
+ - **アカウントロック**:連続 `maxLoginAttempts` 回の失敗で `lockoutDuration` 秒ロック。
194
+ - **方式ごとの有効化**:`login.password` / `login.otp` / `login.ldap` / `login.apikey` で制御。
195
+ - **OTP の安全性**:rejection sampling で剰余偏りを除去、定時間比較でタイミング攻撃を防ぎ、`resendInterval` で頻度制限。
196
+
197
+ ---
198
+
199
+ ### セッション(session)— `iam.session`
200
+
201
+ #### 主要な型
202
+
203
+ - **`SessionOperations`**:`create` / `get` / `verifyToken` / `update` / `delete` / `deleteByUserId` / `refresh` / `revokeRefresh` / `patchUserSessions`。
204
+ - **`Session`**:userId、roles、permissions、accessToken、期限などを含むセッション。
205
+ - **`TokenPair`**:Token のペア。`{ accessToken, refreshToken, expiresIn, tokenType: 'Bearer' }`。
206
+
207
+ #### 主要な処理
208
+
209
+ **Token 更新(Rotation):**
210
+
211
+ ```
212
+ refresh(refreshToken)
213
+ → キャッシュから refreshToken → { userId, accessToken } を取得
214
+ → accessToken で旧 session を取得
215
+ → 旧 refreshToken を削除(即時無効化で再使用防止)
216
+ → 旧 accessToken の session を削除
217
+ → 旧ユーザー context で新 session を作成
218
+ → 新 TokenPair を返す
219
+ ```
220
+
221
+ **単一端末のログイン:**
222
+
223
+ ```
224
+ create(options) [singleDevice=true]
225
+ → user→tokens の対応から既存の全 Token を削除
226
+ → 新 accessToken + refreshToken を生成
227
+ → session をキャッシュへ保存
228
+ ```
229
+
230
+ #### 使用方法
231
+
232
+ ```ts
233
+ // 创建会话(通常由 login 内部调用)
234
+ const session = await iam.session.create({
235
+ userId: 'user-id',
236
+ username: 'admin',
237
+ roles: ['admin'],
238
+ permissions: ['user:read', 'user:write'],
239
+ })
240
+
241
+ // 验证令牌
242
+ const session = await iam.session.verifyToken(accessToken)
243
+
244
+ // 刷新令牌
245
+ const newTokens = await iam.session.refresh(refreshToken)
246
+
247
+ // 强制下线
248
+ await iam.session.deleteByUserId(userId)
249
+ ```
250
+
251
+ #### 注意点
252
+
253
+ - **キャッシュのみの保存**:DB に保存せず、再起動後はすべてのセッションが無効になります(安全性の設計方針)。
254
+ - **自動延長**:`sliding: true` なら各 `get()` で TTL を延長。
255
+ - **Refresh Rotation**:旧 refreshToken は一度の使用で無効化。再使用は漏洩の兆候。
256
+ - **Token 強度**:256-bit の暗号学的乱数(`crypto.getRandomValues(new Uint8Array(32))`)、base64url 形式。
257
+
258
+ ---
259
+
260
+ ### 認可(authz)— `iam.authz`
261
+
262
+ #### 主要な型
263
+
264
+ - **`AuthzOperations`**:ロール・権限 CRUD、ロールと権限・ユーザーとロールの割り当て、権限確認。
265
+ - **`Role`**:ロール。`{ id, code, name, description, isSystem }`。
266
+ - **`Permission`**:権限。`{ id, code, name, type, resource, action }`、type は `menu` / `api` / `button`。
267
+ - **`PermissionType`**:権限種別の enum。
268
+
269
+ #### 主要な処理
270
+
271
+ **権限確認(`checkPermission`):**
272
+
273
+ ```
274
+ checkPermission(userId, permission)
275
+ → RBAC 無効なら true
276
+ → ユーザーのロール一覧を DB から取得
277
+ → 最高管理者を確認(キャッシュの superAdminRoleId)、あれば true
278
+ → 各ロールの権限を取得(キャッシュ優先、miss は DB から取得して保存)
279
+ → 厳密・ワイルドカード照合(`admin:*` は `admin:read` に一致)
280
+ → true / false を返す
281
+ ```
282
+
283
+ **ロール・権限変更後のセッション同期:**
284
+
285
+ ```
286
+ assignPermissionToRole(roleId, permId)
287
+ → 関連を DB に保存
288
+ → ロール権限のキャッシュを消去
289
+ → 所属ユーザー検索 → 各権限を再解決 → 活動中 session を更新
290
+ (best-effort:失敗は log.error のみで割り当て結果に影響しない)
291
+ ```
292
+
293
+ #### 使用方法
294
+
295
+ ```ts
296
+ // 角色管理
297
+ const role = await iam.authz.createRole({ code: 'editor', name: '编辑' })
298
+ const roles = await iam.authz.getAllRoles({ page: 1, pageSize: 20 })
299
+
300
+ // 权限管理
301
+ const perm = await iam.authz.createPermission({
302
+ code: 'article:publish',
303
+ name: '发布文章',
304
+ type: 'api',
305
+ resource: 'article',
306
+ action: 'publish',
307
+ })
308
+
309
+ // 分配与检查
310
+ await iam.authz.assignRole(userId, role.data.id)
311
+ await iam.authz.assignPermissionToRole(role.data.id, perm.data.id)
312
+ const allowed = await iam.authz.checkPermission(userId, 'article:publish')
313
+
314
+ // 批量同步角色
315
+ await iam.authz.syncRoles(userId, [roleId1, roleId2])
316
+ ```
317
+
318
+ #### 注意点
319
+
320
+ - **ワイルドカード**:`admin:*` は `admin:read`、`admin:write:detail` などに一致。1 つの `*` がコロン以降全体に一致します。
321
+ - **最高管理者**:`rbac.superAdminRole`(既定 `super_admin`)はすべての権限を持ちます。
322
+ - **トランザクション**:`deleteRole` / `deletePermission` は DB トランザクションで関連を削除してから本体を削除。
323
+ - **同期の遅延**:権限変更は稼働中のセッションへ best-effort で反映し、多数のユーザーでは遅延する場合があります。
324
+ - **初期データ**:`seedDefaultData: true` で admin/user/guest と基本権限を冪等に作成。
325
+
326
+ ---
327
+
328
+ ### ユーザー管理(user)— `iam.user`
329
+
330
+ #### 主要な型
331
+
332
+ - **`UserOperations`**:登録、検索、更新、削除、パスワード変更・リセット、強度検証。
333
+ - **`User`**(公開フィールド):`{ id, username, email, phone, displayName, avatarUrl, enabled, roles? }`。
334
+ - **`StoredUser`**:内部保存用ユーザー。`passwordHash`、`loginFailedCount`、`lockedUntil` などの機密フィールドを含みます。
335
+ - **`RegisterOptions`** / **`RegisterResult`**:登録の入力・出力。
336
+ - **`UpdateCurrentUserInput`**:現在のユーザーが変更できるフィールド(`displayName` / `avatarUrl` / `phone` / `metadata`)。
337
+
338
+ #### 主要な処理
339
+
340
+ **ユーザー登録:**
341
+
342
+ ```
343
+ register(options)
344
+ → 登録の有効化を確認
345
+ → 強度検証(Zod schema + 独自ルール)
346
+ → パスワードハッシュ(@h-ai/crypto)
347
+ → DB トランザクション:作成 + 結果取得
348
+ → 既定ロール付与(rbac.defaultRole、トランザクション外、失敗は log のみ)
349
+ → RegisterResult { user, agreements? } を返す
350
+ ```
351
+
352
+ **パスワードリセット:**
353
+
354
+ ```
355
+ requestPasswordReset(email)
356
+ → メールで検索(未存在は静かに ok、列挙攻撃を防止)
357
+ → 256-bit ランダム Token を生成
358
+ → SHA-256(token) → userId をキャッシュ(TTL + maxAttempts)
359
+ → onPasswordResetRequest を呼ぶ(アプリがメール・SMS 送信)
360
+
361
+ confirmPasswordReset(token, newPassword)
362
+ → SHA-256(token) を取得して期限・試行回数を検証
363
+ → 強度検証 → ハッシュ → DB 更新
364
+ → 全活動セッションを削除して再ログインを要求
365
+ → 使用済みリセット Token を削除
366
+ ```
367
+
368
+ #### 使用方法
369
+
370
+ ```ts
371
+ // 注册
372
+ const result = await iam.user.register({
373
+ username: 'alice',
374
+ email: 'alice@example.com',
375
+ password: 'Password123',
376
+ })
377
+
378
+ // 当前用户操作
379
+ const me = await iam.user.getCurrentUser(accessToken)
380
+ await iam.user.updateCurrentUser(accessToken, { displayName: '新名称' })
381
+ await iam.user.changeCurrentUserPassword(accessToken, 'oldPass', 'newPass')
382
+
383
+ // 管理操作
384
+ const users = await iam.user.listUsers({
385
+ page: 1,
386
+ pageSize: 20,
387
+ search: 'alice',
388
+ sortBy: 'createdAt',
389
+ sortDirection: 'desc',
390
+ include: ['roles'],
391
+ })
392
+ await iam.user.updateUser(userId, { enabled: false }) // 禁用用户 → 自动清除会话
393
+ await iam.user.deleteUser(userId) // 删除用户 → 事务清除角色关联
394
+
395
+ // 密码重置
396
+ await iam.user.requestPasswordReset('alice@example.com')
397
+ await iam.user.confirmPasswordReset(token, 'NewPassword456')
398
+ await iam.user.adminResetPassword(userId, 'TempPassword123')
399
+
400
+ // 密码强度验证(同步方法)
401
+ const valid = iam.user.validatePassword('weak')
402
+ ```
403
+
404
+ #### 注意点
405
+
406
+ - **`toUser()` の機密除去**:公開ユーザー情報から `passwordHash`、`loginFailedCount` などを除外。
407
+ - **セッション連動**:`deleteUser` / `updateUser({ enabled: false })` / `changePassword` / `confirmPasswordReset` は対象ユーザーの全セッションを削除。
408
+ - **列挙攻撃対策**:未登録メールへの `requestPasswordReset` はエラーでなく `ok(undefined)`。
409
+ - **validatePassword は同期**:`UserOperations` の唯一の同期メソッドで、ほかは非同期。
410
+ - **部分一致検索**:`listUsers` はユーザー名・メール・電話・表示名を検索し、LIKE のワイルドカードはエスケープ済み。
411
+ - **サーバー側の並び順**:`listUsers` は `username`、`email`、`displayName`、`enabled`、`createdAt` のみを許可リストで対応付け、既定は作成日時の降順。
412
+
413
+ ---
414
+
415
+ ### API Key — `iam.apiKey`
416
+
417
+ #### 主要な型
418
+
419
+ - **`ApiKeyOperations`**:`createApiKey` / `listApiKeys` / `getApiKey` / `revokeApiKey` / `verifyApiKey`。
420
+ - **`ApiKey`**(公開フィールド):`{ id, userId, name, keyPrefix, enabled, expiresAt, scopes }`。
421
+ - **`CreateApiKeyResult`**:`{ apiKey, rawKey }`。平文の鍵を返すのは一度のみ。
422
+
423
+ #### 主要な処理
424
+
425
+ **API Key の検証:**
426
+
427
+ ```
428
+ verifyApiKey(rawKey)
429
+ → rawKey から接頭辞取得(prefix + 最初の 8 文字)
430
+ → DB で接頭辞による候補検索
431
+ → 各 hash を同期検証
432
+ → 成功後に期限・無効化を確認
433
+ → lastUsedAt を非同期更新(応答をブロックしない)
434
+ → ApiKey を返す
435
+ ```
436
+
437
+ #### 使用方法
438
+
439
+ ```ts
440
+ // 创建 API Key
441
+ const result = await iam.apiKey.createApiKey(userId, {
442
+ name: 'CI/CD',
443
+ expirationDays: 90,
444
+ scopes: ['api:read'],
445
+ })
446
+ // result.data.rawKey → 明文密钥(仅此一次展示)
447
+
448
+ // 列出用户的 API Key(服务端分页)
449
+ const keys = await iam.apiKey.listApiKeys(userId, { page: 1, pageSize: 20, search: 'ci' })
450
+ // keys.data → { items, total, page, pageSize }
451
+
452
+ // 吊销
453
+ await iam.apiKey.revokeApiKey(keyId)
454
+ ```
455
+
456
+ #### 注意点
457
+
458
+ - **平文は保存しない**:DB は hash のみを保存し、作成時の `rawKey` が唯一の取得機会。
459
+ - **接頭辞検索**:`keyPrefix` で候補を絞り、全テーブル走査を避けます。
460
+ - **件数制限**:ユーザーあたり最大 `maxKeysPerUser`(既定 10)件。
461
+ - **明示的有効化**:`login.apikey: true` の場合のみ初期化。
462
+
463
+ ---
464
+
465
+ ## その他の使い方
466
+
467
+ API 引数・エラーコード・統合パターンの詳細は中国語の Skill テンプレート(`packages/cli/templates/skills/hai-iam/SKILL.md`)を参照してください。
468
+
469
+ ## フロントエンドの API 呼び出し
470
+
471
+ 公開 HTTP API は `@h-ai/api-contract` で定義し、`@h-ai/api-client` の型付きクライアントで呼び出します:
472
+
473
+ ```ts
474
+ import { apiClient } from '@h-ai/api-client'
475
+
476
+ await apiClient.init({ baseUrl: '/api/v1' })
477
+
478
+ // 登录
479
+ const result = await apiClient.iam.auth.login({
480
+ identifier: 'admin',
481
+ password: 'Password123',
482
+ })
483
+
484
+ // 获取当前用户
485
+ const user = await apiClient.iam.auth.currentUser()
486
+
487
+ // 修改密码
488
+ await apiClient.iam.auth.changePassword({
489
+ oldPassword: 'Password123',
490
+ newPassword: 'NewPassword456',
491
+ })
492
+ ```
493
+
494
+ ## パスワードリセット
495
+
496
+ ```ts
497
+ // 请求重置(即使用户不存在也返回 ok,防止枚举)
498
+ await iam.user.requestPasswordReset('admin@example.com')
499
+
500
+ // 确认重置(校验令牌有效期和尝试次数 → 更新密码 → 清除所有会话)
501
+ const result = await iam.user.confirmPasswordReset(token, 'NewPassword456')
502
+
503
+ // 管理员重置(无需旧密码)
504
+ await iam.user.adminResetPassword(userId, 'TempPassword123')
505
+ ```
506
+
507
+ ## 一度限りのチケット — `iam.ticket`
508
+
509
+ WebSocket URL など Bearer Token を付けられない経路向けの短期チケットです。認証済み HTTP リクエストで、主体・用途・操作に結び付けて発行します。サーバーは初回検証時に**アトミックに消費**し、一度のみ有効です。
510
+
511
+ ```ts
512
+ // 签发(已登录 HTTP 请求内):绑定主体、用途与本次操作,30 秒过期
513
+ const issued = await iam.ticket.issue({
514
+ subjectId: session.userId,
515
+ purpose: 'ai-audio',
516
+ grant: { operation: 'transcribe', model: 'whisper-1' },
517
+ ttlMs: 30_000,
518
+ })
519
+ // issued.data.ticket 交给客户端,用于建立 WebSocket 连接
520
+
521
+ // 消费(服务端建连时):原子单次,返回主体 + 用途 + grant
522
+ const consumed = await iam.ticket.consume(ticket, { purpose: 'ai-audio' })
523
+ if (consumed.success) {
524
+ const { subjectId, grant } = consumed.data
525
+ // 用 grant.operation / grant.model 交叉校验客户端请求
526
+ }
527
+ ```
528
+
529
+ 特徴:暗号学的に安全な乱数、TTL、アトミックな単回消費(並行消費では 1 件のみ成功)、用途の固定による別用途の再使用防止。
530
+
531
+ ## エラー処理
532
+
533
+ すべての操作は `HaiResult<T>` を返し、`HaiIamError` で分岐します:
534
+
535
+ ```ts
536
+ import { HaiIamError, iam } from '@h-ai/iam'
537
+
538
+ const result = await iam.auth.login({ identifier: 'admin', password: 'wrong' })
539
+ if (!result.success) {
540
+ if (result.error.code === HaiIamError.INVALID_CREDENTIALS.code) {
541
+ // 用户名或密码错误
542
+ }
543
+ }
544
+ ```
545
+
546
+ ## テスト
547
+
548
+ ```bash
549
+ pnpm test
550
+ ```
551
+
552
+ ## ライセンス
553
+
554
+ Apache-2.0
555
+
556
+ ロール一覧は `iam.authz.getRoleUserCounts(roleIds)` の 1 回の集計で現在のページの実際の所属人数を取得し、`HaiResult<Map<string, number>>` を返します。関連がなければ 0、検索失敗は読み込みエラーを表示し、0 件の統計を作りません。
package/README.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # @h-ai/iam
2
2
 
3
+ [简体中文](./README.md) | [English](./README.en.md) | [日本語](./README.ja.md)
4
+
5
+ [框架首页](../../README.md) · 相关模块:[@h-ai/audit](../audit/README.md) · [@h-ai/cache](../cache/README.md) · [@h-ai/core](../core/README.md) · [@h-ai/crypto](../crypto/README.md) · [@h-ai/reldb](../reldb/README.md)
6
+
3
7
  身份与访问管理模块,提供统一的 `iam` 对象实现认证、会话、授权与用户管理。
4
8
 
5
9
  ## 功能特性
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@h-ai/iam",
3
3
  "type": "module",
4
- "version": "0.1.0-alpha.55",
4
+ "version": "0.1.0-alpha.57",
5
5
  "description": "Hai Framework identity and access management module.",
6
6
  "license": "Apache-2.0",
7
7
  "exports": {
@@ -26,11 +26,11 @@
26
26
  },
27
27
  "dependencies": {
28
28
  "zod": "^4.4.3",
29
- "@h-ai/audit": "0.1.0-alpha.55",
30
- "@h-ai/cache": "0.1.0-alpha.55",
31
- "@h-ai/core": "0.1.0-alpha.55",
32
- "@h-ai/reldb": "0.1.0-alpha.55",
33
- "@h-ai/crypto": "0.1.0-alpha.55"
29
+ "@h-ai/audit": "0.1.0-alpha.57",
30
+ "@h-ai/cache": "0.1.0-alpha.57",
31
+ "@h-ai/core": "0.1.0-alpha.57",
32
+ "@h-ai/reldb": "0.1.0-alpha.57",
33
+ "@h-ai/crypto": "0.1.0-alpha.57"
34
34
  },
35
35
  "devDependencies": {
36
36
  "@types/node": "^25.9.1",