@h-ai/iam 0.1.0-alpha.61 → 0.1.0-alpha.62

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.ja.md CHANGED
@@ -12,7 +12,7 @@
12
12
  | ------------------------------ | ----------------------------------------------------------------------------------------- |
13
13
  | **認証** | パスワード、OTP(メール・SMS コード)、LDAP、API Key の認証方式 |
14
14
  | **セッション** | 状態を持つセッション(ランダム access token + キャッシュ、任意の自動延長) |
15
- | **認可** | RBAC のロール・権限管理(DB + キャッシュ、ワイルドカード照合) |
15
+ | **認可** | RBAC のロール・権限管理(DB 一括検索、ワイルドカード照合) |
16
16
  | **ユーザー管理** | 登録、検索、更新、パスワードリセット、管理者によるリセット |
17
17
  | **API Key** | scope・期限付き API Key の作成、失効、検証 |
18
18
  | **一度限りのチケット** | Bearer Token を付けられない WebSocket などのための短期チケット(発行 / アトミックな消費) |
@@ -27,7 +27,7 @@ pnpm add @h-ai/iam
27
27
  ## 依存関係
28
28
 
29
29
  - `@h-ai/reldb`:ユーザー・ロール・権限の永続化。**iam.init() より先に初期化**。
30
- - `@h-ai/cache`:セッション・OTP・リセット Token・権限のキャッシュ。**iam.init() より先に初期化**。
30
+ - `@h-ai/cache`:セッション・OTP・リセット Token・一度限りのチケット・共有ロック。**iam.init() より先に初期化**。
31
31
  - `@h-ai/crypto`:内部で使用するパスワードハッシュ。自動初期化。
32
32
  - `@h-ai/audit`:主要な RBAC 操作の監査記録。
33
33
 
@@ -218,7 +218,7 @@ IAM は `iam.init()` / `iam.close()` で状態を管理する**ライフサイ
218
218
  ```
219
219
  iam.init()
220
220
  ├─ session (キャッシュによるセッション、DB 依存なし)
221
- ├─ authz (RBAC ロール・権限、DB + キャッシュ)
221
+ ├─ authz (RBAC ロール・権限、DB 一括検索)
222
222
  ├─ authn (認証、session + authz に依存)
223
223
  ├─ user (ユーザー管理、session + authz + passwordStrategy に依存)
224
224
  └─ seed (初期データ、authz に依存)
@@ -275,32 +275,41 @@ login(credentials)
275
275
  verifyToken(accessToken)
276
276
  → session.get(accessToken) → キャッシュ検索
277
277
  → 自動延長モードなら TTL を延長
278
- → Session を返す(userId、roles、permissions)
278
+ → 現在のユーザー状態・ロール JOIN・権限一括検索(ロールなしなら権限検索を省略)
279
+ → 無効ユーザー・DB 読取失敗は拒否し、API Key scope を再適用
280
+ → 現在の Session を返す(userId、roles、permissions)
279
281
  ```
280
282
 
281
283
  #### 使用方法
282
284
 
283
- `authz.createRole({ ..., permissionIds })` と `authz.updateRole(id, { ..., permissionIds })` は、同じトランザクションでロールと権限集合全体を保存し、途中の失敗はすべてロールバックします。`permissionIds: []` は消去、省略は維持、重複 ID は除去、不明な権限は失敗です。並行更新は最後にコミットした完全な集合を採用し、集合を合成しません。自動管理のコミット後はユーザーセッションを同期し、外部トランザクションでは呼び出し元がコミット後に同期します。Admin Console も直接この API を使い、分割更新や補償削除は行いません。
285
+ 例は reldb/cache/IAM の初期化に成功したサーバーで実行し、検証済みの入力を使います。パスワード認証は既定で有効です。OTP には `login.otp: true`、`otp` チャネル、送信コールバック/Reach テンプレートが必要です。LDAP は `login.ldap: true`、API Key は `login.apikey: true` を有効にします。ログインと登録後ログインは `AuthResult` を返します。登録とパスワード認証のスイッチは独立しています。
284
286
 
