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

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/dist/index.js CHANGED
@@ -1,14 +1,21 @@
1
- import { HaiIamError, IamConfigSchema, SessionConfigSchema, SecurityConfigSchema, LoginConfigSchema, OtpConfigSchema, ApiKeyConfigSchema, AuthUiConfigSchema, PasswordConfigSchema, RbacConfigSchema, AgreementConfigSchema, PasswordResetConfigSchema, RegisterConfigSchema } from './chunk-VCE74BFW.js';
2
- export { AgreementConfigSchema, ApiKeyConfigSchema, AuthStrategyTypeSchema, AuthUiConfigSchema, HaiIamError, IamConfigSchema, LdapConfigSchema, LoginConfigSchema, OtpConfigSchema, PasswordConfigSchema, PasswordResetConfigSchema, RbacConfigSchema, RegisterConfigSchema, SecurityConfigSchema, SessionConfigSchema } from './chunk-VCE74BFW.js';
1
+ import { HaiIamError, IamConfigSchema, SessionConfigSchema, SecurityConfigSchema, LoginConfigSchema, OtpConfigSchema, ApiKeyConfigSchema, AuthUiConfigSchema, PasswordConfigSchema, RbacConfigSchema, AgreementConfigSchema, PasswordResetConfigSchema, RegisterConfigSchema } from './chunk-KPG3M34Q.js';
2
+ export { AccountConfigSchema, AgreementConfigSchema, ApiKeyConfigSchema, AuthStrategyTypeSchema, AuthUiConfigSchema, CaptchaConfigSchema, HaiIamError, IamConfigSchema, LdapConfigSchema, LoginAccessConfigSchema, LoginConfigSchema, OtpConfigSchema, PasswordConfigSchema, PasswordResetConfigSchema, RbacConfigSchema, RegisterConfigSchema, SecurityConfigSchema, SessionConfigSchema } from './chunk-KPG3M34Q.js';
3
3
  import { cache } from '@h-ai/cache';
4
4
  import { core, err, ok } from '@h-ai/core';
5
5
  import { crypto as crypto$1 } from '@h-ai/crypto';
6
6
  import { migrateSchema, reldb, schemaColumns, BaseReldbCrudRepository, HaiReldbError } from '@h-ai/reldb';
7
- import { createHash, randomBytes } from 'crypto';
7
+ import { createHash, randomBytes, randomInt } from 'crypto';
8
+ import { isIP, BlockList } from 'net';
9
+ import { verifySolution, pbkdf2, createChallenge } from 'altcha/lib';
10
+ import { z } from 'zod';
8
11
  import { audit } from '@h-ai/audit';
9
12
 
10
13
  // messages/en-US.json
