@h-ai/iam 0.1.0-alpha.66 → 0.1.0-alpha.68
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +107 -76
- package/README.ja.md +104 -75
- package/README.md +106 -77
- package/dist/browser.d.ts +2320 -0
- package/dist/browser.js +3 -0
- package/dist/browser.js.map +1 -0
- package/dist/chunk-VCE74BFW.js +243 -0
- package/dist/chunk-VCE74BFW.js.map +1 -0
- package/dist/index.d.ts +19 -1623
- package/dist/index.js +789 -1173
- package/dist/index.js.map +1 -1
- package/package.json +12 -9
package/README.en.md
CHANGED
|
@@ -4,9 +4,11 @@
|
|
|
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
|
+
|
|
7
9
|
Identity and access management through a unified `iam` object for authentication, sessions, authorization, and user management.
|
|
8
10
|
|
|
9
|
-
##
|
|
11
|
+
## Core features
|
|
10
12
|
|
|
11
13
|
| Feature | Description |
|
|
12
14
|
| -------------------- | ---------------------------------------------------------------------------------------------------------------------- |
|
|
@@ -18,27 +20,29 @@ Identity and access management through a unified `iam` object for authentication
|
|
|
18
20
|
| **One-time tickets** | Short-lived capability tickets (issue / atomic consume) for channels such as WebSocket that cannot carry Bearer tokens |
|
|
19
21
|
| **Frontend client** | HTTP API contracts for independent frontends |
|
|
20
22
|
|
|
21
|
-
##
|
|
23
|
+
## Getting started
|
|
24
|
+
|
|
25
|
+
### Installation
|
|
22
26
|
|
|
23
27
|
```bash
|
|
24
28
|
pnpm add @h-ai/iam
|
|
25
29
|
```
|
|
26
30
|
|
|
27
|
-
|
|
31
|
+
### Dependencies
|
|
28
32
|
|
|
29
33
|
- `@h-ai/reldb`: persistence for users/roles/permissions; **initialize before iam.init()**.
|
|
30
|
-
- `@h-ai/cache`: sessions/OTP/
|
|
34
|
+
- `@h-ai/cache`: sessions/OTP/one-time tickets; **initialize before iam.init()**.
|
|
31
35
|
- `@h-ai/crypto`: password hashing, used internally and initialized automatically.
|
|
32
36
|
- `@h-ai/audit`: audit records for key RBAC operations.
|
|
33
37
|
|
|
34
38
|
- `@h-ai/reach`: **optional**, installed, initialized and injected by the application for standard notifications; custom callbacks do not require Reach.
|
|
35
39
|
- `ldapts`: **optional**, loaded only when LDAP is explicitly enabled without an injected `ldapClientFactory`.
|
|
36
40
|
|
|
37
|
-
|
|
41
|
+
### Optional LDAP and notifications
|
|
38
42
|
|
|
39
43
|
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.
|
|
40
44
|
|
|
41
|
-
|
|
45
|
+
#### LDAP / Active Directory
|
|
42
46
|
|
|
43
47
|
For the default client run `pnpm add ldapts`, then explicitly set `login.ldap: true` and connection settings. Connection settings alone do not enable LDAP. Enabling the default client without the SDK returns an initialization configuration error.
|
|
44
48
|
|
|
@@ -58,7 +62,7 @@ ldap:
|
|
|
58
62
|
connectTimeout: 5000
|
|
59
63
|
```
|
|
60
64
|
|
|
61
|
-
`ldaps://` uses TLS; `ldap://` with `useTls: true` uses StartTLS. For Active Directory use `searchFilter: "(sAMAccountName={{username}})"` and `usernameAttribute: sAMAccountName`; the username placeholder is escaped before searching. First login synchronizes a local user and assigns the default role; disabled accounts and lockouts still apply.
|
|
65
|
+
`ldaps://` uses TLS; `ldap://` with `useTls: true` uses StartTLS. For Active Directory use `searchFilter: "(sAMAccountName={{username}})"` and `usernameAttribute: sAMAccountName`; the username placeholder is escaped before searching. First login synchronizes a local user and assigns the default role; disabled accounts and lockouts still apply. `ldapSyncUser: false` requires an existing enabled local user with a database credential generation; no virtual DN user is created. You may inject a `ldapClientFactory` returning `bind/search/unbind` methods with HaiResult results. An injected factory does not require ldapts.
|
|
62
66
|
|
|
63
67
|
```ts
|
|
64
68
|
import type { IamConfigSettingsInput } from '@h-ai/iam'
|
|
@@ -75,7 +79,7 @@ if (!result.success)
|
|
|
75
79
|
return result
|
|
76
80
|
```
|
|
77
81
|
|
|
78
|
-
|
|
82
|
+
#### Automatic delivery with optional Reach
|
|
79
83
|
|
|
80
84
|
Run `pnpm add @h-ai/reach`. The application configures, initializes and closes Reach; IAM uses only the injected instance. These SMTP/HTTP providers are examples; the SMS gateway accepts the [Reach notification interface](../reach/README.en.md). Console logs metadata and does not deliver messages.
|
|
81
85
|
|
|
@@ -133,7 +137,7 @@ if (!initialized.success)
|
|
|
133
137
|
return iam.auth.sendOtp('user@example.com')
|
|
134
138
|
```
|
|
135
139
|
|
|
136
|
-
|
|
140
|
+
#### Custom email and SMS delivery
|
|
137
141
|
|
|
138
142
|
Inject `onOtpSendEmail`, `onOtpSendSms` and `onPasswordResetRequest` to use existing application services. Implement only needed callbacks or mix them with Reach; explicit callbacks take priority. These services are application implementations whose promises resolve only after successful delivery. If a service returns HaiResult, check `success` and throw inside the callback on failure. IAM catches callback exceptions and returns a business error without exposing codes, tokens or internal delivery details.
|
|
139
143
|
|
|
@@ -145,26 +149,26 @@ import { mailService, smsService } from './notifications.js'
|
|
|
145
149
|
// 调用方须检查服务投递结果,失败时拒绝 Promise,不能吞掉错误。
|
|
146
150
|
const initialized = await iam.init({
|
|
147
151
|
otp: { email: true, sms: true },
|
|
148
|
-
onOtpSendEmail: async (email, code) => {
|
|
149
|
-
await mailService.send({ to: email, subject: 'Verification code', text: code })
|
|
152
|
+
onOtpSendEmail: async (email, code, context) => {
|
|
153
|
+
await mailService.send({ to: email, subject: 'Verification code', text: code, idempotencyKey: context.idempotencyKey })
|
|
150
154
|
},
|
|
151
|
-
onOtpSendSms: async (phone, code) => {
|
|
152
|
-
await smsService.send({ to: phone, code })
|
|
155
|
+
onOtpSendSms: async (phone, code, context) => {
|
|
156
|
+
await smsService.send({ to: phone, code, idempotencyKey: context.idempotencyKey })
|
|
153
157
|
},
|
|
154
158
|
passwordReset: { mode: 'email' },
|
|
155
|
-
onPasswordResetRequest: async (user, token,
|
|
159
|
+
onPasswordResetRequest: async (user, token, context) => {
|
|
156
160
|
if (!user.email)
|
|
157
161
|
throw new Error('Reset recipient unavailable')
|
|
158
162
|
const url = new URL('https://app.example.com/auth/reset-password')
|
|
159
163
|
url.searchParams.set('token', token)
|
|
160
|
-
await mailService.send({ to: user.email, subject: 'Password reset', text: url.toString(), expiresAt })
|
|
164
|
+
await mailService.send({ to: user.email, subject: 'Password reset', text: url.toString(), expiresAt: context.expiresAt, idempotencyKey: context.idempotencyKey })
|
|
161
165
|
},
|
|
162
166
|
})
|
|
163
167
|
if (!initialized.success)
|
|
164
168
|
return initialized
|
|
165
169
|
```
|
|
166
170
|
|
|
167
|
-
|
|
171
|
+
### Quick start
|
|
168
172
|
|
|
169
173
|
```ts
|
|
170
174
|
import { cache } from '@h-ai/cache'
|
|
@@ -209,9 +213,9 @@ LDAP is disabled by default. Public OTP methods require `otp` settings and send
|
|
|
209
213
|
|
|
210
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.
|
|
211
215
|
|
|
212
|
-
|
|
216
|
+
### Design
|
|
213
217
|
|
|
214
|
-
|
|
218
|
+
#### Architecture
|
|
215
219
|
|
|
216
220
|
IAM is a **lifecycle singleton** managed through `iam.init()` / `iam.close()`. Five internal subsystems initialize in dependency order:
|
|
217
221
|
|
|
@@ -228,29 +232,29 @@ All subsystems use **factories + closures** (for example `createSessionOperation
|
|
|
228
232
|
|
|
229
233
|
Before initialization, every subsystem method returns `NOT_INITIALIZED` through a `NotInitializedKit` proxy.
|
|
230
234
|
|
|
231
|
-
|
|
235
|
+
#### Data storage
|
|
232
236
|
|
|
233
|
-
| Data
|
|
234
|
-
|
|
|
235
|
-
| Users, roles, permissions, associations, API keys
|
|
236
|
-
| Sessions (accessToken → Session)
|
|
237
|
-
|
|
|
238
|
-
| OTP codes
|
|
239
|
-
| Password reset tokens
|
|
240
|
-
| API Key
|
|
237
|
+
| Data | Storage | Description |
|
|
238
|
+
| ------------------------------------------------- | -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
|
|
239
|
+
| Users, roles, permissions, associations, API keys | DB (5 base tables, 1 optional API Key table) | `hai_iam_users`、`hai_iam_roles`、`hai_iam_permissions`、`hai_iam_role_permissions`、`hai_iam_user_roles`、`hai_iam_api_keys` |
|
|
240
|
+
| Sessions (accessToken → Session) | Cache | TTL, optional sliding renewal |
|
|
241
|
+
| Refresh state | Redis user partition | Atomic with session/index |
|
|
242
|
+
| OTP codes | Cache | TTL + attempt limit |
|
|
243
|
+
| Password reset tokens | User database row | SM3 hash, DB expiry, attempts and conditional password commit |
|
|
244
|
+
| API Key | DB | `hai_iam_api_keys`; store hashes only, never plaintext; prefix narrows candidates |
|
|
241
245
|
|
|
242
246
|
---
|
|
243
247
|
|
|
244
|
-
|
|
248
|
+
#### Authentication (authn) — `iam.auth`
|
|
245
249
|
|
|
246
|
-
|
|
250
|
+
##### Key types
|
|
247
251
|
|
|
248
252
|
- **`AuthnOperations`**: `login` / `loginWithOtp` / `loginWithLdap` / `loginWithApiKey` / `logout` / `verifyToken` / `sendOtp` / `registerAndLogin`.
|
|
249
253
|
- **`AuthStrategy`**: strategy interface (`type` + `authenticate` + `challenge?`), implemented for password, OTP, LDAP, and API Key.
|
|
250
254
|
- **`AuthResult`**: login result with user details, `TokenPair` (accessToken + refreshToken), and roles/permissions.
|
|
251
255
|
- **`Credentials`**: a credential union combining `{ type: 'password' | 'otp' | 'ldap' | 'apikey' }` with strategy-specific credentials.
|
|
252
256
|
|
|
253
|
-
|
|
257
|
+
##### Key workflows
|
|
254
258
|
|
|
255
259
|
**Password login:**
|
|
256
260
|
|
|
@@ -274,13 +278,14 @@ login(credentials)
|
|
|
274
278
|
```
|
|
275
279
|
verifyToken(accessToken)
|
|
276
280
|
→ session.get(accessToken) → cache lookup
|
|
281
|
+
→ Read the persistent user and validate enabled status and the session-bound credentialVersion
|
|
277
282
|
→ Extend TTL automatically when sliding renewal is enabled
|
|
278
|
-
→ Read current user status, role JOIN and
|
|
279
|
-
→ Reject disabled
|
|
283
|
+
→ Read the current user again, recheck enabled status and credentialVersion, then read the role JOIN and batched permissions (skip permissions for no roles)
|
|
284
|
+
→ Reject any persistent read failure, missing/disabled user or credential-version mismatch; intersect API Key scopes again
|
|
280
285
|
→ Return current Session (userId, roles, permissions)
|
|
281
286
|
```
|
|
282
287
|
|
|
283
|
-
|
|
288
|
+
##### Usage
|
|
284
289
|
|
|
285
290
|
These examples run on the server after successful reldb/cache/IAM initialization, with validated request input. Password login is available by default. OTP requires `login.otp: true`, an `otp` channel and delivery callbacks/Reach templates; LDAP requires `login.ldap: true` (see configuration above); API Key requires `login.apikey: true` (see the complete flow below). Login returns `AuthResult`; registration with login returns the same authentication structure. Registration and password login have separate switches.
|
|
286
291
|
|
|
@@ -312,47 +317,47 @@ async function registerWithSession(username: string, password: string) {
|
|
|
312
317
|
}
|
|
313
318
|
```
|
|
314
319
|
|
|
315
|
-
|
|
320
|
+
##### Considerations
|
|
316
321
|
|
|
317
322
|
- **Timing attack mitigation**: perform a dummy password hash for nonexistent users to keep response timing consistent.
|
|
318
323
|
- **Account lockout**: lock for `lockoutDuration` seconds after `maxLoginAttempts` consecutive failures.
|
|
319
324
|
- **Independent strategy toggles**: `login.password` / `login.otp` / `login.ldap` / `login.apikey`.
|
|
320
325
|
- **OTP security**: rejection sampling avoids modulo bias, constant-time comparison mitigates timing attacks, and `resendInterval` limits frequency.
|
|
321
326
|
|
|
327
|
+
`iam.auth.logout(accessToken)` uses the complete opaque token to revoke its access and refresh credentials in one user-partition CAS, including when access has expired. Invalid or absent tokens are idempotent successes. Cache read/commit failures return failure and can be retried with the same token after recovery. The HTTP endpoint revokes only a possessed token, accepts no userId target and does not require a still-authenticated access token. Callers must inspect `success`; clearing local cookies or secure storage is not confirmation of server revocation and failures must be shown.
|
|
328
|
+
|
|
322
329
|
---
|
|
323
330
|
|
|
324
|
-
|
|
331
|
+
#### Sessions (session) — `iam.session`
|
|
325
332
|
|
|
326
|
-
|
|
333
|
+
##### Key types
|
|
327
334
|
|
|
328
335
|
- **`SessionOperations`**: `create` / `get` / `verifyToken` / `update` / `delete` / `deleteByUserId` / `refresh` / `revokeRefresh` / `patchUserSessions`.
|
|
329
336
|
- **`Session`**: userId, roles, permissions, accessToken, expiration, and other session data.
|
|
330
337
|
- **`TokenPair`**: `{ accessToken, refreshToken, expiresIn, tokenType: 'Bearer' }`.
|
|
331
338
|
|
|
332
|
-
|
|
339
|
+
##### Key workflows
|
|
333
340
|
|
|
334
341
|
**Token refresh (rotation):**
|
|
335
342
|
|
|
336
343
|
```
|
|
337
344
|
refresh(refreshToken)
|
|
338
|
-
→ Read
|
|
339
|
-
→
|
|
340
|
-
→
|
|
341
|
-
→
|
|
342
|
-
→ Create a new session with the old user context
|
|
343
|
-
→ Return new TokenPair
|
|
345
|
+
→ Read the user partition and session version; validate authorization
|
|
346
|
+
→ Build candidate tokens without publishing them
|
|
347
|
+
→ Single-key CAS rechecks version/refresh and atomically replaces the old session
|
|
348
|
+
→ Return the unique successor only after commit
|
|
344
349
|
```
|
|
345
350
|
|
|
346
351
|
**Single-device login:**
|
|
347
352
|
|
|
348
353
|
```
|
|
349
354
|
create(options) [singleDevice=true]
|
|
350
|
-
→
|
|
351
|
-
→
|
|
352
|
-
→
|
|
355
|
+
→ Build candidate accessToken + refreshToken
|
|
356
|
+
→ Single-key CAS replaces all old sessions and their index together
|
|
357
|
+
→ Failed commits preserve the previous session
|
|
353
358
|
```
|
|
354
359
|
|
|
355
|
-
|
|
360
|
+
##### Usage
|
|
356
361
|
|
|
357
362
|
Use `tokens.accessToken` to authenticate requests and `tokens.refreshToken` to obtain a new pair; `expiresIn` is measured in seconds. Independent APIs use `Authorization: Bearer <accessToken>`. For SvelteKit Cookie endpoints, use kit's standard authentication integration described below.
|
|
358
363
|
|
|
@@ -387,27 +392,35 @@ async function forceSignOut(userId: string) {
|
|
|
387
392
|
}
|
|
388
393
|
```
|
|
389
394
|
|
|
390
|
-
|
|
395
|
+
##### Considerations
|
|
391
396
|
|
|
392
397
|
- **Cache-only storage**: Memory sessions are lost on restart; shared Redis sessions survive IAM service restarts until TTL or revocation.
|
|
393
398
|
- **Sliding renewal**: with `sliding: true`, each `get()` extends TTL automatically.
|
|
394
399
|
- **Refresh rotation**: old refreshToken expires after one use; replay detection indicates token leakage.
|
|
395
400
|
- **Token strength**: 256-bit cryptographic randomness (`crypto.getRandomValues(new Uint8Array(32))`), base64url encoded.
|
|
396
401
|
|
|
402
|
+
`create({ maxAge })` overrides the configured default with a positive safe integer in seconds, persisted as `Session.maxAge`. Issuance and refresh return that duration in `expiresIn`. Sliding renewal uses this duration and returns committed `lastActiveAt/expiresAt`; delayed requests cannot shorten a newer renewal. Explicit `update` applies valid date patches and creates a new content version. Clients schedule refresh from the returned `expiresIn` rather than a hardcoded default.
|
|
403
|
+
|
|
404
|
+
Access and refresh expire independently. The user partition remains until the latest live access/refresh deadline, while access authentication checks only its own `expiresAt`. An expired access token can still be rotated with a live refresh token. Each rotation starts a new `refreshTokenMaxAge` period and invalidates both old credentials. Access deletion, logoutAll and single-device replacement also revoke refresh-only records; revokeRefresh preserves a still-live access token. Refresh routes validate refresh credentials without requiring a live access token first.
|
|
405
|
+
|
|
406
|
+
Session cache read/commit failures return failed HaiResult values, never absence/expiry or cleanup compensation. Sessions and their index share one document, so batch updates no longer use per-token get/ttl calls or a separate index; cleanup uses confirmed absolute deadlines only. After recovery, re-read and retry updates/logout; logoutAll still covers every original record. Malformed state fails closed and is preserved for operator repair instead of being overwritten with an empty document.
|
|
407
|
+
|
|
408
|
+
Session.credentialVersion binds the database credential generation at creation. Only trusted authenticated strategies supply CreateSessionOptions.expectedCredentialVersion, and stale generations fail. Low-level create without that option still reads a real enabled local user and binds its current generation; it cannot create an authenticated identity without a database record.
|
|
409
|
+
|
|
397
410
|
---
|
|
398
411
|
|
|
399
|
-
|
|
412
|
+
#### Authorization (authz) — `iam.authz`
|
|
400
413
|
|
|
401
414
|
Role lists use `iam.authz.getRoleUserCounts(roleIds)` to aggregate actual membership for the current page in one query, returning `HaiResult<Map<string, number>>`. Missing relations mean zero; query failures display an error instead of invented zero counts.
|
|
402
415
|
|
|
403
|
-
|
|
416
|
+
##### Key types
|
|
404
417
|
|
|
405
418
|
- **`AuthzOperations`**: role/permission CRUD, role-permission/user-role assignments, and permission checks.
|
|
406
419
|
- **`Role`**: `{ id, code, name, description, isSystem }`.
|
|
407
420
|
- **`Permission`**: `{ id, code, name, type, resource, action }`; type is `menu` / `api` / `button`.
|
|
408
421
|
- **`PermissionType`**: permission type enum.
|
|
409
422
|
|
|
410
|
-
|
|
423
|
+
##### Key workflows
|
|
411
424
|
|
|
412
425
|
**Permission checks (`checkPermission`):**
|
|
413
426
|
|
|
@@ -430,7 +443,7 @@ assignPermissionToRole(roleId, permId)
|
|
|
430
443
|
(best-effort: failures only log.error; permission assignment still succeeds)
|
|
431
444
|
```
|
|
432
445
|
|
|
433
|
-
|
|
446
|
+
##### Usage
|
|
434
447
|
|
|
435
448
|
Create permissions, save permission **IDs** through the role's `permissionIds`, and assign a role by its **ID**. Session roles/permissions and guards use stable **codes**; display names can change independently. These examples assume RBAC is enabled. Applications must authorize administrative operations and check resource access before calling them.
|
|
436
449
|
|
|
@@ -478,19 +491,19 @@ async function currentAuthorization(userId: string) {
|
|
|
478
491
|
}
|
|
479
492
|
```
|
|
480
493
|
|
|
481
|
-
|
|
494
|
+
##### Considerations
|
|
482
495
|
|
|
483
496
|
- **Wildcard matching**: `admin:*` matches `admin:read`, `admin:write:detail`, etc.; one `*` matches everything after the colon.
|
|
484
497
|
- **Super-admin role**: `rbac.superAdminRole` (default `super_admin`) automatically grants all permissions.
|
|
485
498
|
- **Transactions**: `deleteRole` / `deletePermission` clear associations before deleting entities in a DB transaction.
|
|
486
|
-
- **Session synchronization**: cache updates are best-effort. Protected requests use `iam.auth.verifyToken` to
|
|
499
|
+
- **Session synchronization**: cache updates are best-effort. Protected requests use `iam.auth.verifyToken` to validate the session-bound persistent credential version, then recheck current user status and credential version and read fresh authorization. Failure at either read rejects authentication. `iam.session.get/verifyToken` also checks persistent user enabled status and `credentialVersion`, but its role/permission fields remain a session snapshot and do not replace current authorization. `iam.authz.getUserAuthorization(userId)` shares the role JOIN and batched permission query.
|
|
487
500
|
- **Seed data**: `seedDefaultData: true` idempotently creates admin/user/guest roles and basic permissions.
|
|
488
501
|
|
|
489
502
|
---
|
|
490
503
|
|
|
491
|
-
|
|
504
|
+
#### Users (user) — `iam.user`
|
|
492
505
|
|
|
493
|
-
|
|
506
|
+
##### Key types
|
|
494
507
|
|
|
495
508
|
- **`UserOperations`**: registration, queries, updates, deletion, password changes/resets, and password strength validation.
|
|
496
509
|
- **`User`** (public fields): `{ id, username, email, phone, displayName, avatarUrl, enabled, roles? }`.
|
|
@@ -498,7 +511,7 @@ async function currentAuthorization(userId: string) {
|
|
|
498
511
|
- **`RegisterOptions`** / **`RegisterResult`**: registration input/output.
|
|
499
512
|
- **`UpdateCurrentUserInput`**: fields the current user may change (`username` / `email` / `displayName` / `avatarUrl` / `phone` / `metadata`).
|
|
500
513
|
|
|
501
|
-
|
|
514
|
+
##### Key workflows
|
|
502
515
|
|
|
503
516
|
**User registration:**
|
|
504
517
|
|
|
@@ -528,7 +541,7 @@ confirmPasswordReset(token, newPassword)
|
|
|
528
541
|
→ Delete consumed reset token
|
|
529
542
|
```
|
|
530
543
|
|
|
531
|
-
|
|
544
|
+
##### Usage
|
|
532
545
|
|
|
533
546
|
`register` returns a user without a session; use `auth.registerAndLogin` when both are needed. `createUser` is an administrative operation independent of self-registration settings. Use `getCurrentUser/updateCurrentUser` for profile fields: username/email/displayName/avatarUrl/phone/metadata. Security fields and roles require authorized administrative operations.
|
|
534
547
|
|
|
@@ -586,7 +599,7 @@ async function disableUser(userId: string) {
|
|
|
586
599
|
}
|
|
587
600
|
```
|
|
588
601
|
|
|
589
|
-
|
|
602
|
+
##### Considerations
|
|
590
603
|
|
|
591
604
|
- **`toUser()` redaction**: public users omit sensitive fields such as `passwordHash` and `loginFailedCount`.
|
|
592
605
|
- **Session invalidation**: `deleteUser` / `updateUser({ enabled: false })` / `changePassword` / `confirmPasswordReset` clear all active sessions for affected users.
|
|
@@ -597,17 +610,17 @@ async function disableUser(userId: string) {
|
|
|
597
610
|
|
|
598
611
|
---
|
|
599
612
|
|
|
600
|
-
|
|
613
|
+
#### API keys — `iam.apiKey`
|
|
601
614
|
|
|
602
615
|
`scopes` intersect the subject's permissions and support `*` and namespace `:*`. The default empty array grants identity only, without operation permissions. Restricted keys have empty session `roles` and use permission guards; explicit `*` never exceeds subject authorization. API Key login binds the key ID: get/verifyToken/refresh recheck its persistent state and reject revoked, disabled, expired keys or DB failures. Standalone verifyApiKey returns verified key metadata and still requires subject/operation authorization.
|
|
603
616
|
|
|
604
|
-
|
|
617
|
+
##### Key types
|
|
605
618
|
|
|
606
619
|
- **`ApiKeyOperations`**: `createApiKey` / `listApiKeys` / `getApiKey` / `revokeApiKey` / `verifyApiKey`.
|
|
607
620
|
- **`ApiKey`** (public fields): `{ id, userId, name, keyPrefix, enabled, expiresAt, scopes }`.
|
|
608
621
|
- **`CreateApiKeyResult`**: `{ apiKey, rawKey }`; the plaintext key is returned only once.
|
|
609
622
|
|
|
610
|
-
|
|
623
|
+
##### Key workflows
|
|
611
624
|
|
|
612
625
|
**API key verification:**
|
|
613
626
|
|
|
@@ -621,7 +634,7 @@ verifyApiKey(rawKey)
|
|
|
621
634
|
→ Return ApiKey
|
|
622
635
|
```
|
|
623
636
|
|
|
624
|
-
|
|
637
|
+
##### Usage
|
|
625
638
|
|
|
626
639
|
Enable API Key during initialization. A Key is a restricted user credential: **effective permissions = current subject permissions ∩ scopes**. Scopes use permission codes, not role/display names or resource IDs. They neither create permissions nor replace tenant/record ownership checks.
|
|
627
640
|
|
|
@@ -707,7 +720,7 @@ Key-derived sessions remain bound to the Key when refreshed through `iam.session
|
|
|
707
720
|
|
|
708
721
|
`expirationDays: 0` means no expiration; omission uses `apikey.defaultExpirationDays`. Set an explicit duration for time-limited access. Never put raw Keys or Tokens in URLs, logs, public configuration or ordinary localStorage.
|
|
709
722
|
|
|
710
|
-
|
|
723
|
+
##### Considerations
|
|
711
724
|
|
|
712
725
|
- **No plaintext persistence**: only hashes are stored; `rawKey` at creation is the sole opportunity to retrieve it.
|
|
713
726
|
- **Prefix lookup**: `keyPrefix` narrows candidates without a full table scan.
|
|
@@ -716,11 +729,11 @@ Key-derived sessions remain bound to the Key when refreshed through `iam.session
|
|
|
716
729
|
|
|
717
730
|
---
|
|
718
731
|
|
|
719
|
-
|
|
732
|
+
### More usage
|
|
720
733
|
|
|
721
734
|
See the skill template (Chinese) for detailed API parameters, codes, and integration patterns: `packages/cli/templates/skills/hai-iam/SKILL.md`.
|
|
722
735
|
|
|
723
|
-
|
|
736
|
+
### Frontend API calls
|
|
724
737
|
|
|
725
738
|
Public HTTP APIs are defined by `@h-ai/api-contract` and called through the `@h-ai/api-client` typed client:
|
|
726
739
|
|
|
@@ -745,7 +758,9 @@ await apiClient.iam.auth.changePassword({
|
|
|
745
758
|
})
|
|
746
759
|
```
|
|
747
760
|
|
|
748
|
-
|
|
761
|
+
The browser entry exports configuration schemas, types and HaiIamError, without the server `iam` instance. Authenticate through the HTTP/typed client.
|
|
762
|
+
|
|
763
|
+
### Password reset
|
|
749
764
|
|
|
750
765
|
Password reset requires `passwordReset.enabled` and a working delivery callback/Reach template. Success does not prove that the account exists or the recipient received a message. Handle delivery/internal failures through HaiResult. Reset tokens expire, limit attempts, and permit one successful consumption; success revokes old sessions. Administrator resets do not require the old password, but applications must authorize the administrator.
|
|
751
766
|
|
|
@@ -768,7 +783,9 @@ async function resetByAdministrator(userId: string, newPassword: string) {
|
|
|
768
783
|
}
|
|
769
784
|
```
|
|
770
785
|
|
|
771
|
-
|
|
786
|
+
Cache cleanup may return a failure after the new password and credential generation have committed; they are not rolled back. Sign in again instead of automatically retrying with old credentials.
|
|
787
|
+
|
|
788
|
+
### One-time tickets — `iam.ticket`
|
|
772
789
|
|
|
773
790
|
Short-lived capability tickets for channels that cannot carry Bearer tokens, such as WebSocket URLs. An authenticated HTTP request issues a ticket bound to a subject, purpose, and operation. The server **atomically consumes** it on first validation, making it valid only once.
|
|
774
791
|
|
|
@@ -801,7 +818,9 @@ async function consumeAudioTicket(ticket: string) {
|
|
|
801
818
|
|
|
802
819
|
Features: cryptographically secure randomness, TTL, atomic single use (only one concurrent consumer succeeds), and purpose binding to prevent cross-purpose replay.
|
|
803
820
|
|
|
804
|
-
|
|
821
|
+
### Configuration driven authentication
|
|
822
|
+
|
|
823
|
+
Password login submits only `identifier` and `password`; `AuthPanel` maps the `LoginForm` field `username` to `identifier`. Server settings `session.maxAge` / `session.refreshTokenMaxAge` control session lifetimes. The login form has no per-login persistent-session option.
|
|
805
824
|
|
|
806
825
|
`iam.publicConfig` and the client's `client.iam.auth.publicConfig()` HTTP contract expose a safe configuration whitelist and only advertise configured, available methods. `login.password/otp/ldap`, `otp.email/sms`, `register.enabled/defaultEnabled`, `passwordReset.enabled/mode/resetUrl` and agreement display flags are independent. `ui` configures card/split layout, title, subtitle, brand text, Logo and illustration URLs. Browser links only accept HTTP(S). LDAP is optional and disabled by default; install ldapts only for the default client, or inject a custom factory.
|
|
807
826
|
|
|
@@ -811,13 +830,13 @@ SvelteKit routes export handlers from `kit.auth.createApi(iam)`; use `kit.auth.p
|
|
|
811
830
|
|
|
812
831
|
Agreements open in a Modal without losing form input. URL pages must allow iframe embedding; scripts are disabled and a new-window fallback is available. LoginForm/RegisterForm expose `header/extraFields/footer` snippets. AuthPanel forwards `loginHeader/loginFields/loginFooter` and `registerHeader/registerFields/registerFooter`; application state and actions handle additional fields. See [UI usage](../ui/README.en.md#configuration-driven-authentication).
|
|
813
832
|
|
|
814
|
-
All nodes
|
|
833
|
+
All nodes share the same database and Redis; Memory is for single-process tests. OTP code, attempts and cooldown share one identity key with CAS publication and consumption. Conflicts reject stale generations and consumption preserves cooldown. Password reset hash, expiry, attempts and resetVersion live in the user database row. Policy failures count, and one conditional SQL statement changes the password, consumes the token and advances credentialVersion. New tokens, later attempts, disablement or expiry reject delayed commits. Requesting a reset only advances resetVersion; successful password changes advance credentialVersion. Authentication captures the verified credential generation; session creation, authentication and refresh check the current database generation, so failed cache cleanup cannot authorize old sessions. Notification callbacks run after publication and receive { expiresAt, idempotencyKey } as their third argument. Applications and Reach providers must durably deduplicate that key. Exceptions/timeouts mean delivery may have succeeded: the result is unknown, with no automatic resend or rollback. Exactly-once external effects require receiver-side idempotency.
|
|
815
834
|
|
|
816
835
|
Standard endpoints: `GET /auth/config`, `POST /auth/login/ldap`, `POST /auth/password-reset/request`, `POST /auth/password-reset/confirm`. Security errors: `SECURITY_BUSY (hai:iam:023)` and `PASSWORD_RESET_DISABLED (hai:iam:024)`. Declare optional fields such as `ldap` in YAML before overriding them, or replace the module configuration with a JSON value in `HAI_IAM`. Existing fields support overrides such as `HAI_IAM_LOGIN_OTP`.
|
|
817
836
|
|
|
818
837
|
`passwordReset.mode` selects `email` (default, `password_reset`) or `phone` (`password_reset_sms`); the public config tells the recovery form which identifier to request.
|
|
819
838
|
|
|
820
|
-
|
|
839
|
+
#### Markdown agreements
|
|
821
840
|
|
|
822
841
|
Configure `agreements.userAgreementMarkdown` and `privacyPolicyMarkdown` to load agreement content directly from configuration. YAML `|` preserves paragraphs and newlines. The modal reuses the safe MarkdownRenderer with raw HTML, scripts and code tools disabled. Markdown takes priority over a URL for the same agreement; a URL remains an optional external-view link. URL-only agreements use a sandboxed iframe.
|
|
823
842
|
|
|
@@ -840,7 +859,7 @@ agreements:
|
|
|
840
859
|
# privacyPolicyUrl: https://example.com/privacy
|
|
841
860
|
```
|
|
842
861
|
|
|
843
|
-
|
|
862
|
+
### Error handling
|
|
844
863
|
|
|
845
864
|
All operations return `HaiResult<T>`; branch on `HaiIamError`:
|
|
846
865
|
|
|
@@ -855,14 +874,26 @@ if (!result.success) {
|
|
|
855
874
|
}
|
|
856
875
|
```
|
|
857
876
|
|
|
858
|
-
|
|
877
|
+
### Optional integrations
|
|
878
|
+
|
|
879
|
+
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.
|
|
880
|
+
|
|
881
|
+
Sessions and refresh consumption share one versioned `hai:iam:sessions:{SHA256(userId)}` document, replacing separate mappings/indexes. Bearer tokens contain an untrusted routing hint plus 256 random bits; clients must treat them as opaque strings. Rotation checks `Session.version` at commit. Memory is for single-process tests; production nodes share Redis, including Cluster. Conflicts retry from the latest state and return CONFLICT when exhausted; one refresh cannot publish two successors. An ambiguous lost response requires failing closed and signing in again, without assuming the old token remains valid. All records remain covered by the same user index.
|
|
882
|
+
|
|
883
|
+
`iam.session.update(token, patch, expectedVersion)` requires the Session.version from the read. Stale versions return CONFLICT; revoked tokens return SESSION_NOT_FOUND. Timestamp-only sliding updates keep the content version, allowing normal parallel requests. Batch permission updates bind the initial token/version set and never apply delayed snapshots to later sign-ins; newer content versions require re-reading authorization before retry.
|
|
884
|
+
|
|
885
|
+
## Development and testing
|
|
886
|
+
|
|
887
|
+
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-与示例验证).
|
|
859
888
|
|
|
860
889
|
```bash
|
|
861
|
-
pnpm
|
|
890
|
+
pnpm --filter @h-ai/iam typecheck
|
|
891
|
+
pnpm --filter @h-ai/iam lint
|
|
892
|
+
pnpm --filter @h-ai/iam build
|
|
893
|
+
pnpm --filter @h-ai/iam test
|
|
894
|
+
node scripts/check-module-jsdoc.mjs --package iam
|
|
862
895
|
```
|
|
863
896
|
|
|
864
897
|
## License
|
|
865
898
|
|
|
866
899
|
Apache-2.0
|
|
867
|
-
|
|
868
|
-
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.
|