285
- ```ts
286
- // 密码登录
287
- const result = await iam.auth.login({ identifier: 'admin', password: 'Password123' })
287
+ OTP は送信と受信したコードによるログインの二段階です。両方とも HaiResult を返します。LDAP は `iam.auth.loginWithLdap({ username, password })`、API Key は `loginWithApiKey({ key })` を使います。`data` の前に `success` を確認してください。
288
288
 
289
- // OTP 登录
290
- await iam.auth.sendOtp('user@example.com')
291
- const result = await iam.auth.loginWithOtp({ identifier: 'user@example.com', code: '123456' })
289
+ ```ts
290
+ import { iam } from '@h-ai/iam'
292
291
 
293
- // LDAP 登录(内置客户端,可选注入 ldapClientFactory)
294
- const result = await iam.auth.loginWithLdap({ username: 'jdoe', password: 'pass' })
292
+ async function passwordLogin(identifier: string, password: string) {
293
+ const result = await iam.auth.login({ identifier, password })
294
+ if (!result.success)
295
+ return result
296
+ // 成功后返回 user、tokens、roles、permissions;令牌只交给安全存储。
297
+ return result
298
+ }
295
299
 
296
- // API Key 登录(需启用 login.apikey: true)
297
- const result = await iam.auth.loginWithApiKey({ key: 'hai_xxxx...' })
300
+ async function requestLoginCode(identifier: string) {
301
+ // identifier 为启用通道对应的邮箱/手机号,返回 expiresAt。
302
+ return iam.auth.sendOtp(identifier)
303
+ }
298
304
 
299
- // 验证令牌
300
- const session = await iam.auth.verifyToken(accessToken)
305
+ async function otpLogin(identifier: string, code: string) {
306
+ // 用户收到验证码并输入后调用;发送失败时不能伪报成功。
307
+ return iam.auth.loginWithOtp({ identifier, code })
308
+ }
301
309
 
302
- // 注册并登录(一站式)
303
- const result = await iam.auth.registerAndLogin({ username: 'new', password: 'Pass123' })
310
+ async function registerWithSession(username: string, password: string) {
311
+ return iam.auth.registerAndLogin({ username, password })
312
+ }
304
313
  ```
305
314
 
306
315
  #### 注意点
@@ -345,23 +354,37 @@ create(options) [singleDevice=true]
345
354
 
346
355
  #### 使用方法
347
356
 
357
+ `tokens.accessToken` でリクエストを認証し、`tokens.refreshToken` で新しいペアを取得します。`expiresIn` は秒です。独立 API は `Authorization: Bearer <accessToken>`、SvelteKit の Cookie 入口は後述の kit 標準連携を使います。
358
+
359
+ 保護されたリクエストは `iam.auth.verifyToken` で現在のユーザー状態・ロール・権限を読みます。`iam.session.get/verifyToken` は低水準のスナップショットで、現在の認可を代替しません。`session.create` は信頼できるサーバー連携専用で、クライアント指定のロール/権限/Key バインドを受け入れません。
360
+
348
361
  ```ts
349
- // 创建会话(通常由 login 内部调用)
350
- const session = await iam.session.create({
351
- userId: 'user-id',
352
- username: 'admin',
353
- roles: ['admin'],
354
- permissions: ['user:read', 'user:write'],
355
- })
362
+ import { iam } from '@h-ai/iam'
356
363
 
