@h-ai/iam 0.1.0-alpha.61 → 0.1.0-alpha.63
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 +268 -113
- package/README.ja.md +268 -113
- package/README.md +268 -113
- package/dist/index.d.ts +23 -6
- package/dist/index.js +139 -44
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
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
|
|
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
|
-
→
|
|
278
|
+
→ 現在のユーザー状態・ロール JOIN・権限一括検索(ロールなしなら権限検索を省略)
|
|
279
|
+
→ 無効ユーザー・DB 読取失敗は拒否し、API Key scope を再適用
|
|
280
|
+
→ 現在の Session を返す(userId、roles、permissions)
|
|
279
281
|
```
|
|
280
282
|
|
|
281
283
|
#### 使用方法
|
|
282
284
|
|
|
283
|
-
`
|
|
285
|
+
例は reldb/cache/IAM の初期化に成功したサーバーで実行し、検証済みの入力を使います。パスワード認証は既定で有効です。OTP には `login.otp: true`、`otp` チャネル、送信コールバック/Reach テンプレートが必要です。LDAP は `login.ldap: true`、API Key は `login.apikey: true` を有効にします。ログインと登録後ログインは `AuthResult` を返します。登録とパスワード認証のスイッチは独立しています。
|
|
284
286
|
|
|
285
|
-
|
|
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
|
-
|
|
290
|
-
|
|
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
|
-
|
|
294
|
-
const result = await iam.auth.
|
|
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
|
-
|
|
297
|
-
|
|
300
|
+
async function requestLoginCode(identifier: string) {
|
|
301
|
+
// identifier 为启用通道对应的邮箱/手机号,返回 expiresAt。
|
|
302
|
+
return iam.auth.sendOtp(identifier)
|
|
303
|
+
}
|
|
298
304
|
|
|
299
|
-
|
|
300
|
-
|
|
305
|
+
async function otpLogin(identifier: string, code: string) {
|
|
306
|
+
// 用户收到验证码并输入后调用;发送失败时不能伪报成功。
|
|
307
|
+
return iam.auth.loginWithOtp({ identifier, code })
|
|
308
|
+
}
|
|
301
309
|
|
|
302
|
-
|
|
303
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
380
|
+
async function signOut(accessToken: string) {
|
|
381
|
+
return iam.auth.logout(accessToken)
|
|
382
|
+
}
|
|
362
383
|
|
|
363
|
-
|
|
364
|
-
|
|
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
|
-
→
|
|
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
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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
|
-
|
|
429
|
-
|
|
430
|
-
|
|
470
|
+
async function replaceUserRoles(userId: string, roleIds: string[]) {
|
|
471
|
+
// 完整替换用户的角色集合;[] 移除全部角色。
|
|
472
|
+
return iam.authz.syncRoles(userId, roleIds)
|
|
473
|
+
}
|
|
431
474
|
|
|
432
|
-
|
|
433
|
-
|
|
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
|
-
-
|
|
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
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
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
|
-
|
|
515
|
-
|
|
516
|
-
|
|
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
|
-
|
|
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
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
scopes
|
|
563
|
-
|
|
564
|
-
|
|
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
|
-
|
|
567
|
-
|
|
568
|
-
|
|
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
|
-
|
|
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
|
-
|
|
616
|
-
await iam.user.requestPasswordReset('admin@example.com')
|
|
753
|
+
import { iam } from '@h-ai/iam'
|
|
617
754
|
|
|
618
|
-
|
|
619
|
-
|
|
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
|
-
|
|
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
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
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
|
-
|
|
640
|
-
const consumed = await iam.ticket.consume(ticket, { purpose: 'ai-audio' })
|
|
641
|
-
if (consumed.success)
|
|
642
|
-
|
|
643
|
-
//
|
|
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
|
|