@h-ai/iam 1.0.0-beta.2 → 1.0.0-beta.21

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 CHANGED
@@ -4,23 +4,9 @@
4
4
 
5
5
  [Framework home](../../README.en.md) · Related modules: [@h-ai/audit](../audit/README.en.md) · [@h-ai/cache](../cache/README.en.md) · [@h-ai/core](../core/README.en.md) · [@h-ai/crypto](../crypto/README.en.md) · [@h-ai/reldb](../reldb/README.en.md) · [@h-ai/reach](../reach/README.en.md)
6
6
 
7
- ## Introduction
8
-
9
7
  Identity and access management through a unified `iam` object for authentication, sessions, authorization, and user management.
10
8
 
11
- ## Core features
12
-
13
- | Feature | Description |
14
- | -------------------- | ---------------------------------------------------------------------------------------------------------------------- |
15
- | **Authentication** | Password, OTP (email/SMS codes), LDAP, and API Key strategies |
16
- | **Sessions** | Stateful sessions (random access tokens + cache, optional sliding renewal) |
17
- | **Authorization** | RBAC roles and permissions (batched DB reads, wildcard matching) |
18
- | **User management** | Registration, queries, updates, password reset, and admin password reset |
19
- | **API Key** | Create, revoke, and verify API keys with scopes and expiration |
20
- | **One-time tickets** | Short-lived capability tickets (issue / atomic consume) for channels such as WebSocket that cannot carry Bearer tokens |
21
- | **Frontend client** | HTTP API contracts for independent frontends |
22
-
23
- ## Getting started
9
+ ## Quick Start
24
10
 
25
11
  ### Installation
26
12
 
@@ -38,6 +24,223 @@ pnpm add @h-ai/iam
38
24
  - `@h-ai/reach`: **optional**, installed, initialized and injected by the application for standard notifications; custom callbacks do not require Reach.
39
25
  - `ldapts`: **optional**, loaded only when LDAP is explicitly enabled without an injected `ldapClientFactory`.
40
26
 