357
- // 验证令牌
358
- const session = await iam.session.verifyToken(accessToken)
364
+ async function authenticateRequest(accessToken: string) {
365
+ // 受保护请求读取当前用户状态及角色/权限,失败时停止业务操作。
366
+ const verified = await iam.auth.verifyToken(accessToken)
367
+ if (!verified.success)
368
+ return verified
369
+ return verified
370
+ }
371
+
372
+ async function rotateTokens(refreshToken: string) {
373
+ const refreshed = await iam.session.refresh(refreshToken)
374
+ if (!refreshed.success)
375
+ return refreshed
376
+ // 安全存储替换两个令牌;下一次请求仍走 auth.verifyToken。
377
+ return refreshed
378
+ }
359
379
 
360
- // 刷新令牌
361
- const newTokens = await iam.session.refresh(refreshToken)
380
+ async function signOut(accessToken: string) {
381
+ return iam.auth.logout(accessToken)
382
+ }
362
383
 
363
- // 强制下线
364
- await iam.session.deleteByUserId(userId)
384
+ async function forceSignOut(userId: string) {
385
+ // 调用前已验证管理员权限,返回撤销的会话数。
386
+ return iam.session.deleteByUserId(userId)
387
+ }
365
388
  ```
366
389
 
367
390
  #### 注意点
@@ -393,7 +416,7 @@ checkPermission(userId, permission)
393
416
  → RBAC 無効なら true
394
417
  → ユーザーのロール一覧を DB から取得
395
418
  → 最高管理者を確認(キャッシュの superAdminRoleId)、あれば true
396
- → 各ロールの権限を取得(キャッシュ優先、miss は DB から取得して保存)
419
+ → 各ロールの権限コードを DB で一括取得。読み取り失敗はエラーを返す
397
420
  → 厳密・ワイルドカード照合(`admin:*` は `admin:read` に一致)
398
421
  → true / false を返す
399
422
  ```
@@ -403,34 +426,56 @@ checkPermission(userId, permission)
403
426
  ```
404
427
  assignPermissionToRole(roleId, permId)
405
428
  → 関連を DB に保存
406
- → ロール権限のキャッシュを消去
407
429
  → 所属ユーザー検索 → 各権限を再解決 → 活動中 session を更新
408
430
  (best-effort:失敗は log.error のみで割り当て結果に影響しない)
409
431
  ```
410
432
 
411
433
  #### 使用方法
412
434
 
435
+ 権限を作成し、その **ID** を `permissionIds` に保存してロールを作成し、ロール **ID** でユーザーに割り当てます。セッションとガードは安定した **code** を使います。表示名は独立して変更できます。例は RBAC が有効であることを前提とし、管理操作の権限とリソース範囲はアプリで確認します。
436
+
437
+ `createRole/updateRole` はロール情報と完全な権限集合を同一トランザクションに保存します。省略は維持、空配列は解除、重複は除去、不明な ID は失敗です。同時更新は最後にコミットした完全な集合を採用します。自管理トランザクションはコミット後にセッションを同期し、外部トランザクションは呼び出し側がコミット後に同期します。
438
+
439
+ `checkPermission(userId, code)` と `getUserAuthorization` はユーザーの RBAC を読み、リクエストの API Key scopes を考慮しません。Key リクエストでは `iam.auth.verifyToken` の収束済み権限を使用し、userId の再照会だけで許可しないでください。serv の `.permission('record:read')` は厳密な権限ガードです。メニュー/ボタンの非表示は API 認可の代替ではありません。
440
+
413
441
  ```ts
414
- // 角色管理
415
- const role = await iam.authz.createRole({ code: 'editor', name: '编辑' })
416
- const roles = await iam.authz.getAllRoles({ page: 1, pageSize: 20 })
417
-
418
- // 权限管理
419
- const perm = await iam.authz.createPermission({
420
- code: 'article:publish',
421
- name: '发布文章',
422
- type: 'api',
423
- resource: 'article',
424
- action: 'publish',
425
- })
442
+ import { iam } from '@h-ai/iam'
443
+
444
+ async function provisionReader(userId: string) {
445
+ // 示例初始化权限/角色;生产种子应先按 code 查询,保证幂等。
446
+ const permission = await iam.authz.createPermission({
447
+ code: 'record:read',
448
+ name: '读取记录',
449
+ type: 'api',
450
+ resource: 'record',
451
+ action: 'read',
452
+ })
453
+ if (!permission.success)
454
+ return permission
455
+ const role = await iam.authz.createRole({
456
+ code: 'record_reader',
457
+ name: '记录读取者',
458
+ permissionIds: [permission.data.id],
459
+ })
460
+ if (!role.success)
461
+ return role
462
+ return iam.authz.assignRole(userId, role.data.id)
463
+ }
464
+
465
+ async function replaceRolePermissions(roleId: string, permissionIds: string[]) {
466
+ // 完整替换:[] 清空;省略 permissionIds 则保持已有权限。
467
+ return iam.authz.updateRole(roleId, { permissionIds })
468
+ }
426
469
 
427
- // 分配与检查
428
- await iam.authz.assignRole(userId, role.data.id)
429
- await iam.authz.assignPermissionToRole(role.data.id, perm.data.id)
430
- const allowed = await iam.authz.checkPermission(userId, 'article:publish')
470
+ async function replaceUserRoles(userId: string, roleIds: string[]) {
471
+ // 完整替换用户的角色集合;[] 移除全部角色。
472
+ return iam.authz.syncRoles(userId, roleIds)
473
+ }
431
474
 
432
- // 批量同步角色
433
- await iam.authz.syncRoles(userId, [roleId1, roleId2])
475
+ async function currentAuthorization(userId: string) {
476
+ // 返回 roles/permissions 的 code 数组,不接受 ID 或显示名称代替 code。
477
+ return iam.authz.getUserAuthorization(userId)
478
+ }
434
479
  ```