11
14
  var en_US_default = {
15
+ iam_invitationDisabled: "Invitations are disabled or a required delivery channel is unavailable.",
16
+ iam_invitationDeliveryUnknown: "Account created; invitation delivery is unconfirmed. Check delivery before requesting a new reset notification.",
17
+ iam_reauthenticationRequired: "Verify your password again to continue this operation.",
18
+ iam_loginAccessDenied: "Sign-in is not allowed from this address or at this time.",
12
19
  iam_batchUserIdsInvalid: "Provide at most 1000 non-empty user IDs",
13
20
  $schema: "https://inlang.com/schema/inlang-message-format",
14
21
  iam_securityStateFailed: "Unable to update authentication security state. Please try again later.",
@@ -196,11 +203,27 @@ var en_US_default = {
196
203
  iam_sessionStateFailed: "Shared session state operation failed",
197
204
  iam_sessionStateConflict: "Session state changed; retry the operation",
198
205
  iam_sessionMaxAgeInvalid: "Invalid session lifetime or timestamp",
199
- iam_notificationDeliveryUnknown: "Notification delivery is unknown and may have succeeded; do not resend automatically."
206
+ iam_sessionLimitExceeded: "The active session limit has been reached. Sign out another session first.",
207
+ iam_accountPolicyInvalid: "Invalid account type, status, or expiry.",
208
+ iam_accountUpdateFailed: "Account update failed. Please try again later.",
209
+ iam_passwordChangeRequired: "Change your initial password before signing in.",
210
+ iam_passwordMustDiffer: "The new password must differ from the current password.",
211
+ iam_passwordCharacterClasses: "Use at least {count} character classes in the password.",
212
+ iam_notificationDeliveryUnknown: "Notification delivery is unknown and may have succeeded; do not resend automatically.",
213
+ iam_captchaRequired: "Complete the security check first",
214
+ iam_captchaInvalid: "Security check is invalid or expired; try again",
215
+ iam_captchaDisabled: "Security challenges are disabled",
216
+ iam_captchaUnavailable: "Security check is temporarily unavailable",
217
+ iam_sensitiveOperationsInvalid: "Invalid sensitive operation IDs: use at most 1000 entries of 1\u2013128 characters each.",
218
+ iam_terminalTypeInvalid: "Invalid terminal type: use web, app, desktop or api."
200
219
  };
201
220
 
202
221
  // messages/ja-JP.json
203
222
  var ja_JP_default = {
223
+ iam_invitationDisabled: "\u62DB\u5F85\u6A5F\u80FD\u304C\u7121\u52B9\u3001\u307E\u305F\u306F\u5FC5\u8981\u306A\u901A\u77E5\u7D4C\u8DEF\u304C\u5229\u7528\u3067\u304D\u307E\u305B\u3093\u3002",
224
+ iam_invitationDeliveryUnknown: "\u30A2\u30AB\u30A6\u30F3\u30C8\u306F\u4F5C\u6210\u6E08\u307F\u3067\u3059\u304C\u62DB\u5F85\u306E\u914D\u9001\u306F\u672A\u78BA\u8A8D\u3067\u3059\u3002\u914D\u9001\u72B6\u6CC1\u3092\u78BA\u8A8D\u3057\u3066\u304B\u3089\u518D\u8A2D\u5B9A\u901A\u77E5\u3092\u8981\u6C42\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
225
+ iam_reauthenticationRequired: "\u3053\u306E\u64CD\u4F5C\u3092\u7D9A\u3051\u308B\u306B\u306F\u30D1\u30B9\u30EF\u30FC\u30C9\u3092\u518D\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
226
+ iam_loginAccessDenied: "\u3053\u306E\u30A2\u30C9\u30EC\u30B9\u307E\u305F\u306F\u6642\u9593\u5E2F\u304B\u3089\u306E\u30ED\u30B0\u30A4\u30F3\u306F\u8A31\u53EF\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002",
204
227
  iam_batchUserIdsInvalid: "\u7A7A\u3067\u306A\u3044\u30E6\u30FC\u30B6\u30FCID\u30921000\u4EF6\u4EE5\u5185\u3067\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044",
205
228
  $schema: "https://inlang.com/schema/inlang-message-format",
206
229
  iam_securityStateFailed: "\u8A8D\u8A3C\u30BB\u30AD\u30E5\u30EA\u30C6\u30A3\u72B6\u614B\u3092\u66F4\u65B0\u3067\u304D\u307E\u305B\u3093\u3002\u3057\u3070\u3089\u304F\u3057\u3066\u304B\u3089\u518D\u8A66\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
@@ -388,11 +411,27 @@ var ja_JP_default = {
388
411
  iam_sessionStateFailed: "\u5171\u6709\u30BB\u30C3\u30B7\u30E7\u30F3\u72B6\u614B\u306E\u64CD\u4F5C\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
389
412
  iam_sessionStateConflict: "\u30BB\u30C3\u30B7\u30E7\u30F3\u72B6\u614B\u304C\u5909\u66F4\u3055\u308C\u307E\u3057\u305F\u3002\u64CD\u4F5C\u3092\u518D\u8A66\u884C\u3057\u3066\u304F\u3060\u3055\u3044",
390
413
  iam_sessionMaxAgeInvalid: "\u30BB\u30C3\u30B7\u30E7\u30F3\u306E\u6709\u52B9\u671F\u9593\u307E\u305F\u306F\u30BF\u30A4\u30E0\u30B9\u30BF\u30F3\u30D7\u304C\u7121\u52B9\u3067\u3059",
391
- iam_notificationDeliveryUnknown: "\u901A\u77E5\u306E\u914D\u4FE1\u7D50\u679C\u306F\u4E0D\u660E\u3067\u3001\u6210\u529F\u3057\u305F\u53EF\u80FD\u6027\u304C\u3042\u308A\u307E\u3059\u3002\u81EA\u52D5\u3067\u518D\u9001\u3057\u306A\u3044\u3067\u304F\u3060\u3055\u3044\u3002"
414
+ iam_sessionLimitExceeded: "\u6709\u52B9\u306A\u30BB\u30C3\u30B7\u30E7\u30F3\u6570\u304C\u4E0A\u9650\u306B\u9054\u3057\u307E\u3057\u305F\u3002\u4ED6\u306E\u30BB\u30C3\u30B7\u30E7\u30F3\u304B\u3089\u30ED\u30B0\u30A2\u30A6\u30C8\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
415
+ iam_accountPolicyInvalid: "\u30A2\u30AB\u30A6\u30F3\u30C8\u7A2E\u5225\u3001\u72B6\u614B\u3001\u307E\u305F\u306F\u6709\u52B9\u671F\u9650\u304C\u7121\u52B9\u3067\u3059\u3002",
416
+ iam_accountUpdateFailed: "\u30A2\u30AB\u30A6\u30F3\u30C8\u306E\u66F4\u65B0\u306B\u5931\u6557\u3057\u307E\u3057\u305F\u3002\u5F8C\u3067\u518D\u8A66\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
417
+ iam_passwordChangeRequired: "\u6700\u521D\u306E\u30D1\u30B9\u30EF\u30FC\u30C9\u3092\u5909\u66F4\u3057\u3066\u304B\u3089\u30ED\u30B0\u30A4\u30F3\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
418
+ iam_passwordMustDiffer: "\u65B0\u3057\u3044\u30D1\u30B9\u30EF\u30FC\u30C9\u306F\u73FE\u5728\u306E\u30D1\u30B9\u30EF\u30FC\u30C9\u3068\u7570\u306A\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002",
419
+ iam_passwordCharacterClasses: "\u30D1\u30B9\u30EF\u30FC\u30C9\u306B\u306F\u5C11\u306A\u304F\u3068\u3082{count}\u7A2E\u985E\u306E\u6587\u5B57\u3092\u542B\u3081\u3066\u304F\u3060\u3055\u3044\u3002",
420
+ iam_notificationDeliveryUnknown: "\u901A\u77E5\u306E\u914D\u4FE1\u7D50\u679C\u306F\u4E0D\u660E\u3067\u3001\u6210\u529F\u3057\u305F\u53EF\u80FD\u6027\u304C\u3042\u308A\u307E\u3059\u3002\u81EA\u52D5\u3067\u518D\u9001\u3057\u306A\u3044\u3067\u304F\u3060\u3055\u3044\u3002",
421
+ iam_captchaRequired: "\u5148\u306B\u30BB\u30AD\u30E5\u30EA\u30C6\u30A3\u78BA\u8A8D\u3092\u5B8C\u4E86\u3057\u3066\u304F\u3060\u3055\u3044",
422
+ iam_captchaInvalid: "\u30BB\u30AD\u30E5\u30EA\u30C6\u30A3\u78BA\u8A8D\u304C\u7121\u52B9\u307E\u305F\u306F\u671F\u9650\u5207\u308C\u3067\u3059\u3002\u518D\u8A66\u884C\u3057\u3066\u304F\u3060\u3055\u3044",
423
+ iam_captchaDisabled: "\u30BB\u30AD\u30E5\u30EA\u30C6\u30A3\u78BA\u8A8D\u306F\u7121\u52B9\u3067\u3059",
424
+ iam_captchaUnavailable: "\u30BB\u30AD\u30E5\u30EA\u30C6\u30A3\u78BA\u8A8D\u3092\u4E00\u6642\u7684\u306B\u5229\u7528\u3067\u304D\u307E\u305B\u3093",
425
+ iam_sensitiveOperationsInvalid: "\u91CD\u8981\u64CD\u4F5C ID \u306F\u6700\u59271000\u4EF6\u3001\u54041\uFF5E128\u6587\u5B57\u3067\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
426
+ iam_terminalTypeInvalid: "\u7AEF\u672B\u7A2E\u5225\u306F web\u3001app\u3001desktop\u3001api \u306E\u3044\u305A\u308C\u304B\u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
392
427
  };
393
428
 
394
429
  // messages/zh-CN.json
395
430
  var zh_CN_default = {
431
+ iam_invitationDisabled: "\u9080\u8BF7\u529F\u80FD\u672A\u542F\u7528\uFF0C\u6216\u6240\u9700\u901A\u77E5\u901A\u9053\u672A\u5C31\u7EEA\u3002",
432
+ iam_invitationDeliveryUnknown: "\u8D26\u53F7\u5DF2\u521B\u5EFA\uFF0C\u9080\u8BF7\u6295\u9012\u5C1A\u672A\u786E\u8BA4\u3002\u8BF7\u6838\u5BF9\u6295\u9012\u540E\u518D\u8BF7\u6C42\u65B0\u7684\u91CD\u7F6E\u901A\u77E5\u3002",
433
+ iam_reauthenticationRequired: "\u8BF7\u91CD\u65B0\u9A8C\u8BC1\u5BC6\u7801\u540E\u7EE7\u7EED\u6B64\u64CD\u4F5C\u3002",
434
+ iam_loginAccessDenied: "\u5F53\u524D\u5730\u5740\u6216\u65F6\u6BB5\u4E0D\u5141\u8BB8\u767B\u5F55\u3002",
396
435
  iam_batchUserIdsInvalid: "\u8BF7\u63D0\u4F9B\u6700\u591A 1000 \u4E2A\u975E\u7A7A\u7528\u6237 ID",
397
436
  $schema: "https://inlang.com/schema/inlang-message-format",
398
437
  iam_securityStateFailed: "\u65E0\u6CD5\u66F4\u65B0\u8BA4\u8BC1\u5B89\u5168\u72B6\u6001\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5",
@@ -580,7 +619,19 @@ var zh_CN_default = {
580
619
  iam_sessionStateFailed: "\u4F1A\u8BDD\u5171\u4EAB\u72B6\u6001\u64CD\u4F5C\u5931\u8D25",
581
620
  iam_sessionStateConflict: "\u4F1A\u8BDD\u72B6\u6001\u5DF2\u53D8\u5316\uFF0C\u8BF7\u91CD\u8BD5",
582
621
  iam_sessionMaxAgeInvalid: "\u4F1A\u8BDD\u6709\u6548\u671F\u6216\u65F6\u95F4\u5B57\u6BB5\u65E0\u6548",
583
- iam_notificationDeliveryUnknown: "\u901A\u77E5\u53EF\u80FD\u5DF2\u9001\u8FBE\uFF0C\u7ED3\u679C\u672A\u77E5\uFF1B\u8BF7\u52FF\u81EA\u52A8\u91CD\u53D1\u3002"
622
+ iam_sessionLimitExceeded: "\u6709\u6548\u4F1A\u8BDD\u6570\u91CF\u5DF2\u8FBE\u4E0A\u9650\uFF0C\u8BF7\u5148\u9000\u51FA\u5176\u4ED6\u4F1A\u8BDD",
623
+ iam_accountPolicyInvalid: "\u8D26\u53F7\u7C7B\u578B\u3001\u72B6\u6001\u6216\u5230\u671F\u65F6\u95F4\u65E0\u6548",
624
+ iam_accountUpdateFailed: "\u8D26\u53F7\u66F4\u65B0\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5",
625
+ iam_passwordChangeRequired: "\u8BF7\u5148\u66F4\u6362\u521D\u59CB\u5BC6\u7801\u540E\u767B\u5F55",
626
+ iam_passwordMustDiffer: "\u65B0\u5BC6\u7801\u4E0D\u80FD\u4E0E\u5F53\u524D\u5BC6\u7801\u76F8\u540C",
627
+ iam_passwordCharacterClasses: "\u5BC6\u7801\u81F3\u5C11\u5E94\u5305\u542B {count} \u7C7B\u5B57\u7B26",
628
+ iam_notificationDeliveryUnknown: "\u901A\u77E5\u53EF\u80FD\u5DF2\u9001\u8FBE\uFF0C\u7ED3\u679C\u672A\u77E5\uFF1B\u8BF7\u52FF\u81EA\u52A8\u91CD\u53D1\u3002",
629
+ iam_captchaRequired: "\u8BF7\u5148\u5B8C\u6210\u5B89\u5168\u9A8C\u8BC1",
630
+ iam_captchaInvalid: "\u5B89\u5168\u9A8C\u8BC1\u65E0\u6548\u6216\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u8BD5",
631
+ iam_captchaDisabled: "\u5B89\u5168\u9A8C\u8BC1\u672A\u542F\u7528",
632
+ iam_captchaUnavailable: "\u5B89\u5168\u9A8C\u8BC1\u6682\u4E0D\u53EF\u7528\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5",
633
+ iam_sensitiveOperationsInvalid: "\u654F\u611F\u64CD\u4F5C ID \u5217\u8868\u65E0\u6548\uFF1A\u6700\u591A 1000 \u9879\uFF0C\u6BCF\u9879\u4E3A 1\u2013128 \u5B57\u7B26\u3002",
634
+ iam_terminalTypeInvalid: "\u7EC8\u7AEF\u7C7B\u578B\u65E0\u6548\uFF0C\u8BF7\u4F7F\u7528 web\u3001app\u3001desktop \u6216 api\u3002"
584
635
  };
585
636
 
586
637
  // src/iam-i18n.ts
@@ -720,8 +771,8 @@ var DbApiKeyRepository = class extends BaseReldbCrudRepository {
720
771
  }
721
772
  return ok(result.data);
722
773
  }
723
- async findByKeyPrefix(prefix, tx) {
724
- const result = await this.findAll({ where: "key_prefix = ?", params: [prefix] }, tx);
774
+ async findByKeyPrefix(prefix2, tx) {
775
+ const result = await this.findAll({ where: "key_prefix = ?", params: [prefix2] }, tx);
725
776
  if (!result.success) {
726
777
  return this.buildQueryError(result.error);
727
778
  }
@@ -806,8 +857,140 @@ function buildAgreementDisplay(config, enabled) {
806
857
  showOnLogin: config.showOnLogin
807
858
  };
808
859
  }
860
+ var BASE64_PLUS_REGEX = /\+/g;
861
+ var BASE64_SLASH_REGEX = /\//g;
862
+ var BASE64_TRAILING_PADDING_REGEX = /=+$/;
863
+ function generateToken() {
864
+ const bytes = new Uint8Array(32);
865
+ crypto.getRandomValues(bytes);
866
+ const base64 = btoa(String.fromCharCode(...bytes));
867
+ return base64.replace(BASE64_PLUS_REGEX, "-").replace(BASE64_SLASH_REGEX, "_").replace(BASE64_TRAILING_PADDING_REGEX, "");
868
+ }
869
+ function getUserSessionScope(userId) {
870
+ return createHash("sha256").update(userId).digest("hex");
871
+ }
872
+ function generateSessionToken(userId) {
873
+ return `s1.${getUserSessionScope(userId)}.${generateToken()}`;
874
+ }
875
+ function getTokenSessionScope(token) {
876
+ return /^s1\.([a-f0-9]{64})\.[\w-]{43}$/.exec(token)?.[1];
877
+ }
878
+ function buildSession(options, now, sessionTtl, accessToken, credentialVersion) {
879
+ return {
880
+ version: crypto.randomUUID(),
881
+ credentialVersion,
882
+ apiKeyId: options.apiKeyId,
883
+ apiKeyScopes: options.apiKeyScopes,
884
+ userId: options.userId,
885
+ username: options.username,
886
+ displayName: options.displayName,
887
+ avatarUrl: options.avatarUrl,
888
+ roles: options.roles ?? [],
889
+ permissions: options.permissions ?? [],
890
+ source: options.source,
891
+ terminalType: options.terminalType ?? "web",
892
+ accessToken,
893
+ createdAt: now,
894
+ startedAt: now,
895
+ lastActiveAt: now,
896
+ expiresAt: new Date(now.getTime() + sessionTtl * 1e3),
897
+ maxAge: sessionTtl,
898
+ data: options.data
899
+ };
900
+ }
901
+ function applySessionPatch(session, patch) {
902
+ const nextSession = { ...session };
903
+ if (patch.data !== void 0) {
904
+ nextSession.data = { ...nextSession.data, ...patch.data };
905
+ }
906
+ if (patch.roles !== void 0) {
907
+ nextSession.roles = patch.roles;
908
+ }
909
+ if (patch.permissions !== void 0) {
910
+ nextSession.permissions = patch.permissions;
911
+ }
912
+ if (patch.username !== void 0) {
913
+ nextSession.username = patch.username;
914
+ }
915
+ if (patch.displayName !== void 0) {
916
+ nextSession.displayName = patch.displayName;
917
+ }
918
+ if (patch.avatarUrl !== void 0) {
919
+ nextSession.avatarUrl = patch.avatarUrl;
920
+ }
921
+ if (patch.source !== void 0) {
922
+ nextSession.source = patch.source;
923
+ }
924
+ if (patch.expiresAt !== void 0)
925
+ nextSession.expiresAt = new Date(patch.expiresAt);
926
+ nextSession.lastActiveAt = patch.lastActiveAt !== void 0 ? new Date(patch.lastActiveAt) : /* @__PURE__ */ new Date();
927
+ return nextSession;
928
+ }
929
+
930
+ // src/ticket/iam-ticket-functions.ts
931
+ var logger = core.logger.child({ module: "iam", scope: "ticket" });
932
+ var TICKET_KEY_PREFIX = "hai:iam:ticket:";
933
+ var DEFAULT_TICKET_TTL_MS = 3e4;
934
+ function buildTicketKey(ticket) {
935
+ return `${TICKET_KEY_PREFIX}${ticket}`;
936
+ }
937
+ function createTicketOperations() {
938
+ return {
939
+ async issue(options) {
940
+ const ttlMs = options.ttlMs !== void 0 && options.ttlMs > 0 ? options.ttlMs : DEFAULT_TICKET_TTL_MS;
941
+ const ticket = generateToken();
942
+ const expiresAt = Date.now() + ttlMs;
943
+ const record = {
944
+ subjectId: options.subjectId,
945
+ purpose: options.purpose,
946
+ grant: options.grant ?? {},
947
+ expiresAt
948
+ };
949
+ const saved = await cache.kv.set(buildTicketKey(ticket), record, { px: ttlMs, nx: true });
950
+ if (!saved.success || !saved.data) {
951
+ logger.warn("Failed to issue ticket", { purpose: options.purpose });
952
+ return err(HaiIamError.TICKET_ISSUE_FAILED, iamM("iam_ticketIssueFailed"));
953
+ }
954
+ return ok({ ticket, expiresAt });
955
+ },
956
+ async consume(ticket, options) {
957
+ if (!ticket) {
958
+ return err(HaiIamError.TICKET_INVALID, iamM("iam_ticketInvalid"));
959
+ }
960
+ const key = buildTicketKey(ticket);
961
+ const found = await cache.kv.get(key);
962
+ if (!found.success) {
963
+ return err(HaiIamError.TICKET_ISSUE_FAILED, iamM("iam_ticketIssueFailed"));
964
+ }
965
+ const record = found.data;
966
+ if (!record) {
967
+ return err(HaiIamError.TICKET_INVALID, iamM("iam_ticketInvalid"));
968
+ }
969
+ const removed = await cache.kv.del(key);
970
+ if (!removed.success || removed.data !== 1) {
971
+ return err(HaiIamError.TICKET_INVALID, iamM("iam_ticketInvalid"));
972
+ }
973
+ if (record.expiresAt <= Date.now()) {
974
+ return err(HaiIamError.TICKET_EXPIRED, iamM("iam_ticketExpired"));
975
+ }
976
+ if (options?.purpose !== void 0 && options.purpose !== record.purpose) {
977
+ return err(HaiIamError.TICKET_INVALID, iamM("iam_ticketInvalid"));
978
+ }
979
+ return ok({
980
+ subjectId: record.subjectId,
981
+ purpose: record.purpose,
982
+ grant: record.grant
983
+ });
984
+ }
985
+ };
986
+ }
809
987
  var TABLE_NAME2 = "hai_iam_users";
810
988
  var USER_FIELDS = [
989
+ { fieldName: "mustChangePassword", columnName: "must_change_password", def: { type: "BOOLEAN", notNull: true, defaultValue: false }, select: true, create: true, update: false },
990
+ { fieldName: "accountType", columnName: "account_type", def: { type: "TEXT", notNull: true, defaultValue: "permanent" }, select: true, create: true, update: true },
991
+ { fieldName: "accountStatus", columnName: "account_status", def: { type: "TEXT", notNull: true, defaultValue: "active" }, select: true, create: true, update: true },
992
+ { fieldName: "accountExpiresAt", columnName: "account_expires_at", def: { type: "TIMESTAMP" }, select: true, create: true, update: true },
993
+ { fieldName: "lastLoginAt", columnName: "last_login_at", def: { type: "TIMESTAMP" }, select: true, create: false, update: false },
811
994
  { fieldName: "credentialVersion", columnName: "credential_version", def: { type: "TEXT", notNull: true, defaultValue: "" }, select: true, create: false, update: false },
812
995
  { fieldName: "resetVersion", columnName: "reset_version", def: { type: "TEXT", notNull: true, defaultValue: "" }, select: true, create: false, update: false },
813
996
  { fieldName: "resetHash", columnName: "reset_hash", def: { type: "TEXT" }, select: true, create: false, update: false },
@@ -909,6 +1092,14 @@ var USER_FIELDS = [
909
1092
  create: true,
910
1093
  update: true
911
1094
  },
1095
+ {
1096
+ fieldName: "firstLoginFailedAt",
1097
+ columnName: "first_login_failed_at",
1098
+ def: { type: "TIMESTAMP" },
1099
+ select: true,
1100
+ create: true,
1101
+ update: true
1102
+ },
912
1103
  {
913
1104
  fieldName: "lastLoginFailedAt",
914
1105
  columnName: "last_login_failed_at",
@@ -980,11 +1171,22 @@ var DbUserRepository = class extends BaseReldbCrudRepository {
980
1171
  return "(UNIX_TIMESTAMP(CURRENT_TIMESTAMP(3)) * 1000)";
981
1172
  return "CAST((julianday('now') - 2440587.5) * 86400000 AS INTEGER)";
982
1173
  }
1174
+ async recordSuccessfulLogin(userId) {
1175
+ const result = await reldb.sql.execute(`UPDATE ${TABLE_NAME2} SET last_login_at = ? WHERE id = ?`, [reldb.config?.type === "sqlite" ? Date.now() : /* @__PURE__ */ new Date(), userId]);
1176
+ return result.success ? ok(void 0) : this.buildQueryError(result.error, result.error);
1177
+ }
1178
+ async suspendInactive(cutoff) {
1179
+ const result = await reldb.sql.execute(
1180
+ `UPDATE ${TABLE_NAME2} SET account_status = ?, credential_version = ?, reset_version = ?, reset_hash = NULL, reset_expires_at = NULL WHERE enabled = ? AND account_status = ? AND COALESCE(last_login_at, created_at) <= ?`,
1181
+ ["dormant", crypto.randomUUID(), crypto.randomUUID(), reldb.config?.type === "postgresql" ? true : 1, "active", reldb.config?.type === "sqlite" ? cutoff.getTime() : cutoff]
1182
+ );
1183
+ return result.success ? ok(result.data.changes) : this.buildQueryError(result.error, result.error);
1184
+ }
983
1185
  /** 改密推进凭据代际;会话存储清理失败也不能让旧凭据恢复授权。 */
984
1186
  async setPassword(input) {
985
1187
  const result = await reldb.sql.execute(
986
- `UPDATE ${TABLE_NAME2} SET password_hash = ?, password_updated_at = ?, credential_version = ?, reset_version = ?, reset_hash = NULL, reset_expires_at = NULL, reset_attempts = 0 WHERE id = ? AND credential_version = ? AND enabled = ?`,
987
- [input.passwordHash, reldb.config?.type === "sqlite" ? Date.now() : /* @__PURE__ */ new Date(), crypto.randomUUID(), crypto.randomUUID(), input.userId, input.expectedVersion, reldb.config?.type === "postgresql" ? true : 1]
1188
+ `UPDATE ${TABLE_NAME2} SET password_hash = ?, password_updated_at = ?, credential_version = ?, reset_version = ?, must_change_password = ?, reset_hash = NULL, reset_expires_at = NULL, reset_attempts = 0 WHERE id = ? AND credential_version = ? AND enabled = ?`,
1189
+ [input.passwordHash, reldb.config?.type === "sqlite" ? Date.now() : /* @__PURE__ */ new Date(), crypto.randomUUID(), crypto.randomUUID(), reldb.config?.type === "postgresql" ? input.mustChangePassword === true : Number(input.mustChangePassword === true), input.userId, input.expectedVersion, reldb.config?.type === "postgresql" ? true : 1]
988
1190
  );
989
1191
  return result.success ? ok(result.data.changes === 1) : this.buildQueryError(result.error, result.error);
990
1192
  }
@@ -1025,17 +1227,22 @@ var DbUserRepository = class extends BaseReldbCrudRepository {
1025
1227
  /** 密码与令牌消费同一行原子提交,禁止先消费 Redis 凭据再无条件写数据库。 */
1026
1228
  async commitPasswordReset(input) {
1027
1229
  const result = await reldb.sql.execute(
1028
- `UPDATE ${TABLE_NAME2} SET password_hash = ?, password_updated_at = ?, credential_version = ?, reset_version = ?, reset_hash = NULL, reset_expires_at = NULL, reset_attempts = 0, login_failed_count = 0, locked_until = NULL, last_login_failed_at = NULL WHERE id = ? AND reset_hash = ? AND reset_version = ? AND credential_version = ? AND reset_expires_at > ${this.databaseNow()} AND enabled = ?`,
1230
+ `UPDATE ${TABLE_NAME2} SET password_hash = ?, password_updated_at = ?, credential_version = ?, reset_version = ?, must_change_password = FALSE, reset_hash = NULL, reset_expires_at = NULL, reset_attempts = 0, login_failed_count = 0, first_login_failed_at = NULL, locked_until = NULL, last_login_failed_at = NULL WHERE id = ? AND reset_hash = ? AND reset_version = ? AND credential_version = ? AND reset_expires_at > ${this.databaseNow()} AND enabled = ?`,
1029
1231
  [input.passwordHash, reldb.config?.type === "sqlite" ? Date.now() : /* @__PURE__ */ new Date(), crypto.randomUUID(), crypto.randomUUID(), input.user.id, input.tokenHash, input.user.resetVersion ?? "", input.user.credentialVersion ?? "", reldb.config?.type === "postgresql" ? true : 1]
1030
1232
  );
1031
1233
  return result.success ? ok(result.data.changes === 1) : this.buildQueryError(result.error, result.error);
1032
1234
  }
1033
1235
  async recordLoginFailure(userId, policy) {
1236
+ if (policy.lockoutEnabled === false)
1237
+ return ok(void 0);
1034
1238
  const sqlite = reldb.config?.type === "sqlite";
1035
- const now = sqlite ? Date.now() : /* @__PURE__ */ new Date();
1036
- const deadline = Date.now() + policy.lockoutDuration * 1e3;
1239
+ const timestamp = Date.now();
1240
+ const now = sqlite ? timestamp : new Date(timestamp);
1241
+ const cutoff = timestamp - (policy.failureWindow ?? 86400) * 1e3;
1242
+ const windowStart = sqlite ? cutoff : new Date(cutoff);
1243
+ const deadline = timestamp + policy.lockoutDuration * 1e3;
1037
1244
  const lockedUntil = sqlite ? deadline : new Date(deadline);
1038
- const expired = await reldb.sql.execute(`UPDATE ${TABLE_NAME2} SET login_failed_count = 0, locked_until = NULL, last_login_failed_at = NULL WHERE id = ? AND locked_until <= ?`, [userId, now]);
1245
+ const expired = await reldb.sql.execute(`UPDATE ${TABLE_NAME2} SET login_failed_count = 0, locked_until = NULL, first_login_failed_at = ?, last_login_failed_at = NULL WHERE id = ? AND (locked_until <= ? OR (locked_until IS NULL AND (first_login_failed_at IS NULL OR first_login_failed_at <= ?)))`, [now, userId, now, windowStart]);
1039
1246
  if (!expired.success)
1040
1247
  return this.buildQueryError(expired.error, expired.error);
1041
1248
  const result = await reldb.sql.execute(
@@ -1046,10 +1253,13 @@ var DbUserRepository = class extends BaseReldbCrudRepository {
1046
1253
  return this.buildQueryError(result.error, result.error);
1047
1254
  return ok(void 0);
1048
1255
  }
1049
- async resetLoginFailures(userId, expectedCredentialVersion) {
1256
+ async resetLoginFailures(userId, expectedCredentialVersion, lockoutEnabled = true) {
1257
+ const params = [userId, expectedCredentialVersion, reldb.config?.type === "postgresql" ? true : 1];
1258
+ if (lockoutEnabled)
1259
+ params.push(reldb.config?.type === "sqlite" ? Date.now() : /* @__PURE__ */ new Date());
1050
1260
  const result = await reldb.sql.execute(
1051
- `UPDATE ${TABLE_NAME2} SET login_failed_count = 0, last_login_failed_at = NULL, locked_until = NULL WHERE id = ? AND credential_version = ? AND enabled = ? AND (locked_until IS NULL OR locked_until <= ?)`,
1052
- [userId, expectedCredentialVersion, reldb.config?.type === "postgresql" ? true : 1, reldb.config?.type === "sqlite" ? Date.now() : /* @__PURE__ */ new Date()]
1261
+ `UPDATE ${TABLE_NAME2} SET login_failed_count = 0, first_login_failed_at = NULL, last_login_failed_at = NULL, locked_until = NULL WHERE id = ? AND credential_version = ? AND enabled = ? ${lockoutEnabled ? "AND (locked_until IS NULL OR locked_until <= ?)" : ""}`,
1262
+ params
1053
1263
  );
1054
1264
  if (!result.success)
1055
1265
  return this.buildQueryError(result.error, result.error);
@@ -1061,7 +1271,7 @@ var DbUserRepository = class extends BaseReldbCrudRepository {
1061
1271
  return err(HaiIamError.USER_DISABLED, iamM("iam_accountDisabled"));
1062
1272
  if ((user.data.credentialVersion ?? "") !== expectedCredentialVersion)
1063
1273
  return err(HaiIamError.SESSION_INVALID, iamM("iam_sessionExpired"));
1064
- if (user.data.lockedUntil && user.data.lockedUntil > /* @__PURE__ */ new Date())
1274
+ if (lockoutEnabled && user.data.lockedUntil && user.data.lockedUntil > /* @__PURE__ */ new Date())
1065
1275
  return err(HaiIamError.USER_LOCKED, iamM("iam_accountLocked"));
1066
1276
  }
1067
1277
  return ok(void 0);
@@ -1134,25 +1344,23 @@ var DbUserRepository = class extends BaseReldbCrudRepository {
1134
1344
  return ok(result.data[0] ?? null);
1135
1345
  }
1136
1346
  };
1137
-
1138
- // src/authn/apikey/iam-authn-apikey-scope.ts
1139
- function covers(grant, permission) {
1140
- return grant === "*" || grant === permission || grant.endsWith(":*") && permission.startsWith(grant.slice(0, -1));
1347
+ function checkPasswordState(user, policy) {
1348
+ if (policy?.requireInitialChange && user.mustChangePassword)
1349
+ return err(HaiIamError.PASSWORD_CHANGE_REQUIRED, iamM("iam_passwordChangeRequired"));
1350
+ const days = policy?.expirationDays ?? 0;
1351
+ if (days > 0 && user.passwordHash && (!user.passwordUpdatedAt || user.passwordUpdatedAt.getTime() + days * 864e5 <= Date.now()))
1352
+ return err(HaiIamError.PASSWORD_EXPIRED, iamM("iam_passwordExpired"));
1353
+ return ok(void 0);
1141
1354
  }
1142
- function applyApiKeyScopes(access, scopes) {
1143
- const permissions = /* @__PURE__ */ new Set();
1144
- for (const permission of access.permissions) {
1145
- for (const scope of scopes) {
1146
- if (covers(permission, scope))
1147
- permissions.add(scope);
1148
- else if (covers(scope, permission))
1149
- permissions.add(permission);
1150
- }
1355
+ function isUserActive(user) {
1356
+ return user.enabled && (user.accountStatus ?? "active") === "active" && (user.accountType !== "temporary" || !!user.accountExpiresAt) && (!user.accountExpiresAt || new Date(user.accountExpiresAt).getTime() > Date.now());
1357
+ }
1358
+ function validateAccountPolicy(user) {
1359
+ if (user.accountType !== void 0 && !["permanent", "temporary"].includes(user.accountType) || user.accountStatus !== void 0 && !["active", "dormant", "closed"].includes(user.accountStatus) || user.accountExpiresAt != null && (!(user.accountExpiresAt instanceof Date) || !Number.isFinite(user.accountExpiresAt.getTime())) || user.accountType === "temporary" && !user.accountExpiresAt) {
1360
+ return err(HaiIamError.INVALID_ARGUMENT, iamM("iam_accountPolicyInvalid"));
1151
1361
  }
1152
- return { ...access, roles: scopes.includes("*") ? access.roles : [], permissions: [...permissions] };
1362
+ return ok(void 0);
1153
1363
  }
1154
-
1155
- // src/user/iam-user-utils.ts
1156
1364
  function toUser(storedUser) {
1157
1365
  return {
1158
1366
  id: storedUser.id,
@@ -1162,6 +1370,11 @@ function toUser(storedUser) {
1162
1370
  displayName: storedUser.displayName,
1163
1371
  avatarUrl: storedUser.avatarUrl,
1164
1372
  enabled: storedUser.enabled,
1373
+ accountType: storedUser.accountType ?? "permanent",
1374
+ accountStatus: storedUser.accountStatus ?? "active",
1375
+ accountExpiresAt: storedUser.accountExpiresAt ?? null,
1376
+ lastLoginAt: storedUser.lastLoginAt ?? null,
1377
+ mustChangePassword: storedUser.mustChangePassword ?? false,
1165
1378
  emailVerified: storedUser.emailVerified,
1166
1379
  phoneVerified: storedUser.phoneVerified,
1167
1380
  createdAt: storedUser.createdAt,
@@ -1169,6 +1382,23 @@ function toUser(storedUser) {
1169
1382
  metadata: storedUser.metadata
1170
1383
  };
1171
1384
  }
1385
+
1386
+ // src/authn/apikey/iam-authn-apikey-scope.ts
1387
+ function covers(grant, permission) {
1388
+ return grant === "*" || grant === permission || grant.endsWith(":*") && permission.startsWith(grant.slice(0, -1));
1389
+ }
1390
+ function applyApiKeyScopes(access, scopes) {
1391
+ const permissions = /* @__PURE__ */ new Set();
1392
+ for (const permission of access.permissions) {
1393
+ for (const scope of scopes) {
1394
+ if (covers(permission, scope))
1395
+ permissions.add(scope);
1396
+ else if (covers(scope, permission))
1397
+ permissions.add(permission);
1398
+ }
1399
+ }
1400
+ return { ...access, roles: scopes.includes("*") ? access.roles : [], permissions: [...permissions] };
1401
+ }
1172
1402
  function ensureCredentialType(credentials, type) {
1173
1403
  if (credentials.type !== type) {
1174
1404
  return err(
@@ -1178,8 +1408,8 @@ function ensureCredentialType(credentials, type) {
1178
1408
  }
1179
1409
  return ok(credentials);
1180
1410
  }
1181
- function isAccountLocked(user) {
1182
- if (!user.lockedUntil) {
1411
+ function isAccountLocked(user, policy) {
1412
+ if (policy?.lockoutEnabled === false || !user.lockedUntil) {
1183
1413
  return false;
1184
1414
  }
1185
1415
  return /* @__PURE__ */ new Date() < user.lockedUntil;
@@ -1187,16 +1417,16 @@ function isAccountLocked(user) {
1187
1417
  async function recordLoginFailure(userRepository, user, policy) {
1188
1418
  return userRepository.recordLoginFailure(user.id, policy);
1189
1419
  }
1190
- async function resetLoginFailures(userRepository, user) {
1191
- return userRepository.resetLoginFailures(user.id, user.credentialVersion ?? "");
1420
+ async function resetLoginFailures(userRepository, user, policy) {
1421
+ return userRepository.resetLoginFailures(user.id, user.credentialVersion ?? "", policy?.lockoutEnabled);
1192
1422
  }
1193
1423
 
1194
1424
  // src/authn/apikey/iam-authn-apikey-strategy.ts
1195
- var logger = core.logger.child({ module: "iam", scope: "apikey-strategy" });
1425
+ var logger2 = core.logger.child({ module: "iam", scope: "apikey-strategy" });
1196
1426
  var KEY_PREFIX_LENGTH = 12;
1197
- function generateRawKey(prefix) {
1427
+ function generateRawKey(prefix2) {
1198
1428
  const hex = randomBytes(32).toString("hex");
1199
- return `${prefix}${hex}`;
1429
+ return `${prefix2}${hex}`;
1200
1430
  }
1201
1431
  function extractKeyPrefix(rawKey) {
1202
1432
  return rawKey.slice(0, KEY_PREFIX_LENGTH);
@@ -1219,8 +1449,8 @@ function createApiKeyStrategy(config) {
1219
1449
  const { userRepository, apiKeyRepository } = config;
1220
1450
  const passwordOps = crypto$1.password;
1221
1451
  async function verifyRawKey(rawKey) {
1222
- const prefix = extractKeyPrefix(rawKey);
1223
- const candidatesResult = await apiKeyRepository.findByKeyPrefix(prefix);
1452
+ const prefix2 = extractKeyPrefix(rawKey);
1453
+ const candidatesResult = await apiKeyRepository.findByKeyPrefix(prefix2);
1224
1454
  if (!candidatesResult.success) {
1225
1455
  return candidatesResult;
1226
1456
  }
@@ -1237,7 +1467,7 @@ function createApiKeyStrategy(config) {
1237
1467
  return err(HaiIamError.APIKEY_EXPIRED, iamM("iam_apikeyExpired"));
1238
1468
  }
1239
1469
  apiKeyRepository.updateFields(candidate.id, { lastUsedAt: /* @__PURE__ */ new Date() }).catch(() => {
1240
- logger.warn("Failed to update API Key lastUsedAt", { keyId: candidate.id });
1470
+ logger2.warn("Failed to update API Key lastUsedAt", { keyId: candidate.id });
1241
1471
  });
1242
1472
  return ok(toApiKey(candidate));
1243
1473
  }
@@ -1264,10 +1494,10 @@ function createApiKeyStrategy(config) {
1264
1494
  if (!userResult.data) {
1265
1495
  return err(HaiIamError.USER_NOT_FOUND, iamM("iam_userNotExist"));
1266
1496
  }
1267
- if (!userResult.data.enabled) {
1497
+ if (!isUserActive(userResult.data)) {
1268
1498
  return err(HaiIamError.USER_DISABLED, iamM("iam_accountDisabled"));
1269
1499
  }
1270
- logger.info("API Key authentication succeeded", { userId: userResult.data.id, keyId: apiKey.id });
1500
+ logger2.debug("API Key authentication succeeded", { userId: userResult.data.id, keyId: apiKey.id });
1271
1501
  return ok({ user: { ...toUser(userResult.data), credentialVersion: userResult.data.credentialVersion ?? "" }, apiKey });
1272
1502
  }
1273
1503
  };
@@ -1304,7 +1534,7 @@ function createApiKeyStrategy(config) {
1304
1534
  const insertResult = await apiKeyRepository.insert(storedApiKey);
1305
1535
  if (!insertResult.success)
1306
1536
  return insertResult;
1307
- logger.info("API Key created", { userId, keyId: id, name: options.name });
1537
+ logger2.info("API Key created", { userId, keyId: id, name: options.name });
1308
1538
  return ok({ apiKey: toApiKey(storedApiKey), rawKey });
1309
1539
  } catch (error) {
1310
1540
  return err(HaiIamError.INTERNAL_ERROR, iamM("iam_apikeyCreateFailed", { params: { message: String(error) } }), error);
@@ -1326,7 +1556,7 @@ function createApiKeyStrategy(config) {
1326
1556
  const result = await apiKeyRepository.removeById(keyId);
1327
1557
  if (!result.success)
1328
1558
  return result;
1329
- logger.info("API Key revoked", { keyId });
1559
+ logger2.info("API Key revoked", { keyId });
1330
1560
  return ok(void 0);
1331
1561
  },
1332
1562
  async verifyApiKey(rawKey) {
@@ -1335,6 +1565,120 @@ function createApiKeyStrategy(config) {
1335
1565
  };
1336
1566
  return { strategy, apiKeyFunctions };
1337
1567
  }
1568
+ function addresses(entries) {
1569
+ const list = new BlockList();
1570
+ for (const entry of entries) {
1571
+ const [address, prefix2] = entry.split("/");
1572
+ const family = isIP(address) === 4 ? "ipv4" : "ipv6";
1573
+ if (prefix2 === void 0)
1574
+ list.addAddress(address, family);
1575
+ else
1576
+ list.addSubnet(address, Number(prefix2), family);
1577
+ }
1578
+ return list;
1579
+ }
1580
+ function createLoginAccessCheck(policy) {
1581
+ if (!policy?.enabled)
1582
+ return () => ok(void 0);
1583
+ const allow = addresses(policy.allowIps);
1584
+ const deny = addresses(policy.denyIps);
1585
+ const format = new Intl.DateTimeFormat("en-US", { timeZone: policy.timezone, weekday: "short", hour: "2-digit", minute: "2-digit", hourCycle: "h23" });
1586
+ const weekdays = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
1587
+ return (context) => {
1588
+ const rejected = () => err(HaiIamError.LOGIN_ACCESS_DENIED, iamM("iam_loginAccessDenied"));
1589
+ if (policy.allowIps.length || policy.denyIps.length) {
1590
+ const raw = context?.ip?.trim();
1591
+ const ip = raw?.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/i)?.[1] ?? raw;
1592
+ if (!ip || !isIP(ip))
1593
+ return rejected();
1594
+ const family = isIP(ip) === 4 ? "ipv4" : "ipv6";
1595
+ if (deny.check(ip, family) || policy.allowIps.length && !allow.check(ip, family))
1596
+ return rejected();
1597
+ }
1598
+ if (policy.windows.length) {
1599
+ const parts = Object.fromEntries(format.formatToParts(/* @__PURE__ */ new Date()).map((part) => [part.type, part.value]));
1600
+ const day = weekdays.indexOf(parts.weekday) + 1;
1601
+ const previousDay = day === 1 ? 7 : day - 1;
1602
+ const time = `${parts.hour}:${parts.minute}`;
1603
+ const permitted = policy.windows.some((window) => window.start < window.end ? window.days.includes(day) && time >= window.start && time < window.end : window.days.includes(day) && time >= window.start || window.days.includes(previousDay) && time < window.end);
1604
+ if (!permitted)
1605
+ return rejected();
1606
+ }
1607
+ return ok(void 0);
1608
+ };
1609
+ }
1610
+ var logger3 = core.logger.child({ module: "iam", scope: "captcha" });
1611
+ var prefix = "hai:iam:captcha:";
1612
+ var operationSchema = z.enum(["login", "register", "sendOtp", "passwordReset", "renewPassword"]);
1613
+ var proofSchema = z.object({
1614
+ id: z.string().regex(/^[a-f0-9]{64}$/),
1615
+ solution: z.object({ counter: z.number().int().min(0).max(2e3), derivedKey: z.string().regex(/^[a-f0-9]{64}$/i), time: z.number().finite().nonnegative().optional() })
1616
+ });
1617
+ async function createIamCaptcha(config, input) {
1618
+ if (!config.enabled)
1619
+ return err(HaiIamError.CAPTCHA_DISABLED, iamM("iam_captchaDisabled"));
1620
+ const operation = operationSchema.safeParse(input?.operation);
1621
+ if (!operation.success)
1622
+ return err(HaiIamError.CAPTCHA_INVALID, iamM("iam_captchaInvalid"));
1623
+ try {
1624
+ const id = randomBytes(32).toString("hex");
1625
+ const signatureSecret = randomBytes(32).toString("hex");
1626
+ const keySecret = randomBytes(32).toString("hex");
1627
+ const challenge = await createChallenge({
1628
+ algorithm: "PBKDF2/SHA-256",
1629
+ cost: config.cost,
1630
+ counter: randomInt(1, config.maxCounter + 1),
1631
+ deriveKey: pbkdf2.deriveKey,
1632
+ hmacSignatureSecret: signatureSecret,
1633
+ hmacKeySignatureSecret: keySecret,
1634
+ data: { operation: operation.data }
1635
+ });
1636
+ const record = { operation: operation.data, challenge, signatureSecret, keySecret };
1637
+ const saved = await cache.kv.set(`${prefix}${id}`, record, { nx: true, ex: config.ttlSeconds });
1638
+ if (!saved.success || !saved.data)
1639
+ return err(HaiIamError.CAPTCHA_UNAVAILABLE, iamM("iam_captchaUnavailable"));
1640
+ return ok({ id, challenge });
1641
+ } catch (error) {
1642
+ logger3.error("Captcha challenge generation failed", { operation: operation.data, error });
1643
+ return err(HaiIamError.CAPTCHA_UNAVAILABLE, iamM("iam_captchaUnavailable"));
1644
+ }
1645
+ }
1646
+ async function verifyIamCaptcha(config, operation, proof) {
1647
+ if (!config.enabled)
1648
+ return ok(void 0);
1649
+ if (!proof)
1650
+ return err(HaiIamError.CAPTCHA_REQUIRED, iamM("iam_captchaRequired"));
1651
+ if (typeof proof !== "string" || proof.length > 4096)
1652
+ return err(HaiIamError.CAPTCHA_INVALID, iamM("iam_captchaInvalid"));
1653
+ let payload;
1654
+ try {
1655
+ const parsed = proofSchema.safeParse(JSON.parse(proof));
1656
+ if (!parsed.success)
1657
+ return err(HaiIamError.CAPTCHA_INVALID, iamM("iam_captchaInvalid"));
1658
+ payload = parsed.data;
1659
+ } catch {
1660
+ return err(HaiIamError.CAPTCHA_INVALID, iamM("iam_captchaInvalid"));
1661
+ }
1662
+ try {
1663
+ const key = `${prefix}${payload.id}`;
1664
+ const found = await cache.kv.get(key);
1665
+ if (!found.success)
1666
+ return err(HaiIamError.CAPTCHA_UNAVAILABLE, iamM("iam_captchaUnavailable"));
1667
+ if (!found.data || found.data.operation !== operation)
1668
+ return err(HaiIamError.CAPTCHA_INVALID, iamM("iam_captchaInvalid"));
1669
+ const record = found.data;
1670
+ const result = await verifySolution({ challenge: record.challenge, solution: payload.solution, deriveKey: pbkdf2.deriveKey, hmacSignatureSecret: record.signatureSecret, hmacKeySignatureSecret: record.keySecret });
1671
+ if (!result.verified)
1672
+ return err(HaiIamError.CAPTCHA_INVALID, iamM("iam_captchaInvalid"));
1673
+ const removed = await cache.kv.del(key);
1674
+ if (!removed.success)
1675
+ return err(HaiIamError.CAPTCHA_UNAVAILABLE, iamM("iam_captchaUnavailable"));
1676
+ return removed.data === 1 ? ok(void 0) : err(HaiIamError.CAPTCHA_INVALID, iamM("iam_captchaInvalid"));
1677
+ } catch (error) {
1678
+ logger3.error("Captcha verification failed", { operation, error });
1679
+ return err(HaiIamError.CAPTCHA_UNAVAILABLE, iamM("iam_captchaUnavailable"));
1680
+ }
1681
+ }
1338
1682
  async function loadLdapClientFactory() {
1339
1683
  const { Client } = await import('ldapts');
1340
1684
  return async (config) => {
@@ -1389,7 +1733,7 @@ async function loadLdapClientFactory() {
1389
1733
  }
1390
1734
  };
1391
1735
  }
1392
- var logger2 = core.logger.child({ module: "iam", scope: "ldap-strategy" });
1736
+ var logger4 = core.logger.child({ module: "iam", scope: "ldap-strategy" });
1393
1737
  function escapeLdapFilterValue(value) {
1394
1738
  return value.replace(/[\\*()/\0]/g, (ch) => {
1395
1739
  const hex = ch.charCodeAt(0).toString(16).padStart(2, "0");
@@ -1405,6 +1749,7 @@ function createLdapStrategy(config) {
1405
1749
  maxLoginAttempts = 5,
1406
1750
  lockoutDuration = 900
1407
1751
  } = config;
1752
+ const failurePolicy = { maxLoginAttempts, lockoutDuration, lockoutEnabled: config.lockoutEnabled ?? true, failureWindow: config.failureWindow ?? 86400 };
1408
1753
  function getAttributeValue(entry, attr) {
1409
1754
  const value = entry.attributes[attr];
1410
1755
  if (Array.isArray(value)) {
@@ -1471,13 +1816,13 @@ function createLdapStrategy(config) {
1471
1816
  }
1472
1817
  let storedUser = localUserResult.data;
1473
1818
  if (storedUser) {
1474
- if (!storedUser.enabled) {
1819
+ if (!isUserActive(storedUser)) {
1475
1820
  return err(
1476
1821
  HaiIamError.USER_DISABLED,
1477
1822
  iamM("iam_accountDisabled")
1478
1823
  );
1479
1824
  }
1480
- if (isAccountLocked(storedUser)) {
1825
+ if (isAccountLocked(storedUser, failurePolicy)) {
1481
1826
  return err(
1482
1827
  HaiIamError.USER_LOCKED,
1483
1828
  iamM("iam_accountLocked")
@@ -1487,11 +1832,11 @@ function createLdapStrategy(config) {
1487
1832
  const userBindResult = await client.bind(entry.dn, password);
1488
1833
  if (!userBindResult.success) {
1489
1834
  if (storedUser) {
1490
- const failureResult = await recordLoginFailure(userRepository, storedUser, { maxLoginAttempts, lockoutDuration });
1835
+ const failureResult = await recordLoginFailure(userRepository, storedUser, failurePolicy);
1491
1836
  if (!failureResult.success)
1492
1837
  return failureResult;
1493
1838
  }
1494
- logger2.warn("LDAP authentication failed", { username });
1839
+ logger4.warn("LDAP authentication failed", { username });
1495
1840
  return err(
1496
1841
  HaiIamError.INVALID_CREDENTIALS,
1497
1842
  iamM("iam_passwordWrong")
@@ -1529,7 +1874,7 @@ function createLdapStrategy(config) {
1529
1874
  return assigned;
1530
1875
  }
1531
1876
  } catch (callbackError) {
1532
- logger2.warn("onUserAutoRegistered callback failed", { userId: storedUser?.id, error: callbackError });
1877
+ logger4.warn("onUserAutoRegistered callback failed", { userId: storedUser?.id, error: callbackError });
1533
1878
  }
1534
1879
  }
1535
1880
  } else if (storedUser && syncUser) {
@@ -1551,10 +1896,10 @@ function createLdapStrategy(config) {
1551
1896
  }
1552
1897
  if (!storedUser)
1553
1898
  return err(HaiIamError.USER_NOT_FOUND, iamM("iam_userNotExist"));
1554
- const resetResult = await resetLoginFailures(userRepository, storedUser);
1899
+ const resetResult = await resetLoginFailures(userRepository, storedUser, failurePolicy);
1555
1900
  if (!resetResult.success)
1556
1901
  return resetResult;
1557
- logger2.info("LDAP authentication succeeded", { userId: storedUser.id });
1902
+ logger4.debug("LDAP authentication succeeded", { userId: storedUser.id });
1558
1903
  return ok({ ...toUser(storedUser), credentialVersion: storedUser.credentialVersion ?? "" });
1559
1904
  } finally {
1560
1905
  await client.unbind();
@@ -1632,7 +1977,7 @@ function createCacheOtpRepository() {
1632
1977
  };
1633
1978
  return otpRepoInstance;
1634
1979
  }
1635
- var logger3 = core.logger.child({ module: "iam", scope: "otp-strategy" });
1980
+ var logger5 = core.logger.child({ module: "iam", scope: "otp-strategy" });
1636
1981
  var PHONE_IDENTIFIER_REGEX = /^\+?\d{8,}$/;
1637
1982
  var PHONE_SEPARATOR_REGEX = /[\s-]/g;
1638
1983
  function generateOtpCode(length) {
@@ -1662,6 +2007,7 @@ function createOtpStrategy(config) {
1662
2007
  const otpConfig = config.otpConfig ? OtpConfigSchema.parse(config.otpConfig) : OtpConfigSchema.parse({});
1663
2008
  const maxLoginAttempts = config.maxLoginAttempts ?? 5;
1664
2009
  const lockoutDuration = config.lockoutDuration ?? 900;
2010
+ const failurePolicy = { maxLoginAttempts, lockoutDuration, lockoutEnabled: config.lockoutEnabled ?? true, failureWindow: config.failureWindow ?? 86400 };
1665
2011
  const registerConfig = config.registerConfig;
1666
2012
  const allowAutoRegister = registerConfig?.enabled ?? config.autoRegister ?? false;
1667
2013
  const defaultEnabled = registerConfig?.defaultEnabled ?? true;
@@ -1683,17 +2029,17 @@ function createOtpStrategy(config) {
1683
2029
  }
1684
2030
  let storedUser = userResult.data;
1685
2031
  if (storedUser) {
1686
- if (!storedUser.enabled) {
2032
+ if (!isUserActive(storedUser)) {
1687
2033
  return err(HaiIamError.USER_DISABLED, iamM("iam_accountDisabled"));
1688
2034
  }
1689
- if (isAccountLocked(storedUser)) {
2035
+ if (isAccountLocked(storedUser, failurePolicy)) {
1690
2036
  return err(HaiIamError.USER_LOCKED, iamM("iam_accountLocked"));
1691
2037
  }
1692
2038
  }
1693
2039
  const consumed = await config.otpRepository.consumeOtp(identifier, code, otpConfig.maxAttempts);
1694
2040
  if (!consumed.success) {
1695
2041
  if (storedUser && consumed.error.code === HaiIamError.OTP_INVALID.code) {
1696
- const failed = await recordLoginFailure(config.userRepository, storedUser, { maxLoginAttempts, lockoutDuration });
2042
+ const failed = await recordLoginFailure(config.userRepository, storedUser, failurePolicy);
1697
2043
  if (!failed.success)
1698
2044
  return failed;
1699
2045
  }
@@ -1729,19 +2075,19 @@ function createOtpStrategy(config) {
1729
2075
  return assigned;
1730
2076
  }
1731
2077
  } catch (callbackError) {
1732
- logger3.warn("onUserAutoRegistered callback failed", { userId: storedUser.id, error: callbackError });
2078
+ logger5.warn("onUserAutoRegistered callback failed", { userId: storedUser.id, error: callbackError });
1733
2079
  }
1734
2080
  }
1735
2081
  }
1736
2082
  if (!storedUser) {
1737
2083
  return err(HaiIamError.USER_NOT_FOUND, iamM("iam_userNotExist"));
1738
2084
  }
1739
- if (!storedUser.enabled)
2085
+ if (!isUserActive(storedUser))
1740
2086
  return err(HaiIamError.USER_DISABLED, iamM("iam_accountDisabled"));
1741
- const resetResult = await resetLoginFailures(config.userRepository, storedUser);
2087
+ const resetResult = await resetLoginFailures(config.userRepository, storedUser, failurePolicy);
1742
2088
  if (!resetResult.success)
1743
2089
  return resetResult;
1744
- logger3.info("OTP authentication succeeded", { userId: storedUser.id });
2090
+ logger5.debug("OTP authentication succeeded", { userId: storedUser.id });
1745
2091
  return ok({ ...toUser(storedUser), credentialVersion: storedUser.credentialVersion ?? "" });
1746
2092
  }
1747
2093
  };
@@ -1785,12 +2131,12 @@ function createOtpStrategy(config) {
1785
2131
  );
1786
2132
  }
1787
2133
  const result = { expiresAt };
1788
- logger3.debug("OTP challenge sent", { identifier, expiresAt });
2134
+ logger5.debug("OTP challenge sent", { identifier, expiresAt });
1789
2135
  return ok(result);
1790
2136
  }
1791
2137
  return { strategy, challenge };
1792
2138
  }
1793
- var logger4 = core.logger.child({ module: "iam", scope: "password-strategy" });
2139
+ var logger6 = core.logger.child({ module: "iam", scope: "password-strategy" });
1794
2140
  var PASSWORD_UPPERCASE_REGEX = /[A-Z]/;
1795
2141
  var PASSWORD_LOWERCASE_REGEX = /[a-z]/;
1796
2142
  var PASSWORD_DIGIT_REGEX = /\d/;
@@ -1799,6 +2145,7 @@ function createPasswordStrategy(config) {
1799
2145
  const passwordConfig = config.passwordConfig ? PasswordConfigSchema.parse(config.passwordConfig) : PasswordConfigSchema.parse({});
1800
2146
  const maxLoginAttempts = config.maxLoginAttempts ?? 5;
1801
2147
  const lockoutDuration = config.lockoutDuration ?? 900;
2148
+ const failurePolicy = { maxLoginAttempts, lockoutDuration, lockoutEnabled: config.lockoutEnabled ?? true, failureWindow: config.failureWindow ?? 86400 };
1802
2149
  const passwordOps = crypto$1.password;
1803
2150
  function mapPasswordError(message) {
1804
2151
  return err(
@@ -1843,21 +2190,16 @@ function createPasswordStrategy(config) {
1843
2190
  iamM("iam_passwordNeedSpecialChar")
1844
2191
  );
1845
2192
  }
2193
+ const classes = [PASSWORD_UPPERCASE_REGEX, PASSWORD_LOWERCASE_REGEX, PASSWORD_DIGIT_REGEX, PASSWORD_SPECIAL_CHAR_REGEX].filter((pattern) => pattern.test(password)).length;
2194
+ if (classes < passwordConfig.minCharacterClasses)
2195
+ return err(HaiIamError.PASSWORD_POLICY_VIOLATION, iamM("iam_passwordCharacterClasses", { params: { count: passwordConfig.minCharacterClasses } }));
1846
2196
  return ok(void 0);
1847
2197
  }
1848
- function isPasswordExpired(user) {
1849
- if (passwordConfig.expirationDays <= 0 || !user.passwordUpdatedAt) {
1850
- return false;
1851
- }
1852
- const expirationDate = new Date(user.passwordUpdatedAt);
1853
- expirationDate.setDate(expirationDate.getDate() + passwordConfig.expirationDays);
1854
- return /* @__PURE__ */ new Date() > expirationDate;
1855
- }
1856
2198
  function checkAccountStatus(user) {
1857
- if (!user.enabled) {
2199
+ if (!isUserActive(user)) {
1858
2200
  return err(HaiIamError.USER_DISABLED, iamM("iam_accountDisabled"));
1859
2201
  }
1860
- if (isAccountLocked(user)) {
2202
+ if (isAccountLocked(user, failurePolicy)) {
1861
2203
  return err(HaiIamError.USER_LOCKED, iamM("iam_accountLocked"));
1862
2204
  }
1863
2205
  return ok(void 0);
@@ -1871,10 +2213,10 @@ function createPasswordStrategy(config) {
1871
2213
  return mapPasswordError(verifyResult.error.message);
1872
2214
  }
1873
2215
  if (!verifyResult.data) {
1874
- const failureResult = await recordLoginFailure(config.userRepository, user, { maxLoginAttempts, lockoutDuration });
2216
+ const failureResult = await recordLoginFailure(config.userRepository, user, failurePolicy);
1875
2217
  if (!failureResult.success)
1876
2218
  return failureResult;
1877
- logger4.warn("Password verification failed", { userId: user.id });
2219
+ logger6.warn("Password verification failed", { userId: user.id });
1878
2220
  return err(HaiIamError.INVALID_CREDENTIALS, iamM("iam_passwordWrong"));
1879
2221
  }
1880
2222
  return ok(void 0);
@@ -1902,13 +2244,13 @@ function createPasswordStrategy(config) {
1902
2244
  const pwResult = await verifyUserPassword(storedUser, password);
1903
2245
  if (!pwResult.success)
1904
2246
  return pwResult;
1905
- if (isPasswordExpired(storedUser)) {
1906
- return err(HaiIamError.PASSWORD_EXPIRED, iamM("iam_passwordExpired"));
1907
- }
1908
- const resetResult = await resetLoginFailures(config.userRepository, storedUser);
2247
+ const passwordState = checkPasswordState(storedUser, passwordConfig);
2248
+ if (!passwordState.success)
2249
+ return passwordState;
2250
+ const resetResult = await resetLoginFailures(config.userRepository, storedUser, failurePolicy);
1909
2251
  if (!resetResult.success)
1910
2252
  return resetResult;
1911
- logger4.info("Password authentication succeeded", { userId: storedUser.id });
2253
+ logger6.debug("Password authentication succeeded", { userId: storedUser.id });
1912
2254
  return ok({ ...toUser(storedUser), credentialVersion: storedUser.credentialVersion ?? "" });
1913
2255
  }
1914
2256
  };
@@ -1928,7 +2270,7 @@ function createPasswordStrategy(config) {
1928
2270
  }
1929
2271
 
1930
2272
  // src/authn/iam-authn-functions.ts
1931
- var logger5 = core.logger.child({ module: "iam", scope: "authn" });
2273
+ var logger7 = core.logger.child({ module: "iam", scope: "authn" });
1932
2274
  async function createAuthnOperations(deps) {
1933
2275
  try {
1934
2276
  const { config, sessionFunctions, authzFunctions, ldapClientFactory, ldapSyncUser, onOtpSendEmail, onOtpSendSms } = deps;
@@ -1949,6 +2291,8 @@ async function createAuthnOperations(deps) {
1949
2291
  passwordConfig: config.password,
1950
2292
  userRepository,
1951
2293
  maxLoginAttempts: securityConfig.maxLoginAttempts,
2294
+ lockoutEnabled: securityConfig.lockoutEnabled,
2295
+ failureWindow: securityConfig.failureWindow,
1952
2296
  lockoutDuration: securityConfig.lockoutDuration
1953
2297
  });
1954
2298
  let otpResult;
@@ -1962,6 +2306,8 @@ async function createAuthnOperations(deps) {
1962
2306
  autoRegister: true,
1963
2307
  registerConfig: config.register,
1964
2308
  maxLoginAttempts: securityConfig.maxLoginAttempts,
2309
+ lockoutEnabled: securityConfig.lockoutEnabled,
2310
+ failureWindow: securityConfig.failureWindow,
1965
2311
  lockoutDuration: securityConfig.lockoutDuration,
1966
2312
  onUserAutoRegistered,
1967
2313
  onOtpSendEmail,
@@ -1978,6 +2324,8 @@ async function createAuthnOperations(deps) {
1978
2324
  syncUser: ldapSyncUser ?? true,
1979
2325
  defaultEnabled: config.register.defaultEnabled,
1980
2326
  maxLoginAttempts: securityConfig.maxLoginAttempts,
2327
+ lockoutEnabled: securityConfig.lockoutEnabled,
2328
+ failureWindow: securityConfig.failureWindow,
1981
2329
  lockoutDuration: securityConfig.lockoutDuration,
1982
2330
  onUserAutoRegistered
1983
2331
  });
@@ -2006,10 +2354,10 @@ async function createAuthnOperations(deps) {
2006
2354
  userRepository,
2007
2355
  config
2008
2356
  });
2009
- logger5.info("Authn sub-feature initialized");
2357
+ logger7.info("Authn sub-feature initialized");
2010
2358
  return ok({ authn: operations.authn, loginRegisteredUser: operations.loginRegisteredUser, passwordStrategyResult: passwordResult, apiKeyFunctions });
2011
2359
  } catch (error) {
2012
- logger5.error("Authn sub-feature initialization failed", { error });
2360
+ logger7.error("Authn sub-feature initialization failed", { error });
2013
2361
  return err(
2014
2362
  HaiIamError.CONFIG_ERROR,
2015
2363
  iamM("iam_initComponentFailed"),
@@ -2031,6 +2379,7 @@ function buildAuthnOperations(deps) {
2031
2379
  } = deps;
2032
2380
  const loginConfig = LoginConfigSchema.parse(config.login ?? {});
2033
2381
  const agreementConfig = AgreementConfigSchema.parse(config.agreements ?? {});
2382
+ const checkAccess = createLoginAccessCheck(config.loginAccess);
2034
2383
  function loginDisabled(type) {
2035
2384
  if (!loginConfig[type]) {
2036
2385
  return err(
@@ -2061,9 +2410,20 @@ function buildAuthnOperations(deps) {
2061
2410
  iamM("iam_featureNotImplemented")
2062
2411
  );
2063
2412
  }
2064
- async function buildAuthResult(user, apiKey) {
2065
- if (!user.enabled)
2413
+ async function buildAuthResult(user, apiKey, context) {
2414
+ const permitted = checkAccess(context);
2415
+ if (!permitted.success)
2416
+ return permitted;
2417
+ if (!isUserActive(user))
2418
+ return err(HaiIamError.USER_DISABLED, iamM("iam_accountDisabled"));
2419
+ const stored = await userRepository.findById(user.id);
2420
+ if (!stored.success)
2421
+ return stored;
2422
+ if (!stored.data || !isUserActive(stored.data))
2066
2423
  return err(HaiIamError.USER_DISABLED, iamM("iam_accountDisabled"));
2424
+ const passwordState = checkPasswordState(stored.data, config.password);
2425
+ if (!passwordState.success)
2426
+ return passwordState;
2067
2427
  const authorization = await authzFunctions.getUserAuthorization(user.id);
2068
2428
  if (!authorization.success)
2069
2429
  return authorization;
@@ -2077,7 +2437,8 @@ function buildAuthnOperations(deps) {
2077
2437
  avatarUrl: user.avatarUrl,
2078
2438
  ...effective,
2079
2439
  apiKeyId: apiKey?.id,
2080
- apiKeyScopes: apiKey?.scopes
2440
+ apiKeyScopes: apiKey?.scopes,
2441
+ terminalType: context?.terminalType ?? (apiKey ? "api" : "web")
2081
2442
  });
2082
2443
  if (!sessionResult.success) {
2083
2444
  return sessionResult;
@@ -2087,6 +2448,13 @@ function buildAuthnOperations(deps) {
2087
2448
  if (!tokenPair) {
2088
2449
  return err(HaiIamError.SESSION_CREATE_FAILED, iamM("iam_createSessionFailed"));
2089
2450
  }
2451
+ const recorded = await userRepository.recordSuccessfulLogin(user.id);
2452
+ if (!recorded.success) {
2453
+ const revoked = await sessionFunctions.delete(session.accessToken);
2454
+ if (!revoked.success)
2455
+ logger7.error("Failed to revoke session after login timestamp write failure", { userId: user.id, error: revoked.error });
2456
+ return recorded;
2457
+ }
2090
2458
  const { credentialVersion, ...publicUser } = user;
2091
2459
  return ok({
2092
2460
  user: publicUser,
@@ -2095,7 +2463,13 @@ function buildAuthnOperations(deps) {
2095
2463
  agreements: buildAgreementDisplay(agreementConfig, agreementConfig.showOnLogin)
2096
2464
  });
2097
2465
  }
2098
- async function loginWithStrategy(type, credentials, strategy) {
2466
+ async function loginWithStrategy(type, credentials, strategy, context) {
2467
+ const captcha = await verifyIamCaptcha(config.captcha, "login", credentials.captcha);
2468
+ if (!captcha.success)
2469
+ return captcha;
2470
+ const permitted = checkAccess(context);
2471
+ if (!permitted.success)
2472
+ return permitted;
2099
2473
  const disabled = loginDisabled(type);
2100
2474
  if (disabled)
2101
2475
  return disabled;
@@ -2107,33 +2481,104 @@ function buildAuthnOperations(deps) {
2107
2481
  ...credentials
2108
2482
  });
2109
2483
  if (!authResult.success) {
2110
- logger5.warn("Login failed", { type, reason: authResult.error.code });
2484
+ logger7.warn("Login failed", { type, reason: authResult.error.code });
2111
2485
  return authResult;
2112
2486
  }
2113
- const result = await buildAuthResult(authResult.data);
2487
+ if (credentials.expectedUserId && credentials.expectedUserId !== authResult.data.id)
2488
+ return err(HaiIamError.INVALID_CREDENTIALS, iamM("iam_passwordWrong"));
2489
+ const result = await buildAuthResult(authResult.data, void 0, context);
2114
2490
  if (result.success) {
2115
- logger5.info("Login succeeded", { type, userId: authResult.data.id });
2491
+ logger7.info("Login succeeded", { type, userId: authResult.data.id });
2116
2492
  }
2117
2493
  return result;
2118
2494
  }
2119
- return { loginRegisteredUser: async (user) => {
2495
+ async function verifyCurrentSession(accessToken) {
2496
+ const session = await sessionFunctions.verifyToken(accessToken);
2497
+ if (!session.success)
2498
+ return session;
2499
+ const [user, authorization] = await Promise.all([
2500
+ userRepository.findById(session.data.userId),
2501
+ authzFunctions.getUserAuthorization(session.data.userId)
2502
+ ]);
2503
+ if (!user.success)
2504
+ return user;
2505
+ if (!user.data?.enabled)
2506
+ return err(HaiIamError.USER_DISABLED, iamM("iam_accountDisabled"));
2507
+ if (session.data.credentialVersion !== (user.data.credentialVersion ?? ""))
2508
+ return err(HaiIamError.SESSION_INVALID, iamM("iam_sessionExpired"));
2509
+ if (!authorization.success)
2510
+ return authorization;
2511
+ const current = { ...session.data, username: user.data.username, displayName: user.data.displayName, avatarUrl: user.data.avatarUrl, ...authorization.data };
2512
+ return ok(current.apiKeyId ? applyApiKeyScopes(current, current.apiKeyScopes ?? []) : current);
2513
+ }
2514
+ const tickets = createTicketOperations();
2515
+ const sensitive = config.reauthentication;
2516
+ async function requiresProof(operation) {
2517
+ if (!sensitive?.enabled)
2518
+ return ok(false);
2519
+ const configured = await authzFunctions.getSensitiveOperations();
2520
+ if (!configured.success)
2521
+ return configured;
2522
+ return ok(sensitive.operations.includes(operation) || configured.data.includes(operation));
2523
+ }
2524
+ return { loginRegisteredUser: async (user, context) => {
2120
2525
  const stored = await userRepository.findById(user.id);
2121
2526
  if (!stored.success)
2122
2527
  return stored;
2123
- if (!stored.data?.enabled)
2528
+ if (!stored.data || !isUserActive(stored.data))
2124
2529
  return err(HaiIamError.USER_DISABLED, iamM("iam_accountDisabled"));
2125
- return buildAuthResult({ ...user, credentialVersion: stored.data.credentialVersion ?? "" });
2530
+ return buildAuthResult({ ...user, credentialVersion: stored.data.credentialVersion ?? "" }, void 0, context);
2126
2531
  }, authn: {
2127
- async login(credentials) {
2128
- return loginWithStrategy("password", credentials, passwordStrategy);
2532
+ createCaptcha: (input) => createIamCaptcha(config.captcha, input),
2533
+ async reauthenticate(accessToken, password, operation) {
2534
+ const required = await requiresProof(operation);
2535
+ if (!required.success)
2536
+ return required;
2537
+ if (!required.data)
2538
+ return err(HaiIamError.PERMISSION_DENIED, iamM("iam_reauthenticationRequired"));
2539
+ const current = await verifyCurrentSession(accessToken);
2540
+ if (!current.success)
2541
+ return current;
2542
+ const stored = await userRepository.findById(current.data.userId);
2543
+ if (!stored.success)
2544
+ return stored;
2545
+ if (!stored.data)
2546
+ return err(HaiIamError.INVALID_CREDENTIALS, iamM("iam_passwordWrong"));
2547
+ const verified = await passwordStrategy.authenticate({ type: "password", identifier: stored.data.username, password });
2548
+ if (!verified.success)
2549
+ return verified;
2550
+ if (verified.data.credentialVersion !== current.data.credentialVersion)
2551
+ return err(HaiIamError.SESSION_INVALID, iamM("iam_sessionExpired"));
2552
+ return tickets.issue({ subjectId: current.data.userId, purpose: `iam:reauth:${operation}`, ttlMs: sensitive.ttlSeconds * 1e3, grant: { operation, sessionVersion: current.data.version, credentialVersion: current.data.credentialVersion } });
2553
+ },
2554
+ async checkSensitiveOperation(accessToken, operation, ticket) {
2555
+ const current = await verifyCurrentSession(accessToken);
2556
+ if (!current.success)
2557
+ return current;
2558
+ const required = await requiresProof(operation);
2559
+ if (!required.success)
2560
+ return required;
2561
+ if (!required.data)
2562
+ return ok(void 0);
2563
+ if (!ticket)
2564
+ return err(HaiIamError.REAUTHENTICATION_REQUIRED, iamM("iam_reauthenticationRequired"));
2565
+ const proof = await tickets.consume(ticket, { purpose: `iam:reauth:${operation}` });
2566
+ if (!proof.success)
2567
+ return proof;
2568
+ if (proof.data.subjectId !== current.data.userId || proof.data.grant.operation !== operation || proof.data.grant.sessionVersion !== current.data.version || proof.data.grant.credentialVersion !== current.data.credentialVersion)
2569
+ return err(HaiIamError.REAUTHENTICATION_REQUIRED, iamM("iam_reauthenticationRequired"));
2570
+ return ok(void 0);
2571
+ },
2572
+ async login(credentials, context) {
2573
+ return loginWithStrategy("password", credentials, passwordStrategy, context);
2129
2574
  },
2130
- async loginWithOtp(credentials) {
2131
- return loginWithStrategy("otp", credentials, otpStrategy);
2575
+ async loginWithOtp(credentials, context) {
2576
+ return loginWithStrategy("otp", credentials, otpStrategy, context);
2132
2577
  },
2133
- async loginWithLdap(credentials) {
2134
- return loginWithStrategy("ldap", credentials, ldapStrategy);
2578
+ async loginWithLdap(credentials, context) {
2579
+ return loginWithStrategy("ldap", credentials, ldapStrategy, context);
2135
2580
  },
2136
- async loginWithApiKey(credentials) {
2581
+ async loginWithApiKey(credentials, context) {
2137
2582
  const disabled = loginDisabled("apikey");
2138
2583
  if (disabled)
2139
2584
  return disabled;
@@ -2142,31 +2587,16 @@ function buildAuthnOperations(deps) {
2142
2587
  const authenticated = await apiKeyStrategy.authenticate({ type: "apikey", ...credentials });
2143
2588
  if (!authenticated.success)
2144
2589
  return authenticated;
2145
- return buildAuthResult(authenticated.data.user, authenticated.data.apiKey);
2590
+ return buildAuthResult(authenticated.data.user, authenticated.data.apiKey, context);
2146
2591
  },
2147
2592
  async logout(accessToken) {
2148
2593
  return sessionFunctions.delete(accessToken);
2149
2594
  },
2150
- async verifyToken(accessToken) {
2151
- const session = await sessionFunctions.verifyToken(accessToken);
2152
- if (!session.success)
2153
- return session;
2154
- const [user, authorization] = await Promise.all([
2155
- userRepository.findById(session.data.userId),
2156
- authzFunctions.getUserAuthorization(session.data.userId)
2157
- ]);
2158
- if (!user.success)
2159
- return user;
2160
- if (!user.data?.enabled)
2161
- return err(HaiIamError.USER_DISABLED, iamM("iam_accountDisabled"));
2162
- if (session.data.credentialVersion !== (user.data.credentialVersion ?? ""))
2163
- return err(HaiIamError.SESSION_INVALID, iamM("iam_sessionExpired"));
2164
- if (!authorization.success)
2165
- return authorization;
2166
- const current = { ...session.data, username: user.data.username, displayName: user.data.displayName, avatarUrl: user.data.avatarUrl, ...authorization.data };
2167
- return ok(current.apiKeyId ? applyApiKeyScopes(current, current.apiKeyScopes ?? []) : current);
2168
- },
2169
- async sendOtp(identifier) {
2595
+ verifyToken: verifyCurrentSession,
2596
+ async sendOtp(identifier, options) {
2597
+ const captcha = await verifyIamCaptcha(config.captcha, "sendOtp", options?.captcha);
2598
+ if (!captcha.success)
2599
+ return captcha;
2170
2600
  const disabled = loginDisabled("otp");
2171
2601
  if (disabled) {
2172
2602
  return disabled;
@@ -2183,6 +2613,7 @@ function buildAuthnOperations(deps) {
2183
2613
  }
2184
2614
  var TABLE_NAME3 = "hai_iam_permissions";
2185
2615
  var PERMISSION_FIELDS = [
2616
+ { fieldName: "sensitiveOperations", columnName: "sensitive_operations", def: { type: "JSON" }, select: true, create: true, update: true },
2186
2617
  {
2187
2618
  fieldName: "id",
2188
2619
  columnName: "id",
@@ -2272,6 +2703,27 @@ async function createDbPermissionRepository() {
2272
2703
  return repo;
2273
2704
  }
2274
2705
  var DbPermissionRepository = class extends BaseReldbCrudRepository {
2706
+ async getSensitiveOperations() {
2707
+ const result = await reldb.sql.query("SELECT sensitive_operations FROM hai_iam_permissions WHERE sensitive_operations IS NOT NULL");
2708
+ if (!result.success)
2709
+ return result;
2710
+ const operations = /* @__PURE__ */ new Set();
2711
+ try {
2712
+ for (const row of result.data) {
2713
+ const values = typeof row.sensitive_operations === "string" ? JSON.parse(row.sensitive_operations) : row.sensitive_operations;
2714
+ if (!Array.isArray(values) || values.length > 1e3)
2715
+ return err(HaiIamError.REPOSITORY_ERROR, iamM("iam_reauthenticationRequired"));
2716
+ for (const value of values) {
2717
+ if (typeof value !== "string" || !value.trim() || value.length > 128)
2718
+ return err(HaiIamError.REPOSITORY_ERROR, iamM("iam_reauthenticationRequired"));
2719
+ operations.add(value.trim());
2720
+ }
2721
+ }
2722
+ return ok([...operations].sort());
2723
+ } catch {
2724
+ return err(HaiIamError.REPOSITORY_ERROR, iamM("iam_reauthenticationRequired"));
2725
+ }
2726
+ }
2275
2727
  constructor() {
2276
2728
  super(reldb, {
2277
2729
  table: TABLE_NAME3,
@@ -2840,7 +3292,7 @@ var DbRoleRepository = class extends BaseReldbCrudRepository {
2840
3292
  };
2841
3293
 
2842
3294
  // src/authz/iam-authz-functions.ts
2843
- var logger6 = core.logger.child({ module: "iam", scope: "authz" });
3295
+ var logger8 = core.logger.child({ module: "iam", scope: "authz" });
2844
3296
  async function createAuthzOperations(deps) {
2845
3297
  try {
2846
3298
  const { config, session } = deps;
@@ -2862,10 +3314,10 @@ async function createAuthzOperations(deps) {
2862
3314
  userRoleRepository: userRoleResult.data,
2863
3315
  session
2864
3316
  });
2865
- logger6.info("Authz sub-feature initialized");
3317
+ logger8.info("Authz sub-feature initialized");
2866
3318
  return ok(manager);
2867
3319
  } catch (error) {
2868
- logger6.error("Authz sub-feature initialization failed", { error });
3320
+ logger8.error("Authz sub-feature initialization failed", { error });
2869
3321
  return err(
2870
3322
  HaiIamError.CONFIG_ERROR,
2871
3323
  iamM("iam_initComponentFailed"),
@@ -2905,8 +3357,8 @@ function createRbacManager(config) {
2905
3357
  return true;
2906
3358
  }
2907
3359
  if (code.endsWith(":*")) {
2908
- const prefix = code.slice(0, -1);
2909
- return permission.startsWith(prefix);
3360
+ const prefix2 = code.slice(0, -1);
3361
+ return permission.startsWith(prefix2);
2910
3362
  }
2911
3363
  return false;
2912
3364
  }
@@ -2961,19 +3413,19 @@ function createRbacManager(config) {
2961
3413
  async function syncSessionPermissionsForRole(roleId) {
2962
3414
  const userIdsResult = await userRoleRepository.getUserIdsByRoleId(roleId);
2963
3415
  if (!userIdsResult.success) {
2964
- logger6.error("Failed to query users for permission sync", { roleId, error: userIdsResult.error.message });
3416
+ logger8.error("Failed to query users for permission sync", { roleId, error: userIdsResult.error.message });
2965
3417
  return;
2966
3418
  }
2967
3419
  await Promise.allSettled(
2968
3420
  userIdsResult.data.map(async (userId) => {
2969
3421
  const permResult = await resolveUserPermissionCodes(userId);
2970
3422
  if (!permResult.success) {
2971
- logger6.error("Failed to resolve permissions for session sync", { userId, roleId, error: permResult.error.message });
3423
+ logger8.error("Failed to resolve permissions for session sync", { userId, roleId, error: permResult.error.message });
2972
3424
  return;
2973
3425
  }
2974
3426
  const syncResult = await session.patchUserSessions(userId, { permissions: permResult.data });
2975
3427
  if (!syncResult.success) {
2976
- logger6.error("Failed to sync session permissions", { userId, roleId, error: syncResult.error.message });
3428
+ logger8.error("Failed to sync session permissions", { userId, roleId, error: syncResult.error.message });
2977
3429
  }
2978
3430
  })
2979
3431
  );
@@ -2987,17 +3439,17 @@ function createRbacManager(config) {
2987
3439
  if (roleCodesResult.success) {
2988
3440
  updates.roles = roleCodesResult.data;
2989
3441
  } else {
2990
- logger6.error("Failed to resolve roles for session sync", { userId, error: roleCodesResult.error.message });
3442
+ logger8.error("Failed to resolve roles for session sync", { userId, error: roleCodesResult.error.message });
2991
3443
  }
2992
3444
  if (permCodesResult.success) {
2993
3445
  updates.permissions = permCodesResult.data;
2994
3446
  } else {
2995
- logger6.error("Failed to resolve permissions for session sync", { userId, error: permCodesResult.error.message });
3447
+ logger8.error("Failed to resolve permissions for session sync", { userId, error: permCodesResult.error.message });
2996
3448
  }
2997
3449
  if (updates.roles !== void 0 || updates.permissions !== void 0) {
2998
3450
  const syncResult = await session.patchUserSessions(userId, updates);
2999
3451
  if (!syncResult.success) {
3000
- logger6.error("Failed to patch user sessions", { userId, error: syncResult.error.message });
3452
+ logger8.error("Failed to patch user sessions", { userId, error: syncResult.error.message });
3001
3453
  }
3002
3454
  }
3003
3455
  }
@@ -3021,6 +3473,22 @@ function createRbacManager(config) {
3021
3473
  return ok(void 0);
3022
3474
  }
3023
3475
  return {
3476
+ getSensitiveOperations: () => rbacConfig.enabled ? permissionRepository.getSensitiveOperations() : Promise.resolve(ok([])),
3477
+ async setPermissionSensitiveOperations(permissionId, operations) {
3478
+ if (!Array.isArray(operations) || operations.length > 1e3 || operations.some((value) => typeof value !== "string" || !value.trim() || value.length > 128))
3479
+ return err(HaiIamError.INVALID_ARGUMENT, iamM("iam_sensitiveOperationsInvalid"));
3480
+ const updated = await permissionRepository.updateById(permissionId, { sensitiveOperations: [...new Set(operations.map((value) => value.trim()))] });
3481
+ if (!updated.success)
3482
+ return updated;
3483
+ if (updated.data.changes === 0) {
3484
+ const existing = await permissionRepository.findById(permissionId);
3485
+ if (!existing.success)
3486
+ return existing;
3487
+ if (!existing.data)
3488
+ return err(HaiIamError.PERMISSION_NOT_FOUND, iamM("iam_permissionNotExist"));
3489
+ }
3490
+ return ok(void 0);
3491
+ },
3024
3492
  async checkPermission(userId, permission) {
3025
3493
  if (!rbacConfig.enabled) {
3026
3494
  return ok(true);
@@ -3065,8 +3533,7 @@ function createRbacManager(config) {
3065
3533
  }
3066
3534
  const result = await userRoleRepository.assign(userId, roleId, tx);
3067
3535
  if (result.success) {
3068
- logger6.info("Role assigned to user", { userId, roleId });
3069
- void audit.log({ action: "role.assign", resource: "iam_user_role", resourceId: userId, details: { roleId } });
3536
+ logger8.info("Role assigned to user", { userId, roleId });
3070
3537
  if (!tx) {
3071
3538
  await syncUserSessionAfterRoleChange(userId);
3072
3539
  }
@@ -3076,8 +3543,7 @@ function createRbacManager(config) {
3076
3543
  async removeRole(userId, roleId, tx) {
3077
3544
  const result = await userRoleRepository.remove(userId, roleId, tx);
3078
3545
  if (result.success) {
3079
- logger6.info("Role removed from user", { userId, roleId });
3080
- void audit.log({ action: "role.remove", resource: "iam_user_role", resourceId: userId, details: { roleId } });
3546
+ logger8.info("Role removed from user", { userId, roleId });
3081
3547
  if (!tx) {
3082
3548
  await syncUserSessionAfterRoleChange(userId);
3083
3549
  }
@@ -3145,8 +3611,7 @@ function createRbacManager(config) {
3145
3611
  error
3146
3612
  );
3147
3613
  }
3148
- logger6.info("Roles synced for user", { userId, added: toAdd.length, removed: toRemove.length });
3149
- void audit.log({ action: "roles.sync", resource: "iam_user_role", resourceId: userId, details: { added: toAdd, removed: toRemove } });
3614
+ logger8.info("Roles synced for user", { userId, added: toAdd.length, removed: toRemove.length });
3150
3615
  if (ownTx) {
3151
3616
  await syncUserSessionAfterRoleChange(userId);
3152
3617
  }
@@ -3198,8 +3663,7 @@ function createRbacManager(config) {
3198
3663
  return mapRepositoryError2("iam_createRoleFailed", commitResult.error.message);
3199
3664
  }
3200
3665
  }
3201
- logger6.info("Role created", { roleId: createdResult.data.id, code: role.code });
3202
- void audit.helper.crud({ action: "create", resource: "iam_role", resourceId: createdResult.data.id, details: { code: role.code } });
3666
+ logger8.info("Role created", { roleId: createdResult.data.id, code: role.code });
3203
3667
  if (role.code === rbacConfig.superAdminRole) {
3204
3668
  superAdminRoleId = void 0;
3205
3669
  }
@@ -3296,10 +3760,9 @@ function createRbacManager(config) {
3296
3760
  affectedUsersResult.data.map((userId) => syncUserSessionAfterRoleChange(userId))
3297
3761
  );
3298
3762
  } else {
3299
- logger6.error("Failed to query affected users after updateRole", { roleId, error: affectedUsersResult.error.message });
3763
+ logger8.error("Failed to query affected users after updateRole", { roleId, error: affectedUsersResult.error.message });
3300
3764
  }
3301
3765
  }
3302
- void audit.helper.crud({ action: "update", resource: "iam_role", resourceId: roleId, details: data });
3303
3766
  return ok(updatedResult.data);
3304
3767
  } catch (error) {
3305
3768
  if (ownTx)
@@ -3372,12 +3835,15 @@ function createRbacManager(config) {
3372
3835
  );
3373
3836
  }
3374
3837
  superAdminRoleId = void 0;
3375
- logger6.info("Role deleted", { roleId });
3376
- void audit.helper.crud({ action: "delete", resource: "iam_role", resourceId: roleId });
3838
+ logger8.info("Role deleted", { roleId });
3377
3839
  return ok(void 0);
3378
3840
  },
3379
3841
  // ─── 权限管理 ───
3380
3842
  async createPermission(permission, tx) {
3843
+ const operations = permission.sensitiveOperations ?? [];
3844
+ if (!Array.isArray(operations) || operations.length > 1e3 || operations.some((value) => typeof value !== "string" || !value.trim() || value.length > 128))
3845
+ return err(HaiIamError.INVALID_ARGUMENT, iamM("iam_sensitiveOperationsInvalid"));
3846
+ permission = { ...permission, sensitiveOperations: [...new Set(operations.map((value) => value.trim()))] };
3381
3847
  const ownTx = !tx;
3382
3848
  if (!tx) {
3383
3849
  const txResult = await reldb.tx.begin();
@@ -3413,8 +3879,7 @@ function createRbacManager(config) {
3413
3879
  return mapRepositoryError2("iam_createPermissionFailed", commitResult.error.message);
3414
3880
  }
3415
3881
  }
3416
- logger6.info("Permission created", { permissionId: createdResult.data.id, code: permission.code });
3417
- void audit.helper.crud({ action: "create", resource: "iam_permission", resourceId: createdResult.data.id, details: { code: permission.code } });
3882
+ logger8.info("Permission created", { permissionId: createdResult.data.id, code: permission.code });
3418
3883
  return ok(createdResult.data);
3419
3884
  } catch (error) {
3420
3885
  if (ownTx)
@@ -3518,8 +3983,7 @@ function createRbacManager(config) {
3518
3983
  affectedRoleIds.map((roleId) => syncSessionPermissionsForRole(roleId))
3519
3984
  );
3520
3985
  }
3521
- logger6.info("Permission deleted", { permissionId });
3522
- void audit.helper.crud({ action: "delete", resource: "iam_permission", resourceId: permissionId });
3986
+ logger8.info("Permission deleted", { permissionId });
3523
3987
  return ok(void 0);
3524
3988
  },
3525
3989
  async assignPermissionToRole(roleId, permissionId, tx) {
@@ -3541,8 +4005,7 @@ function createRbacManager(config) {
3541
4005
  }
3542
4006
  const assignResult = await rolePermissionRepository.assign(roleId, permissionId, tx);
3543
4007
  if (assignResult.success) {
3544
- logger6.info("Permission assigned to role", { roleId, permissionId });
3545
- void audit.log({ action: "permission.assign", resource: "iam_role_permission", resourceId: roleId, details: { permissionId } });
4008
+ logger8.info("Permission assigned to role", { roleId, permissionId });
3546
4009
  if (!tx) {
3547
4010
  await syncSessionPermissionsForRole(roleId);
3548
4011
  }
@@ -3571,8 +4034,7 @@ function createRbacManager(config) {
3571
4034
  }
3572
4035
  const removeResult = await rolePermissionRepository.remove(roleId, permissionId, tx);
3573
4036
  if (removeResult.success) {
3574
- logger6.info("Permission removed from role", { roleId, permissionId });
3575
- void audit.log({ action: "permission.remove", resource: "iam_role_permission", resourceId: roleId, details: { permissionId } });
4037
+ logger8.info("Permission removed from role", { roleId, permissionId });
3576
4038
  if (!tx) {
3577
4039
  await syncSessionPermissionsForRole(roleId);
3578
4040
  }
@@ -3590,7 +4052,109 @@ function createRbacManager(config) {
3590
4052
  }
3591
4053
  };
3592
4054
  }
4055
+ var logger9 = core.logger.child({ module: "iam", scope: "audit" });
4056
+ async function observe(operation, event, tx) {
4057
+ const result = await operation();
4058
+ const fields = typeof event === "function" ? event(result) : event;
4059
+ if (!fields)
4060
+ return result;
4061
+ if (tx && !result.success) {
4062
+ logger9.warn("IAM transactional operation failed", { action: fields.action, resource: fields.resource, reasonCode: result.error.code });
4063
+ return result;
4064
+ }
4065
+ const recorded = await audit.capture({
4066
+ ...fields,
4067
+ category: "system",
4068
+ outcome: result.success ? "success" : [401, 403, 409, 423, 429].includes(result.error.httpStatus ?? 500) ? "denied" : "failure",
4069
+ details: { ...fields.details, ...!result.success ? { reasonCode: result.error.code } : {} }
4070
+ }, { tx });
4071
+ if (!recorded.success) {
4072
+ logger9.error("IAM event audit failed", { action: fields.action, resource: fields.resource, error: recorded.error });
4073
+ if (tx)
4074
+ return recorded;
4075
+ }
4076
+ return result;
4077
+ }
4078
+ function loginEvent(method, result, context) {
4079
+ return {
4080
+ action: "auth.login",
4081
+ resource: "iam.auth",
4082
+ details: { method },
4083
+ // 登录结果本身已完成鉴别,不把客户端提交的标识声明当成已验证主体。
4084
+ userId: result.success ? result.data.user.id : null,
4085
+ actorSnapshot: result.success ? { id: result.data.user.id, username: result.data.user.username, displayName: result.data.user.displayName, roles: result.data.roles } : null,
4086
+ ipAddress: context?.ip,
4087
+ userAgent: context?.userAgent?.slice(0, 4096),
4088
+ requestId: context?.requestId
4089
+ };
4090
+ }
4091
+ function observeAuth(operations) {
4092
+ return {
4093
+ ...operations,
4094
+ login: (credentials, context) => observe(() => operations.login(credentials, context), (result) => loginEvent("password", result, context)),
4095
+ loginWithOtp: (credentials, context) => observe(() => operations.loginWithOtp(credentials, context), (result) => loginEvent("otp", result, context)),
4096
+ loginWithLdap: (credentials, context) => observe(() => operations.loginWithLdap(credentials, context), (result) => loginEvent("ldap", result, context)),
4097
+ loginWithApiKey: (credentials, context) => observe(() => operations.loginWithApiKey(credentials, context), (result) => loginEvent("apikey", result, context)),
4098
+ registerAndLogin: (options, context) => observe(() => operations.registerAndLogin(options, context), (result) => loginEvent("registration", result, context)),
4099
+ logout: (token) => observe(() => operations.logout(token), { action: "auth.logout", resource: "iam.auth" }),
4100
+ sendOtp: (identifier, options) => observe(() => operations.sendOtp(identifier, options), { action: "otp.send", resource: "iam.auth" }),
4101
+ reauthenticate: (token, password, operation) => observe(() => operations.reauthenticate(token, password, operation), { action: "auth.reauthenticate", resource: "iam.auth", details: { operation } }),
4102
+ checkSensitiveOperation: (token, operation, ticket) => observe(() => operations.checkSensitiveOperation(token, operation, ticket), (result) => !result.success ? { action: "auth.sensitive-denied", resource: "iam.auth", details: { operation } } : void 0)
4103
+ };
4104
+ }
4105
+ function observeUsers(operations) {
4106
+ return {
4107
+ ...operations,
4108
+ register: (options) => observe(() => operations.register(options), (result) => ({ action: "user.register", resource: "iam.users", resourceId: result.success ? result.data.user.id : void 0 })),
4109
+ createUser: (options) => observe(() => operations.createUser(options), (result) => ({ action: "user.create", resource: "iam.users", resourceId: result.success ? result.data.user.id : void 0 })),
4110
+ inviteUser: (options) => observe(() => operations.inviteUser(options), (result) => ({ action: "user.invite", resource: "iam.users", resourceId: result.success ? result.data.user.id : void 0 })),
4111
+ updateUser: (id, data) => observe(() => operations.updateUser(id, data), { action: "user.update", resource: "iam.users", resourceId: id, details: { fields: Object.keys(data), ...data.accountStatus ? { accountStatus: data.accountStatus } : {}, ...data.accountType ? { accountType: data.accountType } : {} } }),
4112
+ updateCurrentUser: (token, data) => observe(() => operations.updateCurrentUser(token, data), (result) => ({ action: "user.profile", resource: "iam.users", resourceId: result.success ? result.data.id : void 0, details: { fields: Object.keys(data) } })),
4113
+ deleteUser: (id) => observe(() => operations.deleteUser(id), { action: "user.delete", resource: "iam.users", resourceId: id }),
4114
+ adminResetPassword: (id, password) => observe(() => operations.adminResetPassword(id, password), { action: "password.admin-reset", resource: "iam.users", resourceId: id }),
4115
+ changePassword: (id, previous, next) => observe(() => operations.changePassword(id, previous, next), { action: "password.change", resource: "iam.users", resourceId: id }),
4116
+ changeCurrentUserPassword: (token, previous, next) => observe(() => operations.changeCurrentUserPassword(token, previous, next), { action: "password.change", resource: "iam.users" }),
4117
+ renewPassword: (input) => observe(() => operations.renewPassword(input), { action: "password.renew", resource: "iam.users" }),
4118
+ requestPasswordReset: (identifier, options) => observe(() => operations.requestPasswordReset(identifier, options), { action: "password.reset-request", resource: "iam.users" }),
4119
+ confirmPasswordReset: (token, password) => observe(() => operations.confirmPasswordReset(token, password), { action: "password.reset-complete", resource: "iam.users" }),
4120
+ scanInactiveAccounts: () => observe(() => operations.scanInactiveAccounts(), (result) => result.success && result.data === 0 ? void 0 : { action: "account.inactivity-scan", resource: "iam.users", details: result.success ? { count: result.data } : {} })
4121
+ };
4122
+ }
4123
+ function observeAuthorization(operations) {
4124
+ return {
4125
+ ...operations,
4126
+ assignRole: (id, roleId, tx) => observe(() => operations.assignRole(id, roleId, tx), { action: "role.assign", resource: "iam.user-roles", resourceId: id, details: { roleId } }, tx),
4127
+ removeRole: (id, roleId, tx) => observe(() => operations.removeRole(id, roleId, tx), { action: "role.remove", resource: "iam.user-roles", resourceId: id, details: { roleId } }, tx),
4128
+ syncRoles: (id, roleIds, tx) => observe(() => operations.syncRoles(id, roleIds, tx), { action: "role.sync", resource: "iam.user-roles", resourceId: id, details: { roleIds } }, tx),
4129
+ createRole: (input, tx) => observe(() => operations.createRole(input, tx), (result) => ({ action: "role.create", resource: "iam.roles", resourceId: result.success ? result.data.id : void 0, details: { code: input.code } }), tx),
4130
+ updateRole: (id, data, tx) => observe(() => operations.updateRole(id, data, tx), { action: "role.update", resource: "iam.roles", resourceId: id, details: { fields: Object.keys(data), permissionIds: data.permissionIds } }, tx),
4131
+ deleteRole: (id, tx) => observe(() => operations.deleteRole(id, tx), { action: "role.delete", resource: "iam.roles", resourceId: id }, tx),
4132
+ createPermission: (input, tx) => observe(() => operations.createPermission(input, tx), (result) => ({ action: "permission.create", resource: "iam.permissions", resourceId: result.success ? result.data.id : void 0, details: { code: input.code } }), tx),
4133
+ setPermissionSensitiveOperations: (id, operationIds) => observe(() => operations.setPermissionSensitiveOperations(id, operationIds), { action: "permission.sensitive-operations", resource: "iam.permissions", resourceId: id, details: { operations: operationIds } }),
4134
+ deletePermission: (id, tx) => observe(() => operations.deletePermission(id, tx), { action: "permission.delete", resource: "iam.permissions", resourceId: id }, tx),
4135
+ assignPermissionToRole: (id, permissionId, tx) => observe(() => operations.assignPermissionToRole(id, permissionId, tx), { action: "permission.assign", resource: "iam.role-permissions", resourceId: id, details: { permissionId } }, tx),
4136
+ removePermissionFromRole: (id, permissionId, tx) => observe(() => operations.removePermissionFromRole(id, permissionId, tx), { action: "permission.remove", resource: "iam.role-permissions", resourceId: id, details: { permissionId } }, tx)
4137
+ };
4138
+ }
4139
+ function observeSessions(operations) {
4140
+ return {
4141
+ ...operations,
4142
+ create: (options) => observe(() => operations.create(options), (result) => ({ action: "session.create", resource: "iam.sessions", resourceId: result.success ? result.data.version : options.userId })),
4143
+ delete: (token) => observe(() => operations.delete(token), { action: "session.delete", resource: "iam.sessions" }),
4144
+ deleteByUserId: (id) => observe(() => operations.deleteByUserId(id), (result) => ({ action: "session.revoke-user", resource: "iam.sessions", resourceId: id, details: result.success ? { count: result.data } : {} })),
4145
+ refresh: (token) => observe(() => operations.refresh(token), { action: "session.refresh", resource: "iam.sessions" }),
4146
+ revokeRefresh: (token) => observe(() => operations.revokeRefresh(token), { action: "session.revoke-refresh", resource: "iam.sessions" })
4147
+ };
4148
+ }
4149
+ function observeApiKeys(operations) {
4150
+ return {
4151
+ ...operations,
4152
+ createApiKey: (userId, options) => observe(() => operations.createApiKey(userId, options), (result) => ({ action: "apikey.create", resource: "iam.apikeys", resourceId: result.success ? result.data.apiKey.id : void 0, details: { subjectId: userId, scopes: options.scopes } })),
4153
+ revokeApiKey: (id) => observe(() => operations.revokeApiKey(id), { action: "apikey.revoke", resource: "iam.apikeys", resourceId: id })
4154
+ };
4155
+ }
3593
4156
  var columns = { credential_version: "VARCHAR(255) NOT NULL DEFAULT ''", reset_version: "VARCHAR(255) NOT NULL DEFAULT ''", reset_hash: "TEXT", reset_expires_at: "BIGINT", reset_attempts: "INTEGER NOT NULL DEFAULT 0" };
4157
+ var accountColumns = { account_type: "VARCHAR(16) NOT NULL DEFAULT 'permanent'", account_status: "VARCHAR(16) NOT NULL DEFAULT 'active'", account_expires_at: "TIMESTAMP", last_login_at: "TIMESTAMP" };
3594
4158
  var steps = [{
3595
4159
  version: 1,
3596
4160
  name: "credential-and-reset-versions",
@@ -3611,6 +4175,59 @@ var steps = [{
3611
4175
  }
3612
4176
  return ok(void 0);
3613
4177
  }
4178
+ }, {
4179
+ version: 2,
4180
+ name: "account-lifecycle",
4181
+ async needed(sql) {
4182
+ const found = await schemaColumns(reldb, "hai_iam_users", sql);
4183
+ return found.success ? ok(found.data.length > 0 && Object.keys(accountColumns).some((name) => !found.data.includes(name))) : found;
4184
+ },
4185
+ async apply(sql) {
4186
+ const found = await schemaColumns(reldb, "hai_iam_users", sql);
4187
+ if (!found.success)
4188
+ return found;
4189
+ for (const [name, definition] of Object.entries(accountColumns)) {
4190
+ if (found.data.includes(name))
4191
+ continue;
4192
+ const added = await sql.execute(`ALTER TABLE hai_iam_users ADD COLUMN ${name} ${definition}`);
4193
+ if (!added.success)
4194
+ return added;
4195
+ }
4196
+ return ok(void 0);
4197
+ }
4198
+ }, {
4199
+ version: 3,
4200
+ name: "initial-password-state",
4201
+ async needed(sql) {
4202
+ const found = await schemaColumns(reldb, "hai_iam_users", sql);
4203
+ return found.success ? ok(found.data.length > 0 && !found.data.includes("must_change_password")) : found;
4204
+ },
4205
+ async apply(sql) {
4206
+ const added = await sql.execute("ALTER TABLE hai_iam_users ADD COLUMN must_change_password BOOLEAN NOT NULL DEFAULT FALSE");
4207
+ return added.success ? ok(void 0) : added;
4208
+ }
4209
+ }, {
4210
+ version: 4,
4211
+ name: "login-failure-window",
4212
+ async needed(sql) {
4213
+ const found = await schemaColumns(reldb, "hai_iam_users", sql);
4214
+ return found.success ? ok(found.data.length > 0 && !found.data.includes("first_login_failed_at")) : found;
4215
+ },
4216
+ async apply(sql) {
4217
+ const added = await sql.execute("ALTER TABLE hai_iam_users ADD COLUMN first_login_failed_at TIMESTAMP");
4218
+ return added.success ? ok(void 0) : added;
4219
+ }
4220
+ }, {
4221
+ version: 5,
4222
+ name: "permission-sensitive-operations",
4223
+ async needed(sql) {
4224
+ const found = await schemaColumns(reldb, "hai_iam_permissions", sql);
4225
+ return found.success ? ok(found.data.length > 0 && !found.data.includes("sensitive_operations")) : found;
4226
+ },
4227
+ async apply(sql) {
4228
+ const added = await sql.execute("ALTER TABLE hai_iam_permissions ADD COLUMN sensitive_operations JSON");
4229
+ return added.success ? ok(void 0) : added;
4230
+ }
3614
4231
  }];
3615
4232
  function migrateIam(options = {}) {
3616
4233
  return migrateSchema(reldb, "iam", steps, options);
@@ -3658,9 +4275,11 @@ async function resolveIamNotifications(input) {
3658
4275
  // src/iam-public-config.ts
3659
4276
  function buildIamPublicConfig(config, runtime) {
3660
4277
  const otp = OtpConfigSchema.parse(config.otp ?? {});
4278
+ const session = SessionConfigSchema.parse(config.session ?? {});
3661
4279
  const email = config.login.otp && !!config.otp && otp.email && !!runtime.onOtpSendEmail;
3662
4280
  const sms = config.login.otp && !!config.otp && otp.sms && !!runtime.onOtpSendSms;
3663
4281
  return {
4282
+ captcha: { enabled: config.captcha.enabled },
3664
4283
  password: PasswordConfigSchema.parse(config.password ?? {}),
3665
4284
  register: { enabled: config.register.enabled },
3666
4285
  login: { password: config.login.password, otp: email || sms, ldap: config.login.ldap && !!config.ldap, email, sms },
@@ -3668,10 +4287,12 @@ function buildIamPublicConfig(config, runtime) {
3668
4287
  otp: { length: otp.length, resendInterval: otp.resendInterval },
3669
4288
  agreements: { ...config.agreements },
3670
4289
  ui: AuthUiConfigSchema.parse(config.ui ?? {}),
3671
- session: { maxAge: SessionConfigSchema.parse(config.session ?? {}).maxAge }
4290
+ session: { maxAge: session.maxAge, endOnPageHide: session.endOnPageHide },
4291
+ reauthentication: { enabled: config.reauthentication?.enabled ?? false, operations: [...config.reauthentication?.operations ?? []] },
4292
+ initialCredentials: { enabled: Boolean(config.initialCredentials?.enabled && config.passwordReset?.enabled !== false && runtime.onPasswordResetRequest) }
3672
4293
  };
3673
4294
  }
3674
- var logger7 = core.logger.child({ module: "iam", scope: "seed" });
4295
+ var logger10 = core.logger.child({ module: "iam", scope: "seed" });
3675
4296
  var DEFAULT_ROLES = [
3676
4297
  { code: "admin", name: () => iamM("iam_seedRoleAdminName"), description: () => iamM("iam_seedRoleAdminDesc"), isSystem: true },
3677
4298
  { code: "user", name: () => iamM("iam_seedRoleUserName"), description: () => iamM("iam_seedRoleUserDesc"), isSystem: true },
@@ -3771,10 +4392,10 @@ async function seedIamData(authz) {
3771
4392
  }
3772
4393
  }
3773
4394
  }
3774
- logger7.info("IAM seed data initialized");
4395
+ logger10.info("IAM seed data initialized");
3775
4396
  return ok(void 0);
3776
4397
  } catch (error) {
3777
- logger7.error("Failed to seed IAM data", { error });
4398
+ logger10.error("Failed to seed IAM data", { error });
3778
4399
  return err(
3779
4400
  HaiIamError.REPOSITORY_ERROR,
3780
4401
  iamM("iam_initSeedDataFailed"),
@@ -3782,81 +4403,12 @@ async function seedIamData(authz) {
3782
4403
  );
3783
4404
  }
3784
4405
  }
3785
- var BASE64_PLUS_REGEX = /\+/g;
3786
- var BASE64_SLASH_REGEX = /\//g;
3787
- var BASE64_TRAILING_PADDING_REGEX = /=+$/;
3788
- function generateToken() {
3789
- const bytes = new Uint8Array(32);
3790
- crypto.getRandomValues(bytes);
3791
- const base64 = btoa(String.fromCharCode(...bytes));
3792
- return base64.replace(BASE64_PLUS_REGEX, "-").replace(BASE64_SLASH_REGEX, "_").replace(BASE64_TRAILING_PADDING_REGEX, "");
3793
- }
3794
- function getUserSessionScope(userId) {
3795
- return createHash("sha256").update(userId).digest("hex");
3796
- }
3797
- function generateSessionToken(userId) {
3798
- return `s1.${getUserSessionScope(userId)}.${generateToken()}`;
3799
- }
3800
- function getTokenSessionScope(token) {
3801
- return /^s1\.([a-f0-9]{64})\.[\w-]{43}$/.exec(token)?.[1];
3802
- }
3803
- function buildSession(options, now, sessionTtl, accessToken, credentialVersion) {
3804
- return {
3805
- version: crypto.randomUUID(),
3806
- credentialVersion,
3807
- apiKeyId: options.apiKeyId,
3808
- apiKeyScopes: options.apiKeyScopes,
3809
- userId: options.userId,
3810
- username: options.username,
3811
- displayName: options.displayName,
3812
- avatarUrl: options.avatarUrl,
3813
- roles: options.roles ?? [],
3814
- permissions: options.permissions ?? [],
3815
- source: options.source,
3816
- accessToken,
3817
- createdAt: now,
3818
- lastActiveAt: now,
3819
- expiresAt: new Date(now.getTime() + sessionTtl * 1e3),
3820
- maxAge: sessionTtl,
3821
- data: options.data
3822
- };
3823
- }
3824
- function applySessionPatch(session, patch) {
3825
- const nextSession = { ...session };
3826
- if (patch.data !== void 0) {
3827
- nextSession.data = { ...nextSession.data, ...patch.data };
3828
- }
3829
- if (patch.roles !== void 0) {
3830
- nextSession.roles = patch.roles;
3831
- }
3832
- if (patch.permissions !== void 0) {
3833
- nextSession.permissions = patch.permissions;
3834
- }
3835
- if (patch.username !== void 0) {
3836
- nextSession.username = patch.username;
3837
- }
3838
- if (patch.displayName !== void 0) {
3839
- nextSession.displayName = patch.displayName;
3840
- }
3841
- if (patch.avatarUrl !== void 0) {
3842
- nextSession.avatarUrl = patch.avatarUrl;
3843
- }
3844
- if (patch.source !== void 0) {
3845
- nextSession.source = patch.source;
3846
- }
3847
- if (patch.expiresAt !== void 0)
3848
- nextSession.expiresAt = new Date(patch.expiresAt);
3849
- nextSession.lastActiveAt = patch.lastActiveAt !== void 0 ? new Date(patch.lastActiveAt) : /* @__PURE__ */ new Date();
3850
- return nextSession;
3851
- }
3852
-
3853
- // src/session/iam-session-repository-cache.ts
3854
4406
  function retainedUntil(entry) {
3855
4407
  return Math.max(new Date(entry.session.expiresAt).getTime(), entry.refreshToken ? entry.refreshExpiresAt : 0);
3856
4408
  }
3857
4409
  function restoreSession(session) {
3858
- const result = { ...session, createdAt: new Date(session.createdAt), lastActiveAt: new Date(session.lastActiveAt), expiresAt: new Date(session.expiresAt) };
3859
- if (typeof result.credentialVersion !== "string" || !result.version || !Number.isSafeInteger(result.maxAge) || result.maxAge <= 0 || !Array.isArray(result.roles) || !result.roles.every((role) => typeof role === "string") || !Array.isArray(result.permissions) || !result.permissions.every((permission) => typeof permission === "string") || ![result.createdAt, result.lastActiveAt, result.expiresAt].every((date) => Number.isFinite(date.getTime()))) {
4410
+ const result = { ...session, createdAt: new Date(session.createdAt), startedAt: new Date(session.startedAt ?? session.createdAt), lastActiveAt: new Date(session.lastActiveAt), expiresAt: new Date(session.expiresAt) };
4411
+ if (result.terminalType !== void 0 && !["web", "app", "desktop", "api"].includes(result.terminalType) || typeof result.credentialVersion !== "string" || !result.version || !Number.isSafeInteger(result.maxAge) || result.maxAge <= 0 || !Array.isArray(result.roles) || !result.roles.every((role) => typeof role === "string") || !Array.isArray(result.permissions) || !result.permissions.every((permission) => typeof permission === "string") || ![result.createdAt, result.startedAt, result.lastActiveAt, result.expiresAt].every((date) => Number.isFinite(date.getTime()))) {
3860
4412
  throw new TypeError("Invalid session state");
3861
4413
  }
3862
4414
  return result;
@@ -3867,7 +4419,16 @@ function stateKey(scope) {
3867
4419
  function repositoryFailure(cause) {
3868
4420
  return err(HaiIamError.REPOSITORY_ERROR, iamM("iam_sessionStateFailed"), cause);
3869
4421
  }
3870
- function createCacheSessionRepository(refreshTokenMaxAge) {
4422
+ function createCacheSessionRepository(refreshTokenMaxAge, policy = {}) {
4423
+ const idleTimeout = policy.idleTimeout ?? 0;
4424
+ const absoluteTimeout = policy.absoluteTimeout ?? 0;
4425
+ const maxSessions = policy.maxSessions ?? 0;
4426
+ function policyExpired(session) {
4427
+ const now = Date.now();
4428
+ const activeAt = new Date(session.lastActiveAt).getTime();
4429
+ const startedAt = new Date(session.startedAt ?? session.createdAt).getTime();
4430
+ return idleTimeout > 0 && activeAt + idleTimeout * 1e3 <= now || absoluteTimeout > 0 && startedAt + absoluteTimeout * 1e3 <= now;
4431
+ }
3871
4432
  async function read(scope) {
3872
4433
  try {
3873
4434
  const result = await cache.kv.get(stateKey(scope));
@@ -3897,7 +4458,7 @@ function createCacheSessionRepository(refreshTokenMaxAge) {
3897
4458
  return current;
3898
4459
  const next = current.data ? JSON.parse(JSON.stringify(current.data)) : { revision: "", sessions: {} };
3899
4460
  for (const [token, entry] of Object.entries(next.sessions)) {
3900
- if (retainedUntil(entry) <= Date.now())
4461
+ if (retainedUntil(entry) <= Date.now() || policyExpired(entry.session))
3901
4462
  delete next.sessions[token];
3902
4463
  }
3903
4464
  const outcome = operation(next);
@@ -3926,16 +4487,40 @@ function createCacheSessionRepository(refreshTokenMaxAge) {
3926
4487
  }
3927
4488
  return {
3928
4489
  save: (session, tokens, singleDevice = false) => mutate(getUserSessionScope(session.userId), (state) => {
3929
- if (singleDevice)
3930
- state.sessions = {};
4490
+ const active = Object.entries(state.sessions).filter(([, entry]) => entry.session.credentialVersion === session.credentialVersion);
4491
+ const terminal = session.terminalType ?? "web";
4492
+ const limits = policy.terminalLimits ?? {};
4493
+ const cap = limits[terminal];
4494
+ if (Object.keys(limits).length && cap === void 0)
4495
+ return err(HaiIamError.SESSION_LIMIT_EXCEEDED, iamM("iam_sessionLimitExceeded"));
4496
+ const removed = /* @__PURE__ */ new Set();
4497
+ if (singleDevice) {
4498
+ if (policy.singleDeviceMode === "reject" && active.length)
4499
+ return err(HaiIamError.SESSION_LIMIT_EXCEEDED, iamM("iam_sessionLimitExceeded"));
4500
+ if (policy.singleDeviceMode !== "reject")
4501
+ Object.keys(state.sessions).forEach((token) => removed.add(token));
4502
+ } else if (cap) {
4503
+ const same = active.filter(([, entry]) => (entry.session.terminalType ?? "web") === terminal).sort(([firstToken, first], [secondToken, second]) => new Date(first.session.startedAt ?? first.session.createdAt).getTime() - new Date(second.session.startedAt ?? second.session.createdAt).getTime() || firstToken.localeCompare(secondToken));
4504
+ if (same.length >= cap) {
4505
+ if (policy.terminalLimitMode !== "replace")
4506
+ return err(HaiIamError.SESSION_LIMIT_EXCEEDED, iamM("iam_sessionLimitExceeded"));
4507
+ same.slice(0, same.length - cap + 1).forEach(([token]) => removed.add(token));
4508
+ }
4509
+ }
4510
+ if (maxSessions > 0 && active.filter(([token]) => !removed.has(token)).length >= maxSessions)
4511
+ return err(HaiIamError.SESSION_LIMIT_EXCEEDED, iamM("iam_sessionLimitExceeded"));
4512
+ removed.forEach((token) => delete state.sessions[token]);
3931
4513
  return storeSession(state, session, tokens);
3932
4514
  }),
3933
4515
  rotate: (previous, refreshToken, session, tokens) => mutate(getUserSessionScope(previous.userId), (state) => {
3934
4516
  const old = state.sessions[previous.accessToken];
3935
- if (!old || old.refreshToken !== refreshToken || old.refreshExpiresAt <= Date.now())
4517
+ if (!old || old.refreshToken !== refreshToken || old.refreshExpiresAt <= Date.now() || policyExpired(old.session))
3936
4518
  return err(HaiIamError.TOKEN_EXPIRED, iamM("iam_refreshTokenExpired"));
3937
4519
  if (old.session.version !== previous.version || session.userId !== previous.userId)
3938
4520
  return err(HaiIamError.CONFLICT, iamM("iam_sessionStateConflict"));
4521
+ session.startedAt = new Date(old.session.startedAt ?? old.session.createdAt);
4522
+ if (idleTimeout > 0)
4523
+ session.lastActiveAt = new Date(old.session.lastActiveAt);
3939
4524
  delete state.sessions[previous.accessToken];
3940
4525
  return storeSession(state, session, tokens);
3941
4526
  }),
@@ -3950,7 +4535,7 @@ function createCacheSessionRepository(refreshTokenMaxAge) {
3950
4535
  if (!record)
3951
4536
  return ok(null);
3952
4537
  const session = restoreSession(record.session);
3953
- return ok(session.expiresAt.getTime() <= Date.now() ? null : session);
4538
+ return ok(session.expiresAt.getTime() <= Date.now() || policyExpired(session) ? null : session);
3954
4539
  },
3955
4540
  async getByRefreshToken(token) {
3956
4541
  const scope = getTokenSessionScope(token);
@@ -3962,7 +4547,8 @@ function createCacheSessionRepository(refreshTokenMaxAge) {
3962
4547
  const record = Object.values(result.data?.sessions ?? {}).find((entry) => entry.refreshToken === token && entry.refreshExpiresAt > Date.now());
3963
4548
  if (!record)
3964
4549
  return ok(null);
3965
- return ok(restoreSession(record.session));
4550
+ const session = restoreSession(record.session);
4551
+ return ok(policyExpired(session) ? null : session);
3966
4552
  },
3967
4553
  async updateByAccessToken(token, patch, expectedVersion) {
3968
4554
  const scope = getTokenSessionScope(token);
@@ -3970,10 +4556,12 @@ function createCacheSessionRepository(refreshTokenMaxAge) {
3970
4556
  return err(HaiIamError.SESSION_NOT_FOUND, iamM("iam_sessionNotExist"));
3971
4557
  return mutate(scope, (state) => {
3972
4558
  const entry = state.sessions[token];
3973
- if (!entry || new Date(entry.session.expiresAt).getTime() <= Date.now())
4559
+ if (!entry || new Date(entry.session.expiresAt).getTime() <= Date.now() || policyExpired(entry.session))
3974
4560
  return err(HaiIamError.SESSION_NOT_FOUND, iamM("iam_sessionNotExist"));
3975
4561
  if (!expectedVersion || entry.session.version !== expectedVersion)
3976
4562
  return err(HaiIamError.CONFLICT, iamM("iam_sessionStateConflict"));
4563
+ if (patch.terminalType !== void 0 && patch.terminalType !== (entry.session.terminalType ?? "web"))
4564
+ return err(HaiIamError.CONFLICT, iamM("iam_sessionStateConflict"));
3977
4565
  const updated = applySessionPatch(restoreSession(entry.session), patch);
3978
4566
  if (![updated.expiresAt, updated.lastActiveAt].every((date) => Number.isFinite(date.getTime())))
3979
4567
  return err(HaiIamError.INVALID_ARGUMENT, iamM("iam_sessionMaxAgeInvalid"));
@@ -3987,7 +4575,7 @@ function createCacheSessionRepository(refreshTokenMaxAge) {
3987
4575
  return err(HaiIamError.SESSION_NOT_FOUND, iamM("iam_sessionNotExist"));
3988
4576
  return mutate(scope, (state) => {
3989
4577
  const entry = state.sessions[token];
3990
- if (!entry || new Date(entry.session.expiresAt).getTime() <= Date.now())
4578
+ if (!entry || new Date(entry.session.expiresAt).getTime() <= Date.now() || policyExpired(entry.session))
3991
4579
  return err(HaiIamError.SESSION_NOT_FOUND, iamM("iam_sessionNotExist"));
3992
4580
  if (entry.session.version !== expectedVersion)
3993
4581
  return err(HaiIamError.CONFLICT, iamM("iam_sessionStateConflict"));
@@ -3995,7 +4583,7 @@ function createCacheSessionRepository(refreshTokenMaxAge) {
3995
4583
  const next = {
3996
4584
  ...current,
3997
4585
  lastActiveAt: new Date(Math.max(current.lastActiveAt.getTime(), now.getTime())),
3998
- expiresAt: new Date(Math.max(current.expiresAt.getTime(), now.getTime() + current.maxAge * 1e3))
4586
+ expiresAt: policy.sliding === false ? current.expiresAt : new Date(Math.max(current.expiresAt.getTime(), now.getTime() + current.maxAge * 1e3))
3999
4587
  };
4000
4588
  entry.session = next;
4001
4589
  return ok(next);
@@ -4053,26 +4641,28 @@ function stripInternalSessionFields(session) {
4053
4641
  const hasExtraKeys = Object.keys(rest).length > 0;
4054
4642
  return { ...session, data: hasExtraKeys ? rest : void 0 };
4055
4643
  }
4056
- var logger8 = core.logger.child({ module: "iam", scope: "session" });
4644
+ var logger11 = core.logger.child({ module: "iam", scope: "session" });
4057
4645
  async function createSessionOperations(deps) {
4058
4646
  try {
4059
4647
  const { config } = deps;
4060
4648
  const sessionConfig = SessionConfigSchema.parse(config.session ?? {});
4061
4649
  const sessionRepository = createCacheSessionRepository(
4062
- sessionConfig.refreshTokenMaxAge ?? 604800
4650
+ sessionConfig.refreshTokenMaxAge ?? 604800,
4651
+ sessionConfig
4063
4652
  );
4064
4653
  const functions = buildSessionFunctions({
4065
4654
  readCredential: deps.readCredential,
4066
4655
  maxAge: sessionConfig.maxAge,
4067
4656
  sliding: sessionConfig.sliding,
4068
4657
  singleDevice: sessionConfig.singleDevice,
4658
+ idleTimeout: sessionConfig.idleTimeout,
4069
4659
  sessionRepository,
4070
4660
  apiKeyRepository: config.login.apikey ? await createDbApiKeyRepository() : void 0
4071
4661
  });
4072
- logger8.info("Session sub-feature initialized");
4662
+ logger11.info("Session sub-feature initialized");
4073
4663
  return ok(functions);
4074
4664
  } catch (error) {
4075
- logger8.error("Session sub-feature initialization failed", { error });
4665
+ logger11.error("Session sub-feature initialization failed", { error });
4076
4666
  return err(
4077
4667
  HaiIamError.CONFIG_ERROR,
4078
4668
  iamM("iam_initComponentFailed"),
@@ -4086,6 +4676,8 @@ function buildSessionFunctions(config) {
4086
4676
  const singleDevice = config.singleDevice ?? false;
4087
4677
  const repo = config.sessionRepository;
4088
4678
  function candidate(options, credentialVersion) {
4679
+ if (options.terminalType !== void 0 && !["web", "app", "desktop", "api"].includes(options.terminalType))
4680
+ return err(HaiIamError.INVALID_ARGUMENT, iamM("iam_terminalTypeInvalid"));
4089
4681
  const now = /* @__PURE__ */ new Date();
4090
4682
  const ttl = options.maxAge ?? maxAge;
4091
4683
  if (!Number.isSafeInteger(ttl) || ttl <= 0 || !Number.isFinite(new Date(now.getTime() + ttl * 1e3).getTime()))
@@ -4141,7 +4733,7 @@ function buildSessionFunctions(config) {
4141
4733
  if (!saveResult.success) {
4142
4734
  return saveResult;
4143
4735
  }
4144
- logger8.debug("Session created", { userId: options.userId });
4736
+ logger11.debug("Session created", { userId: options.userId });
4145
4737
  return ok(session);
4146
4738
  } catch (error) {
4147
4739
  return err(
@@ -4176,7 +4768,7 @@ function buildSessionFunctions(config) {
4176
4768
  if (!validated.success)
4177
4769
  return validated;
4178
4770
  let output = validated.data;
4179
- if (sliding) {
4771
+ if (sliding || (config.idleTimeout ?? 0) > 0) {
4180
4772
  const updateResult = await repo.touchByAccessToken(accessToken, session.version, /* @__PURE__ */ new Date());
4181
4773
  if (!updateResult.success) {
4182
4774
  return updateResult;
@@ -4202,7 +4794,7 @@ function buildSessionFunctions(config) {
4202
4794
  return repo.updateByAccessToken(accessToken, data, expectedVersion);
4203
4795
  },
4204
4796
  async delete(accessToken) {
4205
- logger8.debug("Session delete requested");
4797
+ logger11.debug("Session delete requested");
4206
4798
  return repo.removeByAccessToken(accessToken);
4207
4799
  },
4208
4800
  async deleteByUserId(userId) {
@@ -4244,6 +4836,7 @@ function buildSessionFunctions(config) {
4244
4836
  apiKeyId: effectiveSession.apiKeyId,
4245
4837
  apiKeyScopes: effectiveSession.apiKeyScopes,
4246
4838
  source: oldSession.source,
4839
+ terminalType: oldSession.terminalType ?? "web",
4247
4840
  maxAge: oldSession.maxAge,
4248
4841
  data: oldSession.data ? { ...oldSession.data, _tokenPair: void 0 } : void 0
4249
4842
  }, oldSession.credentialVersion);
@@ -4255,7 +4848,7 @@ function buildSessionFunctions(config) {
4255
4848
  const current = await credential(oldSession.userId, oldSession.credentialVersion);
4256
4849
  if (!current.success)
4257
4850
  return current;
4258
- logger8.debug("Token refreshed", { userId: oldSession.userId });
4851
+ logger11.debug("Token refreshed", { userId: oldSession.userId });
4259
4852
  return ok(next.data.tokens);
4260
4853
  } catch (error) {
4261
4854
  return err(
@@ -4273,63 +4866,7 @@ function buildSessionFunctions(config) {
4273
4866
  }
4274
4867
  };
4275
4868
  }
4276
- var logger9 = core.logger.child({ module: "iam", scope: "ticket" });
4277
- var TICKET_KEY_PREFIX = "hai:iam:ticket:";
4278
- var DEFAULT_TICKET_TTL_MS = 3e4;
4279
- function buildTicketKey(ticket) {
4280
- return `${TICKET_KEY_PREFIX}${ticket}`;
4281
- }
4282
- function createTicketOperations() {
4283
- return {
4284
- async issue(options) {
4285
- const ttlMs = options.ttlMs !== void 0 && options.ttlMs > 0 ? options.ttlMs : DEFAULT_TICKET_TTL_MS;
4286
- const ticket = generateToken();
4287
- const expiresAt = Date.now() + ttlMs;
4288
- const record = {
4289
- subjectId: options.subjectId,
4290
- purpose: options.purpose,
4291
- grant: options.grant ?? {},
4292
- expiresAt
4293
- };
4294
- const saved = await cache.kv.set(buildTicketKey(ticket), record, { px: ttlMs, nx: true });
4295
- if (!saved.success || !saved.data) {
4296
- logger9.warn("Failed to issue ticket", { purpose: options.purpose });
4297
- return err(HaiIamError.TICKET_ISSUE_FAILED, iamM("iam_ticketIssueFailed"));
4298
- }
4299
- return ok({ ticket, expiresAt });
4300
- },
4301
- async consume(ticket, options) {
4302
- if (!ticket) {
4303
- return err(HaiIamError.TICKET_INVALID, iamM("iam_ticketInvalid"));
4304
- }
4305
- const key = buildTicketKey(ticket);
4306
- const found = await cache.kv.get(key);
4307
- if (!found.success) {
4308
- return err(HaiIamError.TICKET_ISSUE_FAILED, iamM("iam_ticketIssueFailed"));
4309
- }
4310
- const record = found.data;
4311
- if (!record) {
4312
- return err(HaiIamError.TICKET_INVALID, iamM("iam_ticketInvalid"));
4313
- }
4314
- const removed = await cache.kv.del(key);
4315
- if (!removed.success || removed.data !== 1) {
4316
- return err(HaiIamError.TICKET_INVALID, iamM("iam_ticketInvalid"));
4317
- }
4318
- if (record.expiresAt <= Date.now()) {
4319
- return err(HaiIamError.TICKET_EXPIRED, iamM("iam_ticketExpired"));
4320
- }
4321
- if (options?.purpose !== void 0 && options.purpose !== record.purpose) {
4322
- return err(HaiIamError.TICKET_INVALID, iamM("iam_ticketInvalid"));
4323
- }
4324
- return ok({
4325
- subjectId: record.subjectId,
4326
- purpose: record.purpose,
4327
- grant: record.grant
4328
- });
4329
- }
4330
- };
4331
- }
4332
- var logger10 = core.logger.child({ module: "iam", scope: "user" });
4869
+ var logger12 = core.logger.child({ module: "iam", scope: "user" });
4333
4870
  var USER_SORT_COLUMNS = {
4334
4871
  /** 用户名列。 */
4335
4872
  username: "username",
@@ -4354,10 +4891,10 @@ async function createUserOperations(deps) {
4354
4891
  config,
4355
4892
  onPasswordResetRequest
4356
4893
  });
4357
- logger10.info("User sub-feature initialized");
4894
+ logger12.info("User sub-feature initialized");
4358
4895
  return ok(functions);
4359
4896
  } catch (error) {
4360
- logger10.error("User sub-feature initialization failed", { error });
4897
+ logger12.error("User sub-feature initialization failed", { error });
4361
4898
  return err(
4362
4899
  HaiIamError.CONFIG_ERROR,
4363
4900
  iamM("iam_initComponentFailed"),
@@ -4420,10 +4957,26 @@ async function validateUniqueFieldsForUpdate(userRepository, userId, data) {
4420
4957
  }
4421
4958
  return ok(void 0);
4422
4959
  }
4960
+ function validatePasswordReplacement(ctx, user, password) {
4961
+ const strength = ctx.validatePassword(password);
4962
+ if (!strength.success)
4963
+ return strength;
4964
+ if (user.passwordHash && (user.mustChangePassword && ctx.config.password?.requireInitialChange || ctx.config.password?.preventSamePassword || (ctx.config.password?.expirationDays ?? 0) > 0)) {
4965
+ const same = crypto$1.password.verify(password, user.passwordHash);
4966
+ if (!same.success)
4967
+ return same;
4968
+ if (same.data)
4969
+ return err(HaiIamError.PASSWORD_POLICY_VIOLATION, iamM("iam_passwordMustDiffer"));
4970
+ }
4971
+ return ok(void 0);
4972
+ }
4423
4973
  function buildRegistrationOps(ctx) {
4424
4974
  const { userRepository, authzFunctions, config, registerConfig, agreementConfig } = ctx;
4425
4975
  const { validatePassword, hashPassword } = ctx;
4426
4976
  async function validateRegisterPreconditions(options) {
4977
+ const accountPolicy = validateAccountPolicy(options);
4978
+ if (!accountPolicy.success)
4979
+ return accountPolicy;
4427
4980
  const validateResult = validatePassword(options.password);
4428
4981
  if (!validateResult.success)
4429
4982
  return validateResult;
@@ -4459,7 +5012,7 @@ function buildRegistrationOps(ctx) {
4459
5012
  return err(HaiIamError.ROLE_NOT_FOUND, iamM("iam_roleNotExist"));
4460
5013
  return authzFunctions.assignRole(userId, roleResult.data.id);
4461
5014
  }
4462
- async function createUser(options, enabled) {
5015
+ async function createUser(options, enabled, managed = false) {
4463
5016
  const preResult = await validateRegisterPreconditions(options);
4464
5017
  if (!preResult.success)
4465
5018
  return preResult;
@@ -4477,6 +5030,10 @@ function buildRegistrationOps(ctx) {
4477
5030
  phone: options.phone,
4478
5031
  displayName: options.displayName,
4479
5032
  enabled,
5033
+ accountType: options.accountType ?? "permanent",
5034
+ accountStatus: options.accountStatus ?? "active",
5035
+ accountExpiresAt: options.accountExpiresAt,
5036
+ mustChangePassword: managed && config.password?.requireInitialChange === true,
4480
5037
  emailVerified: false,
4481
5038
  phoneVerified: false,
4482
5039
  passwordHash: hashResult.data,
@@ -4506,15 +5063,18 @@ function buildRegistrationOps(ctx) {
4506
5063
  await userRepository.deleteById(createdUser.id);
4507
5064
  return assigned;
4508
5065
  }
4509
- logger10.info("User registered", { userId: createdUser.id, username: options.username });
5066
+ logger12.info("User registered", { userId: createdUser.id, username: options.username });
4510
5067
  return ok({
4511
5068
  user: toUser(createdUser),
4512
5069
  agreements: buildAgreementDisplay(agreementConfig, agreementConfig.showOnRegister)
4513
5070
  });
4514
5071
  }
4515
5072
  return {
4516
- createUser: (options) => createUser(options, true),
4517
- register(options) {
5073
+ createUser: (options) => createUser(options, true, true),
5074
+ async register(options) {
5075
+ const captcha = await verifyIamCaptcha(ctx.config.captcha, "register", options.captcha);
5076
+ if (!captcha.success)
5077
+ return captcha;
4518
5078
  if (!registerConfig.enabled)
4519
5079
  return Promise.resolve(err(HaiIamError.REGISTER_DISABLED, iamM("iam_registerDisabled")));
4520
5080
  return createUser(options, registerConfig.defaultEnabled);
@@ -4693,6 +5253,18 @@ function buildUserMutationOps(ctx) {
4693
5253
  return ok(toUser(updatedResult.data));
4694
5254
  },
4695
5255
  async updateUser(userId, data) {
5256
+ const currentAccount = await userRepository.findById(userId);
5257
+ if (!currentAccount.success)
5258
+ return currentAccount;
5259
+ if (!currentAccount.data)
5260
+ return err(HaiIamError.USER_NOT_FOUND, iamM("iam_userNotExist"));
5261
+ const accountPolicy = validateAccountPolicy({
5262
+ accountType: data.accountType === void 0 ? currentAccount.data.accountType : data.accountType,
5263
+ accountStatus: data.accountStatus === void 0 ? currentAccount.data.accountStatus : data.accountStatus,
5264
+ accountExpiresAt: data.accountExpiresAt === void 0 ? currentAccount.data.accountExpiresAt : data.accountExpiresAt
5265
+ });
5266
+ if (!accountPolicy.success)
5267
+ return accountPolicy;
4696
5268
  if (!hasUpdateFields(data)) {
4697
5269
  const currentResult = await userRepository.findById(userId);
4698
5270
  if (!currentResult.success) {
@@ -4710,18 +5282,37 @@ function buildUserMutationOps(ctx) {
4710
5282
  if (!uniqueResult.success) {
4711
5283
  return uniqueResult;
4712
5284
  }
4713
- const updateResult = await userRepository.updateById(userId, data);
4714
- if (!updateResult.success) {
4715
- return mapUpdateErrorAsDomainError(updateResult.error);
4716
- }
4717
- if (updateResult.data.changes === 0) {
4718
- return err(
4719
- HaiIamError.USER_NOT_FOUND,
4720
- iamM("iam_userNotExist")
4721
- );
5285
+ const changesSecurity = data.enabled !== void 0 || data.accountStatus !== void 0 || data.accountType !== void 0 || data.accountExpiresAt !== void 0;
5286
+ const transaction = changesSecurity ? await reldb.tx.begin() : void 0;
5287
+ if (transaction && !transaction.success)
5288
+ return transaction;
5289
+ const tx = transaction?.success ? transaction.data : void 0;
5290
+ let transactionCommitted = false;
5291
+ try {
5292
+ const updateResult = await userRepository.updateById(userId, data, tx);
5293
+ if (!updateResult.success)
5294
+ return mapUpdateErrorAsDomainError(updateResult.error);
5295
+ if (updateResult.data.changes === 0)
5296
+ return err(HaiIamError.USER_NOT_FOUND, iamM("iam_userNotExist"));
5297
+ if (tx) {
5298
+ const advanced = await tx.execute("UPDATE hai_iam_users SET credential_version = ?, reset_version = ?, reset_hash = NULL, reset_expires_at = NULL WHERE id = ? AND credential_version = ?", [globalThis.crypto.randomUUID(), globalThis.crypto.randomUUID(), userId, currentAccount.data.credentialVersion ?? ""]);
5299
+ if (!advanced.success || advanced.data.changes !== 1)
5300
+ return advanced.success ? err(HaiIamError.CONFLICT, iamM("iam_sessionStateConflict")) : advanced;
5301
+ const committed = await tx.commit();
5302
+ if (!committed.success)
5303
+ return committed;
5304
+ transactionCommitted = true;
5305
+ }
5306
+ } catch (cause) {
5307
+ return err(HaiIamError.REPOSITORY_ERROR, iamM("iam_accountUpdateFailed"), cause);
5308
+ } finally {
5309
+ if (tx && !transactionCommitted)
5310
+ await tx.rollback();
4722
5311
  }
4723
- if (data.enabled === false) {
4724
- await sessionFunctions.deleteByUserId(userId);
5312
+ if (tx) {
5313
+ const revoked = await sessionFunctions.deleteByUserId(userId);
5314
+ if (!revoked.success)
5315
+ logger12.warn("Account credentials revoked; session cache cleanup failed", { userId, error: revoked.error });
4725
5316
  }
4726
5317
  const updatedResult = await userRepository.findById(userId);
4727
5318
  if (!updatedResult.success) {
@@ -4736,7 +5327,7 @@ function buildUserMutationOps(ctx) {
4736
5327
  return ok(toUser(updatedResult.data));
4737
5328
  },
4738
5329
  async deleteUser(userId) {
4739
- logger10.debug("Deleting user", { userId });
5330
+ logger12.debug("Deleting user", { userId });
4740
5331
  const userResult = await userRepository.findById(userId);
4741
5332
  if (!userResult.success) {
4742
5333
  return mapRepositoryError("iam_queryUserFailed", userResult.error.message);
@@ -4776,17 +5367,17 @@ function buildUserMutationOps(ctx) {
4776
5367
  );
4777
5368
  }
4778
5369
  await sessionFunctions.deleteByUserId(userId);
4779
- logger10.info("User deleted", { userId });
5370
+ logger12.info("User deleted", { userId });
4780
5371
  return ok(void 0);
4781
5372
  }
4782
5373
  };
4783
5374
  }
4784
5375
  function buildPasswordChangeOps(ctx) {
4785
5376
  const { userRepository, sessionFunctions } = ctx;
4786
- const { validatePassword, hashPassword } = ctx;
5377
+ const { hashPassword } = ctx;
4787
5378
  return {
4788
5379
  async adminResetPassword(userId, newPassword) {
4789
- logger10.debug("Admin resetting user password", { userId });
5380
+ logger12.debug("Admin resetting user password", { userId });
4790
5381
  const userResult = await userRepository.findById(userId);
4791
5382
  if (!userResult.success) {
4792
5383
  return mapRepositoryError("iam_queryUserFailed", userResult.error.message);
@@ -4797,13 +5388,13 @@ function buildPasswordChangeOps(ctx) {
4797
5388
  iamM("iam_userNotExist")
4798
5389
  );
4799
5390
  }
4800
- const validateResult = validatePassword(newPassword);
5391
+ const validateResult = validatePasswordReplacement(ctx, userResult.data, newPassword);
4801
5392
  if (!validateResult.success)
4802
5393
  return validateResult;
4803
5394
  const hashResult = hashPassword(newPassword);
4804
5395
  if (!hashResult.success)
4805
5396
  return hashResult;
4806
- const updateResult = await userRepository.setPassword({ userId, expectedVersion: userResult.data.credentialVersion ?? "", passwordHash: hashResult.data });
5397
+ const updateResult = await userRepository.setPassword({ userId, expectedVersion: userResult.data.credentialVersion ?? "", passwordHash: hashResult.data, mustChangePassword: ctx.config.password?.requireInitialChange === true });
4807
5398
  if (!updateResult.success)
4808
5399
  return updateResult;
4809
5400
  if (!updateResult.data)
@@ -4811,7 +5402,7 @@ function buildPasswordChangeOps(ctx) {
4811
5402
  const revoked = await sessionFunctions.deleteByUserId(userId);
4812
5403
  if (!revoked.success)
4813
5404
  return revoked;
4814
- logger10.info("Admin reset password", { userId });
5405
+ logger12.info("Admin reset password", { userId });
4815
5406
  return ok(void 0);
4816
5407
  },
4817
5408
  async changePassword(userId, oldPassword, newPassword) {
@@ -4826,10 +5417,10 @@ function buildPasswordChangeOps(ctx) {
4826
5417
  );
4827
5418
  }
4828
5419
  const user = userResult.data;
4829
- if (!user.enabled) {
5420
+ if (!isUserActive(user)) {
4830
5421
  return err(HaiIamError.USER_DISABLED, iamM("iam_accountDisabled"));
4831
5422
  }
4832
- if (isAccountLocked(user)) {
5423
+ if (isAccountLocked(user, ctx.loginConfig)) {
4833
5424
  return err(HaiIamError.USER_LOCKED, iamM("iam_accountLocked"));
4834
5425
  }
4835
5426
  if (!user.passwordHash) {
@@ -4840,10 +5431,7 @@ function buildPasswordChangeOps(ctx) {
4840
5431
  }
4841
5432
  const verifyResult = crypto$1.password.verify(oldPassword, user.passwordHash);
4842
5433
  if (!verifyResult.success || !verifyResult.data) {
4843
- const failureResult = await recordLoginFailure(userRepository, user, {
4844
- maxLoginAttempts: ctx.loginConfig.maxLoginAttempts,
4845
- lockoutDuration: ctx.loginConfig.lockoutDuration
4846
- });
5434
+ const failureResult = await recordLoginFailure(userRepository, user, ctx.loginConfig);
4847
5435
  if (!failureResult.success)
4848
5436
  return failureResult;
4849
5437
  return err(
@@ -4851,10 +5439,10 @@ function buildPasswordChangeOps(ctx) {
4851
5439
  iamM("iam_originalPasswordWrong")
4852
5440
  );
4853
5441
  }
4854
- const resetResult = await resetLoginFailures(userRepository, user);
5442
+ const resetResult = await resetLoginFailures(userRepository, user, ctx.loginConfig);
4855
5443
  if (!resetResult.success)
4856
5444
  return resetResult;
4857
- const validateResult = validatePassword(newPassword);
5445
+ const validateResult = validatePasswordReplacement(ctx, user, newPassword);
4858
5446
  if (!validateResult.success)
4859
5447
  return validateResult;
4860
5448
  const hashResult = hashPassword(newPassword);
@@ -4868,7 +5456,7 @@ function buildPasswordChangeOps(ctx) {
4868
5456
  const revoked = await sessionFunctions.deleteByUserId(userId);
4869
5457
  if (!revoked.success)
4870
5458
  return revoked;
4871
- logger10.info("Password changed", { userId });
5459
+ logger12.info("Password changed", { userId });
4872
5460
  return ok(void 0);
4873
5461
  },
4874
5462
  /**
@@ -4888,48 +5476,58 @@ function buildPasswordChangeOps(ctx) {
4888
5476
  }
4889
5477
  };
4890
5478
  }
5479
+ async function issuePasswordReset(ctx, stored) {
5480
+ const { userRepository, onPasswordResetRequest, resetConfig } = ctx;
5481
+ if (!onPasswordResetRequest)
5482
+ return err(HaiIamError.PASSWORD_RESET_DISABLED, iamM("iam_passwordResetDisabled"));
5483
+ const user = toUser(stored);
5484
+ const token = generateToken();
5485
+ const expiresAt = new Date(Date.now() + resetConfig.tokenExpiresIn * 1e3);
5486
+ const tokenHash = crypto$1.hash.hash(token);
5487
+ if (!tokenHash.success)
5488
+ return tokenHash;
5489
+ const saveResult = await userRepository.publishPasswordReset({ user: stored, tokenHash: tokenHash.data, expiresAt });
5490
+ if (!saveResult.success)
5491
+ return saveResult;
5492
+ if (!saveResult.data)
5493
+ return err(HaiIamError.CONFLICT, iamM("iam_securityStateFailed"));
5494
+ try {
5495
+ await onPasswordResetRequest(user, token, { expiresAt, idempotencyKey: `iam-reset:${tokenHash.data}` });
5496
+ } catch {
5497
+ logger12.error("Password reset callback failed", { userId: user.id });
5498
+ return err(HaiIamError.INTERNAL_ERROR, iamM("iam_notificationDeliveryUnknown"));
5499
+ }
5500
+ logger12.info("Password reset token generated", { userId: user.id });
5501
+ return ok(void 0);
5502
+ }
4891
5503
  function buildPasswordResetOps(ctx) {
4892
5504
  const { userRepository, sessionFunctions, onPasswordResetRequest } = ctx;
4893
- const { validatePassword, hashPassword } = ctx;
5505
+ const { hashPassword } = ctx;
4894
5506
  return {
4895
- async requestPasswordReset(identifier) {
4896
- logger10.debug("Password reset requested", { identifier });
5507
+ async requestPasswordReset(identifier, options) {
5508
+ const captcha = await verifyIamCaptcha(ctx.config.captcha, "passwordReset", options?.captcha);
5509
+ if (!captcha.success)
5510
+ return captcha;
5511
+ logger12.debug("Password reset requested", { identifier });
4897
5512
  const resetConfig = ctx.resetConfig;
4898
5513
  if (!resetConfig.enabled || !onPasswordResetRequest)
4899
5514
  return err(HaiIamError.PASSWORD_RESET_DISABLED, iamM("iam_passwordResetDisabled"));
4900
5515
  const userResult = await userRepository.findByIdentifier(identifier);
4901
5516
  if (!userResult.success) {
4902
- logger10.warn("Failed to look up user for password reset", { identifier });
5517
+ logger12.warn("Failed to look up user for password reset", { identifier });
4903
5518
  return ok(void 0);
4904
5519
  }
4905
5520
  if (!userResult.data) {
4906
- logger10.debug("User not found for password reset, returning ok to prevent enumeration", { identifier });
5521
+ logger12.debug("User not found for password reset, returning ok to prevent enumeration", { identifier });
4907
5522
  return ok(void 0);
4908
5523
  }
4909
5524
  const user = toUser(userResult.data);
4910
5525
  if (!user.enabled)
4911
5526
  return ok(void 0);
4912
- const token = generateToken();
4913
- const expiresAt = new Date(Date.now() + resetConfig.tokenExpiresIn * 1e3);
4914
- const tokenHash = crypto$1.hash.hash(token);
4915
- if (!tokenHash.success)
4916
- return tokenHash;
4917
- const saveResult = await userRepository.publishPasswordReset({ user: userResult.data, tokenHash: tokenHash.data, expiresAt });
4918
- if (!saveResult.success)
4919
- return saveResult;
4920
- if (!saveResult.data)
4921
- return err(HaiIamError.CONFLICT, iamM("iam_securityStateFailed"));
4922
- try {
4923
- await onPasswordResetRequest(user, token, { expiresAt, idempotencyKey: `iam-reset:${tokenHash.data}` });
4924
- } catch {
4925
- logger10.error("Password reset callback failed", { userId: user.id });
4926
- return err(HaiIamError.INTERNAL_ERROR, iamM("iam_notificationDeliveryUnknown"));
4927
- }
4928
- logger10.info("Password reset token generated", { userId: user.id });
4929
- return ok(void 0);
5527
+ return issuePasswordReset(ctx, userResult.data);
4930
5528
  },
4931
5529
  async confirmPasswordReset(token, newPassword) {
4932
- logger10.debug("Confirming password reset");
5530
+ logger12.debug("Confirming password reset");
4933
5531
  const resetConfig = ctx.resetConfig;
4934
5532
  if (!resetConfig.enabled)
4935
5533
  return err(HaiIamError.PASSWORD_RESET_DISABLED, iamM("iam_passwordResetDisabled"));
@@ -4939,7 +5537,7 @@ function buildPasswordResetOps(ctx) {
4939
5537
  const claimed = await userRepository.claimPasswordReset(tokenHash.data, resetConfig.maxAttempts);
4940
5538
  if (!claimed.success)
4941
5539
  return claimed;
4942
- const validateResult = validatePassword(newPassword);
5540
+ const validateResult = validatePasswordReplacement(ctx, claimed.data, newPassword);
4943
5541
  if (!validateResult.success)
4944
5542
  return validateResult;
4945
5543
  const hashResult = hashPassword(newPassword);
@@ -4953,7 +5551,7 @@ function buildPasswordResetOps(ctx) {
4953
5551
  const revoked = await sessionFunctions.deleteByUserId(claimed.data.id);
4954
5552
  if (!revoked.success)
4955
5553
  return revoked;
4956
- logger10.info("Password reset confirmed", { userId: claimed.data.id });
5554
+ logger12.info("Password reset confirmed", { userId: claimed.data.id });
4957
5555
  return ok(void 0);
4958
5556
  }
4959
5557
  };
@@ -4969,17 +5567,56 @@ function buildUserFunctions(deps) {
4969
5567
  resetConfig: PasswordResetConfigSchema.parse(deps.config.passwordReset ?? {}),
4970
5568
  loginConfig: SecurityConfigSchema.parse(deps.config.security ?? {})
4971
5569
  };
5570
+ const passwordChanges = buildPasswordChangeOps(ctx);
5571
+ const registration = buildRegistrationOps(ctx);
4972
5572
  return {
4973
- ...buildRegistrationOps(ctx),
5573
+ ...registration,
4974
5574
  ...buildUserQueryOps(ctx),
4975
5575
  ...buildUserMutationOps(ctx),
4976
- ...buildPasswordChangeOps(ctx),
4977
- ...buildPasswordResetOps(ctx)
5576
+ ...passwordChanges,
5577
+ ...buildPasswordResetOps(ctx),
5578
+ async inviteUser(options) {
5579
+ const destination = ctx.resetConfig.mode === "phone" ? options.phone : options.email;
5580
+ if (!deps.config.initialCredentials?.enabled || !ctx.resetConfig.enabled || !ctx.onPasswordResetRequest || !destination)
5581
+ return err(HaiIamError.INVITATION_DISABLED, iamM("iam_invitationDisabled"));
5582
+ const policy = deps.passwordStrategyResult.passwordConfig;
5583
+ const length = Math.min(policy.maxLength, Math.max(32, policy.minLength));
5584
+ const password = `Aa1!${Array.from({ length: Math.ceil(length / 32) }, () => generateToken()).join("")}`.slice(0, length);
5585
+ const created = await registration.createUser({ ...options, password });
5586
+ if (!created.success)
5587
+ return created;
5588
+ const stored = await deps.userRepository.findById(created.data.user.id);
5589
+ if (!stored.success || !stored.data)
5590
+ return err(HaiIamError.INVITATION_DELIVERY_UNKNOWN, iamM("iam_invitationDeliveryUnknown"));
5591
+ const delivered = await issuePasswordReset(ctx, stored.data);
5592
+ if (!delivered.success)
5593
+ return err(HaiIamError.INVITATION_DELIVERY_UNKNOWN, iamM("iam_invitationDeliveryUnknown"), delivered.error);
5594
+ return created;
5595
+ },
5596
+ async renewPassword(input) {
5597
+ const captcha = await verifyIamCaptcha(deps.config.captcha, "renewPassword", input.captcha);
5598
+ if (!captcha.success)
5599
+ return captcha;
5600
+ const found = await deps.userRepository.findByIdentifier(input.identifier);
5601
+ if (!found.success)
5602
+ return found;
5603
+ if (!found.data) {
5604
+ crypto$1.password.hash("dummy-password-to-prevent-timing-leak");
5605
+ return err(HaiIamError.INVALID_CREDENTIALS, iamM("iam_passwordWrong"));
5606
+ }
5607
+ return passwordChanges.changePassword(found.data.id, input.password, input.newPassword);
5608
+ },
5609
+ async scanInactiveAccounts() {
5610
+ const days = deps.config.accounts?.dormantAfterDays ?? 0;
5611
+ if (days === 0)
5612
+ return ok(0);
5613
+ return deps.userRepository.suspendInactive(new Date(Date.now() - days * 864e5));
5614
+ }
4978
5615
  };
4979
5616
  }
4980
5617
 
4981
5618
  // src/iam-main.ts
4982
- var logger11 = core.logger.child({ module: "iam", scope: "main" });
5619
+ var logger13 = core.logger.child({ module: "iam", scope: "main" });
4983
5620
  var initInProgress = false;
4984
5621
  var currentConfig = null;
4985
5622
  var currentPublicConfig = null;
@@ -4989,6 +5626,8 @@ var currentAuthz = null;
4989
5626
  var currentSession = null;
4990
5627
  var currentApiKey = null;
4991
5628
  var currentTicket = null;
5629
+ var accountScanTimer;
5630
+ var accountScanRunning;
4992
5631
  var notInitialized = core.module.createNotInitializedKit(
4993
5632
  HaiIamError.NOT_INITIALIZED,
4994
5633
  () => iamM("iam_notInitialized")
@@ -5008,7 +5647,7 @@ var notInitializedUser = new Proxy({}, {
5008
5647
  var iam = {
5009
5648
  async init(config) {
5010
5649
  if (initInProgress) {
5011
- logger11.warn("IAM init already in progress, skipping concurrent call");
5650
+ logger13.warn("IAM init already in progress, skipping concurrent call");
5012
5651
  return err(
5013
5652
  HaiIamError.CONFIG_ERROR,
5014
5653
  iamM("iam_initInProgress")
@@ -5017,11 +5656,11 @@ var iam = {
5017
5656
  initInProgress = true;
5018
5657
  try {
5019
5658
  if (currentConfig !== null) {
5020
- logger11.warn("IAM module is already initialized, reinitializing");
5659
+ logger13.warn("IAM module is already initialized, reinitializing");
5021
5660
  await iam.close();
5022
5661
  }
5023
5662
  const { ldapClientFactory, ldapSyncUser } = config;
5024
- logger11.info("Initializing IAM module");
5663
+ logger13.info("Initializing IAM module");
5025
5664
  if (!reldb.isInitialized) {
5026
5665
  return err(
5027
5666
  HaiIamError.CONFIG_ERROR,
@@ -5046,7 +5685,7 @@ var iam = {
5046
5685
  }
5047
5686
  const parseResult = IamConfigSchema.safeParse(config);
5048
5687
  if (!parseResult.success) {
5049
- logger11.error("IAM config validation failed", { error: parseResult.error.message });
5688
+ logger13.error("IAM config validation failed", { error: parseResult.error.message });
5050
5689
  return err(
5051
5690
  HaiIamError.CONFIG_ERROR,
5052
5691
  iamM("iam_configError", { params: { error: parseResult.error.message } }),
@@ -5055,7 +5694,7 @@ var iam = {
5055
5694
  }
5056
5695
  const parsed = parseResult.data;
5057
5696
  const { onPasswordResetRequest, onOtpSendEmail, onOtpSendSms } = await resolveIamNotifications(config);
5058
- logger11.info("IAM notification channels resolved", { email: Boolean(onOtpSendEmail), sms: Boolean(onOtpSendSms), passwordReset: Boolean(onPasswordResetRequest) });
5697
+ logger13.info("IAM notification channels resolved", { email: Boolean(onOtpSendEmail), sms: Boolean(onOtpSendSms), passwordReset: Boolean(onPasswordResetRequest) });
5059
5698
  const migration = await migrateIam();
5060
5699
  if (!migration.success)
5061
5700
  return migration;
@@ -5066,20 +5705,27 @@ var iam = {
5066
5705
  const found = await credentialUsers.findById(userId);
5067
5706
  if (!found.success)
5068
5707
  return found;
5069
- return ok(found.data ? { version: found.data.credentialVersion ?? "", enabled: found.data.enabled } : null);
5708
+ if (found.data) {
5709
+ const passwordState = checkPasswordState(found.data, parsed.password);
5710
+ if (!passwordState.success)
5711
+ return passwordState;
5712
+ }
5713
+ return ok(found.data ? { version: found.data.credentialVersion ?? "", enabled: isUserActive(found.data) } : null);
5070
5714
  }
5071
5715
  });
5072
5716
  if (!sessionResult.success) {
5073
5717
  return sessionResult;
5074
5718
  }
5075
- const authzResult = await createAuthzOperations({ config: parsed, session: sessionResult.data });
5719
+ const sessions = observeSessions(sessionResult.data);
5720
+ const authzResult = await createAuthzOperations({ config: parsed, session: sessions });
5076
5721
  if (!authzResult.success) {
5077
5722
  return authzResult;
5078
5723
  }
5724
+ const authorization = observeAuthorization(authzResult.data);
5079
5725
  const authnResult = await createAuthnOperations({
5080
5726
  config: parsed,
5081
- sessionFunctions: sessionResult.data,
5082
- authzFunctions: authzResult.data,
5727
+ sessionFunctions: sessions,
5728
+ authzFunctions: authorization,
5083
5729
  ldapClientFactory,
5084
5730
  ldapSyncUser,
5085
5731
  onOtpSendEmail,
@@ -5091,41 +5737,57 @@ var iam = {
5091
5737
  const userResult = await createUserOperations({
5092
5738
  config: parsed,
5093
5739
  passwordStrategyResult: authnResult.data.passwordStrategyResult,
5094
- sessionFunctions: sessionResult.data,
5095
- authzFunctions: authzResult.data,
5740
+ sessionFunctions: sessions,
5741
+ authzFunctions: authorization,
5096
5742
  onPasswordResetRequest
5097
5743
  });
5098
5744
  if (!userResult.success) {
5099
5745
  return userResult;
5100
5746
  }
5747
+ const users = observeUsers(userResult.data);
5101
5748
  if (parsed.seedDefaultData) {
5102
- const seedResult = await seedIamData(authzResult.data);
5749
+ const seedResult = await seedIamData(authorization);
5103
5750
  if (!seedResult.success) {
5104
5751
  return seedResult;
5105
5752
  }
5106
5753
  }
5107
- currentSession = sessionResult.data;
5108
- currentAuthz = authzResult.data;
5109
- currentUser = userResult.data;
5754
+ currentSession = sessions;
5755
+ currentAuthz = authorization;
5756
+ currentUser = users;
5110
5757
  currentConfig = parsed;
5111
5758
  currentPublicConfig = buildIamPublicConfig(parsed, { ...config, onPasswordResetRequest, onOtpSendEmail, onOtpSendSms });
5112
- currentApiKey = authnResult.data.apiKeyFunctions;
5759
+ currentApiKey = authnResult.data.apiKeyFunctions ? observeApiKeys(authnResult.data.apiKeyFunctions) : null;
5113
5760
  currentTicket = createTicketOperations();
5114
5761
  const authn = authnResult.data.authn;
5115
- currentAuth = {
5762
+ currentAuth = observeAuth({
5116
5763
  ...authn,
5117
- async registerAndLogin(options) {
5764
+ async registerAndLogin(options, context) {
5118
5765
  const regResult = await currentUser.register(options);
5119
5766
  if (!regResult.success) {
5120
5767
  return regResult;
5121
5768
  }
5122
- return authnResult.data.loginRegisteredUser(regResult.data.user);
5769
+ return authnResult.data.loginRegisteredUser(regResult.data.user, context);
5123
5770
  }
5124
- };
5125
- logger11.info("IAM module initialized");
5771
+ });
5772
+ if ((parsed.accounts?.dormantAfterDays ?? 0) > 0) {
5773
+ accountScanTimer = setInterval(() => {
5774
+ if (accountScanRunning)
5775
+ return;
5776
+ accountScanRunning = users.scanInactiveAccounts().then((result) => {
5777
+ if (!result.success)
5778
+ logger13.error("Inactive account scan failed", { error: result.error });
5779
+ }).catch((error) => {
5780
+ logger13.error("Inactive account scan failed", { error });
5781
+ }).finally(() => {
5782
+ accountScanRunning = void 0;
5783
+ });
5784
+ }, (parsed.accounts?.scanIntervalSeconds ?? 3600) * 1e3);
5785
+ accountScanTimer.unref();
5786
+ }
5787
+ logger13.info("IAM module initialized");
5126
5788
  return ok(void 0);
5127
5789
  } catch (error) {
5128
- logger11.error("IAM module initialization failed", { error });
5790
+ logger13.error("IAM module initialization failed", { error });
5129
5791
  return err(
5130
5792
  HaiIamError.CONFIG_ERROR,
5131
5793
  iamM("iam_initFailed"),
@@ -5138,6 +5800,18 @@ var iam = {
5138
5800
  get auth() {
5139
5801
  return currentAuth ?? notInitializedAuth;
5140
5802
  },
5803
+ async getPublicConfig() {
5804
+ const snapshot = currentPublicConfig ? structuredClone(currentPublicConfig) : null;
5805
+ const authz = currentAuthz;
5806
+ if (!snapshot || !authz)
5807
+ return notInitialized.result();
5808
+ if (!snapshot.reauthentication.enabled)
5809
+ return ok(snapshot);
5810
+ const configured = await authz.getSensitiveOperations();
5811
+ if (!configured.success)
5812
+ return configured;
5813
+ return ok({ ...snapshot, reauthentication: { ...snapshot.reauthentication, operations: [.../* @__PURE__ */ new Set([...snapshot.reauthentication.operations, ...configured.data])].sort() } });
5814
+ },
5141
5815
  get user() {
5142
5816
  return currentUser ?? notInitializedUser;
5143
5817
  },
@@ -5167,10 +5841,13 @@ var iam = {
5167
5841
  },
5168
5842
  async close() {
5169
5843
  if (currentConfig === null && currentAuth === null && currentUser === null && currentAuthz === null && currentSession === null) {
5170
- logger11.info("IAM module already closed, skipping");
5844
+ logger13.info("IAM module already closed, skipping");
5171
5845
  return;
5172
5846
  }
5173
- logger11.info("Closing IAM module");
5847
+ logger13.info("Closing IAM module");
5848
+ clearInterval(accountScanTimer);
5849
+ accountScanTimer = void 0;
5850
+ await accountScanRunning;
5174
5851
  currentAuth = null;
5175
5852
  currentUser = null;
5176
5853
  currentAuthz = null;
@@ -5184,7 +5861,7 @@ var iam = {
5184
5861
  resetUserRepoSingleton();
5185
5862
  resetRoleRepoSingleton();
5186
5863
  resetPermissionRepoSingleton();
5187
- logger11.info("IAM module closed");
5864
+ logger13.info("IAM module closed");
5188
5865
  }
5189
5866
  };
5190
5867