@h-ai/iam 0.1.0-alpha.74 → 0.1.0-alpha.75

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
@@ -33,7 +33,7 @@ pnpm add @h-ai/iam
33
33
  - `@h-ai/reldb`: persistence for users/roles/permissions; **initialize before iam.init()**.
34
34
  - `@h-ai/cache`: sessions/OTP/one-time tickets; **initialize before iam.init()**.
35
35
  - `@h-ai/crypto`: password hashing, used internally and initialized automatically.
36
- - `@h-ai/audit`: audit records for key RBAC operations.
36
+ - `@h-ai/audit`: automatic IAM security events after host initialization. An unused collector is not called; enabled audit failures remain observable and caller-owned transactions must roll back.
37
37
 
38
38
  - `@h-ai/reach`: **optional**, installed, initialized and injected by the application for standard notifications; custom callbacks do not require Reach.
39
39
  - `ldapts`: **optional**, loaded only when LDAP is explicitly enabled without an injected `ldapClientFactory`.
@@ -221,7 +221,7 @@ IAM is a **lifecycle singleton** managed through `iam.init()` / `iam.close()`. F
221
221
 
222
222
  ```
223
223
  iam.init()
224
- ├─ session (cached sessions; no DB dependency)
224
+ ├─ session (cached sessions; current credential state uses shared cache/DB fallback)
225
225
  ├─ authz (RBAC roles/permissions; batched DB reads)
226
226
  ├─ authn (authentication; depends on session + authz)
227
227
  ├─ user (users; depends on session + authz + passwordStrategy)
@@ -278,10 +278,10 @@ login(credentials)
278
278
  ```
279
279
  verifyToken(accessToken)
280
280
  → session.get(accessToken) → cache lookup
281
- → Read the persistent user and validate enabled status and the session-bound credentialVersion
281
+ → Read current user/credential state from the shared epoch cache; check status, expiry and session-bound credentialVersion, falling back to DB when the cache is not trustworthy
282
282
  → Extend TTL automatically when sliding renewal is enabled
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
283
+ → Reuse that user profile and read current roles/permissions from the shared epoch cache; cold authorization reads use one JOIN
284
+ → Reject required state read failures, missing/disabled/expired users or credential-version mismatch; intersect API Key scopes again
285
285
  → Return current Session (userId, roles, permissions)
286
286
  ```
287
287
 
@@ -427,9 +427,8 @@ Role lists use `iam.authz.getRoleUserCounts(roleIds)` to aggregate actual member
427
427
  ```
428
428
  checkPermission(userId, permission)
429
429
  → Return true if RBAC is disabled
430
- → Query user roles (DB)
431
- → Check super-admin role (cached superAdminRoleId); return true if present
432
- → Read these roles' permission codes in one DB query; return errors on read failure
430
+ → Read current roles/permissions from the shared epoch cache; cold reads use one JOIN
431
+ → Return true for the super-admin role; return errors on required state read failure
433
432
  → Match each permission exactly or by wildcard (`admin:*` matches `admin:read`)
434
433
  → Return true / false
435
434
  ```
@@ -496,7 +495,7 @@ async function currentAuthorization(userId: string) {
496
495
  - **Wildcard matching**: `admin:*` matches `admin:read`, `admin:write:detail`, etc.; one `*` matches everything after the colon.
497
496
  - **Super-admin role**: `rbac.superAdminRole` (default `super_admin`) automatically grants all permissions.
498
497
  - **Transactions**: `deleteRole` / `deletePermission` clear associations before deleting entities in a DB transaction.
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.
498
+ - **Session synchronization**: cache updates are best-effort. Protected requests use `iam.auth.verifyToken` to check current user/credential state and authorization through the shared epoch cache, with DB fallback when needed. Required state read failure rejects authentication. `iam.session.get/verifyToken` reuses the same current user/credential read and checks status, expiry and `credentialVersion`, but its role/permission fields remain a session snapshot and do not replace current authorization. `iam.authz.getUserAuthorization(userId)` uses a single JOIN cold read and shared cache.
500
499
  - **Seed data**: `seedDefaultData: true` idempotently creates admin/user/guest roles and basic permissions.
501
500
 
502
501
  ---
@@ -612,7 +611,7 @@ async function disableUser(userId: string) {
612
611
 
613
612
  #### API keys — `iam.apiKey`
614
613
 
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.
614
+ `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 required state read failures. Standalone verifyApiKey returns verified key metadata and still requires subject/operation authorization.
616
615
 