435
480
 
436
481
  #### 注意点
@@ -438,7 +483,7 @@ await iam.authz.syncRoles(userId, [roleId1, roleId2])
438
483
  - **ワイルドカード**:`admin:*` は `admin:read`、`admin:write:detail` などに一致。1 つの `*` がコロン以降全体に一致します。
439
484
  - **最高管理者**:`rbac.superAdminRole`(既定 `super_admin`)はすべての権限を持ちます。
440
485
  - **トランザクション**:`deleteRole` / `deletePermission` は DB トランザクションで関連を削除してから本体を削除。
441
- - **同期の遅延**:権限変更は稼働中のセッションへ best-effort で反映し、多数のユーザーでは遅延する場合があります。
486
+ - **セッション同期**:キャッシュ更新は best-effort です。保護されたリクエストは `iam.auth.verifyToken` で現在の状態・権限を読み、読取失敗を拒否します。`iam.session.verifyToken` はスナップショット検証のみです。`iam.authz.getUserAuthorization(userId)` はロール JOIN と権限一括検索を共有します。
442
487
  - **初期データ**:`seedDefaultData: true` で admin/user/guest と基本権限を冪等に作成。
443
488
 
444
489
  ---
@@ -451,7 +496,7 @@ await iam.authz.syncRoles(userId, [roleId1, roleId2])
451
496
  - **`User`**(公開フィールド):`{ id, username, email, phone, displayName, avatarUrl, enabled, roles? }`。
452
497
  - **`StoredUser`**:内部保存用ユーザー。`passwordHash`、`loginFailedCount`、`lockedUntil` などの機密フィールドを含みます。
453
498
  - **`RegisterOptions`** / **`RegisterResult`**:登録の入力・出力。
454
- - **`UpdateCurrentUserInput`**:現在のユーザーが変更できるフィールド(`displayName` / `avatarUrl` / `phone` / `metadata`)。
499
+ - **`UpdateCurrentUserInput`**:現在のユーザーが変更できるフィールド(`username` / `email` / `displayName` / `avatarUrl` / `phone` / `metadata`)。
455
500
 
456
501
  #### 主要な処理
457
502
 
@@ -485,38 +530,60 @@ confirmPasswordReset(token, newPassword)
485
530
 
