@h-ai/iam 0.1.0-alpha.66 → 0.1.0-alpha.67

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.en.md CHANGED
@@ -4,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
- ## Features
11
+ ## Core features
10
12
 
11
13
  | Feature | Description |
12
14
  | -------------------- | ---------------------------------------------------------------------------------------------------------------------- |
@@ -18,13 +20,15 @@ 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
- ## Installation
23
+ ## Getting started
24
+
25
+ ### Installation
22
26
 
23
27
  ```bash
24
28
  pnpm add @h-ai/iam
25
29
  ```
26
30
 
27
- ## Dependencies
31
+ ### Dependencies
28
32
 
29
33
  - `@h-ai/reldb`: persistence for users/roles/permissions; **initialize before iam.init()**.
30
34
  - `@h-ai/cache`: sessions/OTP/reset tokens/one-time tickets/shared locks; **initialize before iam.init()**.
@@ -34,11 +38,11 @@ pnpm add @h-ai/iam
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
- ## Optional LDAP and notifications
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
- ### LDAP / Active Directory
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
 
@@ -75,7 +79,7 @@ if (!result.success)
75
79
  return result
76
80
  ```
77
81
 
78
- ### Automatic delivery with optional Reach
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
- ### Custom email and SMS delivery
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
 
@@ -164,7 +168,7 @@ if (!initialized.success)
164
168
  return initialized
165
169
  ```
166
170
 
167
- ## Quick start
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
- ## Design
216
+ ### Design
213
217
 
214
- ### Architecture
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,7 +232,7 @@ 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
- ### Data storage
235
+ #### Data storage
232
236
 
233
237
  | Data | Storage | Description |
234
238
  | --------------------------------------------------- | -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
@@ -241,16 +245,16 @@ Before initialization, every subsystem method returns `NOT_INITIALIZED` through
241
245
 
242
246
  ---
243
247
 
244
- ### Authentication (authn) — `iam.auth`
248
+ #### Authentication (authn) — `iam.auth`
245
249
 
246
- #### Key types
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
- #### Key workflows
257
+ ##### Key workflows
254
258
 
255
259
  **Password login:**
256
260
 
@@ -280,7 +284,7 @@ verifyToken(accessToken)
280
284
  → Return current Session (userId, roles, permissions)
281
285
  ```
282
286
 
283
- #### Usage
287
+ ##### Usage
284
288
 
285
289
  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
290
 
@@ -312,7 +316,7 @@ async function registerWithSession(username: string, password: string) {
312
316
  }
313
317
  ```
314
318
 
315
- #### Considerations
319
+ ##### Considerations
316
320
 
317
321
  - **Timing attack mitigation**: perform a dummy password hash for nonexistent users to keep response timing consistent.
318
322
  - **Account lockout**: lock for `lockoutDuration` seconds after `maxLoginAttempts` consecutive failures.
