@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.md
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
| -------------- | -------------------------------------------------------------------------------------- |
|
|
13
13
|
| **认证** | 密码、OTP(邮箱/短信验证码)、LDAP、API Key 多策略认证 |
|
|
14
14
|
| **会话** | 有状态会话(随机访问令牌 + 缓存,滑动续期可选) |
|
|
15
|
-
| **授权** | RBAC 角色与权限管理(DB
|
|
15
|
+
| **授权** | RBAC 角色与权限管理(DB 批量读取,通配符权限匹配) |
|
|
16
16
|
| **用户管理** | 注册、查询、更新、密码重置、管理员重置密码 |
|
|
17
17
|
| **API Key** | API Key 创建、吊销、验证,支持 scope 与过期时间 |
|
|
18
18
|
| **一次性票据** | 短期、一次性能力票据(签发 / 原子消费),用于 WebSocket 等无法携带 Bearer Token 的通道 |
|
|
@@ -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
|
|
30
|
+
- `@h-ai/cache` — 缓存(会话/OTP/重置令牌/一次性票据/共享锁),**需在 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
|
+
→ 用户停用或数据库读取失败时拒绝;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`,注册并登录返回相同认证结构;`register.enabled` 与密码登录开关分别控制。
|
|
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 })`,不要用密码登录替代。所有示例先检查 `success` 再访问 `data`。
|
|
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` 只用于可信服务端集成,不接受客户端自报 roles/permissions 或 API 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** 分配用户。会话中的 `roles`/`permissions` 和守卫使用稳定 **code**;显示 `name` 可独立修改,不能代替 ID/code。示例默认 RBAC 已启用,管理操作须由应用先校验调用者权限和资源范围。
|
|
436
|
+
|
|
437
|
+
`createRole/updateRole` 将角色资料与完整 `permissionIds` 放在同一事务:省略保持、空数组清空、重复去重、未知权限失败;并发以最后提交的完整集合为准。使用自管事务时提交后同步会话;使用外部事务时调用方须在提交后完成同步。
|
|
438
|
+
|
|
439
|
+
`checkPermission(userId, code)` 查询主体的 RBAC 权限,不识别请求所属 API Key 的 scopes;`getUserAuthorization` 也是主体授权读取。API 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` 等(单层 `*` 匹配冒号后全部)
|
|
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']`。批量查询保留去重后的输入顺序,缺失项省略,不能假定第 n 个结果必对应第 n 个输入;按 id 映射关联。各方法返回公开用户结构,管理查询仍需数据范围和字段披露控制。
|
|
536
|
+
|
|
537
|
+
`getUser(id)` 成功但不存在时返回 `null`;`getCurrentUser(token)` 缺失/会话无效时失败。不要把 DB 读取失败解释成空列表。`validatePassword` 为同步 HaiResult 检查;`deleteUser`、停用、密码修改/重置会清除受影响会话。
|
|
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 守卫访问,显式 `*` 也不能超出主体授权。API Key 登录绑定 key ID,派生会话的 get/verifyToken/refresh 每次检查持久化 key,撤销、禁用、过期或 DB 读取失败即拒绝。单独 verifyApiKey 仅返回验证后的 key 元数据,仍需主体与操作授权。
|
|
603
|
+
|
|
535
604
|
#### 关键对象
|
|
536
605
|
|
|
537
606
|
- **`ApiKeyOperations`** — API Key 操作接口:`createApiKey` / `listApiKeys` / `getApiKey` / `revokeApiKey` / `verifyApiKey`
|
|
@@ -554,23 +623,90 @@ verifyApiKey(rawKey)
|
|
|
554
623
|
|
|
555
624
|
#### 使用方式
|
|
556
625
|
|
|
626
|
+
先在初始化配置中启用 API Key。它是用户的受限凭据,不能独立授予用户未拥有的权限:**有效权限 = 当前主体权限 ∩ scopes**。scopes 是与权限 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`,不同 scopes 的结果如下(主体授权改变后重新计算):
|
|
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 会话验证与操作授权 → 列表 → 撤销的服务端示例。先检查所有 HaiResult;每个管理入口还要校验本人归属或管理员权限,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
|
+
创建后只有 `rawKey` 能用于 Key 登录;`apiKey.id` 是管理 ID,`keyPrefix` 仅用于显示/检索,均不能代替 rawKey。`verifyApiKey(rawKey)` 仅验证 Key 并返回元数据,不能代替用户状态、scopes 与操作授权。
|
|
705
|
+
|
|
706
|
+
Key 登录会话绑定 Key ID。刷新使用普通 `iam.session.refresh`,不会丢失该绑定或扩大 scopes。撤销成功后,已有 accessToken 的 get/verify 以及 refreshToken 的 refresh 都会失败;过期、禁用及持久化读取失败同样拒绝。角色撤权由 `iam.auth.verifyToken` 读取当前授权生效。改 scope 使用创建新 Key 后撤销旧 Key,当前 API 不提供 scope 更新操作。
|
|
707
|
+
|
|
708
|
+
`expirationDays: 0` 表示不设过期时间,省略时使用 `apikey.defaultExpirationDays`;需要限时访问时显式设置。明文 Key 和 Token 不进入 URL、日志、公开配置或普通 localStorage。
|
|
709
|
+
|
|
574
710
|
#### 特别关注
|
|
575
711
|
|
|
576
712
|
- **明文不落库**:数据库仅存储 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
|
短期、一次性能力票据。用于无法携带 Bearer Token 的通道(如 WebSocket URL):由已认证的 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
|
|
package/dist/index.d.ts
CHANGED
|
@@ -307,7 +307,7 @@ interface ApiKey {
|
|
|
307
307
|
createdAt: Date;
|
|
308
308
|
/** 最后使用时间 */
|
|
309
309
|
lastUsedAt: Date | null;
|
|
310
|
-
/**
|
|
310
|
+
/** 权限代码范围,支持 * / 命名空间 :*;空数组无操作权限,* 不超出主体权限。 */
|
|
311
311
|
scopes: string[];
|
|
312
312
|
}
|
|
313
313
|
/**
|
|
@@ -329,7 +329,7 @@ interface CreateApiKeyOptions {
|
|
|
329
329
|
name: string;
|
|
330
330
|
/** 有效期天数(0 或不传表示永不过期) */
|
|
331
331
|
expirationDays?: number;
|
|
332
|
-
/**
|
|
332
|
+
/** 权限代码范围;默认空数组。受限 key 不保留角色授权入口,使用 permission 守卫。 */
|
|
333
333
|
scopes?: string[];
|
|
334
334
|
}
|
|
335
335
|
/** API Key 列表允许的服务端排序字段。 */
|
|
@@ -463,6 +463,8 @@ interface Role {
|
|
|
463
463
|
* 授权子功能接口
|
|
464
464
|
*/
|
|
465
465
|
interface AuthzOperations {
|
|
466
|
+
/** 查询当前角色与权限代码,一次读取角色并批量聚合权限,不依赖会话缓存。 */
|
|
467
|
+
getUserAuthorization: (userId: string) => Promise<HaiResult<Pick<Session, 'roles' | 'permissions'>>>;
|
|
466
468
|
/**
|
|
467
469
|
* 检查权限
|
|
468
470
|
*
|
|
@@ -679,6 +681,8 @@ type UserSortDirection = 'asc' | 'desc';
|
|
|
679
681
|
* 用户列表查询选项
|
|
680
682
|
*/
|
|
681
683
|
interface ListUsersOptions extends PaginationOptionsInput {
|
|
684
|
+
/** 角色 code 精确筛选,在数据库分页前执行。 */
|
|
685
|
+
roleCode?: string;
|
|
682
686
|
/** 搜索关键字(模糊匹配用户名、邮箱、手机号、显示名称) */
|
|
683
687
|
search?: string;
|
|
684
688
|
/** 按启用状态过滤,不传则返回全部 */
|
|
@@ -802,6 +806,10 @@ interface UpdateCurrentUserInput {
|
|
|
802
806
|
* 用户子功能接口
|
|
803
807
|
*/
|
|
804
808
|
interface UserOperations {
|
|
809
|
+
/** 批量查询最多 1000 个用户 ID,去重并保持输入顺序,忽略不存在的 ID;可批量包含角色。 */
|
|
810
|
+
getUsers: (userIds: readonly string[], options?: {
|
|
811
|
+
include?: ('roles')[];
|
|
812
|
+
}) => Promise<HaiResult<User[]>>;
|
|
805
813
|
/** 管理端创建账户,不受自助注册开关限制;调用端必须执行管理员授权。 */
|
|
806
814
|
createUser: (options: RegisterOptions) => Promise<HaiResult<RegisterResult>>;
|
|
807
815
|
/**
|
|
@@ -980,6 +988,10 @@ interface AuthResult {
|
|
|
980
988
|
* 会话数据
|
|
981
989
|
*/
|
|
982
990
|
interface Session {
|
|
991
|
+
/** API Key 派生会话绑定的 key ID;普通用户登录时省略。 */
|
|
992
|
+
apiKeyId?: string;
|
|
993
|
+
/** 当前 key scopes;get/verify/refresh 每次重新验证持久化 key 并收敛权限。 */
|
|
994
|
+
apiKeyScopes?: string[];
|
|
983
995
|
/** 用户 ID */
|
|
984
996
|
userId: string;
|
|
985
997
|
/** 用户名 */
|
|
@@ -1018,6 +1030,9 @@ type SessionData = Record<string, unknown> & {
|
|
|
1018
1030
|
* 创建会话选项
|
|
1019
1031
|
*/
|
|
1020
1032
|
interface CreateSessionOptions {
|
|
1033
|
+
/** 服务端已验证的 API Key 绑定;不得来自未验证客户端输入。 */
|
|
1034
|
+
apiKeyId?: string;
|
|
1035
|
+
apiKeyScopes?: string[];
|
|
1021
1036
|
/** 用户 ID */
|
|
1022
1037
|
userId: string;
|
|
1023
1038
|
/** 用户名 */
|
|
@@ -1053,7 +1068,7 @@ interface SessionOperations {
|
|
|
1053
1068
|
/**
|
|
1054
1069
|
* 获取会话
|
|
1055
1070
|
*
|
|
1056
|
-
*
|
|
1071
|
+
* 查询缓存中的会话数据。API Key 会话先重新校验持久化 key 与 scopes;滑动窗口模式下自动续期。
|
|
1057
1072
|
* 已过期的会话会自动删除并返回 null。
|
|
1058
1073
|
*
|
|
1059
1074
|
* @param accessToken - 访问令牌
|
|
@@ -1063,7 +1078,7 @@ interface SessionOperations {
|
|
|
1063
1078
|
/**
|
|
1064
1079
|
* 验证访问令牌
|
|
1065
1080
|
*
|
|
1066
|
-
* 等同于 get 但在会话不存在时返回错误而非 null
|
|
1081
|
+
* 等同于 get 但在会话不存在时返回错误而非 null;API Key 撤销/禁用/过期立即拒绝。
|
|
1067
1082
|
*
|
|
1068
1083
|
* @param accessToken - 访问令牌
|
|
1069
1084
|
* @returns 成功返回会话;会话无效返回 SESSION_INVALID
|
|
@@ -1241,7 +1256,8 @@ interface AuthnOperations {
|
|
|
1241
1256
|
/**
|
|
1242
1257
|
* 使用 API Key 登录
|
|
1243
1258
|
*
|
|
1244
|
-
* 验证 API Key 有效性 → 查找关联用户 →
|
|
1259
|
+
* 验证 API Key 有效性 → 查找关联用户 → 按 scopes 收敛授权 → 创建绑定 key 的会话。
|
|
1260
|
+
* 空 scopes 无操作权限;显式 * 也不超出主体权限。受限 key 通过 permission 授权,不保留角色入口。
|
|
1245
1261
|
*
|
|
1246
1262
|
* @param credentials - API Key 凭证
|
|
1247
1263
|
* @returns 成功返回 AuthResult;API Key 无效/过期/禁用返回对应错误
|
|
@@ -1259,7 +1275,8 @@ interface AuthnOperations {
|
|
|
1259
1275
|
/**
|
|
1260
1276
|
* 验证令牌
|
|
1261
1277
|
*
|
|
1262
|
-
*
|
|
1278
|
+
* 验证会话并读取当前用户状态、角色与权限;重新应用 API Key scopes,依赖失败即拒绝。
|
|
1279
|
+
* 会话同步仅是缓存快照,受保护请求使用此入口而非低层 session.verifyToken。
|
|
1263
1280
|
*
|
|
1264
1281
|
* @param accessToken - 访问令牌
|
|
1265
1282
|
* @returns 成功返回会话信息;令牌无效/过期返回 SESSION_INVALID
|