486
531
  #### 使用方法
487
532
 
533
+ `register` はユーザーだけを返し、セッションは作りません。同時ログインには `auth.registerAndLogin` を使います。管理用の `createUser` は自己登録設定と独立しています。自己プロフィールは `getCurrentUser/updateCurrentUser` で username/email/displayName/avatarUrl/phone/metadata を更新します。安全性に関わるフィールドとロールは認可された管理入口を使います。
534
+
535
+ 一覧は `PaginatedResult<User>` を返し、`roleCode` はページング前に絞り込みます。必要時のみ `include: ['roles']` を指定します。一括取得は入力を重複排除した順序で返し、不明 ID は省略するため、配列位置ではなく id で関連付けます。公開 DTO でもデータ範囲と開示フィールドはアプリで制御します。
536
+
537
+ `getUser(id)` は検索成功・不存在時に `null`、`getCurrentUser(token)` は無効セッション/不存在時に失敗します。DB エラーを空データ扱いにしません。`validatePassword` は同期 HaiResult です。削除、無効化、パスワード変更/リセットは対象セッションを取り消します。
538
+
488
539
  ```ts
489
- // 注册
490
- const result = await iam.user.register({
491
- username: 'alice',
492
- email: 'alice@example.com',
493
- password: 'Password123',
494
- })
540
+ import { iam } from '@h-ai/iam'
495
541
 
496
- // 当前用户操作
497
- const me = await iam.user.getCurrentUser(accessToken)
498
- await iam.user.updateCurrentUser(accessToken, { displayName: '新名称' })
499
- await iam.user.changeCurrentUserPassword(accessToken, 'oldPass', 'newPass')
500
-
501
- // 管理操作
502
- const users = await iam.user.listUsers({
503
- page: 1,
504
- pageSize: 20,
505
- search: 'alice',
506
- sortBy: 'createdAt',
507
- sortDirection: 'desc',
508
- include: ['roles'],
509
- })
510
- await iam.user.updateUser(userId, { enabled: false }) // 禁用用户 → 自动清除会话
511
- await iam.user.deleteUser(userId) // 删除用户 → 事务清除角色关联
542
+ async function registerUser(username: string, email: string, password: string) {
543
+ // 返回 RegisterResult,不自动建立会话。
544
+ return iam.user.register({ username, email, password })
545
+ }
546
+
547
+ async function createManagedUser(username: string, password: string) {
548
+ // 先验证调用者管理权限;不受 register.enabled 开关限制。
549
+ return iam.user.createUser({ username, password })
550
+ }
551
+
552
+ async function updateMyProfile(accessToken: string, displayName: string) {
553
+ return iam.user.updateCurrentUser(accessToken, { displayName })
554
+ }
512
555
 
513
- // 密码重置
514
- await iam.user.requestPasswordReset('alice@example.com')
515
- await iam.user.confirmPasswordReset(token, 'NewPassword456')
516
- await iam.user.adminResetPassword(userId, 'TempPassword123')
556
+ async function changeMyPassword(accessToken: string, oldPassword: string, newPassword: string) {
557
+ // 成功后撤销该用户全部会话,客户端回到登录入口。
558
+ return iam.user.changeCurrentUserPassword(accessToken, oldPassword, newPassword)
559
+ }
560
+
561
+ async function listReaders(page: number) {
562
+ const result = await iam.user.listUsers({
563
+ page,
564
+ pageSize: 20,
565
+ roleCode: 'record_reader',
566
+ search: 'alice',
567
+ enabled: true,
568
+ include: ['roles'],
569
+ sortBy: 'createdAt',
570
+ sortDirection: 'desc',
571
+ })
572
+ if (!result.success)
573
+ return result
574
+ // 直接使用 items/total/page/pageSize,不对当前页再次过滤。
575
+ return result
576
+ }
577
+
578
+ async function loadUsers(userIds: readonly string[]) {
579
+ // 最多 1000 个输入 ID;去重保留输入顺序,缺失 ID 省略。
580
+ return iam.user.getUsers(userIds, { include: ['roles'] })
581
+ }
517
582
 
518
- // 密码强度验证(同步方法)
519
- const valid = iam.user.validatePassword('weak')
583
+ async function disableUser(userId: string) {
584
+ // 调用前已验证管理权限;成功后自动清除该用户会话。
585
+ return iam.user.updateUser(userId, { enabled: false })
586
+ }
520
587
  ```
