@h-ai/iam 0.1.0-alpha.72 → 0.1.0-alpha.73

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
@@ -882,10 +882,52 @@ 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
+ ## Self-hosted interactive challenges
886
+
887
+ 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.
888
+
889
+ `iam.auth.createCaptcha({operation})` returns {id,challenge}, for login/register/sendOtp/passwordReset/renewPassword. Solve using official altcha/lib solveChallenge/pbkdf2 and pass JSON.stringify({id,solution}) as captcha. Password/OTP/LDAP login, public registration and anonymous password renewal enforce it on the server. sendOtp(identifier,{captcha}) and requestPasswordReset(identifier,{captcha}) accept an optional second argument. Proofs are purpose-bound, short-lived and atomically consumed before authentication; wrong passwords also consume them. Cache failure never bypasses validation. Cache TTL controls expiry and reinitialization does not extend it. Managed creation/invitations, authenticated password changes and API Key machine authentication retain their own authorization and credentials.
890
+
891
+ Standard Kit/Serv expose POST /auth/captcha (Kit same-origin /api prefix) and preserve captcha. iam.publicConfig.captcha exposes only enabled. AuthPanel/createAuthActions automatically create, asynchronously solve and submit a fresh proof when enabled, and do not load the solver when disabled. Custom forms can reuse the protocol. Enabled UI configuration without createCaptcha fails closed; cancellation/unmount stops subsequent login. Proofs are always redacted as credentials and are not persisted in browser storage. Errors030/031/032/033 mean required, invalid/consumed/expired, disabled and unavailable respectively.
892
+
893
+ ## Optional password and login protection
894
+
895
+ `password.minLength/maxLength` and individual `require*` flags control strength. `minCharacterClasses` accepts any N of uppercase, lowercase, digits and special characters; 0 adds no count requirement. CLI/admin examples use 8 characters and any 3 classes. All server creation/change/reset paths share validation.
896
+
897
+ `expirationDays: 0` disables expiry; positive values are days. `preventSamePassword` is independent. Expiry/initial-change policies reject the current password. `requireInitialChange` applies to administrator-created/reset credentials, rather than self-chosen registration passwords. Initial/expired credentials cannot authorize sessions or refresh existing ones. `iam.user.renewPassword({ identifier, password, newPassword })` verifies the old password and requires fresh login; Kit/Serv and AuthPanel provide the completion flow.
898
+
899
+ `security.lockoutEnabled` defaults to true. `failureWindow` defaults to 86400 seconds from the first failure, `maxLoginAttempts` is the threshold, and `lockoutDuration` is seconds locked. Examples use 1200 seconds. Disabling ignores historical locks. Shared database updates atomically enforce counts/thresholds.
900
+
901
+ `initialCredentials.enabled` enables `iam.user.inviteUser()`: private random initial secrets are never returned or sent; only short-lived single-use setup invitations are delivered. This reuses passwordReset mode, expiry, attempt limits and sensitive delivery. Missing delivery/disabled reset prevents creation. Unconfirmed delivery returns failure while retaining the account/invitation; verify delivery before requesting another reset notification.
902
+
903
+ `loginAccess.enabled` enables restrictions. `allowIps/denyIps` accept IPv4/IPv6/CIDR with deny precedence; empty lists leave that dimension unrestricted. `windows` use `days` (Monday1–Sunday7), `start/end` (HH:mm, inclusive start/exclusive end, overnight supported), and IANA `timezone`. Login methods take trusted `AuthRequestContext` as their second argument: Kit uses platform getClientAddress(), Serv uses context.ip resolved through trusted proxies. Never copy arbitrary forwarding headers or credential JSON addresses. Address restrictions fail closed without a trusted address and cover automatic registration login/API Key login.
904
+
905
+ `reauthentication.enabled/operations` select sensitive operations; `ttlSeconds` bounds proof lifetime. `reauthenticate(token, password, operation)` issues a single-use proof; authorized handlers call `checkSensitiveOperation(token, operation, ticket)` after permission/resource checks. Proofs bind subject/session/operation and grant no extra permissions. Serv IAM writes/admin guards read `x-hai-reauthentication`; Kit business handlers use `kit.guard.requireSensitive(iam, event, operation)` with project-defined IDs. Keep proof tickets briefly in memory. Public configuration exposes switches/operation IDs only.
906
+
907
+ ## Account type and lifecycle
908
+
909
+ Users default to `accountType: 'permanent'` and `accountStatus: 'active'`. Administrative code can create a `temporary` account with `accountExpiresAt`, or use `updateUser` to set `dormant` or `closed`. State and credential generation change in one transaction. Expired or inactive accounts are rejected by every login method, access verification, and refresh. Explicit reactivation requires a new login; old tokens do not revive. `enabled` remains an independent administrative switch. `closed` retains the record; `deleteUser` still physically removes it, while the application owns personal-data retention and cancellation workflows.
910
+
911
+ `accounts.dormantAfterDays` defaults to `0`, disabling automatic dormancy. With `90`, IAM scans active accounts with no successful login in 90 days at `accounts.scanIntervalSeconds` intervals (3600 seconds by default); accounts never used start at creation. Only successful authentication updates `lastLoginAt`. Conditional database updates make repeated scans across nodes idempotent. Trusted code or an existing scheduler may also call `iam.user.scanInactiveAccounts()`. `close()` stops the timer and waits for its in-flight scan. Public registration must not expose administrative lifecycle fields.
912
+
913
+ ## Optional session security policies
914
+
915
+ Set `session.idleTimeout: 1800` to require a new login after 30 minutes without a successful authenticated request; `0` disables it. Access reads, activity commits, and refresh rotation all enforce this limit. A long-lived refresh token cannot revive an idle session. Automatic refresh does not extend the idle deadline. Background business polling also counts as authenticated activity, so applications must choose polling behavior that matches their user activity policy.
916
+
917
+ `absoluteTimeout` limits seconds since the original login and does not restart on refresh; `0` disables it. `maxSessions` limits active sessions per user, including expired access tokens that can still refresh; `0` means unlimited. Concurrent logins check the quota in the same shared-state CAS. `singleDevice: true` replaces the previous session by default; `singleDeviceMode: 'reject'` instead keeps it and rejects a duplicate login. Explicit logout revokes both access and refresh; closing a browser cannot replace server-enforced expiry.
918
+
919
+ When these deadlines are disabled, access and refresh retain their independent lifetimes. Every successful login creates new random credentials. `SESSION_LIMIT_EXCEEDED` reports the configured session quota.
920
+
885
921
  ## Schema and credential notifications
