@h-ai/iam 0.1.0-alpha.60 → 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.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.en.md
CHANGED
|
@@ -12,7 +12,7 @@ Identity and access management through a unified `iam` object for authentication
|
|
|
12
12
|
| -------------------- | ---------------------------------------------------------------------------------------------------------------------- |
|
|
13
13
|
| **Authentication** | Password, OTP (email/SMS codes), LDAP, and API Key strategies |
|
|
14
14
|
| **Sessions** | Stateful sessions (random access tokens + cache, optional sliding renewal) |
|
|
15
|
-
| **Authorization** | RBAC roles and permissions (DB
|
|
15
|
+
| **Authorization** | RBAC roles and permissions (batched DB reads, wildcard matching) |
|
|
16
16
|
| **User management** | Registration, queries, updates, password reset, and admin password reset |
|
|
17
17
|
| **API Key** | Create, revoke, and verify API keys with scopes and expiration |
|
|
18
18
|
| **One-time tickets** | Short-lived capability tickets (issue / atomic consume) for channels such as WebSocket that cannot carry Bearer tokens |
|
|
@@ -27,7 +27,7 @@ pnpm add @h-ai/iam
|
|
|
27
27
|
## Dependencies
|
|
28
28
|
|
|
29
29
|
- `@h-ai/reldb`: persistence for users/roles/permissions; **initialize before iam.init()**.
|
|
30
|
-
- `@h-ai/cache`: sessions/OTP/reset tokens/
|
|
30
|
+
- `@h-ai/cache`: sessions/OTP/reset tokens/one-time tickets/shared locks; **initialize before iam.init()**.
|
|
31
31
|
- `@h-ai/crypto`: password hashing, used internally and initialized automatically.
|
|
32
32
|
- `@h-ai/audit`: audit records for key RBAC operations.
|
|
33
33
|
|
|
@@ -218,7 +218,7 @@ IAM is a **lifecycle singleton** managed through `iam.init()` / `iam.close()`. F
|
|
|
218
218
|
```
|
|
219
219
|
iam.init()
|
|
220
220
|
├─ session (cached sessions; no DB dependency)
|
|
221
|
-
├─ authz (RBAC roles/permissions; DB
|
|
221
|
+
├─ authz (RBAC roles/permissions; batched DB reads)
|
|
222
222
|
├─ authn (authentication; depends on session + authz)
|
|
223
223
|
├─ user (users; depends on session + authz + passwordStrategy)
|
|
224
224
|
└─ seed (seed data; depends on authz)
|
|
@@ -275,32 +275,41 @@ login(credentials)
|
|
|
275
275
|
verifyToken(accessToken)
|
|
276
276
|
→ session.get(accessToken) → cache lookup
|
|
277
277
|
→ Extend TTL automatically when sliding renewal is enabled
|
|
278
|
-
→
|
|
278
|
+
→ Read current user status, role JOIN and one batched permission query (skip permissions for no roles)
|
|
279
|
+
→ Reject disabled users or database failures; intersect API Key scopes again
|
|
280
|
+
→ Return current Session (userId, roles, permissions)
|
|
279
281
|
```
|
|
280
282
|
|
|
281
283
|
#### Usage
|
|
282
284
|
|
|
283
|
-
|
|
285
|
+
These examples run on the server after successful reldb/cache/IAM initialization, with validated request input. Password login is available by default. OTP requires `login.otp: true`, an `otp` channel and delivery callbacks/Reach templates; LDAP requires `login.ldap: true` (see configuration above); API Key requires `login.apikey: true` (see the complete flow below). Login returns `AuthResult`; registration with login returns the same authentication structure. Registration and password login have separate switches.
|
|
284
286
|
|
|
285
|
-
|
|
286
|
-
// 密码登录
|
|
287
|
-
const result = await iam.auth.login({ identifier: 'admin', password: 'Password123' })
|
|
287
|
+
OTP has two interactions: send a code, then log in with the code the user received. Both operations return HaiResult. LDAP uses `iam.auth.loginWithLdap({ username, password })`, and API Key uses `loginWithApiKey({ key })`. Check `success` before reading `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
|
#### Considerations
|
|
@@ -345,23 +354,37 @@ create(options) [singleDevice=true]
|
|
|
345
354
|
|
|
346
355
|
#### Usage
|
|
347
356
|
|
|
357
|
+
Use `tokens.accessToken` to authenticate requests and `tokens.refreshToken` to obtain a new pair; `expiresIn` is measured in seconds. Independent APIs use `Authorization: Bearer <accessToken>`. For SvelteKit Cookie endpoints, use kit's standard authentication integration described below.
|
|
358
|
+
|
|
359
|
+
Protected requests use `iam.auth.verifyToken` to read current user status, roles and permissions. `iam.session.get/verifyToken` exposes a lower-level session snapshot and does not replace current authorization. Only trusted server integrations should use `session.create`; never accept client-provided roles/permissions or API Key bindings.
|
|
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
|
#### Considerations
|
|
@@ -393,7 +416,7 @@ checkPermission(userId, permission)
|
|
|
393
416
|
→ Return true if RBAC is disabled
|
|
394
417
|
→ Query user roles (DB)
|
|
395
418
|
→ Check super-admin role (cached superAdminRoleId); return true if present
|
|
396
|
-
→
|
|
419
|
+
→ Read these roles' permission codes in one DB query; return errors on read failure
|
|
397
420
|
→ Match each permission exactly or by wildcard (`admin:*` matches `admin:read`)
|
|
398
421
|
→ Return true / false
|
|
399
422
|
```
|
|
@@ -403,34 +426,56 @@ checkPermission(userId, permission)
|
|
|
403
426
|
```
|
|
404
427
|
assignPermissionToRole(roleId, permId)
|
|
405
428
|
→ Write association to DB
|
|
406
|
-
→ Clear role permission cache
|
|
407
429
|
→ Find role users → resolve permissions per user → update active sessions
|
|
408
430
|
(best-effort: failures only log.error; permission assignment still succeeds)
|
|
409
431
|
```
|
|
410
432
|
|
|
411
433
|
#### Usage
|
|
412
434
|
|
|
435
|
+
Create permissions, save permission **IDs** through the role's `permissionIds`, and assign a role by its **ID**. Session roles/permissions and guards use stable **codes**; display names can change independently. These examples assume RBAC is enabled. Applications must authorize administrative operations and check resource access before calling them.
|
|
436
|
+
|
|
437
|
+
`createRole/updateRole` saves role metadata and the complete permission set in one transaction: omission preserves it, an empty array clears it, duplicates are removed, and unknown permission IDs fail. Concurrent updates use the last committed complete set. Owned transactions synchronize sessions after commit; callers using an external transaction must synchronize after their commit.
|
|
438
|
+
|
|
439
|
+
`checkPermission(userId, code)` and `getUserAuthorization` read the subject's RBAC authorization without the requesting API Key's scopes. API Key requests must use scoped permissions returned by `iam.auth.verifyToken`, as shown below. Re-querying by userId alone can bypass Key restrictions. serv's `.permission('record:read')` declares an exact permission guard. Hiding a menu/button does not authorize the business 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
|
#### Considerations
|
|
@@ -438,7 +483,7 @@ await iam.authz.syncRoles(userId, [roleId1, roleId2])
|
|
|
438
483
|
- **Wildcard matching**: `admin:*` matches `admin:read`, `admin:write:detail`, etc.; one `*` matches everything after the colon.
|
|
439
484
|
- **Super-admin role**: `rbac.superAdminRole` (default `super_admin`) automatically grants all permissions.
|
|
440
485
|
- **Transactions**: `deleteRole` / `deletePermission` clear associations before deleting entities in a DB transaction.
|
|
441
|
-
- **Session synchronization
|
|
486
|
+
- **Session synchronization**: cache updates are best-effort. Protected requests use `iam.auth.verifyToken` to read current user status and authorization and reject read failures. `iam.session.verifyToken` validates a session snapshot only. `iam.authz.getUserAuthorization(userId)` shares the role JOIN and batched permission query.
|
|
442
487
|
- **Seed data**: `seedDefaultData: true` idempotently creates admin/user/guest roles and basic permissions.
|
|
443
488
|
|
|
444
489
|
---
|
|
@@ -451,7 +496,7 @@ await iam.authz.syncRoles(userId, [roleId1, roleId2])
|
|
|
451
496
|
- **`User`** (public fields): `{ id, username, email, phone, displayName, avatarUrl, enabled, roles? }`.
|
|
452
497
|
- **`StoredUser`**: internal stored user, including sensitive `passwordHash`, `loginFailedCount`, `lockedUntil`, etc.
|
|
453
498
|
- **`RegisterOptions`** / **`RegisterResult`**: registration input/output.
|
|
454
|
-
- **`UpdateCurrentUserInput`**: fields the current user may change (`displayName` / `avatarUrl` / `phone` / `metadata`).
|
|
499
|
+
- **`UpdateCurrentUserInput`**: fields the current user may change (`username` / `email` / `displayName` / `avatarUrl` / `phone` / `metadata`).
|
|
455
500
|
|
|
456
501
|
#### Key workflows
|
|
457
502
|
|
|
@@ -485,38 +530,60 @@ confirmPasswordReset(token, newPassword)
|
|
|
485
530
|
|
|
486
531
|
#### Usage
|
|
487
532
|
|
|
533
|
+
`register` returns a user without a session; use `auth.registerAndLogin` when both are needed. `createUser` is an administrative operation independent of self-registration settings. Use `getCurrentUser/updateCurrentUser` for profile fields: username/email/displayName/avatarUrl/phone/metadata. Security fields and roles require authorized administrative operations.
|
|
534
|
+
|
|
535
|
+
Lists return `PaginatedResult<User>` and filter `roleCode` before pagination; request `include: ['roles']` only when needed. Bulk reads keep deduplicated input order and omit missing IDs, so associate results by id rather than array position. Returned users are public DTOs, but applications still control data scope and field disclosure.
|
|
536
|
+
|
|
537
|
+
`getUser(id)` returns `null` on a successful missing lookup; `getCurrentUser(token)` fails for an invalid session or missing user. Database failures are not empty results. `validatePassword` is synchronous and returns HaiResult. Deletion, disabling and password changes/resets revoke affected sessions.
|
|
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
|
#### Considerations
|
|
@@ -532,6 +599,8 @@ const valid = iam.user.validatePassword('weak')
|
|
|
532
599
|
|
|
533
600
|
### API keys — `iam.apiKey`
|
|
534
601
|
|
|
602
|
+
`scopes` intersect the subject's permissions and support `*` and namespace `:*`. The default empty array grants identity only, without operation permissions. Restricted keys have empty session `roles` and use permission guards; explicit `*` never exceeds subject authorization. API Key login binds the key ID: get/verifyToken/refresh recheck its persistent state and reject revoked, disabled, expired keys or DB failures. Standalone verifyApiKey returns verified key metadata and still requires subject/operation authorization.
|
|
603
|
+
|
|
535
604
|
#### Key types
|
|
536
605
|
|
|
537
606
|
- **`ApiKeyOperations`**: `createApiKey` / `listApiKeys` / `getApiKey` / `revokeApiKey` / `verifyApiKey`.
|
|
@@ -554,23 +623,90 @@ verifyApiKey(rawKey)
|
|
|
554
623
|
|
|
555
624
|
#### Usage
|
|
556
625
|
|
|
626
|
+
Enable API Key during initialization. A Key is a restricted user credential: **effective permissions = current subject permissions ∩ scopes**. Scopes use permission codes, not role/display names or resource IDs. They neither create permissions nor replace tenant/record ownership checks.
|
|
627
|
+
|
|
628
|
+
```yaml
|
|
629
|
+
# config/_iam.yml
|
|
630
|
+
login:
|
|
631
|
+
apikey: true
|
|
632
|
+
apikey:
|
|
633
|
+
maxKeysPerUser: 10
|
|
634
|
+
defaultExpirationDays: 30
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
For a subject holding `record:read`, `record:write` and `user:read`, the results are recalculated whenever subject authorization changes:
|
|
638
|
+
|
|
639
|
+
| scopes | Effective permissions | roles |
|
|
640
|
+
| ------------------------------ | --------------------------------------- | ---------------------- |
|
|
641
|
+
| Omitted or `[]` | Identity only, no operation permissions | `[]` |
|
|
642
|
+
| `['record:read']` | `record:read` | `[]` |
|
|
643
|
+
| `['record:*']` | `record:read`, `record:write` | `[]` |
|
|
644
|
+
| `['record:read', 'user:read']` | Both specified permissions | `[]` |
|
|
645
|
+
| `['missing:read']` | None, because the subject lacks it | `[]` |
|
|
646
|
+
| `['*']` | All current subject permissions | Subject roles retained |
|
|
647
|
+
|
|
648
|
+
`record:*` matches every level after the colon; arbitrary patterns such as `record:r*` are unsupported. If the subject only holds `record:read`, `['record:*']` still grants only read. Intersections with subject wildcards keep the narrower range. Restricted Keys have no role authorization; use permission guards. Choose explicit `*` only when broad access is intended.
|
|
649
|
+
|
|
650
|
+
The server examples cover creation → Key login → Bearer verification and operation authorization → listing → revocation. Administrative endpoints must verify ownership or administrator rights; IAM does not know the caller's business identity.
|
|
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
|
+
Only `rawKey` can be used for Key login. `apiKey.id` is a management ID and `keyPrefix` is for display/search. `verifyApiKey(rawKey)` validates Key metadata only; it does not replace user-status, scope or operation authorization.
|
|
705
|
+
|
|
706
|
+
Key-derived sessions remain bound to the Key when refreshed through `iam.session.refresh`. Successful revocation invalidates existing accessToken get/verify operations and refreshToken refresh operations. Expiration, disabling or persistent-read failures also reject access. Current role changes are enforced through `iam.auth.verifyToken`. There is no scope-update API; create a replacement Key and revoke the old one.
|
|
707
|
+
|
|
708
|
+
`expirationDays: 0` means no expiration; omission uses `apikey.defaultExpirationDays`. Set an explicit duration for time-limited access. Never put raw Keys or Tokens in URLs, logs, public configuration or ordinary localStorage.
|
|
709
|
+
|
|
574
710
|
#### Considerations
|
|
575
711
|
|
|
576
712
|
- **No plaintext persistence**: only hashes are stored; `rawKey` at creation is the sole opportunity to retrieve it.
|
|
@@ -611,15 +747,25 @@ await apiClient.iam.auth.changePassword({
|
|
|
611
747
|
|
|
612
748
|
## Password reset
|
|
613
749
|
|
|
750
|
+
Password reset requires `passwordReset.enabled` and a working delivery callback/Reach template. Success does not prove that the account exists or the recipient received a message. Handle delivery/internal failures through HaiResult. Reset tokens expire, limit attempts, and permit one successful consumption; success revokes old sessions. Administrator resets do not require the old password, but applications must authorize the administrator.
|
|
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
|
## One-time tickets — `iam.ticket`
|
|
@@ -627,20 +773,29 @@ await iam.user.adminResetPassword(userId, 'TempPassword123')
|
|
|
627
773
|
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.
|
|
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
|
|