@@ -321,15 +325,15 @@ async function registerWithSession(username: string, password: string) {
321
325
 
322
326
  ---
323
327
 
324
- ### Sessions (session) — `iam.session`
328
+ #### Sessions (session) — `iam.session`
325
329
 
326
- #### Key types
330
+ ##### Key types
327
331
 
328
332
  - **`SessionOperations`**: `create` / `get` / `verifyToken` / `update` / `delete` / `deleteByUserId` / `refresh` / `revokeRefresh` / `patchUserSessions`.
329
333
  - **`Session`**: userId, roles, permissions, accessToken, expiration, and other session data.
330
334
  - **`TokenPair`**: `{ accessToken, refreshToken, expiresIn, tokenType: 'Bearer' }`.
331
335
 
332
- #### Key workflows
336
+ ##### Key workflows
333
337
 
334
338
  **Token refresh (rotation):**
335
339
 
@@ -352,7 +356,7 @@ create(options) [singleDevice=true]
352
356
  → Store session in cache
353
357
  ```
354
358
 
355
- #### Usage
359
+ ##### Usage
356
360
 
357
361
  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
362
 
@@ -387,7 +391,7 @@ async function forceSignOut(userId: string) {
387
391
  }
388
392
  ```
389
393
 
390
- #### Considerations
394
+ ##### Considerations
391
395
 
392
396
  - **Cache-only storage**: Memory sessions are lost on restart; shared Redis sessions survive IAM service restarts until TTL or revocation.
393
397
  - **Sliding renewal**: with `sliding: true`, each `get()` extends TTL automatically.
@@ -396,18 +400,18 @@ async function forceSignOut(userId: string) {
396
400
 
397
401
  ---
398
402
 
399
- ### Authorization (authz) — `iam.authz`
403
+ #### Authorization (authz) — `iam.authz`
400
404
 
401
405
  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
406
 
403
- #### Key types
407
+ ##### Key types
404
408
 
405
409
  - **`AuthzOperations`**: role/permission CRUD, role-permission/user-role assignments, and permission checks.
406
410
  - **`Role`**: `{ id, code, name, description, isSystem }`.
407
411
  - **`Permission`**: `{ id, code, name, type, resource, action }`; type is `menu` / `api` / `button`.
408
412
  - **`PermissionType`**: permission type enum.
409
413
 
410
- #### Key workflows
414
+ ##### Key workflows
411
415
 
412
416
  **Permission checks (`checkPermission`):**
413
417
 
@@ -430,7 +434,7 @@ assignPermissionToRole(roleId, permId)
430
434
  (best-effort: failures only log.error; permission assignment still succeeds)
431
435
  ```
432
436
 
433
- #### Usage
437
+ ##### Usage
434
438
 
435
439
  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
440
 
@@ -478,7 +482,7 @@ async function currentAuthorization(userId: string) {
478
482
  }
479
483
  ```
480
484
 
481
- #### Considerations
485
+ ##### Considerations
482
486
 
483
487
  - **Wildcard matching**: `admin:*` matches `admin:read`, `admin:write:detail`, etc.; one `*` matches everything after the colon.
484
488
  - **Super-admin role**: `rbac.superAdminRole` (default `super_admin`) automatically grants all permissions.
@@ -488,9 +492,9 @@ async function currentAuthorization(userId: string) {
488
492
 
489
493
  ---
490
494
 
491
- ### Users (user) — `iam.user`
495
+ #### Users (user) — `iam.user`
492
496
 
493
- #### Key types
497
+ ##### Key types
494
498
 
495
499
  - **`UserOperations`**: registration, queries, updates, deletion, password changes/resets, and password strength validation.
496
500
  - **`User`** (public fields): `{ id, username, email, phone, displayName, avatarUrl, enabled, roles? }`.
@@ -498,7 +502,7 @@ async function currentAuthorization(userId: string) {
498
502
  - **`RegisterOptions`** / **`RegisterResult`**: registration input/output.
499
503
  - **`UpdateCurrentUserInput`**: fields the current user may change (`username` / `email` / `displayName` / `avatarUrl` / `phone` / `metadata`).
500
504
 
501
- #### Key workflows
505
+ ##### Key workflows
502
506
 
503
507
  **User registration:**
504
508
 
@@ -528,7 +532,7 @@ confirmPasswordReset(token, newPassword)
528
532
  → Delete consumed reset token
529
533
  ```
530
534
 
531
- #### Usage
535
+ ##### Usage
532
536
 
533
537
  `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
538
 
@@ -586,7 +590,7 @@ async function disableUser(userId: string) {
586
590
  }
587
591
  ```
588
592
 
589
- #### Considerations
593
+ ##### Considerations
590
594
 
591
595
  - **`toUser()` redaction**: public users omit sensitive fields such as `passwordHash` and `loginFailedCount`.
592
596
  - **Session invalidation**: `deleteUser` / `updateUser({ enabled: false })` / `changePassword` / `confirmPasswordReset` clear all active sessions for affected users.
@@ -597,17 +601,17 @@ async function disableUser(userId: string) {
597
601
 
598
602
  ---
599
603
 
600
- ### API keys — `iam.apiKey`
604
+ #### API keys — `iam.apiKey`
601
605
 
602
606
  `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
607
 
604
- #### Key types
608
+ ##### Key types
605
609
 
606
610
  - **`ApiKeyOperations`**: `createApiKey` / `listApiKeys` / `getApiKey` / `revokeApiKey` / `verifyApiKey`.
607
611
  - **`ApiKey`** (public fields): `{ id, userId, name, keyPrefix, enabled, expiresAt, scopes }`.
608
612
  - **`CreateApiKeyResult`**: `{ apiKey, rawKey }`; the plaintext key is returned only once.
609
613
 
610
- #### Key workflows
614
+ ##### Key workflows
611
615
 
612
616
  **API key verification:**
613
617
 
@@ -621,7 +625,7 @@ verifyApiKey(rawKey)
621
625
  → Return ApiKey
622
626
  ```
623
627
 
624
- #### Usage
628
+ ##### Usage
625
629
 
626
630
  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
631
 
@@ -707,7 +711,7 @@ Key-derived sessions remain bound to the Key when refreshed through `iam.session
707
711
 
708
712
  `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
713
 
710
- #### Considerations
714
+ ##### Considerations
711
715
 
712
716
  - **No plaintext persistence**: only hashes are stored; `rawKey` at creation is the sole opportunity to retrieve it.
713
717
  - **Prefix lookup**: `keyPrefix` narrows candidates without a full table scan.
@@ -716,11 +720,11 @@ Key-derived sessions remain bound to the Key when refreshed through `iam.session
716
720
 
717
721
  ---
718
722
 
719
- ## More usage
723
+ ### More usage
720
724
 
721
725
  See the skill template (Chinese) for detailed API parameters, codes, and integration patterns: `packages/cli/templates/skills/hai-iam/SKILL.md`.
722
726
 
723
- ## Frontend API calls
727
+ ### Frontend API calls
724
728
 
725
729
  Public HTTP APIs are defined by `@h-ai/api-contract` and called through the `@h-ai/api-client` typed client:
726
730
 
@@ -745,7 +749,7 @@ await apiClient.iam.auth.changePassword({
745
749
  })
746
750
  ```
747
751
 
748
- ## Password reset
752
+ ### Password reset
749
753
 
750
754
  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
755
 
@@ -768,7 +772,7 @@ async function resetByAdministrator(userId: string, newPassword: string) {
768
772
  }
769
773
  ```
770
774
 
771
- ## One-time tickets — `iam.ticket`
775
+ ### One-time tickets — `iam.ticket`
772
776
 
773
777
  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
778
 
@@ -801,7 +805,7 @@ async function consumeAudioTicket(ticket: string) {
801
805
 
802
806
  Features: cryptographically secure randomness, TTL, atomic single use (only one concurrent consumer succeeds), and purpose binding to prevent cross-purpose replay.
803
807
 
804
- ## Configuration driven authentication
808
+ ### Configuration driven authentication
805
809
 
806
810
  `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
811
 
@@ -817,7 +821,7 @@ Standard endpoints: `GET /auth/config`, `POST /auth/login/ldap`, `POST /auth/pas
817
821
 
818
822
  `passwordReset.mode` selects `email` (default, `password_reset`) or `phone` (`password_reset_sms`); the public config tells the recovery form which identifier to request.
819
823
 
820
- ### Markdown agreements
824
+ #### Markdown agreements
821
825
 
822
826
  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
827
 
@@ -840,7 +844,7 @@ agreements:
840
844
  # privacyPolicyUrl: https://example.com/privacy
841
845
  ```
842
846
 
843
- ## Error handling
847
+ ### Error handling
844
848
 
845
849
  All operations return `HaiResult<T>`; branch on `HaiIamError`:
846
850
 
@@ -855,7 +859,11 @@ if (!result.success) {
855
859
  }
856
860
  ```
857
861
 
858
- ## Tests
862
+ ### Optional integrations
863
+
864
+ 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.
865
+
866
+ ## Development and testing
859
867
 
860
868
  ```bash
861
869
  pnpm test
@@ -864,5 +872,3 @@ pnpm test
864
872
  ## License
865
873
 
866
874
  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.
package/README.ja.md CHANGED
@@ -4,9 +4,11 @@
4
4
 
5
5
  [フレームワークのホーム](../../README.ja.md) · 関連モジュール:[@h-ai/audit](../audit/README.ja.md) · [@h-ai/cache](../cache/README.ja.md) · [@h-ai/core](../core/README.ja.md) · [@h-ai/crypto](../crypto/README.ja.md) · [@h-ai/reldb](../reldb/README.ja.md) · [@h-ai/reach](../reach/README.ja.md)
6
6
 
7
+ ## 概要
8
+
7
9
  共通の `iam` オブジェクトで認証、セッション、認可、ユーザー管理を提供する Identity and Access Management モジュールです。
8
10
 
9
- ## 機能
11
+ ## 主な機能
10
12
 
11
13
  | 機能 | 説明 |
12
14
  | ------------------------------ | ----------------------------------------------------------------------------------------- |
@@ -18,13 +20,15 @@
18
20
  | **一度限りのチケット** | Bearer Token を付けられない WebSocket などのための短期チケット(発行 / アトミックな消費) |
19
21
  | **フロントエンドクライアント** | 独立フロントエンド向けの HTTP API 契約 |
20
22
 
21
- ## インストール
23
+ ## 使い始める
24
+
25
+ ### インストール
22
26
 
23
27
  ```bash
24
28
  pnpm add @h-ai/iam
25
29
  ```
26
30
 
27
- ## 依存関係
31
+ ### 依存関係
28
32
 
29
33
  - `@h-ai/reldb`:ユーザー・ロール・権限の永続化。**iam.init() より先に初期化**。
30
34
  - `@h-ai/cache`:セッション・OTP・リセット Token・一度限りのチケット・共有ロック。**iam.init() より先に初期化**。
@@ -34,11 +38,11 @@ pnpm add @h-ai/iam
34
38
  - `@h-ai/reach`:**任意**。アプリがインストール、初期化、注入して通知テンプレートを使用します。独自送信コールバックでは不要です。
35
39
  - `ldapts`:**任意**。LDAP を明示的に有効化し、`ldapClientFactory` がない場合だけ読み込みます。
36
40
 
37
- ## 任意の LDAP と通知連携
41
+ ### 任意の LDAP と通知連携
38
42
 
39
43
  パスワード認証、ユーザー管理、RBAC は LDAP/Reach なしで動作します。両方とも任意の peer dependency で、必要な連携だけをインストールします。以下は reldb/cache 初期化後の起動・認証関数内の例で、`username/password` は検証済み入力です。
40
44
 
41
- ### LDAP / Active Directory
45
+ #### LDAP / Active Directory
42
46
 
43
47
  既定クライアントは `pnpm add ldapts` で追加し、`login.ldap: true` と接続設定を明示します。接続設定だけでは LDAP は有効になりません。SDK なしで既定クライアントを有効にすると初期化が設定エラーを返します。
44
48
 
@@ -75,7 +79,7 @@ if (!result.success)
75
79
  return result
76
80
  ```
77
81
 
78
- ### 任意の Reach による自動送信
82
+ #### 任意の Reach による自動送信
79
83
 
80
84
  `pnpm add @h-ai/reach` で追加します。設定、初期化、終了はアプリが管理し、IAM は注入されたインスタンスだけを使用します。SMTP/HTTP は例で、SMS ゲートウェイは [Reach 通知仕様](../reach/README.ja.md) を受け取ります。Console はメタデータのみを記録し、実際の送信は行いません。
81
85
 
@@ -133,7 +137,7 @@ if (!initialized.success)
133
137
  return iam.auth.sendOtp('user@example.com')
134
138
  ```
135
139
 
136
- ### 独自メール・SMS 送信
140
+ #### 独自メール・SMS 送信
137
141
 
138
142
  `onOtpSendEmail`、`onOtpSendSms`、`onPasswordResetRequest` を注入して既存の送信サービスを使用します。必要なものだけ実装でき、Reach と併用する場合は明示コールバックを優先します。例のサービスはアプリ実装で、送信成功時のみ Promise を解決します。HaiResult を返すサービスは `success` を検査し、失敗時はコールバック内で例外にします。IAM は例外を捕捉して業務エラーを返し、コードや Token、送信内部情報をユーザーに表示しません。
139
143
 
@@ -164,7 +168,7 @@ if (!initialized.success)
164
168
  return initialized
165
169
  ```
166
170
 
167
- ## クイックスタート
171
+ ### クイックスタート
168
172
 
169
173
  ```ts
170
174
  import { cache } from '@h-ai/cache'
@@ -209,9 +213,9 @@ LDAP は既定で無効です。公開 OTP は `otp` 設定と送信コールバ
209
213
 
210
214
  `iam.config` は**機密情報をマスクした設定スナップショット**を返します。LDAP の `url` / `bindPassword` などは自動的に非表示になります。状態確認用で、元の設定の代わりにはなりません。
211
215
 
212
- ## 設計
216
+ ### 設計
213
217
 
214
- ### アーキテクチャ
218
+ #### アーキテクチャ
215
219
 
216
220
  IAM は `iam.init()` / `iam.close()` で状態を管理する**ライフサイクル付きシングルトン**です。内部の 5 つのサブモジュールを依存順に初期化します:
217
221
 
@@ -228,7 +232,7 @@ iam.init()
228
232
 
229
233
  初期化前のサブモジュール呼び出しは `NotInitializedKit` Proxy により `NOT_INITIALIZED` を返します。
230
234
 
231
- ### データの保存先
235
+ #### データの保存先
232
236
 
233
237
  | データ種別 | 保存先 | 説明 |
234
238
  | ------------------------------------------------- | --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
@@ -241,16 +245,16 @@ iam.init()
241
245
 
242
246
  ---
243
247
 
244
- ### 認証(authn)— `iam.auth`
248
+ #### 認証(authn)— `iam.auth`
245
249
 
246
- #### 主要な型
250
+ ##### 主要な型
247
251
 
248
252
  - **`AuthnOperations`**:認証操作。`login` / `loginWithOtp` / `loginWithLdap` / `loginWithApiKey` / `logout` / `verifyToken` / `sendOtp` / `registerAndLogin`。
249
253
  - **`AuthStrategy`**:認証方式のインターフェース(`type` + `authenticate` + `challenge?`)。パスワード・OTP・LDAP・API Key の 4 実装。
250
254
  - **`AuthResult`**:ユーザー情報、`TokenPair`(accessToken + refreshToken)、ロール・権限を含むログイン結果。
251
255
  - **`Credentials`**:`{ type: 'password' | 'otp' | 'ldap' | 'apikey' }` と各方式の認証情報を組み合わせた共通の共用体型。
252
256
 
253
- #### 主要な処理
257
+ ##### 主要な処理
254
258
 
255
259
  **パスワードによるログイン:**
256
260
 
@@ -280,7 +284,7 @@ verifyToken(accessToken)
280
284
  → 現在の Session を返す(userId、roles、permissions)
281
285
  ```
282
286
 
283
- #### 使用方法
287
+ ##### 使用方法
284
288
 
285
289
  例は reldb/cache/IAM の初期化に成功したサーバーで実行し、検証済みの入力を使います。パスワード認証は既定で有効です。OTP には `login.otp: true`、`otp` チャネル、送信コールバック/Reach テンプレートが必要です。LDAP は `login.ldap: true`、API Key は `login.apikey: true` を有効にします。ログインと登録後ログインは `AuthResult` を返します。登録とパスワード認証のスイッチは独立しています。
286
290
 
@@ -312,7 +316,7 @@ async function registerWithSession(username: string, password: string) {
312
316
  }
313
317
  ```
314
318
 
315
- #### 注意点
319
+ ##### 注意点
316
320
 
317
321
  - **タイミング攻撃対策**:未存在ユーザーにも dummy password hash を実行し、応答時間をそろえます。
318
322
  - **アカウントロック**:連続 `maxLoginAttempts` 回の失敗で `lockoutDuration` 秒ロック。
@@ -321,15 +325,15 @@ async function registerWithSession(username: string, password: string) {
321
325
 
322
326
  ---
323
327
 
324
- ### セッション(session)— `iam.session`
328
+ #### セッション(session)— `iam.session`
325
329
 
326
- #### 主要な型
330
+ ##### 主要な型
327
331
 
328
332
  - **`SessionOperations`**:`create` / `get` / `verifyToken` / `update` / `delete` / `deleteByUserId` / `refresh` / `revokeRefresh` / `patchUserSessions`。
329
333
  - **`Session`**:userId、roles、permissions、accessToken、期限などを含むセッション。
330
334
  - **`TokenPair`**:Token のペア。`{ accessToken, refreshToken, expiresIn, tokenType: 'Bearer' }`。
331
335
 
332
- #### 主要な処理
336
+ ##### 主要な処理
333
337
 
334
338
  **Token 更新(Rotation):**
335
339
 
@@ -352,7 +356,7 @@ create(options) [singleDevice=true]
352
356
  → session をキャッシュへ保存
353
357
  ```
354
358
 
355
- #### 使用方法
359
+ ##### 使用方法
356
360
 
357
361
  `tokens.accessToken` でリクエストを認証し、`tokens.refreshToken` で新しいペアを取得します。`expiresIn` は秒です。独立 API は `Authorization: Bearer <accessToken>`、SvelteKit の Cookie 入口は後述の kit 標準連携を使います。
358
362
 
@@ -387,7 +391,7 @@ async function forceSignOut(userId: string) {
387
391
  }
388
392
  ```
389
393
 
390
- #### 注意点
394
+ ##### 注意点
391
395
 
392
396
  - **キャッシュのみの保存**:Memory は再起動時に消失します。共有 Redis のセッションは IAM サービス再起動後も TTL または失効まで保持します。
393
397
  - **自動延長**:`sliding: true` なら各 `get()` で TTL を延長。
@@ -396,18 +400,18 @@ async function forceSignOut(userId: string) {
396
400
 
397
401
  ---
398
402
 
399
- ### 認可(authz)— `iam.authz`
403
+ #### 認可(authz)— `iam.authz`
400
404
 
401
405
  ロール一覧は `iam.authz.getRoleUserCounts(roleIds)` の 1 回の集計で現在のページの実際の所属人数を取得し、`HaiResult<Map<string, number>>` を返します。関連がなければ 0、検索失敗は読み込みエラーを表示し、0 件の統計を作りません。
402
406
 
403
- #### 主要な型
407
+ ##### 主要な型
404
408
 
405
409
  - **`AuthzOperations`**:ロール・権限 CRUD、ロールと権限・ユーザーとロールの割り当て、権限確認。
406
410
  - **`Role`**:ロール。`{ id, code, name, description, isSystem }`。
407
411
  - **`Permission`**:権限。`{ id, code, name, type, resource, action }`、type は `menu` / `api` / `button`。
408
412
  - **`PermissionType`**:権限種別の enum。
409
413
 
410
- #### 主要な処理
414
+ ##### 主要な処理
411
415
 
412
416
  **権限確認(`checkPermission`):**
413
417
 
@@ -430,7 +434,7 @@ assignPermissionToRole(roleId, permId)
430
434
  (best-effort:失敗は log.error のみで割り当て結果に影響しない)
431
435
  ```
432
436
 
433
- #### 使用方法
437
+ ##### 使用方法
434
438
 
435
439
  権限を作成し、その **ID** を `permissionIds` に保存してロールを作成し、ロール **ID** でユーザーに割り当てます。セッションとガードは安定した **code** を使います。表示名は独立して変更できます。例は RBAC が有効であることを前提とし、管理操作の権限とリソース範囲はアプリで確認します。
436
440
 
@@ -478,7 +482,7 @@ async function currentAuthorization(userId: string) {
478
482
  }
479
483
  ```
480
484
 
481
- #### 注意点
485
+ ##### 注意点
482
486
 
483
487
  - **ワイルドカード**:`admin:*` は `admin:read`、`admin:write:detail` などに一致。1 つの `*` がコロン以降全体に一致します。
484
488
  - **最高管理者**:`rbac.superAdminRole`(既定 `super_admin`)はすべての権限を持ちます。
@@ -488,9 +492,9 @@ async function currentAuthorization(userId: string) {
488
492
 
489
493
  ---
490
494
 
491
- ### ユーザー管理(user)— `iam.user`
495
+ #### ユーザー管理(user)— `iam.user`
492
496
 
493
- #### 主要な型
497
+ ##### 主要な型
494
498
 
495
499
  - **`UserOperations`**:登録、検索、更新、削除、パスワード変更・リセット、強度検証。
496
500
  - **`User`**(公開フィールド):`{ id, username, email, phone, displayName, avatarUrl, enabled, roles? }`。
@@ -498,7 +502,7 @@ async function currentAuthorization(userId: string) {
498
502
  - **`RegisterOptions`** / **`RegisterResult`**:登録の入力・出力。
499
503
  - **`UpdateCurrentUserInput`**:現在のユーザーが変更できるフィールド(`username` / `email` / `displayName` / `avatarUrl` / `phone` / `metadata`)。
500
504
 
501
- #### 主要な処理
505
+ ##### 主要な処理
502
506
 
503
507
  **ユーザー登録:**
504
508
 
@@ -528,7 +532,7 @@ confirmPasswordReset(token, newPassword)
528
532
  → 使用済みリセット Token を削除
529
533
  ```
530
534
 
531
- #### 使用方法
535
+ ##### 使用方法
532
536
 
533
537
  `register` はユーザーだけを返し、セッションは作りません。同時ログインには `auth.registerAndLogin` を使います。管理用の `createUser` は自己登録設定と独立しています。自己プロフィールは `getCurrentUser/updateCurrentUser` で username/email/displayName/avatarUrl/phone/metadata を更新します。安全性に関わるフィールドとロールは認可された管理入口を使います。
534
538
 
@@ -586,7 +590,7 @@ async function disableUser(userId: string) {
586
590
  }
587
591
  ```
588
592
 
589
- #### 注意点
593
+ ##### 注意点
590
594
 
591
595
  - **`toUser()` の機密除去**:公開ユーザー情報から `passwordHash`、`loginFailedCount` などを除外。
592
596
  - **セッション連動**:`deleteUser` / `updateUser({ enabled: false })` / `changePassword` / `confirmPasswordReset` は対象ユーザーの全セッションを削除。
@@ -597,17 +601,17 @@ async function disableUser(userId: string) {
597
601
 
598
602
  ---
599
603
 
600
- ### API Key — `iam.apiKey`
604
+ #### API Key — `iam.apiKey`
601
605
 
602
606
  `scopes` は主体の権限との共通範囲を適用し、`*` と名前空間 `:*` に対応します。既定の空配列は本人確認のみで操作権限を付与しません。制限付き key のセッション `roles` は空で permission ガードを使用し、明示的な `*` も主体の権限を超えません。ログインは key ID に結び付き、get/verifyToken/refresh は永続状態を再確認し、失効・無効・期限切れ・DB エラーを拒否します。verifyApiKey 単独は鍵のメタデータ検証のみで、主体・操作の認可は別途必要です。
603
607
 
604
- #### 主要な型
608
+ ##### 主要な型
605
609
 
606
610
  - **`ApiKeyOperations`**:`createApiKey` / `listApiKeys` / `getApiKey` / `revokeApiKey` / `verifyApiKey`。
607
611
  - **`ApiKey`**(公開フィールド):`{ id, userId, name, keyPrefix, enabled, expiresAt, scopes }`。
608
612
  - **`CreateApiKeyResult`**:`{ apiKey, rawKey }`。平文の鍵を返すのは一度のみ。
609
613
 
610
- #### 主要な処理
614
+ ##### 主要な処理
611
615
 
612
616
  **API Key の検証:**
613
617
 
@@ -621,7 +625,7 @@ verifyApiKey(rawKey)
621
625
  → ApiKey を返す
622
626
  ```
623
627
 
624
- #### 使用方法
628
+ ##### 使用方法
625
629
 
626
630
  初期化設定で API Key を有効にします。Key はユーザーの制限付き認証情報です:**有効権限 = 現在のユーザー権限 ∩ scopes**。scope は権限 code で、ロール名、表示名、リソース ID ではありません。権限を作成せず、テナント/レコード所有権の検証も代替しません。
627
631
 
@@ -707,7 +711,7 @@ Key セッションは `iam.session.refresh` 後も Key に紐付きます。取
707
711
 
708
712
  `expirationDays: 0` は無期限、省略時は `apikey.defaultExpirationDays` を使用します。期限が必要なら明示します。生 Key/Token を URL、ログ、公開設定、通常の localStorage に保存しません。
709
713
 
710
- #### 注意点
714
+ ##### 注意点
711
715
 
712
716
  - **平文は保存しない**:DB は hash のみを保存し、作成時の `rawKey` が唯一の取得機会。
713
717
  - **接頭辞検索**:`keyPrefix` で候補を絞り、全テーブル走査を避けます。
@@ -716,11 +720,11 @@ Key セッションは `iam.session.refresh` 後も Key に紐付きます。取
716
720
 
717
721
  ---
718
722
 
719
- ## その他の使い方
723
+ ### その他の使い方
720
724
 
721
725
  API 引数・エラーコード・統合パターンの詳細は中国語の Skill テンプレート(`packages/cli/templates/skills/hai-iam/SKILL.md`)を参照してください。
722
726
 
723
- ## フロントエンドの API 呼び出し
727
+ ### フロントエンドの API 呼び出し
724
728
 
725
729
  公開 HTTP API は `@h-ai/api-contract` で定義し、`@h-ai/api-client` の型付きクライアントで呼び出します:
726
730
 
@@ -745,7 +749,7 @@ await apiClient.iam.auth.changePassword({
745
749
  })
746
750
  ```
747
751
 
748
- ## パスワードリセット
752
+ ### パスワードリセット
749
753
 
750
754
  リセットには `passwordReset.enabled` と送信コールバック/Reach テンプレートが必要です。成功はアカウントの存在や受信を証明しません。送信・内部処理の HaiResult エラーも処理します。期限/試行回数制限があり、一度だけ成功消費でき、成功時に旧セッションを取り消します。管理者リセットに旧パスワードは不要ですが、アプリで管理権限を確認します。
751
755
 
@@ -768,7 +772,7 @@ async function resetByAdministrator(userId: string, newPassword: string) {
768
772
  }
769
773
  ```
770
774
 
771
- ## 一度限りのチケット — `iam.ticket`
775
+ ### 一度限りのチケット — `iam.ticket`
772
776
 
773
777
  WebSocket URL など Bearer Token を付けられない経路向けの短期チケットです。認証済み HTTP リクエストで、主体・用途・操作に結び付けて発行します。サーバーは初回検証時に**アトミックに消費**し、一度のみ有効です。
774
778
 
@@ -801,7 +805,7 @@ async function consumeAudioTicket(ticket: string) {
801
805
 
802
806
  特徴:暗号学的に安全な乱数、TTL、アトミックな単回消費(並行消費では 1 件のみ成功)、用途の固定による別用途の再使用防止。
803
807
 
804
- ## 設定による認証画面
808
+ ### 設定による認証画面
805
809
 
806
810
  `iam.publicConfig` とクライアントの HTTP `client.iam.auth.publicConfig()` は秘密情報を含まない設定だけを公開し、実際に利用可能な認証方式を表示します。`login.password/otp/ldap`、`otp.email/sms`、`register.enabled/defaultEnabled`、`passwordReset.enabled/mode/resetUrl` と規約表示を個別に設定できます。`ui` はカード/分割レイアウト、タイトル、説明、ブランド、Logo、画像を指定します。LDAP は任意で既定は無効です。既定クライアントを選択する時だけ ldapts を追加するか、独自ファクトリを注入します。
807
811
 
@@ -817,7 +821,7 @@ SvelteKit は `kit.auth.createApi(iam)` と匿名用の `kit.auth.publicPaths`
817
821
 
818
822
  `passwordReset.mode` は `email`(既定、`password_reset`)または `phone`(`password_reset_sms`)を選び、公開設定に応じて復旧フォームが入力欄を切り替えます。
819
823
 
820
- ### Markdown の規約
824
+ #### Markdown の規約
821
825
 
822
826
  `agreements.userAgreementMarkdown` と `privacyPolicyMarkdown` で設定から直接本文を読み込みます。YAML の `|` は段落と改行を保持します。Modal は安全な MarkdownRenderer を再利用し、生 HTML、スクリプト、コードツールを無効にします。同じ規約に本文と URL を指定した場合は本文を優先し、URL は任意の外部表示リンクとして使用します。URL のみの場合は隔離 iframe で表示します。
823
827
 
@@ -840,7 +844,7 @@ agreements:
840
844
  # privacyPolicyUrl: https://example.com/privacy
841
845
  ```
842
846
 
843
- ## エラー処理
847
+ ### エラー処理
844
848
 
845
849
  すべての操作は `HaiResult<T>` を返し、`HaiIamError` で分岐します:
846
850
 
@@ -855,7 +859,7 @@ if (!result.success) {
855
859
  }
856
860
  ```
857
861
 
858
- ## テスト
862
+ ## 開発とテスト
859
863
 
860
864
  ```bash
861
865
  pnpm test
package/README.md CHANGED
@@ -4,9 +4,11 @@
4
4
 
5
5
  [框架首页](../../README.md) · 相关模块:[@h-ai/audit](../audit/README.md) · [@h-ai/cache](../cache/README.md) · [@h-ai/core](../core/README.md) · [@h-ai/crypto](../crypto/README.md) · [@h-ai/reldb](../reldb/README.md) · [@h-ai/reach](../reach/README.md)
6
6
 
7
+ ## 简介
8
+
7
9
  身份与访问管理模块,提供统一的 `iam` 对象实现认证、会话、授权与用户管理。
8
10
 
9
- ## 功能特性
11
+ ## 核心功能清单
10
12
 
11
13
  | 功能 | 说明 |
12
14
  | -------------- | -------------------------------------------------------------------------------------- |
@@ -18,13 +20,15 @@
18
20
  | **一次性票据** | 短期、一次性能力票据(签发 / 原子消费),用于 WebSocket 等无法携带 Bearer Token 的通道 |
19
21
  | **前端客户端** | HTTP API 契约定义,支持独立前端使用 |
20
22
 
21
- ## 安装
23
+ ## 使用入门
24
+
25
+ ### 安装
22
26
 
23
27
  ```bash
24
28
  pnpm add @h-ai/iam
25
29
  ```
26
30
 
27
- ## 依赖
31
+ ### 依赖
28
32
 
29
33
  - `@h-ai/reldb` — 数据库(用户/角色/权限持久化),**需在 iam.init() 前初始化**
30
34
  - `@h-ai/cache` — 缓存(会话/OTP/重置令牌/一次性票据/共享锁),**需在 iam.init() 前初始化**
@@ -34,11 +38,11 @@ pnpm add @h-ai/iam
34
38
  - `@h-ai/reach` — **可选**,应用安装、初始化并注入后可使用标准邮件/短信模板;自定义发送回调无需 Reach。
35
39
  - `ldapts` — **可选**,仅在显式启用 LDAP 且没有注入 `ldapClientFactory` 时加载。
36
40
 
37
- ## 可选 LDAP 与通知接入
41
+ ### 可选 LDAP 与通知接入
38
42
 
39
43
  只使用密码、用户管理和 RBAC 时,无需安装或初始化 LDAP/Reach。两者均为可选 peer dependency,应用只安装选用的集成。下列发送、LDAP 片段位于应用启动/认证函数中,reldb/cache 必须先成功初始化;`username/password` 来自已校验输入。
40
44
 
41
- ### LDAP / Active Directory
45
+ #### LDAP / Active Directory
42
46
 
43
47
  使用默认客户端时执行 `pnpm add ldapts`,再显式设置 `login.ldap: true` 和 `ldap` 连接配置。只提供连接配置不会启用 LDAP。启用默认客户端却缺少 SDK 时,`iam.init()` 返回配置错误。
44
48
 
@@ -75,7 +79,7 @@ if (!result.success)
75
79
  return result
76
80
  ```
77
81
 
78
- ### 使用可选 Reach 自动发送
82
+ #### 使用可选 Reach 自动发送
79
83
 
80
84
  执行 `pnpm add @h-ai/reach`,应用负责 Reach 的配置、初始化和关闭,IAM 只使用传入的实例。以下 SMTP/HTTP Provider 为示例,SMS 网关按 [Reach 接口](../reach/README.md) 接收通知。Console 只记录元数据,不能用于实际收信。
81
85
 
@@ -133,7 +137,7 @@ if (!initialized.success)
133
137
  return iam.auth.sendOtp('user@example.com')
134
138
  ```
135
139
 
136
- ### 应用自定义短信 / 邮件发送
140
+ #### 应用自定义短信 / 邮件发送
137
141
 
138
142
  注入 `onOtpSendEmail`、`onOtpSendSms` 和 `onPasswordResetRequest` 即可接入现有发送服务;可只实现需要的回调,也可与 Reach 混用,显式回调优先。以下服务为应用实现,示例假定其 Promise 仅在投递成功时完成;若服务返回 HaiResult,须检查 `success`,失败时在回调内抛错。IAM 捕获回调异常并返回业务错误,不将验证码、令牌或内部投递详情写入用户提示。
139
143
 
@@ -164,7 +168,7 @@ if (!initialized.success)
164
168
  return initialized
165
169
  ```
166
170
 
167
- ## 快速开始
171
+ ### 快速开始
168
172
 
169
173
  ```ts
170
174
  import { cache } from '@h-ai/cache'
@@ -209,9 +213,9 @@ if (!result.success)
209
213
 
210
214
  `iam.config` 返回的是**脱敏后的配置快照**:LDAP `url` / `bindPassword` 等敏感配置会自动隐藏,便于状态检查但不能代替原始配置源。
211
215
 
212
- ## 设计说明
216
+ ### 设计说明
213
217
 
214
- ### 架构概览
218
+ #### 架构概览
215
219
 
216
220
  IAM 是**生命周期单例**模块,通过 `iam.init()` / `iam.close()` 管理运行时状态。内部由 5 个子模块构成,按依赖顺序初始化:
217
221
 
@@ -228,7 +232,7 @@ iam.init()
228
232
 
229
233
  初始化前访问任何子模块方法,均返回 `NOT_INITIALIZED` 错误(基于 `NotInitializedKit` Proxy)。
230
234
 
231
- ### 数据存储分布
235
+ #### 数据存储分布
232
236
 
233
237
  | 数据类型 | 存储位置 | 说明 |
234
238
  | ------------------------------------------------- | ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
@@ -241,16 +245,16 @@ iam.init()
241
245
 
242
246
  ---
243
247
 
244
- ### 子模块:认证(authn)— `iam.auth`
248
+ #### 子模块:认证(authn)— `iam.auth`
245
249
 
246
- #### 关键对象
250
+ ##### 关键对象
247
251
 
248
252
  - **`AuthnOperations`** — 认证操作接口,包含 `login` / `loginWithOtp` / `loginWithLdap` / `loginWithApiKey` / `logout` / `verifyToken` / `sendOtp` / `registerAndLogin`
249
253
  - **`AuthStrategy`** — 认证策略接口(`type` + `authenticate` + `challenge?`),4 种实现:密码、OTP、LDAP、API Key
250
254
  - **`AuthResult`** — 登录结果,含用户信息、`TokenPair`(accessToken + refreshToken)、角色/权限列表
251
255
  - **`Credentials`** — 统一凭证联合类型(`{ type: 'password' | 'otp' | 'ldap' | 'apikey' } & 具体凭证`)
252
256
 
253
- #### 关键流程
257
+ ##### 关键流程
254
258
 
255
259
  **密码登录流程:**
256
260
 
@@ -280,7 +284,7 @@ verifyToken(accessToken)
280
284
  → 返回当前 Session 对象(含 userId、roles、permissions)
281
285
  ```
282
286
 
283
- #### 使用方式
287
+ ##### 使用方式
284
288
 
285
289
  以下示例运行于服务端,reldb/cache/IAM 已成功初始化,参数来自校验后的请求。密码登录默认可用;OTP 需 `login.otp: true`、`otp` 通道和发送回调/Reach 模板;LDAP 需 `login.ldap: true`(配置见前文);API Key 需 `login.apikey: true`(完整流程见下文)。登录返回 `AuthResult`,注册并登录返回相同认证结构;`register.enabled` 与密码登录开关分别控制。
286
290
 
@@ -312,7 +316,7 @@ async function registerWithSession(username: string, password: string) {
312
316
  }
313
317
  ```
314
318
 
315
- #### 特别关注
319
+ ##### 特别关注
316
320
 
317
321
  - **Timing attack 防护**:用户不存在时执行 dummy password hash,保持响应时间一致
318
322
  - **账户锁定**:连续登录失败 `maxLoginAttempts` 次后锁定 `lockoutDuration` 秒
@@ -321,15 +325,15 @@ async function registerWithSession(username: string, password: string) {
321
325
 
322
326
  ---
323
327
 
324
- ### 子模块:会话(session)— `iam.session`
328
+ #### 子模块:会话(session)— `iam.session`
325
329
 
326
- #### 关键对象
330
+ ##### 关键对象
327
331
 
328
332
  - **`SessionOperations`** — 会话操作接口:`create` / `get` / `verifyToken` / `update` / `delete` / `deleteByUserId` / `refresh` / `revokeRefresh` / `patchUserSessions`
329
333
  - **`Session`** — 会话实体,含 userId、roles、permissions、accessToken、过期时间等
330
334
  - **`TokenPair`** — 令牌对:`{ accessToken, refreshToken, expiresIn, tokenType: 'Bearer' }`
331
335
 
332
- #### 关键流程
336
+ ##### 关键流程
333
337
 
334
338
  **Token 刷新(Rotation 策略):**
335
339
 
@@ -352,7 +356,7 @@ create(options) [singleDevice=true]
352
356
  → 存储 session 到缓存
353
357
  ```
354
358
 
355
- #### 使用方式
359
+ ##### 使用方式
356
360
 
357
361
  登录成功后通过 `tokens.accessToken` 认证请求,通过 `tokens.refreshToken` 换取新令牌;`expiresIn` 单位为秒。独立 API 使用 `Authorization: Bearer <accessToken>`;SvelteKit Cookie 入口使用 kit 的标准认证集成,详见下方前端调用。
358
362
 
@@ -387,7 +391,7 @@ async function forceSignOut(userId: string) {
387
391
  }
388
392
  ```
389
393
 
390
- #### 特别关注
394
+ ##### 特别关注
391
395
 
392
396
  - **纯缓存存储**:不落库;Memory 重启后丢失,共享 Redis 可跨 IAM 服务重启保留会话直至 TTL 或撤销
393
397
  - **滑动续期**:`sliding: true` 时每次 `get()` 自动延长 TTL
@@ -396,18 +400,18 @@ async function forceSignOut(userId: string) {
396
400
 
397
401
  ---
398
402
 
399
- ### 子模块:授权(authz)— `iam.authz`
403
+ #### 子模块:授权(authz)— `iam.authz`
400
404
 
401
405
  角色列表通过 `iam.authz.getRoleUserCounts(roleIds)` 一次聚合当前页角色的真实成员数,返回 `HaiResult<Map<string, number>>`。无关联为 0;查询失败显示加载错误,不生成零统计。
402
406
 
403
- #### 关键对象
407
+ ##### 关键对象
404
408
 
405
409
  - **`AuthzOperations`** — 授权操作接口,覆盖角色 CRUD、权限 CRUD、角色-权限/用户-角色分配、权限检查
406
410
  - **`Role`** — 角色实体:`{ id, code, name, description, isSystem }`
407
411
  - **`Permission`** — 权限实体:`{ id, code, name, type, resource, action }`,type 可为 `menu` / `api` / `button`
408
412
  - **`PermissionType`** — 权限类型枚举
409
413
 
410
- #### 关键流程
414
+ ##### 关键流程
411
415
 
412
416
  **权限检查(`checkPermission`):**
413
417
 
@@ -430,7 +434,7 @@ assignPermissionToRole(roleId, permId)
430
434
  (best-effort:同步失败仅 log.error,不影响权限分配结果)
431
435
  ```
432
436
 
433
- #### 使用方式
437
+ ##### 使用方式
434
438
 
435
439
  先创建权限,使用权限 **ID** 保存角色的 `permissionIds`,再使用角色 **ID** 分配用户。会话中的 `roles`/`permissions` 和守卫使用稳定 **code**;显示 `name` 可独立修改,不能代替 ID/code。示例默认 RBAC 已启用,管理操作须由应用先校验调用者权限和资源范围。
436
440
 
@@ -478,7 +482,7 @@ async function currentAuthorization(userId: string) {
478
482
  }
479
483
  ```
480
484
 
481
- #### 特别关注
485
+ ##### 特别关注
482
486
 
483
487
  - **通配符匹配**:`admin:*` 匹配 `admin:read`、`admin:write:detail` 等(单层 `*` 匹配冒号后全部)
484
488
  - **超管角色**:配置的 `rbac.superAdminRole`(默认 `super_admin`)自动拥有所有权限
@@ -488,9 +492,9 @@ async function currentAuthorization(userId: string) {
488
492
 
489
493
  ---
490
494
 
491
- ### 子模块:用户管理(user)— `iam.user`
495
+ #### 子模块:用户管理(user)— `iam.user`
492
496
 
493
- #### 关键对象
497
+ ##### 关键对象
494
498
 
495
499
  - **`UserOperations`** — 用户操作接口:注册、查询、更新、删除、密码修改与重置、密码强度验证
496
500
  - **`User`** — 用户实体(公开字段):`{ id, username, email, phone, displayName, avatarUrl, enabled, roles? }`
@@ -498,7 +502,7 @@ async function currentAuthorization(userId: string) {
498
502
  - **`RegisterOptions`** / **`RegisterResult`** — 注册输入与输出
499
503
  - **`UpdateCurrentUserInput`** — 当前用户可修改的白名单字段(`username` / `email` / `displayName` / `avatarUrl` / `phone` / `metadata`)
500
504
 
501
- #### 关键流程
505
+ ##### 关键流程
502
506
 
503
507
  **用户注册流程:**
504
508
 
@@ -528,7 +532,7 @@ confirmPasswordReset(token, newPassword)
528
532
  → 删除已使用的重置令牌
529
533
  ```
530
534
 
531
- #### 使用方式
535
+ ##### 使用方式
532
536
 
533
537
  `register` 返回用户但不登录;需要同时登录时使用 `auth.registerAndLogin`。`createUser` 是管理端入口,关闭自助注册不阻止管理员建用户。当前用户资料用 `getCurrentUser/updateCurrentUser`,允许 username/email/displayName/avatarUrl/phone/metadata,安全字段和角色使用经授权的管理入口。
534
538
 
@@ -586,7 +590,7 @@ async function disableUser(userId: string) {
586
590
  }
587
591
  ```
588
592
 
589
- #### 特别关注
593
+ ##### 特别关注
590
594
 
591
595
  - **`toUser()` 字段脱敏**:所有对外返回的用户对象均剥离 `passwordHash`、`loginFailedCount` 等敏感字段
592
596
  - **会话联动**:`deleteUser` / `updateUser({ enabled: false })` / `changePassword` / `confirmPasswordReset` 自动清除受影响用户的所有活跃会话
@@ -597,17 +601,17 @@ async function disableUser(userId: string) {
597
601
 
598
602
  ---
599
603
 
600
- ### 子模块:API Key — `iam.apiKey`
604
+ #### 子模块:API Key — `iam.apiKey`
601
605
 
602
606
  `scopes` 与主体权限取交集,支持 `*` 和命名空间 `:*`;默认空数组只保留身份,不授予操作权限。受限 key 的会话 `roles` 为空,通过 permission 守卫访问,显式 `*` 也不能超出主体授权。API Key 登录绑定 key ID,派生会话的 get/verifyToken/refresh 每次检查持久化 key,撤销、禁用、过期或 DB 读取失败即拒绝。单独 verifyApiKey 仅返回验证后的 key 元数据,仍需主体与操作授权。
603
607
 
604
- #### 关键对象
608
+ ##### 关键对象
605
609
 
606
610
  - **`ApiKeyOperations`** — API Key 操作接口:`createApiKey` / `listApiKeys` / `getApiKey` / `revokeApiKey` / `verifyApiKey`
607
611
  - **`ApiKey`** — API Key 实体(公开字段):`{ id, userId, name, keyPrefix, enabled, expiresAt, scopes }`
608
612
  - **`CreateApiKeyResult`** — 创建结果:`{ apiKey, rawKey }`(明文密钥仅返回一次)
609
613
 
610
- #### 关键流程
614
+ ##### 关键流程
611
615
 
612
616
  **API Key 验证:**
613
617
 
@@ -621,7 +625,7 @@ verifyApiKey(rawKey)
621
625
  → 返回 ApiKey 实体
622
626
  ```
623
627
 
624
- #### 使用方式
628
+ ##### 使用方式
625
629
 
626
630
  先在初始化配置中启用 API Key。它是用户的受限凭据,不能独立授予用户未拥有的权限:**有效权限 = 当前主体权限 ∩ scopes**。scopes 是与权限 code 相同的字符串,不是角色名、菜单名称或资源 ID;不会创建权限,也不会替代租户/记录归属校验。
627
631
 
@@ -707,7 +711,7 @@ Key 登录会话绑定 Key ID。刷新使用普通 `iam.session.refresh`,不
707
711
 
708
712
  `expirationDays: 0` 表示不设过期时间,省略时使用 `apikey.defaultExpirationDays`;需要限时访问时显式设置。明文 Key 和 Token 不进入 URL、日志、公开配置或普通 localStorage。
709
713
 
710
- #### 特别关注
714
+ ##### 特别关注
711
715
 
712
716
  - **明文不落库**:数据库仅存储 hash,创建时返回的 `rawKey` 是唯一获取机会
713
717
  - **前缀检索**:使用 `keyPrefix` 缩小候选集,避免全表扫描
@@ -716,11 +720,11 @@ Key 登录会话绑定 Key ID。刷新使用普通 `iam.session.refresh`,不
716
720
 
717
721
  ---
718
722
 
719
- ## 更多用法
723
+ ### 更多用法
720
724
 
721
725
  详细 API 参数、错误码及集成模式请参考 Skill 模板(`packages/cli/templates/skills/hai-iam/SKILL.md`)。
722
726
 
723
- ## 前端 API 调用
727
+ ### 前端 API 调用
724
728
 
725
729
  公共 HTTP API 由 `@h-ai/api-contract` 定义,并通过 `@h-ai/api-client` 生成 typed client 调用:
726
730
 
@@ -745,7 +749,7 @@ await apiClient.iam.auth.changePassword({
745
749
  })
746
750
  ```
747
751
 
748
- ## 密码重置
752
+ ### 密码重置
749
753
 
750
754
  重置需要启用 `passwordReset.enabled` 和可用的通知回调/Reach 模板。申请成功仅表示请求已处理,不证明账户存在或用户已收信;发送或内部处理失败仍须处理失败 HaiResult。令牌只允许一次成功消费且有有效期/尝试限制,成功后撤销旧会话。管理员重置无需旧密码,但须由应用验证管理权限。
751
755
 
@@ -768,7 +772,7 @@ async function resetByAdministrator(userId: string, newPassword: string) {
768
772
  }
769
773
  ```
770
774
 
771
- ## 一次性票据 — `iam.ticket`
775
+ ### 一次性票据 — `iam.ticket`
772
776
 
773
777
  短期、一次性能力票据。用于无法携带 Bearer Token 的通道(如 WebSocket URL):由已认证的 HTTP 请求签发一个绑定主体、用途与操作的票据,服务端在首次校验时**原子消费**(单次有效)。
774
778
 
@@ -801,7 +805,7 @@ async function consumeAudioTicket(ticket: string) {
801
805
 
802
806
  特性:密码学安全随机值、TTL、原子单次消费(并发消费仅一个成功)、用途绑定(防跨用途重放)。
803
807
 
804
- ## 配置驱动认证页面
808
+ ### 配置驱动认证页面
805
809
 
806
810
  服务端通过 `iam.publicConfig` 提供白名单配置,或通过客户端的 `client.iam.auth.publicConfig()` 标准 HTTP contract 获取。只展示已开启且具备依赖的登录方式;LDAP 接入见上方可选配置,默认关闭;只有选用默认客户端时才安装 ldapts。
807
811
 
@@ -822,7 +826,7 @@ SvelteKit 使用 `kit.auth.createApi(iam)` 导出标准 handler,匿名端点
822
826
 
823
827
  协议在 Modal 中打开,关闭后保留表单输入。使用 URL 时,协议页面须允许 iframe 嵌入;禁止脚本执行,并提供新窗口查看入口。`LoginForm/RegisterForm` 提供 `header/extraFields/footer` snippet,`AuthPanel` 透传 `loginHeader/loginFields/loginFooter` 和 `registerHeader/registerFields/registerFooter`;自定义字段由应用绑定状态并在动作中处理。示例见 [UI 用法](../ui/README.md#配置驱动认证页面)。
824
828
 
825
- ### Markdown 协议内容
829
+ #### Markdown 协议内容
826
830
 
827
831
  协议支持 `agreements.userAgreementMarkdown` 与 `privacyPolicyMarkdown` 直接从配置加载;YAML 的 `|` 保留 Markdown 段落和换行。弹窗复用安全 MarkdownRenderer,关闭原始 HTML、脚本和代码复制工具。同主题同时提供正文与 URL 时优先正文,URL 可作为额外查看入口;只有 URL 时使用隔离 iframe。
828
832
 
@@ -845,7 +849,7 @@ agreements:
845
849
  # privacyPolicyUrl: https://example.com/privacy
846
850
  ```
847
851
 
848
- ### 分布式安全与边界
852
+ #### 分布式安全与边界
849
853
 
850
854
  所有节点必须共享同一数据库和 Redis;Memory 仅用于单进程。登录 `maxLoginAttempts / lockoutDuration` 使用数据库原子更新,到期后的失败重新计数。OTP `maxAttempts / resendInterval` 使用共享锁和独立发送间隔状态,耗尽或消费验证码不能绕过重发限制,并发只消费一次。重置 `tokenExpiresIn / maxAttempts` 使用共享缓存 TTL 和每用户共享锁;每次有效令牌请求(包括密码策略失败)计数,最新令牌有效且只成功消费一次。重置后撤销旧会话、解除登录锁定;禁用用户不能通过密码、LDAP、OTP 登录。密码策略以服务端为准。
851
855
 
@@ -853,7 +857,7 @@ agreements:
853
857
 
854
858
  标准 HTTP 入口:`GET /auth/config`、`POST /auth/login/ldap`、`POST /auth/password-reset/request`、`POST /auth/password-reset/confirm`;验证码继续使用 `/auth/otp/send` 与 `/auth/login/otp`。安全错误:`SECURITY_BUSY (hai:iam:023)`、`PASSWORD_RESET_DISABLED (hai:iam:024)`。
855
859
 
856
- ## 错误处理
860
+ ### 错误处理
857
861
 
858
862
  所有操作返回 `HaiResult<T>`,通过 `HaiIamError` 做分支判断:
859
863
 
@@ -868,7 +872,7 @@ if (!result.success) {
868
872
  }
869
873
  ```
870
874
 
871
- ## 测试
875
+ ## 开发测试
872
876
 
873
877
  ```bash
874
878
  pnpm test
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@h-ai/iam",
3
3
  "type": "module",
4
- "version": "0.1.0-alpha.66",
4
+ "version": "0.1.0-alpha.67",
5
5
  "description": "Hai Framework identity and access management module.",
6
6
  "license": "Apache-2.0",
7
7
  "exports": {
@@ -25,7 +25,7 @@
25
25
  "node": "^22.22.2 || ^24.15.0 || >=26.0.0"
26
26
  },
27
27
  "peerDependencies": {
28
- "@h-ai/reach": "0.1.0-alpha.66",
28
+ "@h-ai/reach": "0.1.0-alpha.67",
29
29
  "ldapts": "^9.2.0"
30
30
  },
31
31
  "peerDependenciesMeta": {
@@ -37,15 +37,15 @@
37
37
  }
38
38
  },
39
39
  "dependencies": {
40
- "@h-ai/audit": "0.1.0-alpha.66",
41
- "@h-ai/cache": "0.1.0-alpha.66",
42
- "@h-ai/core": "0.1.0-alpha.66",
43
- "@h-ai/crypto": "0.1.0-alpha.66",
44
- "@h-ai/reldb": "0.1.0-alpha.66",
40
+ "@h-ai/audit": "0.1.0-alpha.67",
41
+ "@h-ai/cache": "0.1.0-alpha.67",
42
+ "@h-ai/core": "0.1.0-alpha.67",
43
+ "@h-ai/crypto": "0.1.0-alpha.67",
44
+ "@h-ai/reldb": "0.1.0-alpha.67",
45
45
  "zod": "^4.6.5"
46
46
  },
47
47
  "devDependencies": {
48
- "@h-ai/reach": "0.1.0-alpha.66",
48
+ "@h-ai/reach": "0.1.0-alpha.67",
49
49
  "@types/node": "^26.6.5",
50
50
  "ldapts": "^9.2.0",
51
51
  "rimraf": "^6.1.3",