27
+ ```ts
28
+ import { cache } from '@h-ai/cache'
29
+ import { core } from '@h-ai/core'
30
+ import { iam } from '@h-ai/iam'
31
+ import { reldb } from '@h-ai/reldb'
32
+
33
+ // 基础密码认证不需要安装 Reach 或 ldapts。
34
+ async function example() {
35
+ try {
36
+ const db = await reldb.init({ type: 'sqlite', database: './data.db' })
37
+ if (!db.success)
38
+ return db
39
+ const cached = await cache.init({ type: 'memory' })
40
+ if (!cached.success)
41
+ return cached
42
+ const initialized = await iam.init({})
43
+ if (!initialized.success)
44
+ return initialized
45
+ const registered = await iam.user.register({ username: 'demo', password: 'Password123' })
46
+ if (!registered.success)
47
+ return registered
48
+ const login = await iam.auth.login({ identifier: 'demo', password: 'Password123' })
49
+ if (!login.success)
50
+ return login
51
+ return await iam.auth.verifyToken(login.data.tokens.accessToken)
52
+ }
53
+ finally {
54
+ // 应用关闭时按依赖的反向顺序释放。
55
+ await iam.close()
56
+ await cache.close()
57
+ await reldb.close()
58
+ }
59
+ }
60
+
61
+ const result = await example()
62
+ if (!result.success)
63
+ core.logger.error('Authentication example failed', { code: result.error.code })
64
+ ```
65
+
66
+ LDAP is disabled by default. Public OTP methods require `otp` settings and send callbacks; password reset requires a notification callback.
67
+
68
+ `iam.config` returns a **redacted configuration snapshot**. Sensitive LDAP `url` / `bindPassword` and similar fields are hidden automatically. Use it to inspect state, not as the original configuration source.
69
+
70
+ ## Features
71
+
72
+ | Feature | Description |
73
+ | -------------------- | ---------------------------------------------------------------------------------------------------------------------- |
74
+ | **Authentication** | Password, OTP (email/SMS codes), LDAP, and API Key strategies |
75
+ | **Sessions** | Stateful sessions (random access tokens + cache, optional sliding renewal) |
76
+ | **Authorization** | RBAC roles and permissions (batched DB reads, wildcard matching) |
77
+ | **User management** | Registration, queries, updates, password reset, and admin password reset |
78
+ | **API Key** | Create, revoke, and verify API keys with scopes and expiration |
79
+ | **One-time tickets** | Short-lived capability tickets (issue / atomic consume) for channels such as WebSocket that cannot carry Bearer tokens |
80
+ | **Frontend client** | HTTP API contracts for independent frontends |
81
+
82
+ ### Operations and results
83
+
84
+ Use the complete initialization flow in this section first. The host validates and authorizes example business values; check success before reading data from asynchronous domain results. Here void means completion without data, null means absent; synchronous factories/tools return the listed type directly.
85
+
86
+ IDs/codes must come from authorized business flows. Tokens/tickets are not user IDs. options/data follow the operation input type, expectedVersion comes from the read and tx from the active transaction. Low-level writes do not authorize administrators for the host.
87
+
88
+ #### iam.auth
89
+
90
+ Authentication takes strategy credentials and trusted request context; registerAndLogin takes registration input. Success yields user, tokens and roles/permissions; failure creates no session.
91
+
92
+ | Entry | Input parameters | Successful result | Call example |
93
+ | --------------------------- | -------------------- | ----------------- | --------------------------------------------------------- |
94
+ | `iam.auth.login` | credentials, context | `AuthResult` | `await iam.auth.login({ identifier, password })` |
95
+ | `iam.auth.loginWithOtp` | credentials, context | `AuthResult` | `await iam.auth.loginWithOtp({ identifier, code })` |
96
+ | `iam.auth.loginWithLdap` | credentials, context | `AuthResult` | `await iam.auth.loginWithLdap({ username, password })` |
97
+ | `iam.auth.loginWithApiKey` | credentials, context | `AuthResult` | `await iam.auth.loginWithApiKey({ key: rawKey })` |
98
+ | `iam.auth.registerAndLogin` | options, context | `AuthResult` | `await iam.auth.registerAndLogin({ username, password })` |
99
+
100
+ #### iam.auth
101
+
102
+ Challenge takes operation, OTP identifier/challenge; token verification yields current Session, logout void. Reauthentication takes token/password/operation and yields a bound ticket; sensitive check takes token/operation/ticket and yields void.
103
+
104
+ | Entry | Input parameters | Successful result | Call example |
105
+ | ---------------------------------- | -------------------------------- | ---------------------- | -------------------------------------------------------------------------------------- |
106
+ | `iam.auth.createCaptcha` | input | `IamCaptchaChallenge` | `await iam.auth.createCaptcha({ operation: 'login' })` |
107
+ | `iam.auth.sendOtp` | identifier, options | `{ expiresAt: Date; }` | `await iam.auth.sendOtp(identifier)` |
108
+ | `iam.auth.verifyToken` | accessToken | `Session` | `await iam.auth.verifyToken(accessToken)` |
109
+ | `iam.auth.logout` | accessToken | `void` | `await iam.auth.logout(accessToken)` |
110
+ | `iam.auth.reauthenticate` | accessToken, password, operation | `IssuedTicket` | `await iam.auth.reauthenticate(accessToken, password, 'users:delete')` |
111
+ | `iam.auth.checkSensitiveOperation` | accessToken, operation, ticket | `void` | `await iam.auth.checkSensitiveOperation(accessToken, 'users:delete', operationTicket)` |
112
+
113
+ #### iam.user
114
+
115
+ Users take registration input, trusted IDs, filters/page or token/patch: return registration result, user/null, users, page or updated user; delete yields void. Authorize admin actions at the entry.
116
+
117
+ | Entry | Input parameters | Successful result | Call example |
118
+ | ---------------------------- | ----------------- | ----------------------- | ------------------------------------------------------------------------- |
119
+ | `iam.user.createUser` | options | `RegisterResult` | `await iam.user.createUser({ username, password })` |
120
+ | `iam.user.register` | options | `RegisterResult` | `await iam.user.register({ username, password })` |
121
+ | `iam.user.inviteUser` | options | `RegisterResult` | `await iam.user.inviteUser({ username, email })` |
122
+ | `iam.user.getUser` | userId, options | `User \| null` | `await iam.user.getUser(userId)` |
123
+ | `iam.user.getUsers` | userIds, options | `User[]` | `await iam.user.getUsers([userId], { include: ['roles'] })` |
124
+ | `iam.user.listUsers` | options | `PaginatedResult<User>` | `await iam.user.listUsers({ page: 1, pageSize: 20 })` |
125
+ | `iam.user.getCurrentUser` | accessToken | `User` | `await iam.user.getCurrentUser(accessToken)` |
126
+ | `iam.user.updateCurrentUser` | accessToken, data | `User` | `await iam.user.updateCurrentUser(accessToken, { displayName: 'Alice' })` |
127
+ | `iam.user.updateUser` | userId, data | `User` | `await iam.user.updateUser(userId, { displayName: 'Alice' })` |
128
+ | `iam.user.deleteUser` | userId | `void` | `await iam.user.deleteUser(userId)` |
129
+
130
+ #### iam.user
131
+
132
+ Password flows take user ID/old+new passwords, renewal input, reset identifier or purpose token and yield void. validatePassword synchronously checks policy; scanInactiveAccounts returns the suspended count.
133
+
134
+ | Entry | Input parameters | Successful result | Call example |
135
+ | ------------------------------------ | ------------------------------------- | ----------------- | --------------------------------------------------------------------------------- |
136
+ | `iam.user.adminResetPassword` | userId, newPassword | `void` | `await iam.user.adminResetPassword(userId, newPassword)` |
137
+ | `iam.user.renewPassword` | input | `void` | `await iam.user.renewPassword({ identifier, password, newPassword })` |
138
+ | `iam.user.changePassword` | userId, oldPassword, newPassword | `void` | `await iam.user.changePassword(userId, oldPassword, newPassword)` |
139
+ | `iam.user.changeCurrentUserPassword` | accessToken, oldPassword, newPassword | `void` | `await iam.user.changeCurrentUserPassword(accessToken, oldPassword, newPassword)` |
140
+ | `iam.user.requestPasswordReset` | identifier, options | `void` | `await iam.user.requestPasswordReset(identifier)` |
141
+ | `iam.user.confirmPasswordReset` | token, newPassword | `void` | `await iam.user.confirmPasswordReset(resetToken, newPassword)` |
142
+ | `iam.user.validatePassword` | password | `void` | `iam.user.validatePassword(newPassword)` |
143
+ | `iam.user.scanInactiveAccounts` | No arguments | `number` | `await iam.user.scanInactiveAccounts()` |
144
+
145
+ #### iam.authz
146
+
147
+ Authorization takes trusted user IDs and permission code and yields roles/permissions, boolean, permission/role arrays or ID-keyed Maps. Never trust client-declared roles.
148
+
149
+ | Entry | Input parameters | Successful result | Call example |
150
+ | ------------------------------------- | ------------------ | ----------------------------------------- | -------------------------------------------------------- |
151
+ | `iam.authz.getUserAuthorization` | userId | `Pick<Session, "roles" \| "permissions">` | `await iam.authz.getUserAuthorization(userId)` |
152
+ | `iam.authz.checkPermission` | userId, permission | `boolean` | `await iam.authz.checkPermission(userId, 'orders:read')` |
153
+ | `iam.authz.getUserPermissions` | userId | `Permission[]` | `await iam.authz.getUserPermissions(userId)` |
154
+ | `iam.authz.getUserRoles` | userId | `Role[]` | `await iam.authz.getUserRoles(userId)` |
155
+ | `iam.authz.getUserRolesForMany` | userIds | `Map<string, Role[]>` | `await iam.authz.getUserRolesForMany([userId])` |
156
+ | `iam.authz.getRolePermissionsForMany` | roleIds | `Map<string, Permission[]>` | `await iam.authz.getRolePermissionsForMany([roleId])` |
157
+
158
+ #### iam.authz
159
+
160
+ Assignment takes user/role/permission IDs with optional tx and yields void while invalidating shared authorization caches. syncRoles supplies the complete desired set.
161
+
162
+ | Entry | Input parameters | Successful result | Call example |
163
+ | ------------------------------------ | ------------------------ | ----------------- | ---------------------------------------------------------------- |
164
+ | `iam.authz.assignRole` | userId, roleId, tx | `void` | `await iam.authz.assignRole(userId, roleId)` |
165
+ | `iam.authz.removeRole` | userId, roleId, tx | `void` | `await iam.authz.removeRole(userId, roleId)` |
166
+ | `iam.authz.syncRoles` | userId, roleIds, tx | `void` | `await iam.authz.syncRoles(userId, [roleId])` |
167
+ | `iam.authz.assignPermissionToRole` | roleId, permissionId, tx | `void` | `await iam.authz.assignPermissionToRole(roleId, permissionId)` |
168
+ | `iam.authz.removePermissionFromRole` | roleId, permissionId, tx | `void` | `await iam.authz.removePermissionFromRole(roleId, permissionId)` |
169
+
170
+ #### iam.authz
171
+
172
+ RBAC administration takes entity input, ID/code, pagination or patch. Create/update yield entity, lookup entity/null, full listing a page, role permissions an array, user counts a Map, deletion void.
173
+
174
+ | Entry | Input parameters | Successful result | Call example |
175
+ | ------------------------------- | ---------------- | ----------------------------- | ---------------------------------------------------------------------------------------------------- |
176
+ | `iam.authz.createRole` | role, tx | `Role` | `await iam.authz.createRole({ code: 'editor', name: 'Editor', description: null, isSystem: false })` |
177
+ | `iam.authz.getRole` | roleId | `Role \| null` | `await iam.authz.getRole(roleId)` |
178
+ | `iam.authz.getRoleByCode` | code | `Role \| null` | `await iam.authz.getRoleByCode('editor')` |
179
+ | `iam.authz.getAllRoles` | options | `PaginatedResult<Role>` | `await iam.authz.getAllRoles({ page: 1, pageSize: 20 })` |
180
+ | `iam.authz.getRoleUserCounts` | roleIds | `Map<string, number>` | `await iam.authz.getRoleUserCounts([roleId])` |
181
+ | `iam.authz.updateRole` | roleId, data, tx | `Role` | `await iam.authz.updateRole(roleId, { name: 'Editor' })` |
182
+ | `iam.authz.deleteRole` | roleId, tx | `void` | `await iam.authz.deleteRole(roleId)` |
183
+ | `iam.authz.createPermission` | permission, tx | `Permission` | `await iam.authz.createPermission({ code: 'orders:read', name: 'Read orders', type: 'api' })` |
184
+ | `iam.authz.getPermission` | permissionId | `Permission \| null` | `await iam.authz.getPermission(permissionId)` |
185
+ | `iam.authz.getPermissionByCode` | code | `Permission \| null` | `await iam.authz.getPermissionByCode('orders:read')` |
186
+ | `iam.authz.getAllPermissions` | options | `PaginatedResult<Permission>` | `await iam.authz.getAllPermissions({ page: 1, pageSize: 20 })` |
187
+ | `iam.authz.deletePermission` | permissionId, tx | `void` | `await iam.authz.deletePermission(permissionId)` |
188
+ | `iam.authz.getRolePermissions` | roleId | `Permission[]` | `await iam.authz.getRolePermissions(roleId)` |
189
+
190
+ #### iam.authz
191
+
192
+ Bind reauthentication purposes: optional operation yields sensitive operation names; permissionId plus operations saves the mapping and yields void.
193
+
194
+ | Entry | Input parameters | Successful result | Call example |
195
+ | -------------------------------------------- | ------------------------ | ----------------- | ---------------------------------------------------------------------------------- |
196
+ | `iam.authz.getSensitiveOperations` | operation | `string[]` | `await iam.authz.getSensitiveOperations('users:delete')` |
197
+ | `iam.authz.setPermissionSensitiveOperations` | permissionId, operations | `void` | `await iam.authz.setPermissionSensitiveOperations(permissionId, ['users:delete'])` |
198
+
199
+ #### iam.session
200
+
201
+ Low-level sessions: trusted creation yields Session, token get Session/null and verification Session; update requires expectedVersion, user deletion yields count, refresh TokenPair, other mutations void. Protected requests use auth.verifyToken.
202
+
203
+ | Entry | Input parameters | Successful result | Call example |
204
+ | ------------------------------- | ---------------------------------- | ----------------- | ---------------------------------------------------------------------- |
205
+ | `iam.session.create` | options | `Session` | `await iam.session.create(trustedSessionInput)` |
206
+ | `iam.session.get` | accessToken | `Session \| null` | `await iam.session.get(accessToken)` |
207
+ | `iam.session.verifyToken` | accessToken | `Session` | `await iam.session.verifyToken(accessToken)` |
208
+ | `iam.session.update` | accessToken, data, expectedVersion | `void` | `await iam.session.update(accessToken, sessionPatch, expectedVersion)` |
209
+ | `iam.session.delete` | accessToken | `void` | `await iam.session.delete(accessToken)` |
210
+ | `iam.session.deleteByUserId` | userId | `number` | `await iam.session.deleteByUserId(userId)` |
211
+ | `iam.session.refresh` | refreshToken | `TokenPair` | `await iam.session.refresh(refreshToken)` |
212
+ | `iam.session.revokeRefresh` | refreshToken | `void` | `await iam.session.revokeRefresh(refreshToken)` |
213
+ | `iam.session.patchUserSessions` | userId, updates | `void` | `await iam.session.patchUserSessions(userId, sessionFieldUpdates)` |
214
+
215
+ #### iam.apiKey
216
+
217
+ API credentials: user/options return rawKey only at creation; user/page listing yields a page, ID lookup entry/null, revoke void, raw verification a safe ApiKey DTO.
218
+
219
+ | Entry | Input parameters | Successful result | Call example |
220
+ | ------------------------- | ---------------- | ------------------------- | --------------------------------------------------------------------------------------- |
221
+ | `iam.apiKey.createApiKey` | userId, options | `CreateApiKeyResult` | `await iam.apiKey.createApiKey(userId, { name: 'reporting', scopes: ['orders:read'] })` |
222
+ | `iam.apiKey.listApiKeys` | userId, options | `PaginatedResult<ApiKey>` | `await iam.apiKey.listApiKeys(userId, { page: 1, pageSize: 20 })` |
223
+ | `iam.apiKey.getApiKey` | keyId | `ApiKey \| null` | `await iam.apiKey.getApiKey(keyId)` |
224
+ | `iam.apiKey.revokeApiKey` | keyId | `void` | `await iam.apiKey.revokeApiKey(keyId)` |
225
+ | `iam.apiKey.verifyApiKey` | rawKey | `ApiKey` | `await iam.apiKey.verifyApiKey(rawKey)` |
226
+
227
+ #### iam.ticket
228
+
229
+ One-use purpose tickets take subject/purpose/TTL and yield a ticket; consume takes ticket/binding and yields consumed claims. An ordinary token is not a purpose credential.
230
+
231
+ | Entry | Input parameters | Successful result | Call example |
232
+ | -------------------- | ---------------- | ----------------- | ---------------------------------------------------------- |
233
+ | `iam.ticket.issue` | options | `IssuedTicket` | `await iam.ticket.issue(ticketInput)` |
234
+ | `iam.ticket.consume` | ticket, options | `ConsumedTicket` | `await iam.ticket.consume(operationTicket, ticketBinding)` |
235
+
236
+ #### iam
237
+
238
+ No-arg public policy lookup yields IamPublicConfig without directory, notification or database credentials.
239
+
240
+ | Entry | Input parameters | Successful result | Call example |
241
+ | --------------------- | ---------------- | ----------------- | ----------------------------- |
242
+ | `iam.getPublicConfig` | No arguments | `IamPublicConfig` | `await iam.getPublicConfig()` |
243
+
41
244
  ### Optional LDAP and notifications