521
588
 
522
589
  #### 注意点
@@ -532,6 +599,8 @@ const valid = iam.user.validatePassword('weak')
532
599
 
533
600
  ### API Key — `iam.apiKey`
534
601
 
602
+ `scopes` は主体の権限との共通範囲を適用し、`*` と名前空間 `:*` に対応します。既定の空配列は本人確認のみで操作権限を付与しません。制限付き key のセッション `roles` は空で permission ガードを使用し、明示的な `*` も主体の権限を超えません。ログインは key ID に結び付き、get/verifyToken/refresh は永続状態を再確認し、失効・無効・期限切れ・DB エラーを拒否します。verifyApiKey 単独は鍵のメタデータ検証のみで、主体・操作の認可は別途必要です。
603
+
535
604
  #### 主要な型
536
605
 
537
606
  - **`ApiKeyOperations`**:`createApiKey` / `listApiKeys` / `getApiKey` / `revokeApiKey` / `verifyApiKey`。
@@ -554,23 +623,90 @@ verifyApiKey(rawKey)
554
623
 
555
624
  #### 使用方法
556
625
 
626
+ 初期化設定で API Key を有効にします。Key はユーザーの制限付き認証情報です:**有効権限 = 現在のユーザー権限 ∩ scopes**。scope は権限 code で、ロール名、表示名、リソース ID ではありません。権限を作成せず、テナント/レコード所有権の検証も代替しません。
627
+
628
+ ```yaml
629
+ # config/_iam.yml
630
+ login:
631
+ apikey: true
632
+ apikey:
633
+ maxKeysPerUser: 10
634
+ defaultExpirationDays: 30
635
+ ```
636
+
637
+ ユーザーが `record:read`、`record:write`、`user:read` を持つ場合、権限変更時にも交集を再計算します:
638
+
639
+ | scopes | 有効権限 | roles |
640
+ | ------------------------------ | ------------------------------ | -------------------- |
641
+ | 省略または `[]` | 身元のみ、操作権限なし | `[]` |
642
+ | `['record:read']` | `record:read` | `[]` |
643
+ | `['record:*']` | `record:read`、`record:write` | `[]` |
644
+ | `['record:read', 'user:read']` | 指定した二つの権限 | `[]` |
645
+ | `['missing:read']` | ユーザーが持たないため権限なし | `[]` |
646
+ | `['*']` | 現在のユーザー権限すべて | ユーザーロールを維持 |
647
+
648
+ `record:*` はコロン後の全階層に一致します。`record:r*` 等の任意パターンは未対応です。ユーザーが `record:read` のみなら `['record:*']` でも読み取りのみです。ユーザーがワイルドカードを持つ場合も狭い範囲を保持します。制限付き Key はロール認可を持たず、permission ガードを使います。広いアクセスが必要な場合のみ明示的な `*` を選びます。
649
+
650
+ 以下は作成 → Key ログイン → Bearer 検証と操作認可 → 一覧 → 取り消しの例です。管理入口は本人の所有権または管理権限を検証します。IAM 自体は業務上の呼び出し者を知りません。
651
+
557
652
  ```ts
558
- // 创建 API Key
559
- const result = await iam.apiKey.createApiKey(userId, {
560
- name: 'CI/CD',
561
- expirationDays: 90,
562
- scopes: ['api:read'],
563
- })
564
- // result.data.rawKey → 明文密钥(仅此一次展示)
653
+ import { err, HaiCommonError, ok } from '@h-ai/core'
654
+ import { iam } from '@h-ai/iam'
655
+
656
+ async function createReadKey(userId: string) {
657
+ // 调用前校验管理权限或本人归属;scopes 使用权限 code。
658
+ const created = await iam.apiKey.createApiKey(userId, {
659
+ name: 'Read client',
660
+ expirationDays: 30,
661
+ scopes: ['record:read'],
662
+ })
663
+ if (!created.success)
664
+ return created
665
+ // rawKey 仅本次返回;通过安全渠道展示一次,不写日志。
666
+ return created
667
+ }
668
+
669
+ async function apiKeyLogin(rawKey: string) {
670
+ // 通常由客户端取得 Key 后调用;服务器内部验证并绑定 Key。
671
+ return iam.auth.loginWithApiKey({ key: rawKey })
672
+ }
673
+
674
+ async function requireRecordRead(accessToken: string, deniedMessage: string) {
675
+ // deniedMessage 由应用 i18n 提供;先读取当前且已收敛 scope 的会话。
676
+ const verified = await iam.auth.verifyToken(accessToken)
677
+ if (!verified.success)
678
+ return verified
679
+ const allowed = verified.data.permissions.some(code =>
680
+ code === '*' || code === 'record:read'
681
+ || (code.endsWith(':*') && 'record:read'.startsWith(code.slice(0, -1))),
682
+ )
683
+ if (!allowed)
684
+ return err(HaiCommonError.FORBIDDEN, deniedMessage)
685
+ // 这里只确认操作权限;业务层还须检查具体记录的数据归属。
686
+ return ok({ userId: verified.data.userId })
687
+ }
565
688
 
566
- // 列出用户的 API Key(服务端分页)
567
- const keys = await iam.apiKey.listApiKeys(userId, { page: 1, pageSize: 20, search: 'ci' })
568
- // keys.data → { items, total, page, pageSize }
689
+ async function listMyKeys(userId: string) {
690
+ return iam.apiKey.listApiKeys(userId, {
691
+ page: 1,
692
+ pageSize: 20,
693
+ search: 'Read',
694
+ enabled: true,
695
+ })
696
+ }
569
697
 
570
- // 吊销
571
- await iam.apiKey.revokeApiKey(keyId)
698
+ async function revokeKey(keyId: string) {
699
+ // 调用前校验本人归属或管理权限。
700
+ return iam.apiKey.revokeApiKey(keyId)
701
+ }
572
702
  ```
