@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.
- package/README.en.md +253 -85
- package/README.ja.md +256 -88
- package/README.md +259 -91
- 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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1091
|
+
### Schema and credential notifications
|
|
949
1092
|
|
|
950
|
-
After RelDB initialization, `migrateIam({ dryRun: true })` previews
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
|