@h-ai/iam 1.0.0-beta.19 → 1.0.0-beta.20

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.
Files changed (4) hide show
  1. package/README.en.md +253 -85
  2. package/README.ja.md +256 -88
  3. package/README.md +259 -91
  4. package/package.json +8 -8
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.
@@ -179,51 +382,6 @@ if (!initialized.success)
179
382
  return initialized
180
383
  ```
181
384
 
182
- ### Quick start
183
-
184
- ```ts
185
- import { cache } from '@h-ai/cache'
186
- import { core } from '@h-ai/core'
187
- import { iam } from '@h-ai/iam'
188
- import { reldb } from '@h-ai/reldb'
189
-
190
- // 基础密码认证不需要安装 Reach 或 ldapts。
191
- async function example() {
192
- try {
193
- const db = await reldb.init({ type: 'sqlite', database: './data.db' })
194
- if (!db.success)
195
- return db
196
- const cached = await cache.init({ type: 'memory' })
197
- if (!cached.success)
198
- return cached
199
- const initialized = await iam.init({})
200
- if (!initialized.success)
201
- return initialized
202
- const registered = await iam.user.register({ username: 'demo', password: 'Password123' })
203
- if (!registered.success)
204
- return registered
205
- const login = await iam.auth.login({ identifier: 'demo', password: 'Password123' })
206
- if (!login.success)
207
- return login
208
- return await iam.auth.verifyToken(login.data.tokens.accessToken)
209
- }
210
- finally {
211
- // 应用关闭时按依赖的反向顺序释放。
212
- await iam.close()
213
- await cache.close()
214
- await reldb.close()
215
- }
216
- }
217
-
218
- const result = await example()
219
- if (!result.success)
220
- core.logger.error('Authentication example failed', { code: result.error.code })
221
- ```
222
-
223
- LDAP is disabled by default. Public OTP methods require `otp` settings and send callbacks; password reset requires a notification callback.
224
-
225
- `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.
226
-
227
385
  ### Design
228
386
 
229
387
  #### Architecture
@@ -872,21 +1030,6 @@ agreements:
872
1030
  # privacyPolicyUrl: https://example.com/privacy
873
1031
  ```
874
1032
 
875
- ### Error handling
876
-
877
- All operations return `HaiResult<T>`; branch on `HaiIamError`:
878
-
879
- ```ts
880
- import { HaiIamError, iam } from '@h-ai/iam'
881
-
882
- const result = await iam.auth.login({ identifier: 'admin', password: 'wrong' })
883
- if (!result.success) {
884
- if (result.error.code === HaiIamError.INVALID_CREDENTIALS.code) {
885
- // 用户名或密码错误
886
- }
887
- }
888
- ```
889
-
890
1033
  ### Optional integrations
891
1034
 
892
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.
@@ -901,7 +1044,7 @@ Redis caches nonsecret account/credential state, role/permission summaries, sens
901
1044
 
902
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.
903
1046
 
904
- ## Self-hosted interactive challenges
1047
+ ### Self-hosted interactive challenges
905
1048
 
906
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.
907
1050
 
@@ -909,7 +1052,7 @@ captcha.enabled defaults to false. Enable self-hosted ALTCHA proof of work with
909
1052
 
910
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.
911
1054
 
912
- ## Optional password and login protection
1055
+ ### Optional password and login protection
913
1056
 
914
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.
915
1058
 
@@ -927,17 +1070,17 @@ Reusing the current password returns `HaiIamError.PASSWORD_REUSE_NOT_ALLOWED` (`
927
1070
 
928
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.
929
1072
 
930
- 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.
931
1074
 
932
- ## Account type and lifecycle
1075
+ ### Account type and lifecycle
933
1076
 
934
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.
935
1078
 
936
- 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.
937
1080
 
938
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.
939
1082
 
940
- ## Optional session security policies
1083
+ ### Optional session security policies
941
1084
 
942
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.
943
1086
 
@@ -945,9 +1088,9 @@ Set `session.idleTimeout: 1800` to require a new login after 30 minutes without
945
1088
 
946
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.
947
1090
 
948
- ## Schema and credential notifications
1091
+ ### Schema and credential notifications
949
1092
 
950
- 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.
951
1094
 
952
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.
953
1096
 
@@ -955,13 +1098,38 @@ When audit is initialized and capture.enabled is on, IAM records important accou
955
1098
 
956
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.
957
1100
 
958
- ## Page exit session policy
1101
+ ### Page exit session policy
959
1102
 
960
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.
961
1104
 
962
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.
963
1106
 
964
- ## 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
965
1133
 
966
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-与示例验证).
967
1135