573
703
 
704
+ Key ログインには `rawKey` のみ使用できます。`apiKey.id` は管理 ID、`keyPrefix` は表示/検索用です。`verifyApiKey(rawKey)` は Key のメタデータ検証のみで、ユーザー状態・scope・操作認可を代替しません。
705
+
706
+ Key セッションは `iam.session.refresh` 後も Key に紐付きます。取り消し成功後は既存 accessToken の get/verify と refreshToken の refresh が失敗します。期限切れ、無効化、永続化読み取りエラーも拒否します。ロール変更は `iam.auth.verifyToken` の現在の認可で反映します。scope 更新 API はないため、新 Key を作成して旧 Key を取り消します。
707
+
708
+ `expirationDays: 0` は無期限、省略時は `apikey.defaultExpirationDays` を使用します。期限が必要なら明示します。生 Key/Token を URL、ログ、公開設定、通常の localStorage に保存しません。
709
+
574
710
  #### 注意点
575
711
 
576
712
  - **平文は保存しない**:DB は hash のみを保存し、作成時の `rawKey` が唯一の取得機会。
@@ -611,15 +747,25 @@ await apiClient.iam.auth.changePassword({
611
747
 
612
748
  ## パスワードリセット
613
749
 
750
+ リセットには `passwordReset.enabled` と送信コールバック/Reach テンプレートが必要です。成功はアカウントの存在や受信を証明しません。送信・内部処理の HaiResult エラーも処理します。期限/試行回数制限があり、一度だけ成功消費でき、成功時に旧セッションを取り消します。管理者リセットに旧パスワードは不要ですが、アプリで管理権限を確認します。
751
+
614
752
  ```ts
615
- // 请求重置(即使用户不存在也返回 ok,防止枚举)
616
- await iam.user.requestPasswordReset('admin@example.com')
753
+ import { iam } from '@h-ai/iam'
617
754
 
618
- // 确认重置(校验令牌有效期和尝试次数 → 更新密码 → 清除所有会话)
619
- const result = await iam.user.confirmPasswordReset(token, 'NewPassword456')
755
+ async function requestReset(identifier: string) {
756
+ // 缺失用户与有效用户使用相同成功响应,避免枚举账户。
757
+ return iam.user.requestPasswordReset(identifier)
758
+ }
759
+
760
+ async function confirmReset(token: string, newPassword: string) {
761
+ // token 来自通知中的重置链接;成功后重新登录。
762
+ return iam.user.confirmPasswordReset(token, newPassword)
763
+ }
620
764
 
621
- // 管理员重置(无需旧密码)
622
- await iam.user.adminResetPassword(userId, 'TempPassword123')
765
+ async function resetByAdministrator(userId: string, newPassword: string) {
766
+ // 管理权限校验在服务端入口完成;不接受未经校验的 userId。
767
+ return iam.user.adminResetPassword(userId, newPassword)
768
+ }
623
769
  ```
624
770
 
625
771
  ## 一度限りのチケット — `iam.ticket`
@@ -627,20 +773,29 @@ await iam.user.adminResetPassword(userId, 'TempPassword123')
627
773
  WebSocket URL など Bearer Token を付けられない経路向けの短期チケットです。認証済み HTTP リクエストで、主体・用途・操作に結び付けて発行します。サーバーは初回検証時に**アトミックに消費**し、一度のみ有効です。
628
774
 
629
775
  ```ts
630
- // 签发(已登录 HTTP 请求内):绑定主体、用途与本次操作,30 秒过期
631
- const issued = await iam.ticket.issue({
632
- subjectId: session.userId,
633
- purpose: 'ai-audio',
634
- grant: { operation: 'transcribe', model: 'whisper-1' },
635
- ttlMs: 30_000,
636
- })
637
- // issued.data.ticket 交给客户端,用于建立 WebSocket 连接
776
+ import type { Session } from '@h-ai/iam'
777
+ import { iam } from '@h-ai/iam'
778
+
779
+ async function issueAudioTicket(session: Session) {
780
+ // session 已通过当前授权校验,签发前还须验证该操作及资源范围。
781
+ const issued = await iam.ticket.issue({
782
+ subjectId: session.userId,
783
+ purpose: 'ai-audio',
784
+ grant: { operation: 'transcribe', model: 'whisper-1' },
785
+ ttlMs: 30_000,
786
+ })
787
+ if (!issued.success)
788
+ return issued
789
+ // 仅将短期票据交给客户端,不把长期 accessToken 放进 URL。
790
+ return issued
791
+ }
638
792
 
639
- // 消费(服务端建连时):原子单次,返回主体 + 用途 + grant
640
- const consumed = await iam.ticket.consume(ticket, { purpose: 'ai-audio' })
641
- if (consumed.success) {
642
- const { subjectId, grant } = consumed.data
643
- // 用 grant.operation / grant.model 交叉校验客户端请求
793
+ async function consumeAudioTicket(ticket: string) {
794
+ const consumed = await iam.ticket.consume(ticket, { purpose: 'ai-audio' })
795
+ if (!consumed.success)
796
+ return consumed
797
+ // 按 grant 校验本次建连操作/资源;票据本身不自动读取当前 RBAC。
798
+ return consumed
644
799
  }
645
800
  ```
646
801