42
245
 
43
246
  Password authentication, users and RBAC work without LDAP or Reach. Both are optional peer dependencies: install only the integrations you use. The snippets below run inside application startup/authentication functions after reldb/cache initialization; `username/password` are validated input.
@@ -150,7 +353,12 @@ import { mailService, smsService } from './notifications.js'
150
353
  const initialized = await iam.init({
151
354
  otp: { email: true, sms: true },
152
355
  onOtpSendEmail: async (email, code, context) => {
153
- await mailService.send({ to: email, subject: 'Verification code', text: code, idempotencyKey: context.idempotencyKey })
356
+ await mailService.send({
357
+ to: email,
358
+ subject: 'Verification code',
359
+ text: code,
360
+ idempotencyKey: context.idempotencyKey
361
+ })
154
362
  },
155
363
  onOtpSendSms: async (phone, code, context) => {
156
364
  await smsService.send({ to: phone, code, idempotencyKey: context.idempotencyKey })
@@ -161,58 +369,19 @@ const initialized = await iam.init({
161
369
  throw new Error('Reset recipient unavailable')
162
370
  const url = new URL('https://app.example.com/auth/reset-password')
163
371
  url.searchParams.set('token', token)
164
- await mailService.send({ to: user.email, subject: 'Password reset', text: url.toString(), expiresAt: context.expiresAt, idempotencyKey: context.idempotencyKey })
372
+ await mailService.send({
373
+ to: user.email,
374
+ subject: 'Password reset',
375
+ text: url.toString(),
376
+ expiresAt: context.expiresAt,
377
+ idempotencyKey: context.idempotencyKey
378
+ })
165
379
  },
166
380
  })
167
381
  if (!initialized.success)
168
382
  return initialized
169
383
  ```
170
384
 
171
- ### Quick start
172
-
173
- ```ts
174
- import { cache } from '@h-ai/cache'
175
- import { core } from '@h-ai/core'
176
- import { iam } from '@h-ai/iam'
177
- import { reldb } from '@h-ai/reldb'
178
-
179
- // 基础密码认证不需要安装 Reach 或 ldapts。
180
- async function example() {
181
- try {
182
- const db = await reldb.init({ type: 'sqlite', database: './data.db' })
183
- if (!db.success)
184
- return db
185
- const cached = await cache.init({ type: 'memory' })
186
- if (!cached.success)
187
- return cached
188
- const initialized = await iam.init({})
189
- if (!initialized.success)
190
- return initialized
191
- const registered = await iam.user.register({ username: 'demo', password: 'Password123' })
192
- if (!registered.success)
193
- return registered
194
- const login = await iam.auth.login({ identifier: 'demo', password: 'Password123' })
195
- if (!login.success)
196
- return login
197
- return await iam.auth.verifyToken(login.data.tokens.accessToken)
198
- }
199
- finally {
200
- // 应用关闭时按依赖的反向顺序释放。
201
- await iam.close()
202
- await cache.close()
203
- await reldb.close()
204
- }
205
- }
206
-
207
- const result = await example()
208
- if (!result.success)
209
- core.logger.error('Authentication example failed', { code: result.error.code })
210
- ```
211
-
212
- LDAP is disabled by default. Public OTP methods require `otp` settings and send callbacks; password reset requires a notification callback.
213
-
214
- `iam.config` returns a **redacted configuration snapshot**. Sensitive LDAP `url` / `bindPassword` and similar fields are hidden automatically. Use it to inspect state, not as the original configuration source.
215
-
216
385
  ### Design
217
386
 
218
387
  #### Architecture
@@ -589,7 +758,9 @@ async function listReaders(page: number) {
589
758
 
590
759
  async function loadUsers(userIds: readonly string[]) {
591
760
  // 最多 1000 个输入 ID;去重保留输入顺序,缺失 ID 省略。
592
- return iam.user.getUsers(userIds, { include: ['roles'] })
761
+ return iam.user.getUsers(userIds, {
762
+ include: ['roles']
763
+ })
593
764
  }
594
765
 
595
766
  async function makeUserDormant(userId: string) {
@@ -859,21 +1030,6 @@ agreements:
859
1030
  # privacyPolicyUrl: https://example.com/privacy
860
1031
  ```
861
1032
 
862
- ### Error handling
863
-
864
- All operations return `HaiResult<T>`; branch on `HaiIamError`:
865
-
866
- ```ts
867
- import { HaiIamError, iam } from '@h-ai/iam'
868
-
869
- const result = await iam.auth.login({ identifier: 'admin', password: 'wrong' })
870
- if (!result.success) {
871
- if (result.error.code === HaiIamError.INVALID_CREDENTIALS.code) {
872
- // 用户名或密码错误
873
- }
874
- }
875
- ```
876
-
877
1033
  ### Optional integrations
878
1034
 
879
1035
  Role lists aggregate actual member counts for the current page with one `iam.authz.getRoleUserCounts(roleIds)` call, returning `HaiResult<Map<string, number>>`. Unassigned roles get 0; failed queries show loading errors rather than fabricated zero counts.
@@ -888,7 +1044,7 @@ Redis caches nonsecret account/credential state, role/permission summaries, sens
888
1044
 
889
1045
  Failure to register invalidation rejects a write before mutation. Failed invalidation after commit preserves the successful mutation and persistent dirty marker, so later checks keep using DB. Crashed writers or unknown commit results never expire that marker by TTL; alert and repair cache state in a maintenance window after confirming no IAM write transaction remains. All nodes must upgrade together and write via IAM APIs. Raw SQL, old writers and independent table edits require quiesced writes and full IAM cache invalidation. Verified raw Key cache hits avoid repeated expensive hashes; lastUsedAt is sampled during cold verification, with up to about one minute lag.
890
1046
 
891
- ## Self-hosted interactive challenges
1047
+ ### Self-hosted interactive challenges
892
1048
 
893
1049
  captcha.enabled defaults to false. Enable self-hosted ALTCHA proof of work with ttlSeconds (default120, 1–600 seconds), cost (default5000, 100–50000) and maxCounter (default100, 1–2000). No external CAPTCHA account is needed. HMAC secrets and one-use state remain in the existing shared Cache; production still requires a shared backend. Computational challenges increase automation cost and do not guarantee a human requester.
894
1050
 
@@ -896,7 +1052,7 @@ captcha.enabled defaults to false. Enable self-hosted ALTCHA proof of work with
896
1052
 
897
1053
  Standard Kit/Serv expose POST /auth/captcha (Kit same-origin /api prefix) and preserve captcha. iam.publicConfig.captcha exposes only enabled. AuthPanel/createAuthActions automatically create, asynchronously solve and submit a fresh proof when enabled, and do not load the solver when disabled. Custom forms can reuse the protocol. Enabled UI configuration without createCaptcha fails closed; cancellation/unmount stops subsequent login. Proofs are always redacted as credentials and are not persisted in browser storage. Errors030/031/032/033 mean required, invalid/consumed/expired, disabled and unavailable respectively.
898
1054
 
899
- ## Optional password and login protection
1055
+ ### Optional password and login protection
900
1056
 
901
1057
  Public login and `renewPassword` return `INVALID_CREDENTIALS` (“Username or password is incorrect.”) for missing, passwordless, dormant, closed, expired and locked accounts. Restricted accounts do not verify further password guesses: correct and incorrect passwords receive the same response without changing the existing lock counters. Authenticated administration can still inspect the account state. Password computation or storage faults remain infrastructure failures and do not count as incorrect passwords.
902
1058
 
@@ -914,17 +1070,17 @@ Reusing the current password returns `HaiIamError.PASSWORD_REUSE_NOT_ALLOWED` (`
914
1070
 
915
1071
  `reauthentication.enabled/operations` select sensitive operations; `ttlSeconds` bounds proof lifetime. `reauthenticate(token, password, operation)` issues a single-use proof; authorized handlers call `checkSensitiveOperation(token, operation, ticket)` after permission/resource checks. Proofs bind subject/session/operation and grant no extra permissions. Serv IAM writes/admin guards read `x-hai-reauthentication`; Kit business handlers use `kit.guard.requireSensitive(iam, event, operation)` with project-defined IDs. Keep proof tickets briefly in memory. Public configuration exposes switches/operation IDs only. In the standard admin/fullstack UI, an already signed-in user who selects a sensitive action confirms their current account password in a dialog. Cancel does not submit, an incorrect password stops the action, and success continues the original action without creating another login session.
916
1072
 
917
- RBAC permissions accept `sensitiveOperations: string[]`, persisted as nullable `sensitive_operations`. Trusted management code can call `iam.authz.setPermissionSensitiveOperations(permissionId, operations)` and must check `success`. An empty array removes only that permission's additions. With `reauthentication.enabled`, rules from all permission records are combined with file `operations` and enforced live. IDs must match actual business guards; they grant no permissions. Async `iam.getPublicConfig()` returns the combined public policy and fails on database read errors; synchronous `iam.publicConfig` retains the file snapshot only. Disabling RBAC ignores database rules. Migration adds a nullable JSON column while preserving authorization relationships.
1073
+ RBAC permissions accept `sensitiveOperations: string[]`, persisted as nullable `sensitive_operations`. Trusted management code can call `iam.authz.setPermissionSensitiveOperations(permissionId, operations)` and must check `success`. An empty array removes only that permission's additions. With `reauthentication.enabled`, rules from all permission records are combined with file `operations` and enforced live. IDs must match actual business guards; they grant no permissions. Async `iam.getPublicConfig()` returns the combined public policy and fails on database read errors; synchronous `iam.publicConfig` retains the file snapshot only. Disabling RBAC ignores database rules.
918
1074
 
919
- ## Account type and lifecycle
1075
+ ### Account type and lifecycle
920
1076
 
921
1077
  Users default to `accountType: 'permanent'` and `accountStatus: 'active'`. Administrative code can create a `temporary` account with `accountExpiresAt`, or use `updateUser` to set `dormant` or `closed`. State and credential generation change in one transaction. Expired or inactive accounts are rejected by every login method, access verification, and refresh. Explicit reactivation requires a new login; old tokens do not revive. `accountStatus` is the sole lifecycle field; there is no separate user enable/disable switch. `closed` retains the record; `deleteUser` still physically removes it, while the application owns personal-data retention and cancellation workflows.
922
1078
 
923
- Before beta.1, stop older writers and back up the database. Migration v6 converts an old false/NULL `enabled` value with active status to dormant, retains dormant/closed states, invalidates affected credentials, and drops the column. User inputs, filters and sorting now use only `accountStatus`. An image rollback does not restore the old schema; restoring an older version also requires a verified database backup. Authorized administrators can reset passwords in any account state without changing that state. `password.requireInitialChange` controls the subsequent password-change requirement.
1079
+ Authorized administrators may reset passwords in any accountStatus, without changing dormant/closed to active or reviving invalidated credentials. `password.requireInitialChange` controls the next password-change requirement. User filters and sorting use that same accountStatus field.
924
1080
 
925
1081
  `accounts.dormantAfterDays` defaults to `0`, disabling automatic dormancy. With `90`, IAM scans active accounts with no successful login in 90 days at `accounts.scanIntervalSeconds` intervals (3600 seconds by default); accounts never used start at creation. Only successful authentication updates `lastLoginAt`. Conditional database updates make repeated scans across nodes idempotent. Trusted code or an existing scheduler may also call `iam.user.scanInactiveAccounts()`. `close()` stops the timer and waits for its in-flight scan. Public registration must not expose administrative lifecycle fields.
926
1082
 
927
- ## Optional session security policies
1083
+ ### Optional session security policies
928
1084
 
929
1085
  Set `session.idleTimeout: 1800` to require a new login after 30 minutes without a successful authenticated request; `0` disables it. Access reads, activity commits, and refresh rotation all enforce this limit. A long-lived refresh token cannot revive an idle session. Automatic refresh does not extend the idle deadline. Background business polling also counts as authenticated activity, so applications must choose polling behavior that matches their user activity policy.
930
1086
 
@@ -932,9 +1088,9 @@ Set `session.idleTimeout: 1800` to require a new login after 30 minutes without
932
1088
 
933
1089
  When these deadlines are disabled, access and refresh retain their independent lifetimes. Every successful login creates new random credentials. `SESSION_LIMIT_EXCEEDED` reports the configured session quota.
934
1090
 
935
- ## Schema and credential notifications
1091
+ ### Schema and credential notifications
936
1092
 
937
- After RelDB initialization, `migrateIam({ dryRun: true })` previews changes without writes. iam.init upgrades supported credential/reset columns, retaining users/passwords and permissions. Back up and stop old writers first. Legacy sessions without credential versions must sign in again; never roll back credential generations to restore authorization. Default Reach notifications use sensitive/expiresAt/idempotencyKey without plaintext persistence or delayed replay. Custom callbacks must enforce the same expiry and credential protection.
1093
+ After RelDB initialization, `migrateIam({ dryRun: true })` previews persistence preparation without writes. iam.init prepares persistence and credential/reset fields while retaining users, passwords and permissions. Back up and stop conflicting writes before structural changes. Sessions must match the current credential generation; mismatches require login, never lowering the stored generation. Default Reach notifications use sensitive/expiresAt/idempotencyKey without plaintext persistence or delayed replay. Custom callbacks must enforce the same expiry and credential protection.
938
1094
 
939
1095
  When audit is initialized and capture.enabled is on, IAM records important account, authentication, password, authorization, API Key and session events, including failed/denied outcomes and safe reason codes. Initialize audit before IAM and close in reverse order. Passwords, one-time proofs and tokens are never copied into events. Only successful login establishes an actor; trusted service/CLI callers supply actor/correlation through audit.withContext. Events inside an external authorization transaction commit or roll back with that transaction; record failed attempts after rollback at the caller boundary. Audit failures emit diagnostics without converting an applied business result into an unsafe retry.
940
1096
 
@@ -942,13 +1098,38 @@ When audit is initialized and capture.enabled is on, IAM records important accou
942
1098
 
943
1099
  For in-place recovery, use UI `createSessionRecovery` / `SessionRecoveryDialog` with SDK `recoverSession`. Normal password/OTP/LDAP login policies still apply; `expectedUserId` restricts session issuance to the original principal. Only an explicitly authentication-rejected request may be retried once. Ambiguous network failures are not replayed. In-memory drafts lost through document reload/closure cannot be restored.
944
1100
 
945
- ## Page exit session policy
1101
+ ### Page exit session policy
946
1102
 
947
1103
  `session.endOnPageHide` defaults to `false`. Hosts call `actions.bindPageExit({ enabled: config.session.endOnPageHide })` and invoke its returned cleanup on unmount; standard admin/H5/fullstack templates already bind it. A non-BFCache `pagehide` (including reload and full document navigation) makes a best effort POST to `/auth/session/close`, revoking the held access and related refresh through the existing CSRF/encrypted transport with `keepalive` and no automatic refresh. This response does not clear cookies/local credentials, avoiding a late old response overwriting a new login. Explicit logout still revokes and clears current credentials normally. Browsers may omit page exit requests; configure server `idleTimeout`/`absoluteTimeout` as appropriate. Immediate revocation on browser close is not guaranteed.
948
1104
 
949
1105
  The standard `/auth/me` endpoint also checks `iam.profile.update` / `iam.profile.avatar`. Mixed updates prefer an enabled profile rule, otherwise the avatar rule. `authz.getSensitiveOperations(operation?)` can query one operation; omit it to list database rules. Super-administrator identity is read from shared roles on every check. Default seeding rereads concurrent unique conflicts and propagates permission-assignment failures.
950
1106
 
951
- ## Development and testing
1107
+ ## Configuration
1108
+
1109
+ Lifecycle: init takes backend/business configuration; use the module only after success. Await close before releasing borrowed dependencies. Follow the quick-start example and public signatures for exact return values.
1110
+
1111
+ The host loads and validates configuration before passing it to this module. Keep secrets in private server environment files. Select the backend, capacity, deadlines and logging for the actual use case; reinitialize and check the result after changes.
1112
+
1113
+ [Per-option purpose, use cases and examples](../../docs/usage/configuration/identity.md).
1114
+
1115
+ ## Error Handling
1116
+
1117
+ Check result.success for domain operations. Handle failures with the module error definitions and safe messages; retain codes, correlation IDs and sanitized causes in server logs. Synchronous tools, factories, stream iteration and lifecycle calls follow their own signatures. A completed Promise alone is not business success. See each feature for recovery limits.
1118
+
1119
+ All operations return `HaiResult<T>`; branch on `HaiIamError`:
1120
+
1121
+ ```ts
1122
+ import { HaiIamError, iam } from '@h-ai/iam'
1123
+
1124
+ const result = await iam.auth.login({ identifier: 'admin', password: 'wrong' })
1125
+ if (!result.success) {
1126
+ if (result.error.code === HaiIamError.INVALID_CREDENTIALS.code) {
1127
+ // 用户名或密码错误
1128
+ }
1129
+ }
1130
+ ```
1131
+
1132
+ ## Development
952
1133
 
953
1134
  Public examples import from `@h-ai/iam` and distinguish defaultable `IamConfigInput` from parsed `IamConfig`. Initialize server dependencies, check `HaiResult.success`, update sessions with their observed `version`, and reuse persistent notification idempotency keys. Strategy/repository examples marked as internal context do not define application deep imports. The checker verifies types only; Redis/database, LDAP and delivery behavior still require their integration tests. See the [JSDoc conventions (Chinese)](../../.agents/instructions/module-conventions.md#jsdoc-与示例验证).
954
1135