@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 +20 -12
- package/README.ja.md +19 -11
- package/README.md +19 -11
- package/dist/browser.d.ts +3 -3
- package/dist/index.js +332 -181
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
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
|
|
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;
|
|
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
|
|
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
|
-
→
|
|
284
|
-
→ Reject
|
|
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
|
-
→
|
|
431
|
-
→
|
|
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
|
|
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
|
|
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
|
-
→
|
|
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
|
|
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
|
|
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
|
|
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
|
-
→
|
|
281
|
+
→ 共有世代キャッシュから現在のユーザー/資格情報を読み、有効状態・期限・セッションの credentialVersion を検証。不確かな場合は DB を参照
|
|
282
282
|
→ 自動延長モードなら TTL を延長
|
|
283
|
-
→
|
|
284
|
-
→
|
|
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
|
-
→
|
|
431
|
-
→
|
|
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`
|
|
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
|
|
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
|
|
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` —
|
|
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
|
|
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
|
-
→
|
|
281
|
+
→ 读取共享缓存中的当前用户/凭据状态,检查启用状态、期限与会话绑定的 credentialVersion;不可信时回源 DB
|
|
282
282
|
→ 滑动续期模式下自动延长 TTL
|
|
283
|
-
→
|
|
284
|
-
→
|
|
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
|
-
→
|
|
431
|
-
→
|
|
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
|
|
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
|
-
→
|
|
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
|
|
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
|
|
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`)
|