886
922
 
887
923
  After RelDB initialization, `migrateIam({ dryRun: true })` previews changes without writes. iam.init upgrades supported credential/reset columns, retaining users/passwords and permissions. Back up and stop old writers first. Legacy sessions without credential versions must sign in again; never roll back credential generations to restore authorization. Default Reach notifications use sensitive/expiresAt/idempotencyKey without plaintext persistence or delayed replay. Custom callbacks must enforce the same expiry and credential protection.
888
924
 
925
+ When audit is initialized and capture.enabled is on, IAM records important account, authentication, password, authorization, API Key and session events, including failed/denied outcomes and safe reason codes. Initialize audit before IAM and close in reverse order. Passwords, one-time proofs and tokens are never copied into events. Only successful login establishes an actor; trusted service/CLI callers supply actor/correlation through audit.withContext. Events inside an external authorization transaction commit or roll back with that transaction; record failed attempts after rollback at the caller boundary. Audit failures emit diagnostics without converting an applied business result into an unsafe retry.
926
+
927
+ ## Page exit session policy
928
+
929
+ `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.
930
+
889
931
  ## Development and testing
890
932
 
891
933
  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
@@ -401,7 +401,7 @@ async function forceSignOut(userId: string) {
401
401
 
402
402
  `create({ maxAge })` は設定の既定値を正の安全な整数秒で上書きし、`Session.maxAge` に保存します。発行と更新の `expiresIn` はこの値を使います。自動延長は同じ期間を使い、確定済みの `lastActiveAt/expiresAt` を返し、遅いリクエストが新しい延長を短縮しません。明示的な `update` は有効な日時を反映して内容版を更新します。クライアントは返された `expiresIn` から更新時刻を決めます。
403
403
 
404
- access と refresh は独立して期限切れになります。ユーザー文書は両期限の最大まで保持しますが、access 認証は自身の expiresAt のみを確認します。access 期限後も有効な refresh で更新でき、更新ごとに新しい refreshTokenMaxAge を開始して旧資格情報を失効させます。access 削除・全ログアウト・単一端末置換は更新待ち記録も取り消し、revokeRefresh は有効な access を残します。更新経路で旧 access の認証成功を前提にしません。
404
+ 追加の休止・絶対期限が無効な場合、access と refresh は独立して期限切れになります。ユーザー文書は両期限の最大まで保持しますが、access 認証は自身の expiresAt を確認します。追加設定が有効なら access/refresh の両方で休止・絶対期限も確認します。access 期限後も有効な refresh で更新でき、更新ごとに新しい refreshTokenMaxAge を開始して旧資格情報を失効させます。access 削除・全ログアウト・単一端末置換は更新待ち記録も取り消し、revokeRefresh は有効な access を残します。更新経路で旧 access の認証成功を前提にしません。
405
405
 
406
406
  セッションの読取・確定障害は失敗 HaiResult として返し、不存在や期限切れと解釈せず補償削除もしません。索引と会話は同じ文書で保持し、個別 get/ttl や二重索引を廃止します。確定した期限だけで整理し、復旧後の再読取・全ログアウトは元の全記録を対象にします。破損状態は保持して認証を拒否し、空文書で上書きせず運用者が修復します。
407
407
 
@@ -878,10 +878,52 @@ 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
+ captcha.enabled は既定で false。ALTCHA の計算チャレンジを自己ホストし、ttlSeconds(既定120、1–600秒)、cost(既定5000、100–50000)、maxCounter(既定100、1–2000)で期限と計算量を指定します。外部 CAPTCHA アカウントは不要で、HMAC 秘密と一度限りの状態は既存の共有 Cache にのみ保存します。本番では共有バックエンドが必要です。計算量は自動化のコストを増やしますが、利用者が人間であることは保証しません。
884
+
885
+ iam.auth.createCaptcha({operation}) は {id,challenge} を返します。operation は login/register/sendOtp/passwordReset/renewPassword。公式 altcha/lib の solveChallenge/pbkdf2 で解き、JSON.stringify({id,solution}) を captcha に渡します。パスワード/OTP/LDAP ログイン、公開登録、匿名の旧パスワード更新でサーバー側が強制確認します。sendOtp(identifier,{captcha}) と requestPasswordReset(identifier,{captcha}) は任意の第2引数です。用途を固定し、検証後に原子的に消費するため、誤ったパスワードでも再利用できません。Cache 障害で通過せず、TTL は再初期化で延長しません。管理者作成/招待、認証済みの変更、API Key の機械認証は既存の認可と認証情報を使います。
886
+
887
+ Kit/Serv の標準入口は POST /auth/captcha(Kit は同一生成元の /api 接頭辞)。captcha を保持し、iam.publicConfig.captcha は enabled のみ公開します。AuthPanel/createAuthActions は有効時に送信操作で自動生成・非同期求解し、無効時には求解器を読み込みません。独自フォームも同じ手順を使えます。有効設定で createCaptcha がなければ送信を拒否し、取消/破棄後にログインしません。証明は資格情報として常に秘匿し、ブラウザーに永続保存しません。030/031/032/033 は確認必須・無効/使用済/期限切れ・無効設定・一時利用不可です。
888
+
889
+ ## 任意のパスワード・ログイン保護
890
+
891
+ `password.minLength/maxLength` と各 `require*` は強度を指定します。`minCharacterClasses` は大文字、小文字、数字、特殊文字の任意 N 種類を要求し、0は追加制約なしです。CLI/admin 例は8文字・任意3種類です。作成・変更・リセットで同じ検証を使います。
892
+
893
+ `expirationDays: 0` は定期変更を無効にし、正数は日数です。`preventSamePassword` は単独でも有効化でき、有効期限制約・初回変更時は旧パスワードを拒否します。`requireInitialChange` は管理者作成・リセットの認証情報に適用され、自分で登録時に設定したパスワードは再変更不要です。初期・期限切れパスワードでは通常セッションを取得・更新できません。`iam.user.renewPassword({ identifier, password, newPassword })` で旧パスワードを確認し、変更後に再ログインします。Kit/Serv と AuthPanel はこの完了経路を提供します。
894
+
895
+ `security.lockoutEnabled` は既定で有効です。`failureWindow` は最初の失敗からの固定期間(既定86400秒)、`maxLoginAttempts` は閾値、`lockoutDuration` はロック秒数です。例は1200秒です。無効時は既存ロックも無視します。共有DBで計数・閾値を原子的に処理します。
896
+
897
+ `initialCredentials.enabled` は `iam.user.inviteUser()` を有効にします。利用者ごとに非公開のランダム秘密を作り、短期・単回の設定招待だけ送ります。初期秘密は返却・送信しません。passwordReset の通知・期限・試行回数を再利用し、受信者は再設定画面でパスワードを選びます。通知未設定・リセット無効なら作成しません。配送未確認時は失敗とし、作成済みアカウント・招待を保持します。配送を確認後に再設定通知を要求してください。
898
+
899
+ `loginAccess.enabled` は制限を有効にします。`allowIps/denyIps` は IPv4/IPv6/CIDR を受け付け、拒否を優先します。空リストは対象を制限しません。`windows` は `days`(月曜1〜日曜7)、`start/end`(HH:mm、開始を含み終了を含まず、日跨ぎ対応)、IANA `timezone` を指定します。ログインの第2引数は信頼できる `AuthRequestContext` です。Kit はプラットフォーム getClientAddress()、Serv は信頼プロキシで解決した context.ip を使います。任意の転送ヘッダー・入力 JSON の IP を信用してはいけません。アドレス制限時に信頼できるアドレスがなければ拒否し、自動ログイン・API Key にも適用します。
900
+
901
+ `reauthentication.enabled/operations` で対象操作、`ttlSeconds` で証明期限を選びます。`reauthenticate(token, password, operation)` は単回証明を発行し、入口は権限・資源検証後に `checkSensitiveOperation(token, operation, ticket)` を呼びます。主体・現在セッション・操作に結びつけ、権限を増やしません。Serv IAM 更新操作と admin 守衛は `x-hai-reauthentication`、Kit は固有 ID と `kit.guard.requireSensitive(iam, event, operation)` を使います。証明は短時間メモリに保持し、公開設定は開閉・操作リストのみです。
902
+
903
+ ## アカウント種別とライフサイクル
904
+
905
+ 既定は `accountType: 'permanent'` と `accountStatus: 'active'` です。管理側は `accountExpiresAt` を指定して `temporary` アカウントを作成し、`updateUser` で `dormant` または `closed` に変更できます。状態と認証世代を同じトランザクションで更新し、期限切れまたは非アクティブなアカウントはすべてのログイン、access 検証、refresh で拒否します。管理者が明示的に再有効化しても旧トークンは復活せず、再ログインが必要です。`enabled` は独立した管理者スイッチです。`closed` は記録を保持し、物理削除は `deleteUser` が行います。個人データの保持と退会手順はアプリが決定します。
906
+
907
+ `accounts.dormantAfterDays` の既定値 `0` は自動休止を無効にします。`90` にすると、最後のログイン成功から90日間使用していない有効なアカウントを `accounts.scanIntervalSeconds` ごと(既定3600秒)に確認します。未使用のアカウントは作成時刻から計算し、成功した認証だけが `lastLoginAt` を更新します。条件付きSQLのため複数ノードで繰り返しても重複変更しません。信頼された処理や既存スケジューラーから `iam.user.scanInactiveAccounts()` を呼び出すこともできます。`close()` はタイマーを停止して実行中の処理を待ちます。公開登録APIに管理用の項目を公開しないでください。
908
+
909
+ ## 任意のセッションセキュリティ設定
910
+
911
+ `session.idleTimeout: 1800` は認証済みリクエストが30分間ない場合に再ログインを要求します。`0` は無効です。access の読み取り、活動の保存、refresh の更新で同じ期限を確認し、長期 refresh でも休止したセッションを復活できません。自動 refresh は休止期限を延長しません。バックグラウンド業務のポーリングは認証活動になるため、アプリは利用者の操作方針に合わせて設計してください。
912
+
913
+ `absoluteTimeout` は最初のログインからの上限秒数で、更新しても起点を変更しません。`0` は無効です。`maxSessions` は利用者ごとの有効セッション数を制限し、access が期限切れでも更新可能な記録を含みます。`0` は無制限です。同時ログインの上限判定は共有状態の同じCASで行います。`singleDevice: true` は既存セッションを置換し、`singleDeviceMode: 'reject'` を併用すると既存を保持して重複ログインを拒否します。明示的なログアウトは両方のトークンを失効させます。ブラウザーを閉じることはサーバー側の期限の代わりにはなりません。
914
+
915
+ 期限を無効にした場合、access と refresh は独立した有効期間を保ちます。ログイン成功ごとに新しいランダムな認証情報を生成します。`SESSION_LIMIT_EXCEEDED` は設定されたセッション数の上限を示します。
916
+
881
917
  ## スキーマと認証情報の通知
882
918
 
883
919
  RelDB 初期化後の `migrateIam({ dryRun: true })` は読み取り専用です。iam.init は対応する認証・リセット列を補い、ユーザー、パスワードと権限を保持します。事前に旧書込を停止しバックアップしてください。安全世代のない旧セッションは再ログインが必要で、認可復元のため世代を戻してはいけません。既定 Reach 通知は sensitive/expiresAt/idempotencyKey で平文保存や遅延再送を防ぎます。独自コールバックも同じ期限と保護を守ってください。
884
920
 
921
+ audit が初期化済みで capture.enabled が有効なら、IAM はアカウント・認証・パスワード・権限・API Key・セッションの重要イベントと失敗/拒否の安全な理由コードを記録します。audit を IAM より先に初期化し、逆順に終了してください。パスワード・単回証明・トークンは記録しません。ログイン成功後に主体を確定し、信頼できるサービス/CLI は audit.withContext で操作者と関連文脈を設定します。外部権限トランザクション内のイベントは一緒にコミット/ロールバックされ、失敗試行はロールバック後に呼出境界で記録します。監査失敗は診断を出し、反映済み業務を危険な再試行に変えません。
922
+
923
+ ## ページ離脱時のセッション方針
924
+
925
+ `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` を併用してください。閉じた瞬間の失効は保証できません。
926
+
885
927
  ## 開発とテスト
886
928
 
887
929
  公開例は `@h-ai/iam` を使用し、既定値を省略できる `IamConfigInput` と解析済み `IamConfig` を区別します。依存を先に初期化し、`HaiResult.success` を確認し、会話更新には読取時の `version`、通知には永続的な冪等キーを使います。「模块内部上下文」の戦略・倉庫例はアプリ向け深い import の契約ではありません。チェッカーは型だけを確認し、Redis/DB・LDAP・配送の動作には対応する統合テストが必要です。[JSDoc 規約](../../.agents/instructions/module-conventions.md#jsdoc-与示例验证)も参照してください。
package/README.md CHANGED
@@ -401,7 +401,7 @@ async function forceSignOut(userId: string) {
401
401
 
402
402
  `create({ maxAge })` 可覆盖配置默认时长(正安全整数秒),该值持久化为 `Session.maxAge`;签发和刷新返回的 `expiresIn` 均使用它。滑动续期按该时长延长 access,返回已成功提交的 `lastActiveAt/expiresAt`;迟到请求只延长时间,不缩短较新续期。显式 `update` 的有效日期补丁完整生效并产生新内容版本。客户端根据本次返回的 `expiresIn` 安排刷新,不能硬编码配置默认值。
403
403
 
404
- access 与 refresh 独立到期:用户分区保留至所有 access/refresh 的最晚有效期,但 access 认证只检查自己的 `expiresAt`。access 过期仍可凭未过期、未撤销的 refresh 轮换;每次轮换从新签发时刻重新计算 `refreshTokenMaxAge`,旧 access/refresh 同时失效。删除 access、logoutAll、单设备替换均撤销待刷新记录;仅 revokeRefresh 保留仍有效的 access。刷新路由只校验 refresh,不能先要求旧 access 认证成功。
404
+ 未启用额外闲置/绝对期限时,access 与 refresh 独立到期:用户分区保留至所有 access/refresh 的最晚有效期,但 access 认证检查自己的 `expiresAt`。启用额外会话策略时,access/refresh 还必须满足相同的闲置与绝对期限。access 过期仍可凭未过期、未撤销的 refresh 轮换;每次轮换从新签发时刻重新计算 `refreshTokenMaxAge`,旧 access/refresh 同时失效。删除 access、logoutAll、单设备替换均撤销待刷新记录;仅 revokeRefresh 保留仍有效的 access。刷新路由只校验 refresh,不能先要求旧 access 认证成功。
405
405
 
406
406
  会话仓库将缓存读取/提交失败返回为失败 HaiResult,不解释为会话不存在或过期,也不执行清理补偿。用户会话及索引共用一个文档,批量授权更新不再逐 token 调用 get/ttl 或维护第二份索引;只按已确认的绝对期限清理。故障恢复后可重新读取并重试更新/退出,logoutAll 仍覆盖原有全部记录。结构损坏同样失败关闭并保留原值,交由运维处理,禁止用空文档覆盖未知授权状态。
407
407
 
@@ -891,10 +891,52 @@ 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
+ `captcha.enabled` 默认关闭。开启后使用 ALTCHA 自托管计算挑战,配置 `ttlSeconds`(默认120,1–600秒)、`cost`(默认5000,100–50000)和 `maxCounter`(默认100,1–2000)控制期限与工作量。不需要外部验证码账号,HMAC 秘密与单次消费状态只保存在既有共享 Cache;生产仍须使用共享后端。计算挑战增加自动化尝试成本,不保证请求来自真人。
897
+
898
+ `iam.auth.createCaptcha({operation})` 返回 `{id,challenge}`;operation 为 login/register/sendOtp/passwordReset/renewPassword。调用者用官方 `altcha/lib` 的 solveChallenge/pbkdf2 求解,将 `JSON.stringify({id,solution})` 作为 captcha 字段交给对应操作。密码/OTP/LDAP 登录、公开注册、匿名原密码续期都在服务端强制检查;sendOtp(identifier,{captcha}) 和 requestPasswordReset(identifier,{captcha}) 接收可选第二参数。证明用途绑定、短期单用,验证成功后原子删除;密码错误也会消费证明,重试需要新挑战。Cache 故障不降级放行;有效期由 Cache TTL 决定,重初始化不续期。管理员创建/邀请、已登录改密和 API Key 机器鉴别沿用各自授权与凭据,不额外加入交互挑战。
899
+
900
+ 标准 Kit/Serv 暴露 POST /auth/captcha(Kit 同源前缀为 /api),并保留 captcha 字段。`iam.publicConfig.captcha` 只公开 enabled。框架 AuthPanel 与 createAuthActions 在启用后提交时自动生成、异步求解并提交新证明,关闭时不加载求解器;自定义表单可复用同一协议。UI 配置启用但缺少 createCaptcha 动作时拒绝提交,取消/卸载会停止尚未提交的鉴别请求。captcha 证明按凭据规则始终脱敏,不写浏览器持久存储。030/031/032/033 分别表示需要验证、证明无效/已用/过期、未启用、服务不可用。
901
+
902
+ ## 可选密码与登录保护
903
+
904
+ `password.minLength/maxLength` 控制长度,各 `require*` 指定必需类别;`minCharacterClasses` 支持大小写、数字、特殊字符中任意 N 类,0不额外限制。CLI/admin 示例默认8位、任意3类。所有服务端创建、改密和重置入口共用校验。
905
+
906
+ `expirationDays: 0` 关闭定期改密,正数为天数;`preventSamePassword` 可独立开启。启用期限或首次强改时,新旧密码必须不同。`requireInitialChange` 作用于管理端创建/重置的密码,自助注册设密不重复强改。首次/过期密码不能授权普通会话或刷新旧会话;用户通过 `iam.user.renewPassword({ identifier, password, newPassword })` 验证原密码并更换后重新登录,Kit/Serv 与 AuthPanel 提供完成流程。
907
+
908
+ `security.lockoutEnabled` 默认开启;`failureWindow` 默认86400秒,从首次失败起按固定窗口计数,`maxLoginAttempts` 为阈值,`lockoutDuration` 为锁定秒数。示例锁定1200秒。关闭时忽略历史锁;计数与阈值在共享数据库中原子处理。
909
+
910
+ `initialCredentials.enabled` 开启管理端 `iam.user.inviteUser()`:内部生成独立随机秘密,仅发送短期单次设密邀请,不返回/发送初始秘密。沿用 `passwordReset` 的模式、期限、尝试次数和敏感通知通道,接收人通过重置页面设密。关闭重置或缺少通道时不创建账号。投递未知返回失败并保留已创建账号及邀请,核对后再请求重置通知,不自动回滚或重发。
911
+
912
+ `loginAccess.enabled` 开启登录限制;`allowIps/denyIps` 支持 IPv4/IPv6/CIDR,拒绝优先,空列表不限制对应维度。`windows` 用 `days`(周一1至周日7)、`start/end`(HH:mm,含起点不含终点,支持跨午夜)和 IANA `timezone`。登录方法第二参为可信 `AuthRequestContext`:Kit 使用平台 `getClientAddress()`,Serv 使用可信代理策略解析的 `context.ip`。不得复制任意转发头或凭据 JSON 的 IP;地址限制启用但无可信地址时拒绝。注册后自动登录、API Key 登录同样执行。
913
+
914
+ `reauthentication.enabled/operations` 选择敏感操作,`ttlSeconds` 为证明期限。`iam.auth.reauthenticate(token, password, operation)` 验证原密码并签发单次票据;操作入口完成权限/资源检查后用 `checkSensitiveOperation(token, operation, ticket)` 消费。证明绑定主体、当前会话和操作,不提升权限。Serv 标准 IAM 写操作与 admin 守卫读取 `x-hai-reauthentication`;Kit 业务入口用 `kit.guard.requireSensitive(iam, event, operation)`,业务操作 ID 由项目定义。票据只短暂保存在内存,公开配置仅暴露开关/操作清单。
915
+
916
+ ## 账号类型和生命周期
917
+
918
+ 用户默认是 `accountType: 'permanent'`、`accountStatus: 'active'`。管理入口可创建 `temporary` 账号并提供 `accountExpiresAt`,或通过 `updateUser` 将账号转为 `dormant`、`closed`。状态与凭据代际在同一事务更新;账号到期或非激活时,所有登录方式、旧 access 和 refresh 均被拒绝。管理员明确恢复为 `active` 时仍需重新登录,原 token 不会复活;`enabled` 是独立的管理员启停开关。`closed` 保留账号记录,物理删除仍由 `deleteUser` 完成,项目决定个人数据的保留与注销流程。
919
+
920
+ `accounts.dormantAfterDays` 默认 `0`,关闭自动休眠。设为 `90` 时,IAM 每隔 `accounts.scanIntervalSeconds`(默认3600秒)扫描最近90天没有成功登录的激活账号,未登录账号从创建时间起算。`lastLoginAt` 只在成功鉴别时更新。扫描是数据库条件更新,多节点重复执行不会重复变更;`iam.user.scanInactiveAccounts()` 也可由可信后台或既有调度器调用。`close()` 停止定时器并等待本轮扫描完成。公开注册接口不应允许用户自行设置这些管理字段。
921
+
922
+ ## 可选会话安全策略
923
+
924
+ 在 `session` 中设置 `idleTimeout: 1800` 可要求30分钟没有成功认证请求后重新登录;`0` 关闭。该策略在 access 读取、活动提交和 refresh 轮换时检查,不能通过长期 refresh 恢复已闲置的会话。自动 refresh 只换票,不延长闲置期限;后台业务轮询仍会产生认证活动,应用应按实际用户操作设计轮询行为。
925
+
926
+ `absoluteTimeout` 是从首次登录起的秒数上限,刷新不重置起点,`0` 关闭。`maxSessions` 限制每个用户的有效会话数,包含 access 已过期但仍可刷新的记录,`0` 不限制;并发登录在同一共享状态 CAS 中检查配额。`singleDevice: true` 默认顶旧保新;配合 `singleDeviceMode: 'reject'` 可保留旧会话并拒绝重复登录。显式退出撤销 access 和 refresh,关闭浏览器不能替代服务端期限。
927
+
928
+ 未启用上述期限时,保留 access 与 refresh 独立到期的行为。每次成功登录始终生成新随机凭据,不复用客户端会话ID。新增的 `SESSION_LIMIT_EXCEEDED` 表示登录达到已配置的会话上限。
929
+
894
930
  ## 持久结构与凭据通知
895
931
 
896
932
  `migrateIam({ dryRun: true })` 在 RelDB 初始化后只读预检;iam.init 自动补齐支持版本的凭据/重置字段,保留用户与密码,不修改业务权限。升级前备份停旧写入;没有安全代次的旧会话需重新登录,不能回退数据库安全代次来恢复授权。默认 Reach 通知使用 sensitive/expiresAt/idempotencyKey,不落明文、不延迟重投;自定义回调同样须遵守有效期与凭据保护。
897
933
 
934
+ 审计模块已初始化且 capture.enabled 开启时,IAM 自动记录关键账号、鉴别、密码、授权、API Key 和会话事件,包含失败/拒绝与安全原因码。先初始化 audit 再初始化 IAM,关闭时反序。不会记录密码、一次性票据或令牌;成功登录才确认主体,可信服务/CLI 调用应以 audit.withContext 提供操作者和关联信息。外部授权事务的事件随该事务提交或回滚,回滚失败尝试由调用入口在事务结束后记录。审计写失败输出诊断,不把已生效业务改成可盲目重试的失败。
935
+
936
+ ## 页面离开会话策略
937
+
938
+ `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`;不能据此保证关闭即撤销。
939
+
898
940
  ## 开发测试
899
941
 
900
942
  公开示例从 `@h-ai/iam` 导入,区分可省略默认值的 `IamConfigInput` 与 Schema 解析后的 `IamConfig`。服务端依赖先初始化;`HaiResult` 先检查 `success`,会话修改使用读取到的 `version`,通知复用持久幂等键。内部策略/仓储示例标记为“模块内部上下文”,不作为应用的深层导入入口。检查器只验证示例类型;真实 Redis/数据库、LDAP 和通知投递仍需对应集成测试。见[模块 JSDoc 约定](../../.agents/instructions/module-conventions.md#jsdoc-与示例验证)。