617
616
  ##### Key types
618
617
 
@@ -626,7 +625,8 @@ async function disableUser(userId: string) {
626
625
 
627
626
  ```
628
627
  verifyApiKey(rawKey)
629
- → Extract prefix from rawKey (prefix + first 8 characters)
628
+ → Read the shared epoch cache by SHA-256(rawKey) fingerprint; check expiry on hits
629
+ → On a cold read, extract prefix from rawKey (prefix + first 8 characters)
630
630
  → Query candidate keys by prefix from DB
631
631
  → Verify each hash synchronously
632
632
  → If valid, check expiration/disabled state
@@ -830,7 +830,7 @@ SvelteKit routes export handlers from `kit.auth.createApi(iam)`; use `kit.auth.p
830
830
 
831
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).
832
832
 
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.
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 current state via shared generation cache or safe DB fallback, 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.
834
834
 
835
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`.
836
836
 
@@ -882,6 +882,12 @@ Sessions and refresh consumption share one versioned `hai:iam:sessions:{SHA256(u
882
882
 
883
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
884
 
885
+ ### Shared authorization cache
886
+
887
+ Redis caches nonsecret account/credential state, role/permission summaries, sensitive operations and verified API Key DTOs for 60 seconds. Cache values omit database password/Key verification hashes, raw keys and reset tokens; API Key cache addresses use a one-way SHA-256 fingerprint of the raw Key. Account/password/Key expiry is evaluated against current time on every check; authorization cold reads use one JOIN. IAM writers establish a shared dirty marker before DB mutation and advance the epoch after commit/rollback, including caller-owned transactions. A racing cold read cannot publish an obsolete cache entry. Normal hot checks avoid PG; writes, unconfirmed invalidation or cache read failure use the database. Failure of the session Redis store still rejects authentication.
888
+
889
+ Failure to register invalidation rejects a write before mutation. Failed invalidation after commit preserves the successful mutation and persistent dirty marker, so later checks keep using DB. Crashed writers or unknown commit results never expire that marker by TTL; alert and repair cache state in a maintenance window after confirming no IAM write transaction remains. All nodes must upgrade together and write via IAM APIs. Raw SQL, old writers and independent table edits require quiesced writes and full IAM cache invalidation. Verified raw Key cache hits avoid repeated expensive hashes; lastUsedAt is sampled during cold verification, with up to about one minute lag.
890
+
885
891
  ## Self-hosted interactive challenges
886
892
 
887
893
  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.
@@ -934,6 +940,8 @@ For in-place recovery, use UI `createSessionRecovery` / `SessionRecoveryDialog`
934
940
 
935
941
  `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.
936
942
 
943
+ 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.
944
+
937
945
  ## Development and testing
938
946
 
939
947
  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-与示例验证).
package/README.ja.md CHANGED
@@ -33,7 +33,7 @@ pnpm add @h-ai/iam
33
33
  - `@h-ai/reldb`:ユーザー・ロール・権限の永続化。**iam.init() より先に初期化**。
34
34
  - `@h-ai/cache`:セッション・OTP・一度限りのチケット。**iam.init() より先に初期化**。
35
35
  - `@h-ai/crypto`:内部で使用するパスワードハッシュ。自動初期化。
36
- - `@h-ai/audit`:主要な RBAC 操作の監査記録。
36
+ - `@h-ai/audit`:ホストが初期化した場合に IAM の安全イベントを自動記録します。未使用時は収集せず、使用中の書込失敗は記録し、外部トランザクションはロールバックします。
37
37
 
38
38
  - `@h-ai/reach`:**任意**。アプリがインストール、初期化、注入して通知テンプレートを使用します。独自送信コールバックでは不要です。
39
39
  - `ldapts`:**任意**。LDAP を明示的に有効化し、`ldapClientFactory` がない場合だけ読み込みます。
@@ -221,7 +221,7 @@ IAM は `iam.init()` / `iam.close()` で状態を管理する**ライフサイ
221
221
 
222
222
  ```
223
223
  iam.init()
224
- ├─ session (キャッシュによるセッション、DB 依存なし)
224
+ ├─ session (キャッシュによるセッション、現在の資格情報は共有キャッシュ/DB参照)
225
225
  ├─ authz (RBAC ロール・権限、DB 一括検索)
226
226
  ├─ authn (認証、session + authz に依存)
227
227
  ├─ user (ユーザー管理、session + authz + passwordStrategy に依存)
@@ -278,10 +278,10 @@ login(credentials)
278
278
  ```
279
279
  verifyToken(accessToken)
280
280
  → session.get(accessToken) → キャッシュ検索
281
- → 永続ユーザーの有効状態とセッションに紐付く credentialVersion を検証
281
+ → 共有世代キャッシュから現在のユーザー/資格情報を読み、有効状態・期限・セッションの credentialVersion を検証。不確かな場合は DB を参照
282
282
  → 自動延長モードなら TTL を延長
283
- → 現在のユーザーを再読込し、有効状態と credentialVersion を再確認して、ロール JOIN・権限一括検索(ロールなしなら権限検索を省略)
284
- → 永続データの読取失敗、ユーザー不在・無効、資格情報の世代不一致は拒否し、API Key scope を再適用
283
+ → 上記のユーザー情報を再利用し、共有世代キャッシュから現在の認可を取得。コールド読込は単一 JOIN
284
+ → 必要な状態の読取失敗、ユーザー不在・無効・期限切れ、資格情報の世代不一致は拒否し、API Key scope を再適用
285
285
  → 現在の Session を返す(userId、roles、permissions)
286
286
  ```
287
287
 
@@ -427,9 +427,8 @@ Session.credentialVersion は作成時の DB 資格情報世代に結び付け
427
427
  ```
428
428
  checkPermission(userId, permission)
429
429
  → RBAC 無効なら true
430
- → ユーザーのロール一覧を DB から取得
431
- → 最高管理者を確認(キャッシュの superAdminRoleId)、あれば true
432
- → 各ロールの権限コードを DB で一括取得。読み取り失敗はエラーを返す
430
+ → 共有世代キャッシュから現在のロール・権限を取得。コールド読込は単一 JOIN
431
+ → 最高管理者なら true。必要な状態の読取失敗はエラーを返す
433
432
  → 厳密・ワイルドカード照合(`admin:*` は `admin:read` に一致)
434
433
  → true / false を返す
435
434
  ```
@@ -496,7 +495,7 @@ async function currentAuthorization(userId: string) {
496
495
  - **ワイルドカード**:`admin:*` は `admin:read`、`admin:write:detail` などに一致。1 つの `*` がコロン以降全体に一致します。
497
496
  - **最高管理者**:`rbac.superAdminRole`(既定 `super_admin`)はすべての権限を持ちます。
498
497
  - **トランザクション**:`deleteRole` / `deletePermission` は DB トランザクションで関連を削除してから本体を削除。
499
- - **セッション同期**:キャッシュ更新は best-effort です。保護されたリクエストは `iam.auth.verifyToken` でセッションに紐付く永続資格情報の世代を検証し、現在のユーザー状態・資格情報の世代を再確認して最新の認可を読みます。いずれかの読取に失敗すれば拒否します。`iam.session.get/verifyToken` も永続ユーザーの有効状態と `credentialVersion` を検証しますが、ロール・権限はセッションのスナップショットであり、現在の認可を代替しません。`iam.authz.getUserAuthorization(userId)` はロール JOIN と権限一括検索を共有します。
498
+ - **セッション同期**:キャッシュ更新は best-effort です。保護されたリクエストは `iam.auth.verifyToken` で共有世代キャッシュで現在のユーザー/資格情報と認可を検証し、不確かな場合は DB を参照します。必要な読取が失敗すれば拒否します。`iam.session.get/verifyToken` は同じユーザー/資格情報の読取を再利用し、有効状態・期限・`credentialVersion` を検証しますが、ロール・権限はセッションのスナップショットであり、現在の認可を代替しません。`iam.authz.getUserAuthorization(userId)` はロール JOIN と権限一括検索を共有します。
500
499
  - **初期データ**:`seedDefaultData: true` で admin/user/guest と基本権限を冪等に作成。
501
500
 
502
501
  ---
@@ -612,7 +611,7 @@ async function disableUser(userId: string) {
612
611
 
613
612
  #### API Key — `iam.apiKey`
614
613
 
615
- `scopes` は主体の権限との共通範囲を適用し、`*` と名前空間 `:*` に対応します。既定の空配列は本人確認のみで操作権限を付与しません。制限付き key のセッション `roles` は空で permission ガードを使用し、明示的な `*` も主体の権限を超えません。ログインは key ID に結び付き、get/verifyToken/refresh は永続状態を再確認し、失効・無効・期限切れ・DB エラーを拒否します。verifyApiKey 単独は鍵のメタデータ検証のみで、主体・操作の認可は別途必要です。
614
+ `scopes` は主体の権限との共通範囲を適用し、`*` と名前空間 `:*` に対応します。既定の空配列は本人確認のみで操作権限を付与しません。制限付き key のセッション `roles` は空で permission ガードを使用し、明示的な `*` も主体の権限を超えません。ログインは key ID に結び付き、get/verifyToken/refresh は共有キャッシュまたは DB で現在の状態を確認し、失効・無効・期限切れ・必要な状態の読取失敗を拒否します。verifyApiKey 単独は鍵のメタデータ検証のみで、主体・操作の認可は別途必要です。
616
615
 
617
616
  ##### 主要な型
618
617
 
@@ -626,7 +625,8 @@ async function disableUser(userId: string) {
626
625
 
627
626
  ```
628
627
  verifyApiKey(rawKey)
629
- → rawKey から接頭辞取得(prefix + 最初の 8 文字)
628
+ → SHA-256(rawKey) 指紋で共有世代キャッシュを参照し、ヒット時も期限を確認
629
+ → コールド読込では rawKey から接頭辞取得(prefix + 最初の 8 文字)
630
630
  → DB で接頭辞による候補検索
631
631
  → 各 hash を同期検証
632
632
  → 成功後に期限・無効化を確認
@@ -878,6 +878,12 @@ if (!result.success) {
878
878
 
879
879
  `iam.session.update(token, patch, expectedVersion)` は読み取った Session.version が必須です。旧版は CONFLICT、失効 Token は SESSION_NOT_FOUND を返し、新しい状態を上書きしません。自動延長の時刻だけの更新は内容版を変更せず、通常の並行リクエストを許容します。権限一括更新は最初の Token/版集合に限定し、後続ログインに古い値を適用しません。
880
880
 
881
+ ### 共有認可キャッシュ
882
+
883
+ Redisは秘密を含まないアカウント/資格情報の状態、ロール・権限、機密操作、検証済みAPI Key DTOを60秒保存します。キャッシュ値にはDBのパスワード/Key検証ハッシュ、平文Key、再設定トークンを保存しません。API Keyのキャッシュアドレスには平文Keyの一方向SHA-256指紋を使います。期限は毎回現在時刻で検証し、認可のコールド読込は単一JOINです。IAM書込はDB更新前に共有dirtyマーカーを登録し、外部トランザクションもcommit/rollback完了後に世代を更新します。競合したコールド読込は古い値を公開しません。通常の高頻度チェックはPGを照会せず、更新中・無効化未確認・キャッシュ読込障害時はDBを参照します。セッションRedis自体の障害は認証を拒否します。
884
+
885
+ 無効化登録に失敗した書込はDB更新前に拒否します。commit後の無効化失敗は成功済み更新を取り消した扱いにせず、永続dirtyマーカーでDB参照を継続します。プロセスクラッシュやcommit結果不明をTTLで解除せず、IAM書込がない保守時間帯に確認して修復してください。全ノード同時更新とIAM API経由の書込が必要です。直接SQL、旧版ノード、外部テーブル更新は書込停止とIAMキャッシュ全体の無効化が必要です。平文Key検証のヒットは高負荷ハッシュを省略し、lastUsedAtはコールド検証時に記録するため最大約1分遅れます。
886
+
881
887
  ## 自己ホスト型の対話チャレンジ
882
888
 
883
889
  captcha.enabled は既定で false。ALTCHA の計算チャレンジを自己ホストし、ttlSeconds(既定120、1–600秒)、cost(既定5000、100–50000)、maxCounter(既定100、1–2000)で期限と計算量を指定します。外部 CAPTCHA アカウントは不要で、HMAC 秘密と一度限りの状態は既存の共有 Cache にのみ保存します。本番では共有バックエンドが必要です。計算量は自動化のコストを増やしますが、利用者が人間であることは保証しません。
@@ -930,6 +936,8 @@ audit が初期化済みで capture.enabled が有効なら、IAM はアカウ
930
936
 
931
937
  `session.endOnPageHide` の既定値は `false` です。ホストは `actions.bindPageExit({ enabled: config.session.endOnPageHide })` を呼び、破棄時に戻り値の解除関数を実行します。標準 admin/H5/fullstack テンプレートは接続済みです。BFCache 以外の `pagehide`(再読み込み、文書全体の移動を含む)で POST `/auth/session/close` を最善努力で送信し、保持する access と関連 refresh を失効します。既存の CSRF/暗号化転送を使用し、`keepalive` を付け、自動更新しません。古い応答が新しいログインを上書きしないよう Cookie/ローカル資格情報を消去しません。明示的ログアウトは通常どおり失効と消去を行います。ブラウザーが離脱要求を送らない場合もあるため、業務に応じてサーバーの `idleTimeout`/`absoluteTimeout` を併用してください。閉じた瞬間の失効は保証できません。
932
938
 
939
+ 標準 `/auth/me` も `iam.profile.update` / `iam.profile.avatar` を検査します。複合更新では有効な資料変更規則を優先し、それ以外は画像規則を使います。`authz.getSensitiveOperations(operation?)` は単一操作だけ照会でき、省略時はDB規則一覧を返します。超管理者の役割は共有DBから毎回取得し、種子の並行作成競合は再読込、権限割当失敗は上位へ返します。
940
+
933
941
  ## 開発とテスト
934
942
 
935
943
  公開例は `@h-ai/iam` を使用し、既定値を省略できる `IamConfigInput` と解析済み `IamConfig` を区別します。依存を先に初期化し、`HaiResult.success` を確認し、会話更新には読取時の `version`、通知には永続的な冪等キーを使います。「模块内部上下文」の戦略・倉庫例はアプリ向け深い import の契約ではありません。チェッカーは型だけを確認し、Redis/DB・LDAP・配送の動作には対応する統合テストが必要です。[JSDoc 規約](../../.agents/instructions/module-conventions.md#jsdoc-与示例验证)も参照してください。
package/README.md CHANGED
@@ -33,7 +33,7 @@ pnpm add @h-ai/iam
33
33
  - `@h-ai/reldb` — 数据库(用户/角色/权限持久化),**需在 iam.init() 前初始化**
34
34
  - `@h-ai/cache` — 缓存(会话/OTP/一次性票据),**需在 iam.init() 前初始化**
35
35
  - `@h-ai/crypto` — 密码哈希(内部使用,自动初始化)
36
- - `@h-ai/audit` — 审计日志(RBAC 关键操作记录)
36
+ - `@h-ai/audit` — 宿主初始化后自动记录 IAM 安全事件;未选用时不调用采集器。启用后的审计写失败仍记录错误,外部事务应回滚。
37
37
 
38
38
  - `@h-ai/reach` — **可选**,应用安装、初始化并注入后可使用标准邮件/短信模板;自定义发送回调无需 Reach。
39
39
  - `ldapts` — **可选**,仅在显式启用 LDAP 且没有注入 `ldapClientFactory` 时加载。
@@ -221,7 +221,7 @@ IAM 是**生命周期单例**模块,通过 `iam.init()` / `iam.close()` 管理
221
221
 
222
222
  ```
223
223
  iam.init()
224
- ├─ session (缓存会话管理,无 DB 依赖)
224
+ ├─ session (缓存会话管理,当前凭据状态共享缓存/DB 回源)
225
225
  ├─ authz (RBAC 角色/权限,DB 批量读取)
226
226
  ├─ authn (认证策略,依赖 session + authz)
227
227
  ├─ user (用户管理,依赖 session + authz + passwordStrategy)
@@ -278,10 +278,10 @@ login(credentials)
278
278
  ```
279
279
  verifyToken(accessToken)
280
280
  → session.get(accessToken) → 缓存查询
281
- → 读取持久用户,校验启用状态与会话绑定的 credentialVersion
281
+ → 读取共享缓存中的当前用户/凭据状态,检查启用状态、期限与会话绑定的 credentialVersion;不可信时回源 DB
282
282
  → 滑动续期模式下自动延长 TTL
283
- → 再次读取当前用户,复核启用状态与 credentialVersion,并读取角色 JOIN + 批量权限(无角色时省略权限查询)
284
- → 任一持久读取失败、用户缺失/停用或凭据代不匹配时拒绝;API Key 会话再次收敛 scope
283
+ → 复用上述用户资料,读取共享代际缓存中的当前角色/权限;冷读使用单条 JOIN
284
+ → 必要状态读取失败、用户缺失/停用/到期或凭据代不匹配时拒绝;API Key 会话再次收敛 scope
285
285
  → 返回当前 Session 对象(含 userId、roles、permissions)
286
286
  ```
287
287
 
@@ -427,9 +427,8 @@ Session.credentialVersion 绑定创建时的数据库凭据代际;CreateSessio
427
427
  ```
428
428
  checkPermission(userId, permission)
429
429
  → RBAC 未启用 → 直接返回 true
430
- → 查询用户角色列表(DB)
431
- → 检查是否包含超管角色(缓存 superAdminRoleId)→ 是则直接返回 true
432
- → 一次批量查询这些角色的权限代码(DB);读取失败返回错误
430
+ → 从共享代际缓存读取当前角色与权限,冷读使用单条 JOIN
431
+ → 包含超管角色则返回 true;必要状态读取失败返回错误
433
432
  → 逐权限匹配:精确匹配 或 通配符匹配(`admin:*` 匹配 `admin:read`)
434
433
  → 返回 true / false
435
434
  ```
@@ -496,7 +495,7 @@ async function currentAuthorization(userId: string) {
496
495
  - **通配符匹配**:`admin:*` 匹配 `admin:read`、`admin:write:detail` 等(单层 `*` 匹配冒号后全部)
497
496
  - **超管角色**:配置的 `rbac.superAdminRole`(默认 `super_admin`)自动拥有所有权限
498
497
  - **事务保护**:`deleteRole` / `deletePermission` 使用 DB 事务,先清关联再删实体
499
- - **会话同步**:缓存更新为 best-effort;受保护请求使用 `iam.auth.verifyToken`,先验证会话绑定的持久凭据代,再复核当前用户状态与凭据代并读取最新授权,任一步读取失败即拒绝。`iam.session.get/verifyToken` 同样检查持久用户启用状态与 `credentialVersion`,但角色/权限字段仍是会话快照,不能替代当前授权。`iam.authz.getUserAuthorization(userId)` 复用角色 JOIN 与批量权限查询。
498
+ - **会话同步**:缓存更新为 best-effort;受保护请求使用 `iam.auth.verifyToken`,使用共享代际缓存校验当前用户状态、期限及会话绑定的凭据代,再读取当前授权;缓存不可信时回源,必要读取失败即拒绝。`iam.session.get/verifyToken` 复用同一当前用户/凭据读取并检查启用状态与 `credentialVersion`,但角色/权限字段仍是会话快照,不能替代当前授权。`iam.authz.getUserAuthorization(userId)` 复用单次 JOIN 冷读与共享缓存。
500
499
  - **种子数据**:`seedDefaultData: true` 时自动创建 admin/user/guest 三个默认角色及基础权限,幂等执行
501
500
 
502
501
  ---
@@ -626,7 +625,8 @@ async function disableUser(userId: string) {
626
625
 
627
626
  ```
628
627
  verifyApiKey(rawKey)
629
- → 从 rawKey 提取前缀(prefix + 前 8 字符)
628
+ → 以 SHA-256(rawKey) 指纹读取共享代际缓存,命中时校验有效期并返回
629
+ → 冷读从 rawKey 提取前缀(prefix + 前 8 字符)
630
630
  → 按前缀从 DB 检索候选 API Key 列表(缩小范围)
631
631
  → 逐候选项进行 hash 验证(同步操作)
632
632
  → 验证通过 → 检查是否过期/禁用
@@ -866,7 +866,7 @@ agreements:
866
866
 
867
867
  #### 分布式安全与边界
868
868
 
869
- 所有节点共享同一数据库和 Redis;Memory 仅用于单进程测试。OTP 的验证码、尝试次数和冷却期保存在同一身份 key,通过 CAS 发布和消费;冲突拒绝旧代,消费/耗尽保留冷却期。密码重置的摘要、有效期、尝试次数和 resetVersion 保存在用户数据库行,策略失败计次,最终一条条件 SQL 同时改密、消费令牌并推进 credentialVersion;新令牌、后续尝试、停用或到期都会阻止迟到提交。发送重置请求只推进 resetVersion,不撤销现有会话;成功改密才推进 credentialVersion。认证策略保存已验证的凭据代际,建立会话/认证/刷新均校验数据库当前代际;缓存批量清理失败也不能使旧会话有效。通知只在成功发布后调用,第三参数为 { expiresAt, idempotencyKey },应用及 Reach Provider 必须用该稳定键持久化去重。异常/超时可能已送达,返回结果未知,不自动重发或回滚已发布凭据;没有接收端幂等保障时不能承诺外部副作用恰好一次。
869
+ 所有节点共享同一数据库和 Redis;Memory 仅用于单进程测试。OTP 的验证码、尝试次数和冷却期保存在同一身份 key,通过 CAS 发布和消费;冲突拒绝旧代,消费/耗尽保留冷却期。密码重置的摘要、有效期、尝试次数和 resetVersion 保存在用户数据库行,策略失败计次,最终一条条件 SQL 同时改密、消费令牌并推进 credentialVersion;新令牌、后续尝试、停用或到期都会阻止迟到提交。发送重置请求只推进 resetVersion,不撤销现有会话;成功改密才推进 credentialVersion。认证策略保存已验证的凭据代际,建立会话/认证/刷新均校验共享代际缓存或安全回源的当前状态;缓存批量清理失败也不能使旧会话有效。通知只在成功发布后调用,第三参数为 { expiresAt, idempotencyKey },应用及 Reach Provider 必须用该稳定键持久化去重。异常/超时可能已送达,返回结果未知,不自动重发或回滚已发布凭据;没有接收端幂等保障时不能承诺外部副作用恰好一次。
870
870
 
871
871
  可选字段(如 `ldap`)若尚未写入 YAML,请先声明该字段,或通过 `HAI_IAM` 的 JSON 值整体覆盖模块配置;已有字段支持 `HAI_IAM_LOGIN_OTP` 等环境变量覆盖。
872
872
 
@@ -891,6 +891,12 @@ if (!result.success) {
891
891
 
892
892
  `iam.session.update(token, patch, expectedVersion)` 必须传入读取到的 Session.version。旧版本返回 CONFLICT,已撤销 token 返回 SESSION_NOT_FOUND;不会将旧内容重新写回或覆盖新会话。仅滑动时间戳更新不改变内容版本,允许正常并行请求。批量权限更新绑定首次读取的 token/版本集合,不将迟到数据应用到后续登录;遇到较新内容版本失败,调用方重新查询授权后再提交。
893
893
 
894
+ ### 共享鉴权缓存
895
+
896
+ 验签所需账号/凭据状态、角色权限、敏感操作和已验证 API Key 使用 Redis 共享缓存(60 秒 DTO TTL),缓存值不保存数据库中的密码/API Key 验证哈希、明文密钥或重置令牌;API Key 缓存地址使用明文 Key 的单向 SHA-256 指纹;账号/密码/Key 到期时间每次按当前时间核验。权限冷读使用单次 JOIN。所有 IAM 写入口在数据库变更前建立共享脏标记,提交/回滚后推进代际,外部事务同样等到完成才允许回填;冷读与代际变化并发时不发布旧缓存。正常热路径不查 PG,写入中、失效未确认或缓存读失败时回源;会话 Redis 本身不可用仍拒绝验签。
897
+
898
+ 写前无法登记失效时拒绝变更;已提交后失效失败不会伪装回滚,持久脏标记使后续授权继续读数据库。进程崩溃或提交结果未知时不通过 TTL 擅自清除此标记;应告警并在确认没有 IAM 写事务的维护窗口修复缓存状态。多节点必须同时升级并通过 IAM API 写权限/账号;直接 SQL、旧版本写节点或独立系统改表不触发缓存失效,需停写并使 IAM 缓存整体失效后再恢复,不能与在线缓存写入混用。API Key 明文验证命中缓存后避免重复昂贵哈希,lastUsedAt 在冷验证时采样更新(最多约一分钟延迟)。
899
+
894
900
  ## 自托管交互挑战
895
901
 
896
902
  `captcha.enabled` 默认关闭。开启后使用 ALTCHA 自托管计算挑战,配置 `ttlSeconds`(默认120,1–600秒)、`cost`(默认5000,100–50000)和 `maxCounter`(默认100,1–2000)控制期限与工作量。不需要外部验证码账号,HMAC 秘密与单次消费状态只保存在既有共享 Cache;生产仍须使用共享后端。计算挑战增加自动化尝试成本,不保证请求来自真人。
@@ -943,6 +949,8 @@ Web 原地恢复使用 UI 的 `createSessionRecovery` / `SessionRecoveryDialog`
943
949
 
944
950
  `session.endOnPageHide` 默认 `false`。启用后宿主调用 `actions.bindPageExit({ enabled: config.session.endOnPageHide })` 并在卸载时调用返回的清理函数;标准 admin/H5/fullstack 模板已经接入。非 BFCache 的 `pagehide`(也包括刷新、整页导航)通过 POST `/auth/session/close` 尽力撤销当前 access 及关联 refresh,沿用 CSRF/传输加密,使用 `keepalive` 且不自动刷新会话。该响应不清 Cookie/本地凭据,避免旧响应覆盖下一次登录。显式退出仍使用正常 logout,可靠撤销并清理当前凭据。浏览器可能不发送页面离开请求,因此必须按业务配合服务端 `idleTimeout`/`absoluteTimeout`;不能据此保证关闭即撤销。
945
951
 
952
+ 标准 `/auth/me` 也检查 `iam.profile.update` / `iam.profile.avatar`;混合修改优先选取已启用的资料用途,否则使用头像用途。`authz.getSensitiveOperations(operation?)` 可只查询一个操作;未传则列出全部有效数据库规则。超管身份每次从共享角色读取,默认种子允许并发创建冲突后重读,并传播权限分配失败。
953
+
946
954
  ## 开发测试
947
955
 
948
956
  公开示例从 `@h-ai/iam` 导入,区分可省略默认值的 `IamConfigInput` 与 Schema 解析后的 `IamConfig`。服务端依赖先初始化;`HaiResult` 先检查 `success`,会话修改使用读取到的 `version`,通知复用持久幂等键。内部策略/仓储示例标记为“模块内部上下文”,不作为应用的深层导入入口。检查器只验证示例类型;真实 Redis/数据库、LDAP 和通知投递仍需对应集成测试。见[模块 JSDoc 约定](../../.agents/instructions/module-conventions.md#jsdoc-与示例验证)。
package/dist/browser.d.ts CHANGED
@@ -900,8 +900,8 @@ interface Role {
900
900
  * ```
901
901
  */
902
902
  interface AuthzOperations {
903
- /** 实时读取RBAC中配置的敏感操作;RBAC关闭时为空,读取失败须拒绝受保护操作。 */
904
- getSensitiveOperations: () => Promise<HaiResult<string[]>>;
903
+ /** 实时读取RBAC敏感操作;可仅查询一个操作,未传则返回全部。RBAC关闭时为空,读取失败须拒绝。 */
904
+ getSensitiveOperations: (operation?: string) => Promise<HaiResult<string[]>>;
905
905
  /** 设置资源权限的敏感操作清单;空数组清除数据库追加规则,不清除文件中的规则。 */
906
906
  setPermissionSensitiveOperations: (permissionId: string, operations: string[]) => Promise<HaiResult<void>>;
907
907
  /** 查询当前角色与权限代码,一次读取角色并批量聚合权限,不依赖会话缓存。 */
@@ -912,7 +912,7 @@ interface AuthzOperations {
912
912
  * 超级管理员角色自动拥有所有权限。
913
913
  * 支持通配符匹配(如 `user:*` 匹配 `user:read`)。
914
914
  *
915
- * **数据来源**:角色列表和权限代码均从数据库实时查询。
915
+ * **数据来源**:共享缓存;IAM 写入口在提交前后推进代际,冷读与失效期间查询数据库。
916
916
  *
917
917
  * @param userId - 用户 ID
918
918
  * @param permission - 权限代码(如 `user:read`)