@h-ai/iam 0.1.0-alpha.67 → 0.1.0-alpha.68
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 +55 -30
- package/README.ja.md +56 -31
- package/README.md +57 -32
- package/dist/browser.d.ts +2320 -0
- package/dist/browser.js +3 -0
- package/dist/browser.js.map +1 -0
- package/dist/chunk-VCE74BFW.js +243 -0
- package/dist/chunk-VCE74BFW.js.map +1 -0
- package/dist/index.d.ts +19 -1623
- package/dist/index.js +789 -1173
- package/dist/index.js.map +1 -1
- package/package.json +12 -9
package/README.md
CHANGED
|
@@ -31,7 +31,7 @@ pnpm add @h-ai/iam
|
|
|
31
31
|
### 依赖
|
|
32
32
|
|
|
33
33
|
- `@h-ai/reldb` — 数据库(用户/角色/权限持久化),**需在 iam.init() 前初始化**
|
|
34
|
-
- `@h-ai/cache` — 缓存(会话/OTP
|
|
34
|
+
- `@h-ai/cache` — 缓存(会话/OTP/一次性票据),**需在 iam.init() 前初始化**
|
|
35
35
|
- `@h-ai/crypto` — 密码哈希(内部使用,自动初始化)
|
|
36
36
|
- `@h-ai/audit` — 审计日志(RBAC 关键操作记录)
|
|
37
37
|
|
|
@@ -62,7 +62,7 @@ ldap:
|
|
|
62
62
|
connectTimeout: 5000
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
-
`ldaps://` 使用 TLS;`ldap://` 加 `useTls: true` 使用 StartTLS。Active Directory 可用 `searchFilter: "(sAMAccountName={{username}})"` 和 `usernameAttribute: sAMAccountName
|
|
65
|
+
`ldaps://` 使用 TLS;`ldap://` 加 `useTls: true` 使用 StartTLS。Active Directory 可用 `searchFilter: "(sAMAccountName={{username}})"` 和 `usernameAttribute: sAMAccountName`;占位符会转义后搜索。首次登录默认同步本地用户并分配默认角色,账户禁用与锁定仍有效。`ldapSyncUser: false` 要求已有启用的本地用户以绑定数据库凭据代际;不创建虚拟 DN 用户。可传自定义 `ldapClientFactory`;工厂返回具备 `bind/search/unbind` 的客户端,结果均为 HaiResult。注入工厂时无需安装 ldapts。
|
|
66
66
|
|
|
67
67
|
```ts
|
|
68
68
|
import type { IamConfigSettingsInput } from '@h-ai/iam'
|
|
@@ -149,19 +149,19 @@ import { mailService, smsService } from './notifications.js'
|
|
|
149
149
|
// 调用方须检查服务投递结果,失败时拒绝 Promise,不能吞掉错误。
|
|
150
150
|
const initialized = await iam.init({
|
|
151
151
|
otp: { email: true, sms: true },
|
|
152
|
-
onOtpSendEmail: async (email, code) => {
|
|
153
|
-
await mailService.send({ to: email, subject: 'Verification code', text: code })
|
|
152
|
+
onOtpSendEmail: async (email, code, context) => {
|
|
153
|
+
await mailService.send({ to: email, subject: 'Verification code', text: code, idempotencyKey: context.idempotencyKey })
|
|
154
154
|
},
|
|
155
|
-
onOtpSendSms: async (phone, code) => {
|
|
156
|
-
await smsService.send({ to: phone, code })
|
|
155
|
+
onOtpSendSms: async (phone, code, context) => {
|
|
156
|
+
await smsService.send({ to: phone, code, idempotencyKey: context.idempotencyKey })
|
|
157
157
|
},
|
|
158
158
|
passwordReset: { mode: 'email' },
|
|
159
|
-
onPasswordResetRequest: async (user, token,
|
|
159
|
+
onPasswordResetRequest: async (user, token, context) => {
|
|
160
160
|
if (!user.email)
|
|
161
161
|
throw new Error('Reset recipient unavailable')
|
|
162
162
|
const url = new URL('https://app.example.com/auth/reset-password')
|
|
163
163
|
url.searchParams.set('token', token)
|
|
164
|
-
await mailService.send({ to: user.email, subject: 'Password reset', text: url.toString(), expiresAt })
|
|
164
|
+
await mailService.send({ to: user.email, subject: 'Password reset', text: url.toString(), expiresAt: context.expiresAt, idempotencyKey: context.idempotencyKey })
|
|
165
165
|
},
|
|
166
166
|
})
|
|
167
167
|
if (!initialized.success)
|
|
@@ -234,14 +234,14 @@ iam.init()
|
|
|
234
234
|
|
|
235
235
|
#### 数据存储分布
|
|
236
236
|
|
|
237
|
-
| 数据类型
|
|
238
|
-
|
|
|
239
|
-
| 用户、角色、权限、关联关系、API Key
|
|
240
|
-
| 会话(accessToken → Session)
|
|
241
|
-
|
|
|
242
|
-
| OTP 验证码
|
|
243
|
-
| 密码重置令牌
|
|
244
|
-
| API Key
|
|
237
|
+
| 数据类型 | 存储位置 | 说明 |
|
|
238
|
+
| ----------------------------------- | ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
|
|
239
|
+
| 用户、角色、权限、关联关系、API Key | DB(基础 5 张,可选 API Key 1 张) | `hai_iam_users`、`hai_iam_roles`、`hai_iam_permissions`、`hai_iam_role_permissions`、`hai_iam_user_roles`、`hai_iam_api_keys` |
|
|
240
|
+
| 会话(accessToken → Session) | 缓存 | 有 TTL,滑动续期可选 |
|
|
241
|
+
| Refresh 消费状态 | 缓存的用户分区 | 与 session/索引在单键 CAS 中提交 |
|
|
242
|
+
| OTP 验证码 | 缓存 | 有 TTL + 尝试次数限制 |
|
|
243
|
+
| 密码重置令牌 | 用户数据库行 | SM3 摘要、数据库有效期、尝试次数及最终条件提交 |
|
|
244
|
+
| API Key | DB | `hai_iam_api_keys`,明文不落库,仅存 hash;前缀用于候选检索 |
|
|
245
245
|
|
|
246
246
|
---
|
|
247
247
|
|
|
@@ -278,9 +278,10 @@ login(credentials)
|
|
|
278
278
|
```
|
|
279
279
|
verifyToken(accessToken)
|
|
280
280
|
→ session.get(accessToken) → 缓存查询
|
|
281
|
+
→ 读取持久用户,校验启用状态与会话绑定的 credentialVersion
|
|
281
282
|
→ 滑动续期模式下自动延长 TTL
|
|
282
|
-
→
|
|
283
|
-
→
|
|
283
|
+
→ 再次读取当前用户,复核启用状态与 credentialVersion,并读取角色 JOIN + 批量权限(无角色时省略权限查询)
|
|
284
|
+
→ 任一持久读取失败、用户缺失/停用或凭据代不匹配时拒绝;API Key 会话再次收敛 scope
|
|
284
285
|
→ 返回当前 Session 对象(含 userId、roles、permissions)
|
|
285
286
|
```
|
|
286
287
|
|
|
@@ -323,6 +324,8 @@ async function registerWithSession(username: string, password: string) {
|
|
|
323
324
|
- **认证策略可独立启用/禁用**:通过 `login.password` / `login.otp` / `login.ldap` / `login.apikey` 控制
|
|
324
325
|
- **OTP 安全**:rejection sampling 消除模偏差,常量时间比较防 timing attack,频率限制(`resendInterval`)
|
|
325
326
|
|
|
327
|
+
`iam.auth.logout(accessToken)` 直接按完整不透明令牌在用户分区执行一次 CAS,原子撤销 access 与关联 refresh;access 已到期也不能跳过撤销。非法/不存在的令牌幂等成功,缓存读取或提交故障返回失败,恢复后可用同一令牌重试。HTTP 登出只允许撤销所持令牌,不以 userId 定位他人会话,也不要求该 access 仍通过认证。调用方必须检查 `success`;本地 Cookie/安全存储清理不代表服务端撤销已经确认,失败应显示提示。
|
|
328
|
+
|
|
326
329
|
---
|
|
327
330
|
|
|
328
331
|
#### 子模块:会话(session)— `iam.session`
|
|
@@ -339,21 +342,19 @@ async function registerWithSession(username: string, password: string) {
|
|
|
339
342
|
|
|
340
343
|
```
|
|
341
344
|
refresh(refreshToken)
|
|
342
|
-
→
|
|
343
|
-
→
|
|
344
|
-
→
|
|
345
|
-
→
|
|
346
|
-
→ 创建全新 session(复用旧 session 的用户上下文)
|
|
347
|
-
→ 返回新的 TokenPair
|
|
345
|
+
→ 读取用户分区中的旧会话和版本,验证授权
|
|
346
|
+
→ 构造新令牌候选(尚未生效)
|
|
347
|
+
→ 单键 CAS 再核验版本和 refresh,原子撤销旧会话并发布唯一后继
|
|
348
|
+
→ 仅提交成功后返回 TokenPair
|
|
348
349
|
```
|
|
349
350
|
|
|
350
351
|
**单设备登录:**
|
|
351
352
|
|
|
352
353
|
```
|
|
353
354
|
create(options) [singleDevice=true]
|
|
354
|
-
→
|
|
355
|
-
→
|
|
356
|
-
→
|
|
355
|
+
→ 构造新 accessToken + refreshToken
|
|
356
|
+
→ 单键 CAS 同时替换该用户全部旧会话与索引
|
|
357
|
+
→ 提交失败不踢出原会话
|
|
357
358
|
```
|
|
358
359
|
|
|
359
360
|
##### 使用方式
|
|
@@ -394,10 +395,18 @@ async function forceSignOut(userId: string) {
|
|
|
394
395
|
##### 特别关注
|
|
395
396
|
|
|
396
397
|
- **纯缓存存储**:不落库;Memory 重启后丢失,共享 Redis 可跨 IAM 服务重启保留会话直至 TTL 或撤销
|
|
397
|
-
- **滑动续期**:`sliding: true` 时每次 `get()`
|
|
398
|
-
- **Refresh Rotation**:旧
|
|
398
|
+
- **滑动续期**:`sliding: true` 时每次 `get()` 单调延长 access 期限;分区保留期限另由 access/refresh 最大期限确定
|
|
399
|
+
- **Refresh Rotation**:旧 refresh 仅能成功消费一次;重复请求也可能来自合法并发或响应丢失,不能仅据此断言凭据泄露。
|
|
399
400
|
- **Token 强度**:256-bit crypto random(`crypto.getRandomValues(new Uint8Array(32))`),base64url 编码
|
|
400
401
|
|
|
402
|
+
`create({ maxAge })` 可覆盖配置默认时长(正安全整数秒),该值持久化为 `Session.maxAge`;签发和刷新返回的 `expiresIn` 均使用它。滑动续期按该时长延长 access,返回已成功提交的 `lastActiveAt/expiresAt`;迟到请求只延长时间,不缩短较新续期。显式 `update` 的有效日期补丁完整生效并产生新内容版本。客户端根据本次返回的 `expiresIn` 安排刷新,不能硬编码配置默认值。
|
|
403
|
+
|
|
404
|
+
access 与 refresh 独立到期:用户分区保留至所有 access/refresh 的最晚有效期,但 access 认证只检查自己的 `expiresAt`。access 过期仍可凭未过期、未撤销的 refresh 轮换;每次轮换从新签发时刻重新计算 `refreshTokenMaxAge`,旧 access/refresh 同时失效。删除 access、logoutAll、单设备替换均撤销待刷新记录;仅 revokeRefresh 保留仍有效的 access。刷新路由只校验 refresh,不能先要求旧 access 认证成功。
|
|
405
|
+
|
|
406
|
+
会话仓库将缓存读取/提交失败返回为失败 HaiResult,不解释为会话不存在或过期,也不执行清理补偿。用户会话及索引共用一个文档,批量授权更新不再逐 token 调用 get/ttl 或维护第二份索引;只按已确认的绝对期限清理。故障恢复后可重新读取并重试更新/退出,logoutAll 仍覆盖原有全部记录。结构损坏同样失败关闭并保留原值,交由运维处理,禁止用空文档覆盖未知授权状态。
|
|
407
|
+
|
|
408
|
+
Session.credentialVersion 绑定创建时的数据库凭据代际;CreateSessionOptions.expectedCredentialVersion 仅供已验证策略传入,旧代拒绝。省略它的受信低层 create 仍读取真实、启用的本地用户并绑定当前代,不能伪造缺少数据库记录的身份。
|
|
409
|
+
|
|
401
410
|
---
|
|
402
411
|
|
|
403
412
|
#### 子模块:授权(authz)— `iam.authz`
|
|
@@ -487,7 +496,7 @@ async function currentAuthorization(userId: string) {
|
|
|
487
496
|
- **通配符匹配**:`admin:*` 匹配 `admin:read`、`admin:write:detail` 等(单层 `*` 匹配冒号后全部)
|
|
488
497
|
- **超管角色**:配置的 `rbac.superAdminRole`(默认 `super_admin`)自动拥有所有权限
|
|
489
498
|
- **事务保护**:`deleteRole` / `deletePermission` 使用 DB 事务,先清关联再删实体
|
|
490
|
-
- **会话同步**:缓存更新为 best-effort;受保护请求使用 `iam.auth.verifyToken
|
|
499
|
+
- **会话同步**:缓存更新为 best-effort;受保护请求使用 `iam.auth.verifyToken`,先验证会话绑定的持久凭据代,再复核当前用户状态与凭据代并读取最新授权,任一步读取失败即拒绝。`iam.session.get/verifyToken` 同样检查持久用户启用状态与 `credentialVersion`,但角色/权限字段仍是会话快照,不能替代当前授权。`iam.authz.getUserAuthorization(userId)` 复用角色 JOIN 与批量权限查询。
|
|
491
500
|
- **种子数据**:`seedDefaultData: true` 时自动创建 admin/user/guest 三个默认角色及基础权限,幂等执行
|
|
492
501
|
|
|
493
502
|
---
|
|
@@ -749,6 +758,8 @@ await apiClient.iam.auth.changePassword({
|
|
|
749
758
|
})
|
|
750
759
|
```
|
|
751
760
|
|
|
761
|
+
浏览器条件入口仅导出配置 Schema、类型和 HaiIamError,不包含服务端 `iam` 实例;认证通过 HTTP/typed client 调用服务端。
|
|
762
|
+
|
|
752
763
|
### 密码重置
|
|
753
764
|
|
|
754
765
|
重置需要启用 `passwordReset.enabled` 和可用的通知回调/Reach 模板。申请成功仅表示请求已处理,不证明账户存在或用户已收信;发送或内部处理失败仍须处理失败 HaiResult。令牌只允许一次成功消费且有有效期/尝试限制,成功后撤销旧会话。管理员重置无需旧密码,但须由应用验证管理权限。
|
|
@@ -772,6 +783,8 @@ async function resetByAdministrator(userId: string, newPassword: string) {
|
|
|
772
783
|
}
|
|
773
784
|
```
|
|
774
785
|
|
|
786
|
+
缓存清理失败可能返回失败,但已提交的新密码与凭据代际不会回滚;重新登录,不用旧凭据自动重试。
|
|
787
|
+
|
|
775
788
|
### 一次性票据 — `iam.ticket`
|
|
776
789
|
|
|
777
790
|
短期、一次性能力票据。用于无法携带 Bearer Token 的通道(如 WebSocket URL):由已认证的 HTTP 请求签发一个绑定主体、用途与操作的票据,服务端在首次校验时**原子消费**(单次有效)。
|
|
@@ -807,6 +820,8 @@ async function consumeAudioTicket(ticket: string) {
|
|
|
807
820
|
|
|
808
821
|
### 配置驱动认证页面
|
|
809
822
|
|
|
823
|
+
密码登录只提交 `identifier` 和 `password`;`AuthPanel` 将 `LoginForm` 的 `username` 映射为 `identifier`。会话有效期由服务端 `session.maxAge` / `session.refreshTokenMaxAge` 配置决定,登录表单不提供按次登录的持久会话选项。
|
|
824
|
+
|
|
810
825
|
服务端通过 `iam.publicConfig` 提供白名单配置,或通过客户端的 `client.iam.auth.publicConfig()` 标准 HTTP contract 获取。只展示已开启且具备依赖的登录方式;LDAP 接入见上方可选配置,默认关闭;只有选用默认客户端时才安装 ldapts。
|
|
811
826
|
|
|
812
827
|
| 配置 | 用途 |
|
|
@@ -851,7 +866,7 @@ agreements:
|
|
|
851
866
|
|
|
852
867
|
#### 分布式安全与边界
|
|
853
868
|
|
|
854
|
-
|
|
869
|
+
所有节点共享同一数据库和 Redis;Memory 仅用于单进程测试。OTP 的验证码、尝试次数和冷却期保存在同一身份 key,通过 CAS 发布和消费;冲突拒绝旧代,消费/耗尽保留冷却期。密码重置的摘要、有效期、尝试次数和 resetVersion 保存在用户数据库行,策略失败计次,最终一条条件 SQL 同时改密、消费令牌并推进 credentialVersion;新令牌、后续尝试、停用或到期都会阻止迟到提交。发送重置请求只推进 resetVersion,不撤销现有会话;成功改密才推进 credentialVersion。认证策略保存已验证的凭据代际,建立会话/认证/刷新均校验数据库当前代际;缓存批量清理失败也不能使旧会话有效。通知只在成功发布后调用,第三参数为 { expiresAt, idempotencyKey },应用及 Reach Provider 必须用该稳定键持久化去重。异常/超时可能已送达,返回结果未知,不自动重发或回滚已发布凭据;没有接收端幂等保障时不能承诺外部副作用恰好一次。
|
|
855
870
|
|
|
856
871
|
可选字段(如 `ldap`)若尚未写入 YAML,请先声明该字段,或通过 `HAI_IAM` 的 JSON 值整体覆盖模块配置;已有字段支持 `HAI_IAM_LOGIN_OTP` 等环境变量覆盖。
|
|
857
872
|
|
|
@@ -872,10 +887,20 @@ if (!result.success) {
|
|
|
872
887
|
}
|
|
873
888
|
```
|
|
874
889
|
|
|
890
|
+
会话与 refresh 消费记录统一保存在 `hai:iam:sessions:{SHA256(userId)}` 版本化文档,不再使用独立 token 映射/索引。Bearer token 携带不可作为凭据的分区提示和 256 位随机值;客户端必须视为不透明字符串。`Session.version` 用于轮换提交校验。Memory 仅用于单进程测试,生产所有节点共享 Redis(含 Cluster)。并发冲突从最新文档重试,耗尽返回 CONFLICT;旧 refresh 不会产生第二个后继。若响应丢失后结果不明,调用方按失败关闭并重新登录,不假定旧凭据仍有效;全部会话仍受同一用户状态索引管理。
|
|
891
|
+
|
|
892
|
+
`iam.session.update(token, patch, expectedVersion)` 必须传入读取到的 Session.version。旧版本返回 CONFLICT,已撤销 token 返回 SESSION_NOT_FOUND;不会将旧内容重新写回或覆盖新会话。仅滑动时间戳更新不改变内容版本,允许正常并行请求。批量权限更新绑定首次读取的 token/版本集合,不将迟到数据应用到后续登录;遇到较新内容版本失败,调用方重新查询授权后再提交。
|
|
893
|
+
|
|
875
894
|
## 开发测试
|
|
876
895
|
|
|
896
|
+
公开示例从 `@h-ai/iam` 导入,区分可省略默认值的 `IamConfigInput` 与 Schema 解析后的 `IamConfig`。服务端依赖先初始化;`HaiResult` 先检查 `success`,会话修改使用读取到的 `version`,通知复用持久幂等键。内部策略/仓储示例标记为“模块内部上下文”,不作为应用的深层导入入口。检查器只验证示例类型;真实 Redis/数据库、LDAP 和通知投递仍需对应集成测试。见[模块 JSDoc 约定](../../.agents/instructions/module-conventions.md#jsdoc-与示例验证)。
|
|
897
|
+
|
|
877
898
|
```bash
|
|
878
|
-
pnpm
|
|
899
|
+
pnpm --filter @h-ai/iam typecheck
|
|
900
|
+
pnpm --filter @h-ai/iam lint
|
|
901
|
+
pnpm --filter @h-ai/iam build
|
|
902
|
+
pnpm --filter @h-ai/iam test
|
|
903
|
+
node scripts/check-module-jsdoc.mjs --package iam
|
|
879
904
|
```
|
|
880
905
|
|
|
881
906
|
## 许可证
|