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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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-22SERPFD.js';
2
+ export { AccountConfigSchema, AgreementConfigSchema, ApiKeyConfigSchema, AuthStrategyTypeSchema, AuthUiConfigSchema, CaptchaConfigSchema, HaiIamError, IamConfigSchema, LdapConfigSchema, LoginAccessConfigSchema, LoginConfigSchema, OtpConfigSchema, PasswordConfigSchema, PasswordResetConfigSchema, RbacConfigSchema, RegisterConfigSchema, SecurityConfigSchema, SessionConfigSchema } from './chunk-22SERPFD.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,25 @@ 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"
200
217
  };
201
218
 
202
219
  // messages/ja-JP.json
203
220
  var ja_JP_default = {
221
+ 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",
222
+ 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",
223
+ 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",
224
+ 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
225
  iam_batchUserIdsInvalid: "\u7A7A\u3067\u306A\u3044\u30E6\u30FC\u30B6\u30FCID\u30921000\u4EF6\u4EE5\u5185\u3067\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044",
205
226
  $schema: "https://inlang.com/schema/inlang-message-format",
206
227
  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 +409,25 @@ var ja_JP_default = {
388
409
  iam_sessionStateFailed: "\u5171\u6709\u30BB\u30C3\u30B7\u30E7\u30F3\u72B6\u614B\u306E\u64CD\u4F5C\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
389
410
  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
411
  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"
412
+ 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",
413
+ 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",
414
+ 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",
415
+ 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",
416
+ 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",
417
+ 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",
418
+ 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",
419
+ iam_captchaRequired: "\u5148\u306B\u30BB\u30AD\u30E5\u30EA\u30C6\u30A3\u78BA\u8A8D\u3092\u5B8C\u4E86\u3057\u3066\u304F\u3060\u3055\u3044",
420
+ 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",
421
+ iam_captchaDisabled: "\u30BB\u30AD\u30E5\u30EA\u30C6\u30A3\u78BA\u8A8D\u306F\u7121\u52B9\u3067\u3059",
422
+ iam_captchaUnavailable: "\u30BB\u30AD\u30E5\u30EA\u30C6\u30A3\u78BA\u8A8D\u3092\u4E00\u6642\u7684\u306B\u5229\u7528\u3067\u304D\u307E\u305B\u3093"
392
423
  };
393
424
 
394
425
  // messages/zh-CN.json
395
426
  var zh_CN_default = {
427
+ iam_invitationDisabled: "\u9080\u8BF7\u529F\u80FD\u672A\u542F\u7528\uFF0C\u6216\u6240\u9700\u901A\u77E5\u901A\u9053\u672A\u5C31\u7EEA\u3002",
428
+ 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",
429
+ iam_reauthenticationRequired: "\u8BF7\u91CD\u65B0\u9A8C\u8BC1\u5BC6\u7801\u540E\u7EE7\u7EED\u6B64\u64CD\u4F5C\u3002",
430
+ iam_loginAccessDenied: "\u5F53\u524D\u5730\u5740\u6216\u65F6\u6BB5\u4E0D\u5141\u8BB8\u767B\u5F55\u3002",
396
431
  iam_batchUserIdsInvalid: "\u8BF7\u63D0\u4F9B\u6700\u591A 1000 \u4E2A\u975E\u7A7A\u7528\u6237 ID",
397
432
  $schema: "https://inlang.com/schema/inlang-message-format",
398
433
  iam_securityStateFailed: "\u65E0\u6CD5\u66F4\u65B0\u8BA4\u8BC1\u5B89\u5168\u72B6\u6001\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5",
@@ -580,7 +615,17 @@ var zh_CN_default = {
580
615
  iam_sessionStateFailed: "\u4F1A\u8BDD\u5171\u4EAB\u72B6\u6001\u64CD\u4F5C\u5931\u8D25",
581
616
  iam_sessionStateConflict: "\u4F1A\u8BDD\u72B6\u6001\u5DF2\u53D8\u5316\uFF0C\u8BF7\u91CD\u8BD5",
582
617
  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"
618
+ iam_sessionLimitExceeded: "\u6709\u6548\u4F1A\u8BDD\u6570\u91CF\u5DF2\u8FBE\u4E0A\u9650\uFF0C\u8BF7\u5148\u9000\u51FA\u5176\u4ED6\u4F1A\u8BDD",
619
+ iam_accountPolicyInvalid: "\u8D26\u53F7\u7C7B\u578B\u3001\u72B6\u6001\u6216\u5230\u671F\u65F6\u95F4\u65E0\u6548",
620
+ iam_accountUpdateFailed: "\u8D26\u53F7\u66F4\u65B0\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5",
621
+ iam_passwordChangeRequired: "\u8BF7\u5148\u66F4\u6362\u521D\u59CB\u5BC6\u7801\u540E\u767B\u5F55",
622
+ iam_passwordMustDiffer: "\u65B0\u5BC6\u7801\u4E0D\u80FD\u4E0E\u5F53\u524D\u5BC6\u7801\u76F8\u540C",
623
+ iam_passwordCharacterClasses: "\u5BC6\u7801\u81F3\u5C11\u5E94\u5305\u542B {count} \u7C7B\u5B57\u7B26",
624
+ iam_notificationDeliveryUnknown: "\u901A\u77E5\u53EF\u80FD\u5DF2\u9001\u8FBE\uFF0C\u7ED3\u679C\u672A\u77E5\uFF1B\u8BF7\u52FF\u81EA\u52A8\u91CD\u53D1\u3002",
625
+ iam_captchaRequired: "\u8BF7\u5148\u5B8C\u6210\u5B89\u5168\u9A8C\u8BC1",
626
+ iam_captchaInvalid: "\u5B89\u5168\u9A8C\u8BC1\u65E0\u6548\u6216\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u8BD5",
627
+ iam_captchaDisabled: "\u5B89\u5168\u9A8C\u8BC1\u672A\u542F\u7528",
628
+ iam_captchaUnavailable: "\u5B89\u5168\u9A8C\u8BC1\u6682\u4E0D\u53EF\u7528\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5"
584
629
  };
585
630
 
586
631
  // src/iam-i18n.ts
@@ -720,8 +765,8 @@ var DbApiKeyRepository = class extends BaseReldbCrudRepository {
720
765
  }
721
766
  return ok(result.data);
722
767
  }
723
- async findByKeyPrefix(prefix, tx) {
724
- const result = await this.findAll({ where: "key_prefix = ?", params: [prefix] }, tx);
768
+ async findByKeyPrefix(prefix2, tx) {
769
+ const result = await this.findAll({ where: "key_prefix = ?", params: [prefix2] }, tx);
725
770
  if (!result.success) {
726
771
  return this.buildQueryError(result.error);
727
772
  }
@@ -806,8 +851,139 @@ function buildAgreementDisplay(config, enabled) {
806
851
  showOnLogin: config.showOnLogin
807
852
  };
808
853
  }
854
+ var BASE64_PLUS_REGEX = /\+/g;
855
+ var BASE64_SLASH_REGEX = /\//g;
856
+ var BASE64_TRAILING_PADDING_REGEX = /=+$/;
857
+ function generateToken() {
858
+ const bytes = new Uint8Array(32);
859
+ crypto.getRandomValues(bytes);
860
+ const base64 = btoa(String.fromCharCode(...bytes));
861
+ return base64.replace(BASE64_PLUS_REGEX, "-").replace(BASE64_SLASH_REGEX, "_").replace(BASE64_TRAILING_PADDING_REGEX, "");
862
+ }
863
+ function getUserSessionScope(userId) {
864
+ return createHash("sha256").update(userId).digest("hex");
865
+ }
866
+ function generateSessionToken(userId) {
867
+ return `s1.${getUserSessionScope(userId)}.${generateToken()}`;
868
+ }
869
+ function getTokenSessionScope(token) {
870
+ return /^s1\.([a-f0-9]{64})\.[\w-]{43}$/.exec(token)?.[1];
871
+ }
872
+ function buildSession(options, now, sessionTtl, accessToken, credentialVersion) {
873
+ return {
874
+ version: crypto.randomUUID(),
875
+ credentialVersion,
876
+ apiKeyId: options.apiKeyId,
877
+ apiKeyScopes: options.apiKeyScopes,
878
+ userId: options.userId,
879
+ username: options.username,
880
+ displayName: options.displayName,
881
+ avatarUrl: options.avatarUrl,
882
+ roles: options.roles ?? [],
883
+ permissions: options.permissions ?? [],
884
+ source: options.source,
885
+ accessToken,
886
+ createdAt: now,
887
+ startedAt: now,
888
+ lastActiveAt: now,
889
+ expiresAt: new Date(now.getTime() + sessionTtl * 1e3),
890
+ maxAge: sessionTtl,
891
+ data: options.data
892
+ };
893
+ }
894
+ function applySessionPatch(session, patch) {
895
+ const nextSession = { ...session };
896
+ if (patch.data !== void 0) {
897
+ nextSession.data = { ...nextSession.data, ...patch.data };
898
+ }
899
+ if (patch.roles !== void 0) {
900
+ nextSession.roles = patch.roles;
901
+ }
902
+ if (patch.permissions !== void 0) {
903
+ nextSession.permissions = patch.permissions;
904
+ }
905
+ if (patch.username !== void 0) {
906
+ nextSession.username = patch.username;
907
+ }
908
+ if (patch.displayName !== void 0) {
909
+ nextSession.displayName = patch.displayName;
910
+ }
911
+ if (patch.avatarUrl !== void 0) {
912
+ nextSession.avatarUrl = patch.avatarUrl;
913
+ }
914
+ if (patch.source !== void 0) {
915
+ nextSession.source = patch.source;
916
+ }
917
+ if (patch.expiresAt !== void 0)
918
+ nextSession.expiresAt = new Date(patch.expiresAt);
919
+ nextSession.lastActiveAt = patch.lastActiveAt !== void 0 ? new Date(patch.lastActiveAt) : /* @__PURE__ */ new Date();
920
+ return nextSession;
921
+ }
922
+
923
+ // src/ticket/iam-ticket-functions.ts
924
+ var logger = core.logger.child({ module: "iam", scope: "ticket" });
925
+ var TICKET_KEY_PREFIX = "hai:iam:ticket:";
926
+ var DEFAULT_TICKET_TTL_MS = 3e4;
927
+ function buildTicketKey(ticket) {
928
+ return `${TICKET_KEY_PREFIX}${ticket}`;
929
+ }
930
+ function createTicketOperations() {
931
+ return {
932
+ async issue(options) {
933
+ const ttlMs = options.ttlMs !== void 0 && options.ttlMs > 0 ? options.ttlMs : DEFAULT_TICKET_TTL_MS;
934
+ const ticket = generateToken();
935
+ const expiresAt = Date.now() + ttlMs;
936
+ const record = {
937
+ subjectId: options.subjectId,
938
+ purpose: options.purpose,
939
+ grant: options.grant ?? {},
940
+ expiresAt
941
+ };
942
+ const saved = await cache.kv.set(buildTicketKey(ticket), record, { px: ttlMs, nx: true });
943
+ if (!saved.success || !saved.data) {
944
+ logger.warn("Failed to issue ticket", { purpose: options.purpose });
945
+ return err(HaiIamError.TICKET_ISSUE_FAILED, iamM("iam_ticketIssueFailed"));
946
+ }
947
+ return ok({ ticket, expiresAt });
948
+ },
949
+ async consume(ticket, options) {
950
+ if (!ticket) {
951
+ return err(HaiIamError.TICKET_INVALID, iamM("iam_ticketInvalid"));
952
+ }
953
+ const key = buildTicketKey(ticket);
954
+ const found = await cache.kv.get(key);
955
+ if (!found.success) {
956
+ return err(HaiIamError.TICKET_ISSUE_FAILED, iamM("iam_ticketIssueFailed"));
957
+ }
958
+ const record = found.data;
959
+ if (!record) {
960
+ return err(HaiIamError.TICKET_INVALID, iamM("iam_ticketInvalid"));
961
+ }
962
+ const removed = await cache.kv.del(key);
963
+ if (!removed.success || removed.data !== 1) {
964
+ return err(HaiIamError.TICKET_INVALID, iamM("iam_ticketInvalid"));
965
+ }
966
+ if (record.expiresAt <= Date.now()) {
967
+ return err(HaiIamError.TICKET_EXPIRED, iamM("iam_ticketExpired"));
968
+ }
969
+ if (options?.purpose !== void 0 && options.purpose !== record.purpose) {
970
+ return err(HaiIamError.TICKET_INVALID, iamM("iam_ticketInvalid"));
971
+ }
972
+ return ok({
973
+ subjectId: record.subjectId,
974
+ purpose: record.purpose,
975
+ grant: record.grant
976
+ });
977
+ }
978
+ };
979
+ }
809
980
  var TABLE_NAME2 = "hai_iam_users";
810
981
  var USER_FIELDS = [
982
+ { fieldName: "mustChangePassword", columnName: "must_change_password", def: { type: "BOOLEAN", notNull: true, defaultValue: false }, select: true, create: true, update: false },
983
+ { fieldName: "accountType", columnName: "account_type", def: { type: "TEXT", notNull: true, defaultValue: "permanent" }, select: true, create: true, update: true },
984
+ { fieldName: "accountStatus", columnName: "account_status", def: { type: "TEXT", notNull: true, defaultValue: "active" }, select: true, create: true, update: true },
985
+ { fieldName: "accountExpiresAt", columnName: "account_expires_at", def: { type: "TIMESTAMP" }, select: true, create: true, update: true },
986
+ { fieldName: "lastLoginAt", columnName: "last_login_at", def: { type: "TIMESTAMP" }, select: true, create: false, update: false },
811
987
  { fieldName: "credentialVersion", columnName: "credential_version", def: { type: "TEXT", notNull: true, defaultValue: "" }, select: true, create: false, update: false },
812
988
  { fieldName: "resetVersion", columnName: "reset_version", def: { type: "TEXT", notNull: true, defaultValue: "" }, select: true, create: false, update: false },
813
989
  { fieldName: "resetHash", columnName: "reset_hash", def: { type: "TEXT" }, select: true, create: false, update: false },
@@ -909,6 +1085,14 @@ var USER_FIELDS = [
909
1085
  create: true,
910
1086
  update: true
911
1087
  },
1088
+ {
1089
+ fieldName: "firstLoginFailedAt",
1090
+ columnName: "first_login_failed_at",
1091
+ def: { type: "TIMESTAMP" },
1092
+ select: true,
1093
+ create: true,
1094
+ update: true
1095
+ },
912
1096
  {
913
1097
  fieldName: "lastLoginFailedAt",
914
1098
  columnName: "last_login_failed_at",
@@ -980,11 +1164,22 @@ var DbUserRepository = class extends BaseReldbCrudRepository {
980
1164
  return "(UNIX_TIMESTAMP(CURRENT_TIMESTAMP(3)) * 1000)";
981
1165
  return "CAST((julianday('now') - 2440587.5) * 86400000 AS INTEGER)";
982
1166
  }
1167
+ async recordSuccessfulLogin(userId) {
1168
+ 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]);
1169
+ return result.success ? ok(void 0) : this.buildQueryError(result.error, result.error);
1170
+ }
1171
+ async suspendInactive(cutoff) {
1172
+ const result = await reldb.sql.execute(
1173
+ `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) <= ?`,
1174
+ ["dormant", crypto.randomUUID(), crypto.randomUUID(), reldb.config?.type === "postgresql" ? true : 1, "active", reldb.config?.type === "sqlite" ? cutoff.getTime() : cutoff]
1175
+ );
1176
+ return result.success ? ok(result.data.changes) : this.buildQueryError(result.error, result.error);
1177
+ }
983
1178
  /** 改密推进凭据代际;会话存储清理失败也不能让旧凭据恢复授权。 */
984
1179
  async setPassword(input) {
985
1180
  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]
1181
+ `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 = ?`,
1182
+ [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
1183
  );
989
1184
  return result.success ? ok(result.data.changes === 1) : this.buildQueryError(result.error, result.error);
990
1185
  }
@@ -1025,17 +1220,22 @@ var DbUserRepository = class extends BaseReldbCrudRepository {
1025
1220
  /** 密码与令牌消费同一行原子提交,禁止先消费 Redis 凭据再无条件写数据库。 */
1026
1221
  async commitPasswordReset(input) {
1027
1222
  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 = ?`,
1223
+ `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
1224
  [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
1225
  );
1031
1226
  return result.success ? ok(result.data.changes === 1) : this.buildQueryError(result.error, result.error);
1032
1227
  }
1033
1228
  async recordLoginFailure(userId, policy) {
1229
+ if (policy.lockoutEnabled === false)
1230
+ return ok(void 0);
1034
1231
  const sqlite = reldb.config?.type === "sqlite";
1035
- const now = sqlite ? Date.now() : /* @__PURE__ */ new Date();
1036
- const deadline = Date.now() + policy.lockoutDuration * 1e3;
1232
+ const timestamp = Date.now();
1233
+ const now = sqlite ? timestamp : new Date(timestamp);
1234
+ const cutoff = timestamp - (policy.failureWindow ?? 86400) * 1e3;
1235
+ const windowStart = sqlite ? cutoff : new Date(cutoff);
1236
+ const deadline = timestamp + policy.lockoutDuration * 1e3;
1037
1237
  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]);
1238
+ 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
1239
  if (!expired.success)
1040
1240
  return this.buildQueryError(expired.error, expired.error);
1041
1241
  const result = await reldb.sql.execute(
@@ -1046,10 +1246,13 @@ var DbUserRepository = class extends BaseReldbCrudRepository {
1046
1246
  return this.buildQueryError(result.error, result.error);
1047
1247
  return ok(void 0);
1048
1248
  }
1049
- async resetLoginFailures(userId, expectedCredentialVersion) {
1249
+ async resetLoginFailures(userId, expectedCredentialVersion, lockoutEnabled = true) {
1250
+ const params = [userId, expectedCredentialVersion, reldb.config?.type === "postgresql" ? true : 1];
1251
+ if (lockoutEnabled)
1252
+ params.push(reldb.config?.type === "sqlite" ? Date.now() : /* @__PURE__ */ new Date());
1050
1253
  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()]
1254
+ `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 <= ?)" : ""}`,
1255
+ params
1053
1256
  );
1054
1257
  if (!result.success)
1055
1258
  return this.buildQueryError(result.error, result.error);
@@ -1061,7 +1264,7 @@ var DbUserRepository = class extends BaseReldbCrudRepository {
1061
1264
  return err(HaiIamError.USER_DISABLED, iamM("iam_accountDisabled"));
1062
1265
  if ((user.data.credentialVersion ?? "") !== expectedCredentialVersion)
1063
1266
  return err(HaiIamError.SESSION_INVALID, iamM("iam_sessionExpired"));
1064
- if (user.data.lockedUntil && user.data.lockedUntil > /* @__PURE__ */ new Date())
1267
+ if (lockoutEnabled && user.data.lockedUntil && user.data.lockedUntil > /* @__PURE__ */ new Date())
1065
1268
  return err(HaiIamError.USER_LOCKED, iamM("iam_accountLocked"));
1066
1269
  }
1067
1270
  return ok(void 0);
@@ -1134,25 +1337,23 @@ var DbUserRepository = class extends BaseReldbCrudRepository {
1134
1337
  return ok(result.data[0] ?? null);
1135
1338
  }
1136
1339
  };
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));
1340
+ function checkPasswordState(user, policy) {
1341
+ if (policy?.requireInitialChange && user.mustChangePassword)
1342
+ return err(HaiIamError.PASSWORD_CHANGE_REQUIRED, iamM("iam_passwordChangeRequired"));
1343
+ const days = policy?.expirationDays ?? 0;
1344
+ if (days > 0 && user.passwordHash && (!user.passwordUpdatedAt || user.passwordUpdatedAt.getTime() + days * 864e5 <= Date.now()))
1345
+ return err(HaiIamError.PASSWORD_EXPIRED, iamM("iam_passwordExpired"));
1346
+ return ok(void 0);
1141
1347
  }
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
- }
1348
+ function isUserActive(user) {
1349
+ return user.enabled && (user.accountStatus ?? "active") === "active" && (user.accountType !== "temporary" || !!user.accountExpiresAt) && (!user.accountExpiresAt || new Date(user.accountExpiresAt).getTime() > Date.now());
1350
+ }
1351
+ function validateAccountPolicy(user) {
1352
+ 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) {
1353
+ return err(HaiIamError.INVALID_ARGUMENT, iamM("iam_accountPolicyInvalid"));
1151
1354
  }
1152
- return { ...access, roles: scopes.includes("*") ? access.roles : [], permissions: [...permissions] };
1355
+ return ok(void 0);
1153
1356
  }
1154
-
1155
- // src/user/iam-user-utils.ts
1156
1357
  function toUser(storedUser) {
1157
1358
  return {
1158
1359
  id: storedUser.id,
@@ -1162,6 +1363,11 @@ function toUser(storedUser) {
1162
1363
  displayName: storedUser.displayName,
1163
1364
  avatarUrl: storedUser.avatarUrl,
1164
1365
  enabled: storedUser.enabled,
1366
+ accountType: storedUser.accountType ?? "permanent",
1367
+ accountStatus: storedUser.accountStatus ?? "active",
1368
+ accountExpiresAt: storedUser.accountExpiresAt ?? null,
1369
+ lastLoginAt: storedUser.lastLoginAt ?? null,
1370
+ mustChangePassword: storedUser.mustChangePassword ?? false,
1165
1371
  emailVerified: storedUser.emailVerified,
1166
1372
  phoneVerified: storedUser.phoneVerified,
1167
1373
  createdAt: storedUser.createdAt,
@@ -1169,6 +1375,23 @@ function toUser(storedUser) {
1169
1375
  metadata: storedUser.metadata
1170
1376
  };
1171
1377
  }
1378
+
1379
+ // src/authn/apikey/iam-authn-apikey-scope.ts
1380
+ function covers(grant, permission) {
1381
+ return grant === "*" || grant === permission || grant.endsWith(":*") && permission.startsWith(grant.slice(0, -1));
1382
+ }
1383
+ function applyApiKeyScopes(access, scopes) {
1384
+ const permissions = /* @__PURE__ */ new Set();
1385
+ for (const permission of access.permissions) {
1386
+ for (const scope of scopes) {
1387
+ if (covers(permission, scope))
1388
+ permissions.add(scope);
1389
+ else if (covers(scope, permission))
1390
+ permissions.add(permission);
1391
+ }
1392
+ }
1393
+ return { ...access, roles: scopes.includes("*") ? access.roles : [], permissions: [...permissions] };
1394
+ }
1172
1395
  function ensureCredentialType(credentials, type) {
1173
1396
  if (credentials.type !== type) {
1174
1397
  return err(
@@ -1178,8 +1401,8 @@ function ensureCredentialType(credentials, type) {
1178
1401
  }
1179
1402
  return ok(credentials);
1180
1403
  }
1181
- function isAccountLocked(user) {
1182
- if (!user.lockedUntil) {
1404
+ function isAccountLocked(user, policy) {
1405
+ if (policy?.lockoutEnabled === false || !user.lockedUntil) {
1183
1406
  return false;
1184
1407
  }
1185
1408
  return /* @__PURE__ */ new Date() < user.lockedUntil;
@@ -1187,16 +1410,16 @@ function isAccountLocked(user) {
1187
1410
  async function recordLoginFailure(userRepository, user, policy) {
1188
1411
  return userRepository.recordLoginFailure(user.id, policy);
1189
1412
  }
1190
- async function resetLoginFailures(userRepository, user) {
1191
- return userRepository.resetLoginFailures(user.id, user.credentialVersion ?? "");
1413
+ async function resetLoginFailures(userRepository, user, policy) {
1414
+ return userRepository.resetLoginFailures(user.id, user.credentialVersion ?? "", policy?.lockoutEnabled);
1192
1415
  }
1193
1416
 
1194
1417
  // src/authn/apikey/iam-authn-apikey-strategy.ts
1195
- var logger = core.logger.child({ module: "iam", scope: "apikey-strategy" });
1418
+ var logger2 = core.logger.child({ module: "iam", scope: "apikey-strategy" });
1196
1419
  var KEY_PREFIX_LENGTH = 12;
1197
- function generateRawKey(prefix) {
1420
+ function generateRawKey(prefix2) {
1198
1421
  const hex = randomBytes(32).toString("hex");
1199
- return `${prefix}${hex}`;
1422
+ return `${prefix2}${hex}`;
1200
1423
  }
1201
1424
  function extractKeyPrefix(rawKey) {
1202
1425
  return rawKey.slice(0, KEY_PREFIX_LENGTH);
@@ -1219,8 +1442,8 @@ function createApiKeyStrategy(config) {
1219
1442
  const { userRepository, apiKeyRepository } = config;
1220
1443
  const passwordOps = crypto$1.password;
1221
1444
  async function verifyRawKey(rawKey) {
1222
- const prefix = extractKeyPrefix(rawKey);
1223
- const candidatesResult = await apiKeyRepository.findByKeyPrefix(prefix);
1445
+ const prefix2 = extractKeyPrefix(rawKey);
1446
+ const candidatesResult = await apiKeyRepository.findByKeyPrefix(prefix2);
1224
1447
  if (!candidatesResult.success) {
1225
1448
  return candidatesResult;
1226
1449
  }
@@ -1237,7 +1460,7 @@ function createApiKeyStrategy(config) {
1237
1460
  return err(HaiIamError.APIKEY_EXPIRED, iamM("iam_apikeyExpired"));
1238
1461
  }
1239
1462
  apiKeyRepository.updateFields(candidate.id, { lastUsedAt: /* @__PURE__ */ new Date() }).catch(() => {
1240
- logger.warn("Failed to update API Key lastUsedAt", { keyId: candidate.id });
1463
+ logger2.warn("Failed to update API Key lastUsedAt", { keyId: candidate.id });
1241
1464
  });
1242
1465
  return ok(toApiKey(candidate));
1243
1466
  }
@@ -1264,10 +1487,10 @@ function createApiKeyStrategy(config) {
1264
1487
  if (!userResult.data) {
1265
1488
  return err(HaiIamError.USER_NOT_FOUND, iamM("iam_userNotExist"));
1266
1489
  }
1267
- if (!userResult.data.enabled) {
1490
+ if (!isUserActive(userResult.data)) {
1268
1491
  return err(HaiIamError.USER_DISABLED, iamM("iam_accountDisabled"));
1269
1492
  }
1270
- logger.info("API Key authentication succeeded", { userId: userResult.data.id, keyId: apiKey.id });
1493
+ logger2.info("API Key authentication succeeded", { userId: userResult.data.id, keyId: apiKey.id });
1271
1494
  return ok({ user: { ...toUser(userResult.data), credentialVersion: userResult.data.credentialVersion ?? "" }, apiKey });
1272
1495
  }
1273
1496
  };
@@ -1304,7 +1527,7 @@ function createApiKeyStrategy(config) {
1304
1527
  const insertResult = await apiKeyRepository.insert(storedApiKey);
1305
1528
  if (!insertResult.success)
1306
1529
  return insertResult;
1307
- logger.info("API Key created", { userId, keyId: id, name: options.name });
1530
+ logger2.info("API Key created", { userId, keyId: id, name: options.name });
1308
1531
  return ok({ apiKey: toApiKey(storedApiKey), rawKey });
1309
1532
  } catch (error) {
1310
1533
  return err(HaiIamError.INTERNAL_ERROR, iamM("iam_apikeyCreateFailed", { params: { message: String(error) } }), error);
@@ -1326,7 +1549,7 @@ function createApiKeyStrategy(config) {
1326
1549
  const result = await apiKeyRepository.removeById(keyId);
1327
1550
  if (!result.success)
1328
1551
  return result;
1329
- logger.info("API Key revoked", { keyId });
1552
+ logger2.info("API Key revoked", { keyId });
1330
1553
  return ok(void 0);
1331
1554
  },
1332
1555
  async verifyApiKey(rawKey) {
@@ -1335,6 +1558,120 @@ function createApiKeyStrategy(config) {
1335
1558
  };
1336
1559
  return { strategy, apiKeyFunctions };
1337
1560
  }
1561
+ function addresses(entries) {
1562
+ const list = new BlockList();
1563
+ for (const entry of entries) {
1564
+ const [address, prefix2] = entry.split("/");
1565
+ const family = isIP(address) === 4 ? "ipv4" : "ipv6";
1566
+ if (prefix2 === void 0)
1567
+ list.addAddress(address, family);
1568
+ else
1569
+ list.addSubnet(address, Number(prefix2), family);
1570
+ }
1571
+ return list;
1572
+ }
1573
+ function createLoginAccessCheck(policy) {
1574
+ if (!policy?.enabled)
1575
+ return () => ok(void 0);
1576
+ const allow = addresses(policy.allowIps);
1577
+ const deny = addresses(policy.denyIps);
1578
+ const format = new Intl.DateTimeFormat("en-US", { timeZone: policy.timezone, weekday: "short", hour: "2-digit", minute: "2-digit", hourCycle: "h23" });
1579
+ const weekdays = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
1580
+ return (context) => {
1581
+ const rejected = () => err(HaiIamError.LOGIN_ACCESS_DENIED, iamM("iam_loginAccessDenied"));
1582
+ if (policy.allowIps.length || policy.denyIps.length) {
1583
+ const raw = context?.ip?.trim();
1584
+ const ip = raw?.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/i)?.[1] ?? raw;
1585
+ if (!ip || !isIP(ip))
1586
+ return rejected();
1587
+ const family = isIP(ip) === 4 ? "ipv4" : "ipv6";
1588
+ if (deny.check(ip, family) || policy.allowIps.length && !allow.check(ip, family))
1589
+ return rejected();
1590
+ }
1591
+ if (policy.windows.length) {
1592
+ const parts = Object.fromEntries(format.formatToParts(/* @__PURE__ */ new Date()).map((part) => [part.type, part.value]));
1593
+ const day = weekdays.indexOf(parts.weekday) + 1;
1594
+ const previousDay = day === 1 ? 7 : day - 1;
1595
+ const time = `${parts.hour}:${parts.minute}`;
1596
+ 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);
1597
+ if (!permitted)
1598
+ return rejected();
1599
+ }
1600
+ return ok(void 0);
1601
+ };
1602
+ }
1603
+ var logger3 = core.logger.child({ module: "iam", scope: "captcha" });
1604
+ var prefix = "hai:iam:captcha:";
1605
+ var operationSchema = z.enum(["login", "register", "sendOtp", "passwordReset", "renewPassword"]);
1606
+ var proofSchema = z.object({
1607
+ id: z.string().regex(/^[a-f0-9]{64}$/),
1608
+ 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() })
1609
+ });
1610
+ async function createIamCaptcha(config, input) {
1611
+ if (!config.enabled)
1612
+ return err(HaiIamError.CAPTCHA_DISABLED, iamM("iam_captchaDisabled"));
1613
+ const operation = operationSchema.safeParse(input?.operation);
1614
+ if (!operation.success)
1615
+ return err(HaiIamError.CAPTCHA_INVALID, iamM("iam_captchaInvalid"));
1616
+ try {
1617
+ const id = randomBytes(32).toString("hex");
1618
+ const signatureSecret = randomBytes(32).toString("hex");
1619
+ const keySecret = randomBytes(32).toString("hex");
1620
+ const challenge = await createChallenge({
1621
+ algorithm: "PBKDF2/SHA-256",
1622
+ cost: config.cost,
1623
+ counter: randomInt(1, config.maxCounter + 1),
1624
+ deriveKey: pbkdf2.deriveKey,
1625
+ hmacSignatureSecret: signatureSecret,
1626
+ hmacKeySignatureSecret: keySecret,
1627
+ data: { operation: operation.data }
1628
+ });
1629
+ const record = { operation: operation.data, challenge, signatureSecret, keySecret };
1630
+ const saved = await cache.kv.set(`${prefix}${id}`, record, { nx: true, ex: config.ttlSeconds });
1631
+ if (!saved.success || !saved.data)
1632
+ return err(HaiIamError.CAPTCHA_UNAVAILABLE, iamM("iam_captchaUnavailable"));
1633
+ return ok({ id, challenge });
1634
+ } catch (error) {
1635
+ logger3.error("Captcha challenge generation failed", { operation: operation.data, error });
1636
+ return err(HaiIamError.CAPTCHA_UNAVAILABLE, iamM("iam_captchaUnavailable"));
1637
+ }
1638
+ }
1639
+ async function verifyIamCaptcha(config, operation, proof) {
1640
+ if (!config.enabled)
1641
+ return ok(void 0);
1642
+ if (!proof)
1643
+ return err(HaiIamError.CAPTCHA_REQUIRED, iamM("iam_captchaRequired"));
1644
+ if (typeof proof !== "string" || proof.length > 4096)
1645
+ return err(HaiIamError.CAPTCHA_INVALID, iamM("iam_captchaInvalid"));
1646
+ let payload;
1647
+ try {
1648
+ const parsed = proofSchema.safeParse(JSON.parse(proof));
1649
+ if (!parsed.success)
1650
+ return err(HaiIamError.CAPTCHA_INVALID, iamM("iam_captchaInvalid"));
1651
+ payload = parsed.data;
1652
+ } catch {
1653
+ return err(HaiIamError.CAPTCHA_INVALID, iamM("iam_captchaInvalid"));
1654
+ }
1655
+ try {
1656
+ const key = `${prefix}${payload.id}`;
1657
+ const found = await cache.kv.get(key);
1658
+ if (!found.success)
1659
+ return err(HaiIamError.CAPTCHA_UNAVAILABLE, iamM("iam_captchaUnavailable"));
1660
+ if (!found.data || found.data.operation !== operation)
1661
+ return err(HaiIamError.CAPTCHA_INVALID, iamM("iam_captchaInvalid"));
1662
+ const record = found.data;
1663
+ const result = await verifySolution({ challenge: record.challenge, solution: payload.solution, deriveKey: pbkdf2.deriveKey, hmacSignatureSecret: record.signatureSecret, hmacKeySignatureSecret: record.keySecret });
1664
+ if (!result.verified)
1665
+ return err(HaiIamError.CAPTCHA_INVALID, iamM("iam_captchaInvalid"));
1666
+ const removed = await cache.kv.del(key);
1667
+ if (!removed.success)
1668
+ return err(HaiIamError.CAPTCHA_UNAVAILABLE, iamM("iam_captchaUnavailable"));
1669
+ return removed.data === 1 ? ok(void 0) : err(HaiIamError.CAPTCHA_INVALID, iamM("iam_captchaInvalid"));
1670
+ } catch (error) {
1671
+ logger3.error("Captcha verification failed", { operation, error });
1672
+ return err(HaiIamError.CAPTCHA_UNAVAILABLE, iamM("iam_captchaUnavailable"));
1673
+ }
1674
+ }
1338
1675
  async function loadLdapClientFactory() {
1339
1676
  const { Client } = await import('ldapts');
1340
1677
  return async (config) => {
@@ -1389,7 +1726,7 @@ async function loadLdapClientFactory() {
1389
1726
  }
1390
1727
  };
1391
1728
  }
1392
- var logger2 = core.logger.child({ module: "iam", scope: "ldap-strategy" });
1729
+ var logger4 = core.logger.child({ module: "iam", scope: "ldap-strategy" });
1393
1730
  function escapeLdapFilterValue(value) {
1394
1731
  return value.replace(/[\\*()/\0]/g, (ch) => {
1395
1732
  const hex = ch.charCodeAt(0).toString(16).padStart(2, "0");
@@ -1405,6 +1742,7 @@ function createLdapStrategy(config) {
1405
1742
  maxLoginAttempts = 5,
1406
1743
  lockoutDuration = 900
1407
1744
  } = config;
1745
+ const failurePolicy = { maxLoginAttempts, lockoutDuration, lockoutEnabled: config.lockoutEnabled ?? true, failureWindow: config.failureWindow ?? 86400 };
1408
1746
  function getAttributeValue(entry, attr) {
1409
1747
  const value = entry.attributes[attr];
1410
1748
  if (Array.isArray(value)) {
@@ -1471,13 +1809,13 @@ function createLdapStrategy(config) {
1471
1809
  }
1472
1810
  let storedUser = localUserResult.data;
1473
1811
  if (storedUser) {
1474
- if (!storedUser.enabled) {
1812
+ if (!isUserActive(storedUser)) {
1475
1813
  return err(
1476
1814
  HaiIamError.USER_DISABLED,
1477
1815
  iamM("iam_accountDisabled")
1478
1816
  );
1479
1817
  }
1480
- if (isAccountLocked(storedUser)) {
1818
+ if (isAccountLocked(storedUser, failurePolicy)) {
1481
1819
  return err(
1482
1820
  HaiIamError.USER_LOCKED,
1483
1821
  iamM("iam_accountLocked")
@@ -1487,11 +1825,11 @@ function createLdapStrategy(config) {
1487
1825
  const userBindResult = await client.bind(entry.dn, password);
1488
1826
  if (!userBindResult.success) {
1489
1827
  if (storedUser) {
1490
- const failureResult = await recordLoginFailure(userRepository, storedUser, { maxLoginAttempts, lockoutDuration });
1828
+ const failureResult = await recordLoginFailure(userRepository, storedUser, failurePolicy);
1491
1829
  if (!failureResult.success)
1492
1830
  return failureResult;
1493
1831
  }
1494
- logger2.warn("LDAP authentication failed", { username });
1832
+ logger4.warn("LDAP authentication failed", { username });
1495
1833
  return err(
1496
1834
  HaiIamError.INVALID_CREDENTIALS,
1497
1835
  iamM("iam_passwordWrong")
@@ -1529,7 +1867,7 @@ function createLdapStrategy(config) {
1529
1867
  return assigned;
1530
1868
  }
1531
1869
  } catch (callbackError) {
1532
- logger2.warn("onUserAutoRegistered callback failed", { userId: storedUser?.id, error: callbackError });
1870
+ logger4.warn("onUserAutoRegistered callback failed", { userId: storedUser?.id, error: callbackError });
1533
1871
  }
1534
1872
  }
1535
1873
  } else if (storedUser && syncUser) {
@@ -1551,10 +1889,10 @@ function createLdapStrategy(config) {
1551
1889
  }
1552
1890
  if (!storedUser)
1553
1891
  return err(HaiIamError.USER_NOT_FOUND, iamM("iam_userNotExist"));
1554
- const resetResult = await resetLoginFailures(userRepository, storedUser);
1892
+ const resetResult = await resetLoginFailures(userRepository, storedUser, failurePolicy);
1555
1893
  if (!resetResult.success)
1556
1894
  return resetResult;
1557
- logger2.info("LDAP authentication succeeded", { userId: storedUser.id });
1895
+ logger4.info("LDAP authentication succeeded", { userId: storedUser.id });
1558
1896
  return ok({ ...toUser(storedUser), credentialVersion: storedUser.credentialVersion ?? "" });
1559
1897
  } finally {
1560
1898
  await client.unbind();
@@ -1632,7 +1970,7 @@ function createCacheOtpRepository() {
1632
1970
  };
1633
1971
  return otpRepoInstance;
1634
1972
  }
1635
- var logger3 = core.logger.child({ module: "iam", scope: "otp-strategy" });
1973
+ var logger5 = core.logger.child({ module: "iam", scope: "otp-strategy" });
1636
1974
  var PHONE_IDENTIFIER_REGEX = /^\+?\d{8,}$/;
1637
1975
  var PHONE_SEPARATOR_REGEX = /[\s-]/g;
1638
1976
  function generateOtpCode(length) {
@@ -1662,6 +2000,7 @@ function createOtpStrategy(config) {
1662
2000
  const otpConfig = config.otpConfig ? OtpConfigSchema.parse(config.otpConfig) : OtpConfigSchema.parse({});
1663
2001
  const maxLoginAttempts = config.maxLoginAttempts ?? 5;
1664
2002
  const lockoutDuration = config.lockoutDuration ?? 900;
2003
+ const failurePolicy = { maxLoginAttempts, lockoutDuration, lockoutEnabled: config.lockoutEnabled ?? true, failureWindow: config.failureWindow ?? 86400 };
1665
2004
  const registerConfig = config.registerConfig;
1666
2005
  const allowAutoRegister = registerConfig?.enabled ?? config.autoRegister ?? false;
1667
2006
  const defaultEnabled = registerConfig?.defaultEnabled ?? true;
@@ -1683,17 +2022,17 @@ function createOtpStrategy(config) {
1683
2022
  }
1684
2023
  let storedUser = userResult.data;
1685
2024
  if (storedUser) {
1686
- if (!storedUser.enabled) {
2025
+ if (!isUserActive(storedUser)) {
1687
2026
  return err(HaiIamError.USER_DISABLED, iamM("iam_accountDisabled"));
1688
2027
  }
1689
- if (isAccountLocked(storedUser)) {
2028
+ if (isAccountLocked(storedUser, failurePolicy)) {
1690
2029
  return err(HaiIamError.USER_LOCKED, iamM("iam_accountLocked"));
1691
2030
  }
1692
2031
  }
1693
2032
  const consumed = await config.otpRepository.consumeOtp(identifier, code, otpConfig.maxAttempts);
1694
2033
  if (!consumed.success) {
1695
2034
  if (storedUser && consumed.error.code === HaiIamError.OTP_INVALID.code) {
1696
- const failed = await recordLoginFailure(config.userRepository, storedUser, { maxLoginAttempts, lockoutDuration });
2035
+ const failed = await recordLoginFailure(config.userRepository, storedUser, failurePolicy);
1697
2036
  if (!failed.success)
1698
2037
  return failed;
1699
2038
  }
@@ -1729,19 +2068,19 @@ function createOtpStrategy(config) {
1729
2068
  return assigned;
1730
2069
  }
1731
2070
  } catch (callbackError) {
1732
- logger3.warn("onUserAutoRegistered callback failed", { userId: storedUser.id, error: callbackError });
2071
+ logger5.warn("onUserAutoRegistered callback failed", { userId: storedUser.id, error: callbackError });
1733
2072
  }
1734
2073
  }
1735
2074
  }
1736
2075
  if (!storedUser) {
1737
2076
  return err(HaiIamError.USER_NOT_FOUND, iamM("iam_userNotExist"));
1738
2077
  }
1739
- if (!storedUser.enabled)
2078
+ if (!isUserActive(storedUser))
1740
2079
  return err(HaiIamError.USER_DISABLED, iamM("iam_accountDisabled"));
1741
- const resetResult = await resetLoginFailures(config.userRepository, storedUser);
2080
+ const resetResult = await resetLoginFailures(config.userRepository, storedUser, failurePolicy);
1742
2081
  if (!resetResult.success)
1743
2082
  return resetResult;
1744
- logger3.info("OTP authentication succeeded", { userId: storedUser.id });
2083
+ logger5.info("OTP authentication succeeded", { userId: storedUser.id });
1745
2084
  return ok({ ...toUser(storedUser), credentialVersion: storedUser.credentialVersion ?? "" });
1746
2085
  }
1747
2086
  };
@@ -1785,12 +2124,12 @@ function createOtpStrategy(config) {
1785
2124
  );
1786
2125
  }
1787
2126
  const result = { expiresAt };
1788
- logger3.debug("OTP challenge sent", { identifier, expiresAt });
2127
+ logger5.debug("OTP challenge sent", { identifier, expiresAt });
1789
2128
  return ok(result);
1790
2129
  }
1791
2130
  return { strategy, challenge };
1792
2131
  }
1793
- var logger4 = core.logger.child({ module: "iam", scope: "password-strategy" });
2132
+ var logger6 = core.logger.child({ module: "iam", scope: "password-strategy" });
1794
2133
  var PASSWORD_UPPERCASE_REGEX = /[A-Z]/;
1795
2134
  var PASSWORD_LOWERCASE_REGEX = /[a-z]/;
1796
2135
  var PASSWORD_DIGIT_REGEX = /\d/;
@@ -1799,6 +2138,7 @@ function createPasswordStrategy(config) {
1799
2138
  const passwordConfig = config.passwordConfig ? PasswordConfigSchema.parse(config.passwordConfig) : PasswordConfigSchema.parse({});
1800
2139
  const maxLoginAttempts = config.maxLoginAttempts ?? 5;
1801
2140
  const lockoutDuration = config.lockoutDuration ?? 900;
2141
+ const failurePolicy = { maxLoginAttempts, lockoutDuration, lockoutEnabled: config.lockoutEnabled ?? true, failureWindow: config.failureWindow ?? 86400 };
1802
2142
  const passwordOps = crypto$1.password;
1803
2143
  function mapPasswordError(message) {
1804
2144
  return err(
@@ -1843,21 +2183,16 @@ function createPasswordStrategy(config) {
1843
2183
  iamM("iam_passwordNeedSpecialChar")
1844
2184
  );
1845
2185
  }
2186
+ const classes = [PASSWORD_UPPERCASE_REGEX, PASSWORD_LOWERCASE_REGEX, PASSWORD_DIGIT_REGEX, PASSWORD_SPECIAL_CHAR_REGEX].filter((pattern) => pattern.test(password)).length;
2187
+ if (classes < passwordConfig.minCharacterClasses)
2188
+ return err(HaiIamError.PASSWORD_POLICY_VIOLATION, iamM("iam_passwordCharacterClasses", { params: { count: passwordConfig.minCharacterClasses } }));
1846
2189
  return ok(void 0);
1847
2190
  }
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
2191
  function checkAccountStatus(user) {
1857
- if (!user.enabled) {
2192
+ if (!isUserActive(user)) {
1858
2193
  return err(HaiIamError.USER_DISABLED, iamM("iam_accountDisabled"));
1859
2194
  }
1860
- if (isAccountLocked(user)) {
2195
+ if (isAccountLocked(user, failurePolicy)) {
1861
2196
  return err(HaiIamError.USER_LOCKED, iamM("iam_accountLocked"));
1862
2197
  }
1863
2198
  return ok(void 0);
@@ -1871,10 +2206,10 @@ function createPasswordStrategy(config) {
1871
2206
  return mapPasswordError(verifyResult.error.message);
1872
2207
  }
1873
2208
  if (!verifyResult.data) {
1874
- const failureResult = await recordLoginFailure(config.userRepository, user, { maxLoginAttempts, lockoutDuration });
2209
+ const failureResult = await recordLoginFailure(config.userRepository, user, failurePolicy);
1875
2210
  if (!failureResult.success)
1876
2211
  return failureResult;
1877
- logger4.warn("Password verification failed", { userId: user.id });
2212
+ logger6.warn("Password verification failed", { userId: user.id });
1878
2213
  return err(HaiIamError.INVALID_CREDENTIALS, iamM("iam_passwordWrong"));
1879
2214
  }
1880
2215
  return ok(void 0);
@@ -1902,13 +2237,13 @@ function createPasswordStrategy(config) {
1902
2237
  const pwResult = await verifyUserPassword(storedUser, password);
1903
2238
  if (!pwResult.success)
1904
2239
  return pwResult;
1905
- if (isPasswordExpired(storedUser)) {
1906
- return err(HaiIamError.PASSWORD_EXPIRED, iamM("iam_passwordExpired"));
1907
- }
1908
- const resetResult = await resetLoginFailures(config.userRepository, storedUser);
2240
+ const passwordState = checkPasswordState(storedUser, passwordConfig);
2241
+ if (!passwordState.success)
2242
+ return passwordState;
2243
+ const resetResult = await resetLoginFailures(config.userRepository, storedUser, failurePolicy);
1909
2244
  if (!resetResult.success)
1910
2245
  return resetResult;
1911
- logger4.info("Password authentication succeeded", { userId: storedUser.id });
2246
+ logger6.info("Password authentication succeeded", { userId: storedUser.id });
1912
2247
  return ok({ ...toUser(storedUser), credentialVersion: storedUser.credentialVersion ?? "" });
1913
2248
  }
1914
2249
  };
@@ -1928,7 +2263,7 @@ function createPasswordStrategy(config) {
1928
2263
  }
1929
2264
 
1930
2265
  // src/authn/iam-authn-functions.ts
1931
- var logger5 = core.logger.child({ module: "iam", scope: "authn" });
2266
+ var logger7 = core.logger.child({ module: "iam", scope: "authn" });
1932
2267
  async function createAuthnOperations(deps) {
1933
2268
  try {
1934
2269
  const { config, sessionFunctions, authzFunctions, ldapClientFactory, ldapSyncUser, onOtpSendEmail, onOtpSendSms } = deps;
@@ -1949,6 +2284,8 @@ async function createAuthnOperations(deps) {
1949
2284
  passwordConfig: config.password,
1950
2285
  userRepository,
1951
2286
  maxLoginAttempts: securityConfig.maxLoginAttempts,
2287
+ lockoutEnabled: securityConfig.lockoutEnabled,
2288
+ failureWindow: securityConfig.failureWindow,
1952
2289
  lockoutDuration: securityConfig.lockoutDuration
1953
2290
  });
1954
2291
  let otpResult;
@@ -1962,6 +2299,8 @@ async function createAuthnOperations(deps) {
1962
2299
  autoRegister: true,
1963
2300
  registerConfig: config.register,
1964
2301
  maxLoginAttempts: securityConfig.maxLoginAttempts,
2302
+ lockoutEnabled: securityConfig.lockoutEnabled,
2303
+ failureWindow: securityConfig.failureWindow,
1965
2304
  lockoutDuration: securityConfig.lockoutDuration,
1966
2305
  onUserAutoRegistered,
1967
2306
  onOtpSendEmail,
@@ -1978,6 +2317,8 @@ async function createAuthnOperations(deps) {
1978
2317
  syncUser: ldapSyncUser ?? true,
1979
2318
  defaultEnabled: config.register.defaultEnabled,
1980
2319
  maxLoginAttempts: securityConfig.maxLoginAttempts,
2320
+ lockoutEnabled: securityConfig.lockoutEnabled,
2321
+ failureWindow: securityConfig.failureWindow,
1981
2322
  lockoutDuration: securityConfig.lockoutDuration,
1982
2323
  onUserAutoRegistered
1983
2324
  });
@@ -2006,10 +2347,10 @@ async function createAuthnOperations(deps) {
2006
2347
  userRepository,
2007
2348
  config
2008
2349
  });
2009
- logger5.info("Authn sub-feature initialized");
2350
+ logger7.info("Authn sub-feature initialized");
2010
2351
  return ok({ authn: operations.authn, loginRegisteredUser: operations.loginRegisteredUser, passwordStrategyResult: passwordResult, apiKeyFunctions });
2011
2352
  } catch (error) {
2012
- logger5.error("Authn sub-feature initialization failed", { error });
2353
+ logger7.error("Authn sub-feature initialization failed", { error });
2013
2354
  return err(
2014
2355
  HaiIamError.CONFIG_ERROR,
2015
2356
  iamM("iam_initComponentFailed"),
@@ -2031,6 +2372,7 @@ function buildAuthnOperations(deps) {
2031
2372
  } = deps;
2032
2373
  const loginConfig = LoginConfigSchema.parse(config.login ?? {});
2033
2374
  const agreementConfig = AgreementConfigSchema.parse(config.agreements ?? {});
2375
+ const checkAccess = createLoginAccessCheck(config.loginAccess);
2034
2376
  function loginDisabled(type) {
2035
2377
  if (!loginConfig[type]) {
2036
2378
  return err(
@@ -2061,9 +2403,20 @@ function buildAuthnOperations(deps) {
2061
2403
  iamM("iam_featureNotImplemented")
2062
2404
  );
2063
2405
  }
2064
- async function buildAuthResult(user, apiKey) {
2065
- if (!user.enabled)
2406
+ async function buildAuthResult(user, apiKey, context) {
2407
+ const permitted = checkAccess(context);
2408
+ if (!permitted.success)
2409
+ return permitted;
2410
+ if (!isUserActive(user))
2066
2411
  return err(HaiIamError.USER_DISABLED, iamM("iam_accountDisabled"));
2412
+ const stored = await userRepository.findById(user.id);
2413
+ if (!stored.success)
2414
+ return stored;
2415
+ if (!stored.data || !isUserActive(stored.data))
2416
+ return err(HaiIamError.USER_DISABLED, iamM("iam_accountDisabled"));
2417
+ const passwordState = checkPasswordState(stored.data, config.password);
2418
+ if (!passwordState.success)
2419
+ return passwordState;
2067
2420
  const authorization = await authzFunctions.getUserAuthorization(user.id);
2068
2421
  if (!authorization.success)
2069
2422
  return authorization;
@@ -2087,6 +2440,13 @@ function buildAuthnOperations(deps) {
2087
2440
  if (!tokenPair) {
2088
2441
  return err(HaiIamError.SESSION_CREATE_FAILED, iamM("iam_createSessionFailed"));
2089
2442
  }
2443
+ const recorded = await userRepository.recordSuccessfulLogin(user.id);
2444
+ if (!recorded.success) {
2445
+ const revoked = await sessionFunctions.delete(session.accessToken);
2446
+ if (!revoked.success)
2447
+ logger7.error("Failed to revoke session after login timestamp write failure", { userId: user.id, error: revoked.error });
2448
+ return recorded;
2449
+ }
2090
2450
  const { credentialVersion, ...publicUser } = user;
2091
2451
  return ok({
2092
2452
  user: publicUser,
@@ -2095,7 +2455,13 @@ function buildAuthnOperations(deps) {
2095
2455
  agreements: buildAgreementDisplay(agreementConfig, agreementConfig.showOnLogin)
2096
2456
  });
2097
2457
  }
2098
- async function loginWithStrategy(type, credentials, strategy) {
2458
+ async function loginWithStrategy(type, credentials, strategy, context) {
2459
+ const captcha = await verifyIamCaptcha(config.captcha, "login", credentials.captcha);
2460
+ if (!captcha.success)
2461
+ return captcha;
2462
+ const permitted = checkAccess(context);
2463
+ if (!permitted.success)
2464
+ return permitted;
2099
2465
  const disabled = loginDisabled(type);
2100
2466
  if (disabled)
2101
2467
  return disabled;
@@ -2107,33 +2473,89 @@ function buildAuthnOperations(deps) {
2107
2473
  ...credentials
2108
2474
  });
2109
2475
  if (!authResult.success) {
2110
- logger5.warn("Login failed", { type, reason: authResult.error.code });
2476
+ logger7.warn("Login failed", { type, reason: authResult.error.code });
2111
2477
  return authResult;
2112
2478
  }
2113
- const result = await buildAuthResult(authResult.data);
2479
+ const result = await buildAuthResult(authResult.data, void 0, context);
2114
2480
  if (result.success) {
2115
- logger5.info("Login succeeded", { type, userId: authResult.data.id });
2481
+ logger7.info("Login succeeded", { type, userId: authResult.data.id });
2116
2482
  }
2117
2483
  return result;
2118
2484
  }
2119
- return { loginRegisteredUser: async (user) => {
2485
+ async function verifyCurrentSession(accessToken) {
2486
+ const session = await sessionFunctions.verifyToken(accessToken);
2487
+ if (!session.success)
2488
+ return session;
2489
+ const [user, authorization] = await Promise.all([
2490
+ userRepository.findById(session.data.userId),
2491
+ authzFunctions.getUserAuthorization(session.data.userId)
2492
+ ]);
2493
+ if (!user.success)
2494
+ return user;
2495
+ if (!user.data?.enabled)
2496
+ return err(HaiIamError.USER_DISABLED, iamM("iam_accountDisabled"));
2497
+ if (session.data.credentialVersion !== (user.data.credentialVersion ?? ""))
2498
+ return err(HaiIamError.SESSION_INVALID, iamM("iam_sessionExpired"));
2499
+ if (!authorization.success)
2500
+ return authorization;
2501
+ const current = { ...session.data, username: user.data.username, displayName: user.data.displayName, avatarUrl: user.data.avatarUrl, ...authorization.data };
2502
+ return ok(current.apiKeyId ? applyApiKeyScopes(current, current.apiKeyScopes ?? []) : current);
2503
+ }
2504
+ const tickets = createTicketOperations();
2505
+ const sensitive = config.reauthentication;
2506
+ const requiresProof = (operation) => sensitive?.enabled && sensitive.operations.includes(operation);
2507
+ return { loginRegisteredUser: async (user, context) => {
2120
2508
  const stored = await userRepository.findById(user.id);
2121
2509
  if (!stored.success)
2122
2510
  return stored;
2123
- if (!stored.data?.enabled)
2511
+ if (!stored.data || !isUserActive(stored.data))
2124
2512
  return err(HaiIamError.USER_DISABLED, iamM("iam_accountDisabled"));
2125
- return buildAuthResult({ ...user, credentialVersion: stored.data.credentialVersion ?? "" });
2513
+ return buildAuthResult({ ...user, credentialVersion: stored.data.credentialVersion ?? "" }, void 0, context);
2126
2514
  }, authn: {
2127
- async login(credentials) {
2128
- return loginWithStrategy("password", credentials, passwordStrategy);
2515
+ createCaptcha: (input) => createIamCaptcha(config.captcha, input),
2516
+ async reauthenticate(accessToken, password, operation) {
2517
+ if (!requiresProof(operation))
2518
+ return err(HaiIamError.PERMISSION_DENIED, iamM("iam_reauthenticationRequired"));
2519
+ const current = await verifyCurrentSession(accessToken);
2520
+ if (!current.success)
2521
+ return current;
2522
+ const stored = await userRepository.findById(current.data.userId);
2523
+ if (!stored.success)
2524
+ return stored;
2525
+ if (!stored.data)
2526
+ return err(HaiIamError.INVALID_CREDENTIALS, iamM("iam_passwordWrong"));
2527
+ const verified = await passwordStrategy.authenticate({ type: "password", identifier: stored.data.username, password });
2528
+ if (!verified.success)
2529
+ return verified;
2530
+ if (verified.data.credentialVersion !== current.data.credentialVersion)
2531
+ return err(HaiIamError.SESSION_INVALID, iamM("iam_sessionExpired"));
2532
+ 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 } });
2533
+ },
2534
+ async checkSensitiveOperation(accessToken, operation, ticket) {
2535
+ const current = await verifyCurrentSession(accessToken);
2536
+ if (!current.success)
2537
+ return current;
2538
+ if (!requiresProof(operation))
2539
+ return ok(void 0);
2540
+ if (!ticket)
2541
+ return err(HaiIamError.REAUTHENTICATION_REQUIRED, iamM("iam_reauthenticationRequired"));
2542
+ const proof = await tickets.consume(ticket, { purpose: `iam:reauth:${operation}` });
2543
+ if (!proof.success)
2544
+ return proof;
2545
+ 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)
2546
+ return err(HaiIamError.REAUTHENTICATION_REQUIRED, iamM("iam_reauthenticationRequired"));
2547
+ return ok(void 0);
2129
2548
  },
2130
- async loginWithOtp(credentials) {
2131
- return loginWithStrategy("otp", credentials, otpStrategy);
2549
+ async login(credentials, context) {
2550
+ return loginWithStrategy("password", credentials, passwordStrategy, context);
2132
2551
  },
2133
- async loginWithLdap(credentials) {
2134
- return loginWithStrategy("ldap", credentials, ldapStrategy);
2552
+ async loginWithOtp(credentials, context) {
2553
+ return loginWithStrategy("otp", credentials, otpStrategy, context);
2135
2554
  },
2136
- async loginWithApiKey(credentials) {
2555
+ async loginWithLdap(credentials, context) {
2556
+ return loginWithStrategy("ldap", credentials, ldapStrategy, context);
2557
+ },
2558
+ async loginWithApiKey(credentials, context) {
2137
2559
  const disabled = loginDisabled("apikey");
2138
2560
  if (disabled)
2139
2561
  return disabled;
@@ -2142,31 +2564,16 @@ function buildAuthnOperations(deps) {
2142
2564
  const authenticated = await apiKeyStrategy.authenticate({ type: "apikey", ...credentials });
2143
2565
  if (!authenticated.success)
2144
2566
  return authenticated;
2145
- return buildAuthResult(authenticated.data.user, authenticated.data.apiKey);
2567
+ return buildAuthResult(authenticated.data.user, authenticated.data.apiKey, context);
2146
2568
  },
2147
2569
  async logout(accessToken) {
2148
2570
  return sessionFunctions.delete(accessToken);
2149
2571
  },
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) {
2572
+ verifyToken: verifyCurrentSession,
2573
+ async sendOtp(identifier, options) {
2574
+ const captcha = await verifyIamCaptcha(config.captcha, "sendOtp", options?.captcha);
2575
+ if (!captcha.success)
2576
+ return captcha;
2170
2577
  const disabled = loginDisabled("otp");
2171
2578
  if (disabled) {
2172
2579
  return disabled;
@@ -2840,7 +3247,7 @@ var DbRoleRepository = class extends BaseReldbCrudRepository {
2840
3247
  };
2841
3248
 
2842
3249
  // src/authz/iam-authz-functions.ts
2843
- var logger6 = core.logger.child({ module: "iam", scope: "authz" });
3250
+ var logger8 = core.logger.child({ module: "iam", scope: "authz" });
2844
3251
  async function createAuthzOperations(deps) {
2845
3252
  try {
2846
3253
  const { config, session } = deps;
@@ -2862,10 +3269,10 @@ async function createAuthzOperations(deps) {
2862
3269
  userRoleRepository: userRoleResult.data,
2863
3270
  session
2864
3271
  });
2865
- logger6.info("Authz sub-feature initialized");
3272
+ logger8.info("Authz sub-feature initialized");
2866
3273
  return ok(manager);
2867
3274
  } catch (error) {
2868
- logger6.error("Authz sub-feature initialization failed", { error });
3275
+ logger8.error("Authz sub-feature initialization failed", { error });
2869
3276
  return err(
2870
3277
  HaiIamError.CONFIG_ERROR,
2871
3278
  iamM("iam_initComponentFailed"),
@@ -2905,8 +3312,8 @@ function createRbacManager(config) {
2905
3312
  return true;
2906
3313
  }
2907
3314
  if (code.endsWith(":*")) {
2908
- const prefix = code.slice(0, -1);
2909
- return permission.startsWith(prefix);
3315
+ const prefix2 = code.slice(0, -1);
3316
+ return permission.startsWith(prefix2);
2910
3317
  }
2911
3318
  return false;
2912
3319
  }
@@ -2961,19 +3368,19 @@ function createRbacManager(config) {
2961
3368
  async function syncSessionPermissionsForRole(roleId) {
2962
3369
  const userIdsResult = await userRoleRepository.getUserIdsByRoleId(roleId);
2963
3370
  if (!userIdsResult.success) {
2964
- logger6.error("Failed to query users for permission sync", { roleId, error: userIdsResult.error.message });
3371
+ logger8.error("Failed to query users for permission sync", { roleId, error: userIdsResult.error.message });
2965
3372
  return;
2966
3373
  }
2967
3374
  await Promise.allSettled(
2968
3375
  userIdsResult.data.map(async (userId) => {
2969
3376
  const permResult = await resolveUserPermissionCodes(userId);
2970
3377
  if (!permResult.success) {
2971
- logger6.error("Failed to resolve permissions for session sync", { userId, roleId, error: permResult.error.message });
3378
+ logger8.error("Failed to resolve permissions for session sync", { userId, roleId, error: permResult.error.message });
2972
3379
  return;
2973
3380
  }
2974
3381
  const syncResult = await session.patchUserSessions(userId, { permissions: permResult.data });
2975
3382
  if (!syncResult.success) {
2976
- logger6.error("Failed to sync session permissions", { userId, roleId, error: syncResult.error.message });
3383
+ logger8.error("Failed to sync session permissions", { userId, roleId, error: syncResult.error.message });
2977
3384
  }
2978
3385
  })
2979
3386
  );
@@ -2987,17 +3394,17 @@ function createRbacManager(config) {
2987
3394
  if (roleCodesResult.success) {
2988
3395
  updates.roles = roleCodesResult.data;
2989
3396
  } else {
2990
- logger6.error("Failed to resolve roles for session sync", { userId, error: roleCodesResult.error.message });
3397
+ logger8.error("Failed to resolve roles for session sync", { userId, error: roleCodesResult.error.message });
2991
3398
  }
2992
3399
  if (permCodesResult.success) {
2993
3400
  updates.permissions = permCodesResult.data;
2994
3401
  } else {
2995
- logger6.error("Failed to resolve permissions for session sync", { userId, error: permCodesResult.error.message });
3402
+ logger8.error("Failed to resolve permissions for session sync", { userId, error: permCodesResult.error.message });
2996
3403
  }
2997
3404
  if (updates.roles !== void 0 || updates.permissions !== void 0) {
2998
3405
  const syncResult = await session.patchUserSessions(userId, updates);
2999
3406
  if (!syncResult.success) {
3000
- logger6.error("Failed to patch user sessions", { userId, error: syncResult.error.message });
3407
+ logger8.error("Failed to patch user sessions", { userId, error: syncResult.error.message });
3001
3408
  }
3002
3409
  }
3003
3410
  }
@@ -3065,8 +3472,7 @@ function createRbacManager(config) {
3065
3472
  }
3066
3473
  const result = await userRoleRepository.assign(userId, roleId, tx);
3067
3474
  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 } });
3475
+ logger8.info("Role assigned to user", { userId, roleId });
3070
3476
  if (!tx) {
3071
3477
  await syncUserSessionAfterRoleChange(userId);
3072
3478
  }
@@ -3076,8 +3482,7 @@ function createRbacManager(config) {
3076
3482
  async removeRole(userId, roleId, tx) {
3077
3483
  const result = await userRoleRepository.remove(userId, roleId, tx);
3078
3484
  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 } });
3485
+ logger8.info("Role removed from user", { userId, roleId });
3081
3486
  if (!tx) {
3082
3487
  await syncUserSessionAfterRoleChange(userId);
3083
3488
  }
@@ -3145,8 +3550,7 @@ function createRbacManager(config) {
3145
3550
  error
3146
3551
  );
3147
3552
  }
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 } });
3553
+ logger8.info("Roles synced for user", { userId, added: toAdd.length, removed: toRemove.length });
3150
3554
  if (ownTx) {
3151
3555
  await syncUserSessionAfterRoleChange(userId);
3152
3556
  }
@@ -3198,8 +3602,7 @@ function createRbacManager(config) {
3198
3602
  return mapRepositoryError2("iam_createRoleFailed", commitResult.error.message);
3199
3603
  }
3200
3604
  }
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 } });
3605
+ logger8.info("Role created", { roleId: createdResult.data.id, code: role.code });
3203
3606
  if (role.code === rbacConfig.superAdminRole) {
3204
3607
  superAdminRoleId = void 0;
3205
3608
  }
@@ -3296,10 +3699,9 @@ function createRbacManager(config) {
3296
3699
  affectedUsersResult.data.map((userId) => syncUserSessionAfterRoleChange(userId))
3297
3700
  );
3298
3701
  } else {
3299
- logger6.error("Failed to query affected users after updateRole", { roleId, error: affectedUsersResult.error.message });
3702
+ logger8.error("Failed to query affected users after updateRole", { roleId, error: affectedUsersResult.error.message });
3300
3703
  }
3301
3704
  }
3302
- void audit.helper.crud({ action: "update", resource: "iam_role", resourceId: roleId, details: data });
3303
3705
  return ok(updatedResult.data);
3304
3706
  } catch (error) {
3305
3707
  if (ownTx)
@@ -3372,8 +3774,7 @@ function createRbacManager(config) {
3372
3774
  );
3373
3775
  }
3374
3776
  superAdminRoleId = void 0;
3375
- logger6.info("Role deleted", { roleId });
3376
- void audit.helper.crud({ action: "delete", resource: "iam_role", resourceId: roleId });
3777
+ logger8.info("Role deleted", { roleId });
3377
3778
  return ok(void 0);
3378
3779
  },
3379
3780
  // ─── 权限管理 ───
@@ -3413,8 +3814,7 @@ function createRbacManager(config) {
3413
3814
  return mapRepositoryError2("iam_createPermissionFailed", commitResult.error.message);
3414
3815
  }
3415
3816
  }
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 } });
3817
+ logger8.info("Permission created", { permissionId: createdResult.data.id, code: permission.code });
3418
3818
  return ok(createdResult.data);
3419
3819
  } catch (error) {
3420
3820
  if (ownTx)
@@ -3518,8 +3918,7 @@ function createRbacManager(config) {
3518
3918
  affectedRoleIds.map((roleId) => syncSessionPermissionsForRole(roleId))
3519
3919
  );
3520
3920
  }
3521
- logger6.info("Permission deleted", { permissionId });
3522
- void audit.helper.crud({ action: "delete", resource: "iam_permission", resourceId: permissionId });
3921
+ logger8.info("Permission deleted", { permissionId });
3523
3922
  return ok(void 0);
3524
3923
  },
3525
3924
  async assignPermissionToRole(roleId, permissionId, tx) {
@@ -3541,8 +3940,7 @@ function createRbacManager(config) {
3541
3940
  }
3542
3941
  const assignResult = await rolePermissionRepository.assign(roleId, permissionId, tx);
3543
3942
  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 } });
3943
+ logger8.info("Permission assigned to role", { roleId, permissionId });
3546
3944
  if (!tx) {
3547
3945
  await syncSessionPermissionsForRole(roleId);
3548
3946
  }
@@ -3571,8 +3969,7 @@ function createRbacManager(config) {
3571
3969
  }
3572
3970
  const removeResult = await rolePermissionRepository.remove(roleId, permissionId, tx);
3573
3971
  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 } });
3972
+ logger8.info("Permission removed from role", { roleId, permissionId });
3576
3973
  if (!tx) {
3577
3974
  await syncSessionPermissionsForRole(roleId);
3578
3975
  }
@@ -3590,7 +3987,108 @@ function createRbacManager(config) {
3590
3987
  }
3591
3988
  };
3592
3989
  }
3990
+ var logger9 = core.logger.child({ module: "iam", scope: "audit" });
3991
+ async function observe(operation, event, tx) {
3992
+ const result = await operation();
3993
+ const fields = typeof event === "function" ? event(result) : event;
3994
+ if (!fields)
3995
+ return result;
3996
+ if (tx && !result.success) {
3997
+ logger9.warn("IAM transactional operation failed", { action: fields.action, resource: fields.resource, reasonCode: result.error.code });
3998
+ return result;
3999
+ }
4000
+ const recorded = await audit.capture({
4001
+ ...fields,
4002
+ category: "system",
4003
+ outcome: result.success ? "success" : [401, 403, 409, 423, 429].includes(result.error.httpStatus ?? 500) ? "denied" : "failure",
4004
+ details: { ...fields.details, ...!result.success ? { reasonCode: result.error.code } : {} }
4005
+ }, { tx });
4006
+ if (!recorded.success) {
4007
+ logger9.error("IAM event audit failed", { action: fields.action, resource: fields.resource, error: recorded.error });
4008
+ if (tx)
4009
+ return recorded;
4010
+ }
4011
+ return result;
4012
+ }
4013
+ function loginEvent(method, result, context) {
4014
+ return {
4015
+ action: "auth.login",
4016
+ resource: "iam.auth",
4017
+ details: { method },
4018
+ // 登录结果本身已完成鉴别,不把客户端提交的标识声明当成已验证主体。
4019
+ userId: result.success ? result.data.user.id : null,
4020
+ actorSnapshot: result.success ? { id: result.data.user.id, username: result.data.user.username, displayName: result.data.user.displayName, roles: result.data.roles } : null,
4021
+ ipAddress: context?.ip,
4022
+ userAgent: context?.userAgent?.slice(0, 4096),
4023
+ requestId: context?.requestId
4024
+ };
4025
+ }
4026
+ function observeAuth(operations) {
4027
+ return {
4028
+ ...operations,
4029
+ login: (credentials, context) => observe(() => operations.login(credentials, context), (result) => loginEvent("password", result, context)),
4030
+ loginWithOtp: (credentials, context) => observe(() => operations.loginWithOtp(credentials, context), (result) => loginEvent("otp", result, context)),
4031
+ loginWithLdap: (credentials, context) => observe(() => operations.loginWithLdap(credentials, context), (result) => loginEvent("ldap", result, context)),
4032
+ loginWithApiKey: (credentials, context) => observe(() => operations.loginWithApiKey(credentials, context), (result) => loginEvent("apikey", result, context)),
4033
+ registerAndLogin: (options, context) => observe(() => operations.registerAndLogin(options, context), (result) => loginEvent("registration", result, context)),
4034
+ logout: (token) => observe(() => operations.logout(token), { action: "auth.logout", resource: "iam.auth" }),
4035
+ sendOtp: (identifier, options) => observe(() => operations.sendOtp(identifier, options), { action: "otp.send", resource: "iam.auth" }),
4036
+ reauthenticate: (token, password, operation) => observe(() => operations.reauthenticate(token, password, operation), { action: "auth.reauthenticate", resource: "iam.auth", details: { operation } }),
4037
+ checkSensitiveOperation: (token, operation, ticket) => observe(() => operations.checkSensitiveOperation(token, operation, ticket), (result) => !result.success ? { action: "auth.sensitive-denied", resource: "iam.auth", details: { operation } } : void 0)
4038
+ };
4039
+ }
4040
+ function observeUsers(operations) {
4041
+ return {
4042
+ ...operations,
4043
+ register: (options) => observe(() => operations.register(options), (result) => ({ action: "user.register", resource: "iam.users", resourceId: result.success ? result.data.user.id : void 0 })),
4044
+ createUser: (options) => observe(() => operations.createUser(options), (result) => ({ action: "user.create", resource: "iam.users", resourceId: result.success ? result.data.user.id : void 0 })),
4045
+ inviteUser: (options) => observe(() => operations.inviteUser(options), (result) => ({ action: "user.invite", resource: "iam.users", resourceId: result.success ? result.data.user.id : void 0 })),
4046
+ 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 } : {} } }),
4047
+ 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) } })),
4048
+ deleteUser: (id) => observe(() => operations.deleteUser(id), { action: "user.delete", resource: "iam.users", resourceId: id }),
4049
+ adminResetPassword: (id, password) => observe(() => operations.adminResetPassword(id, password), { action: "password.admin-reset", resource: "iam.users", resourceId: id }),
4050
+ changePassword: (id, previous, next) => observe(() => operations.changePassword(id, previous, next), { action: "password.change", resource: "iam.users", resourceId: id }),
4051
+ changeCurrentUserPassword: (token, previous, next) => observe(() => operations.changeCurrentUserPassword(token, previous, next), { action: "password.change", resource: "iam.users" }),
4052
+ renewPassword: (input) => observe(() => operations.renewPassword(input), { action: "password.renew", resource: "iam.users" }),
4053
+ requestPasswordReset: (identifier, options) => observe(() => operations.requestPasswordReset(identifier, options), { action: "password.reset-request", resource: "iam.users" }),
4054
+ confirmPasswordReset: (token, password) => observe(() => operations.confirmPasswordReset(token, password), { action: "password.reset-complete", resource: "iam.users" }),
4055
+ 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 } : {} })
4056
+ };
4057
+ }
4058
+ function observeAuthorization(operations) {
4059
+ return {
4060
+ ...operations,
4061
+ assignRole: (id, roleId, tx) => observe(() => operations.assignRole(id, roleId, tx), { action: "role.assign", resource: "iam.user-roles", resourceId: id, details: { roleId } }, tx),
4062
+ removeRole: (id, roleId, tx) => observe(() => operations.removeRole(id, roleId, tx), { action: "role.remove", resource: "iam.user-roles", resourceId: id, details: { roleId } }, tx),
4063
+ syncRoles: (id, roleIds, tx) => observe(() => operations.syncRoles(id, roleIds, tx), { action: "role.sync", resource: "iam.user-roles", resourceId: id, details: { roleIds } }, tx),
4064
+ 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),
4065
+ 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),
4066
+ deleteRole: (id, tx) => observe(() => operations.deleteRole(id, tx), { action: "role.delete", resource: "iam.roles", resourceId: id }, tx),
4067
+ 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),
4068
+ deletePermission: (id, tx) => observe(() => operations.deletePermission(id, tx), { action: "permission.delete", resource: "iam.permissions", resourceId: id }, tx),
4069
+ assignPermissionToRole: (id, permissionId, tx) => observe(() => operations.assignPermissionToRole(id, permissionId, tx), { action: "permission.assign", resource: "iam.role-permissions", resourceId: id, details: { permissionId } }, tx),
4070
+ removePermissionFromRole: (id, permissionId, tx) => observe(() => operations.removePermissionFromRole(id, permissionId, tx), { action: "permission.remove", resource: "iam.role-permissions", resourceId: id, details: { permissionId } }, tx)
4071
+ };
4072
+ }
4073
+ function observeSessions(operations) {
4074
+ return {
4075
+ ...operations,
4076
+ create: (options) => observe(() => operations.create(options), (result) => ({ action: "session.create", resource: "iam.sessions", resourceId: result.success ? result.data.version : options.userId })),
4077
+ delete: (token) => observe(() => operations.delete(token), { action: "session.delete", resource: "iam.sessions" }),
4078
+ deleteByUserId: (id) => observe(() => operations.deleteByUserId(id), (result) => ({ action: "session.revoke-user", resource: "iam.sessions", resourceId: id, details: result.success ? { count: result.data } : {} })),
4079
+ refresh: (token) => observe(() => operations.refresh(token), { action: "session.refresh", resource: "iam.sessions" }),
4080
+ revokeRefresh: (token) => observe(() => operations.revokeRefresh(token), { action: "session.revoke-refresh", resource: "iam.sessions" })
4081
+ };
4082
+ }
4083
+ function observeApiKeys(operations) {
4084
+ return {
4085
+ ...operations,
4086
+ 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 } })),
4087
+ revokeApiKey: (id) => observe(() => operations.revokeApiKey(id), { action: "apikey.revoke", resource: "iam.apikeys", resourceId: id })
4088
+ };
4089
+ }
3593
4090
  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" };
4091
+ 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
4092
  var steps = [{
3595
4093
  version: 1,
3596
4094
  name: "credential-and-reset-versions",
@@ -3611,6 +4109,48 @@ var steps = [{
3611
4109
  }
3612
4110
  return ok(void 0);
3613
4111
  }
4112
+ }, {
4113
+ version: 2,
4114
+ name: "account-lifecycle",
4115
+ async needed(sql) {
4116
+ const found = await schemaColumns(reldb, "hai_iam_users", sql);
4117
+ return found.success ? ok(found.data.length > 0 && Object.keys(accountColumns).some((name) => !found.data.includes(name))) : found;
4118
+ },
4119
+ async apply(sql) {
4120
+ const found = await schemaColumns(reldb, "hai_iam_users", sql);
4121
+ if (!found.success)
4122
+ return found;
4123
+ for (const [name, definition] of Object.entries(accountColumns)) {
4124
+ if (found.data.includes(name))
4125
+ continue;
4126
+ const added = await sql.execute(`ALTER TABLE hai_iam_users ADD COLUMN ${name} ${definition}`);
4127
+ if (!added.success)
4128
+ return added;
4129
+ }
4130
+ return ok(void 0);
4131
+ }
4132
+ }, {
4133
+ version: 3,
4134
+ name: "initial-password-state",
4135
+ async needed(sql) {
4136
+ const found = await schemaColumns(reldb, "hai_iam_users", sql);
4137
+ return found.success ? ok(found.data.length > 0 && !found.data.includes("must_change_password")) : found;
4138
+ },
4139
+ async apply(sql) {
4140
+ const added = await sql.execute("ALTER TABLE hai_iam_users ADD COLUMN must_change_password BOOLEAN NOT NULL DEFAULT FALSE");
4141
+ return added.success ? ok(void 0) : added;
4142
+ }
4143
+ }, {
4144
+ version: 4,
4145
+ name: "login-failure-window",
4146
+ async needed(sql) {
4147
+ const found = await schemaColumns(reldb, "hai_iam_users", sql);
4148
+ return found.success ? ok(found.data.length > 0 && !found.data.includes("first_login_failed_at")) : found;
4149
+ },
4150
+ async apply(sql) {
4151
+ const added = await sql.execute("ALTER TABLE hai_iam_users ADD COLUMN first_login_failed_at TIMESTAMP");
4152
+ return added.success ? ok(void 0) : added;
4153
+ }
3614
4154
  }];
3615
4155
  function migrateIam(options = {}) {
3616
4156
  return migrateSchema(reldb, "iam", steps, options);
@@ -3658,9 +4198,11 @@ async function resolveIamNotifications(input) {
3658
4198
  // src/iam-public-config.ts
3659
4199
  function buildIamPublicConfig(config, runtime) {
3660
4200
  const otp = OtpConfigSchema.parse(config.otp ?? {});
4201
+ const session = SessionConfigSchema.parse(config.session ?? {});
3661
4202
  const email = config.login.otp && !!config.otp && otp.email && !!runtime.onOtpSendEmail;
3662
4203
  const sms = config.login.otp && !!config.otp && otp.sms && !!runtime.onOtpSendSms;
3663
4204
  return {
4205
+ captcha: { enabled: config.captcha.enabled },
3664
4206
  password: PasswordConfigSchema.parse(config.password ?? {}),
3665
4207
  register: { enabled: config.register.enabled },
3666
4208
  login: { password: config.login.password, otp: email || sms, ldap: config.login.ldap && !!config.ldap, email, sms },
@@ -3668,10 +4210,12 @@ function buildIamPublicConfig(config, runtime) {
3668
4210
  otp: { length: otp.length, resendInterval: otp.resendInterval },
3669
4211
  agreements: { ...config.agreements },
3670
4212
  ui: AuthUiConfigSchema.parse(config.ui ?? {}),
3671
- session: { maxAge: SessionConfigSchema.parse(config.session ?? {}).maxAge }
4213
+ session: { maxAge: session.maxAge, endOnPageHide: session.endOnPageHide },
4214
+ reauthentication: { enabled: config.reauthentication?.enabled ?? false, operations: [...config.reauthentication?.operations ?? []] },
4215
+ initialCredentials: { enabled: Boolean(config.initialCredentials?.enabled && config.passwordReset?.enabled !== false && runtime.onPasswordResetRequest) }
3672
4216
  };
3673
4217
  }
3674
- var logger7 = core.logger.child({ module: "iam", scope: "seed" });
4218
+ var logger10 = core.logger.child({ module: "iam", scope: "seed" });
3675
4219
  var DEFAULT_ROLES = [
3676
4220
  { code: "admin", name: () => iamM("iam_seedRoleAdminName"), description: () => iamM("iam_seedRoleAdminDesc"), isSystem: true },
3677
4221
  { code: "user", name: () => iamM("iam_seedRoleUserName"), description: () => iamM("iam_seedRoleUserDesc"), isSystem: true },
@@ -3771,10 +4315,10 @@ async function seedIamData(authz) {
3771
4315
  }
3772
4316
  }
3773
4317
  }
3774
- logger7.info("IAM seed data initialized");
4318
+ logger10.info("IAM seed data initialized");
3775
4319
  return ok(void 0);
3776
4320
  } catch (error) {
3777
- logger7.error("Failed to seed IAM data", { error });
4321
+ logger10.error("Failed to seed IAM data", { error });
3778
4322
  return err(
3779
4323
  HaiIamError.REPOSITORY_ERROR,
3780
4324
  iamM("iam_initSeedDataFailed"),
@@ -3782,81 +4326,12 @@ async function seedIamData(authz) {
3782
4326
  );
3783
4327
  }
3784
4328
  }
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
4329
  function retainedUntil(entry) {
3855
4330
  return Math.max(new Date(entry.session.expiresAt).getTime(), entry.refreshToken ? entry.refreshExpiresAt : 0);
3856
4331
  }
3857
4332
  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()))) {
4333
+ 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) };
4334
+ 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.startedAt, result.lastActiveAt, result.expiresAt].every((date) => Number.isFinite(date.getTime()))) {
3860
4335
  throw new TypeError("Invalid session state");
3861
4336
  }
3862
4337
  return result;
@@ -3867,7 +4342,16 @@ function stateKey(scope) {
3867
4342
  function repositoryFailure(cause) {
3868
4343
  return err(HaiIamError.REPOSITORY_ERROR, iamM("iam_sessionStateFailed"), cause);
3869
4344
  }
3870
- function createCacheSessionRepository(refreshTokenMaxAge) {
4345
+ function createCacheSessionRepository(refreshTokenMaxAge, policy = {}) {
4346
+ const idleTimeout = policy.idleTimeout ?? 0;
4347
+ const absoluteTimeout = policy.absoluteTimeout ?? 0;
4348
+ const maxSessions = policy.maxSessions ?? 0;
4349
+ function policyExpired(session) {
4350
+ const now = Date.now();
4351
+ const activeAt = new Date(session.lastActiveAt).getTime();
4352
+ const startedAt = new Date(session.startedAt ?? session.createdAt).getTime();
4353
+ return idleTimeout > 0 && activeAt + idleTimeout * 1e3 <= now || absoluteTimeout > 0 && startedAt + absoluteTimeout * 1e3 <= now;
4354
+ }
3871
4355
  async function read(scope) {
3872
4356
  try {
3873
4357
  const result = await cache.kv.get(stateKey(scope));
@@ -3897,7 +4381,7 @@ function createCacheSessionRepository(refreshTokenMaxAge) {
3897
4381
  return current;
3898
4382
  const next = current.data ? JSON.parse(JSON.stringify(current.data)) : { revision: "", sessions: {} };
3899
4383
  for (const [token, entry] of Object.entries(next.sessions)) {
3900
- if (retainedUntil(entry) <= Date.now())
4384
+ if (retainedUntil(entry) <= Date.now() || policyExpired(entry.session))
3901
4385
  delete next.sessions[token];
3902
4386
  }
3903
4387
  const outcome = operation(next);
@@ -3926,16 +4410,23 @@ function createCacheSessionRepository(refreshTokenMaxAge) {
3926
4410
  }
3927
4411
  return {
3928
4412
  save: (session, tokens, singleDevice = false) => mutate(getUserSessionScope(session.userId), (state) => {
3929
- if (singleDevice)
4413
+ const count = Object.values(state.sessions).filter((entry) => entry.session.credentialVersion === session.credentialVersion).length;
4414
+ if (singleDevice && policy.singleDeviceMode !== "reject") {
3930
4415
  state.sessions = {};
4416
+ } else if (singleDevice && count > 0 || maxSessions > 0 && count >= maxSessions) {
4417
+ return err(HaiIamError.SESSION_LIMIT_EXCEEDED, iamM("iam_sessionLimitExceeded"));
4418
+ }
3931
4419
  return storeSession(state, session, tokens);
3932
4420
  }),
3933
4421
  rotate: (previous, refreshToken, session, tokens) => mutate(getUserSessionScope(previous.userId), (state) => {
3934
4422
  const old = state.sessions[previous.accessToken];
3935
- if (!old || old.refreshToken !== refreshToken || old.refreshExpiresAt <= Date.now())
4423
+ if (!old || old.refreshToken !== refreshToken || old.refreshExpiresAt <= Date.now() || policyExpired(old.session))
3936
4424
  return err(HaiIamError.TOKEN_EXPIRED, iamM("iam_refreshTokenExpired"));
3937
4425
  if (old.session.version !== previous.version || session.userId !== previous.userId)
3938
4426
  return err(HaiIamError.CONFLICT, iamM("iam_sessionStateConflict"));
4427
+ session.startedAt = new Date(old.session.startedAt ?? old.session.createdAt);
4428
+ if (idleTimeout > 0)
4429
+ session.lastActiveAt = new Date(old.session.lastActiveAt);
3939
4430
  delete state.sessions[previous.accessToken];
3940
4431
  return storeSession(state, session, tokens);
3941
4432
  }),
@@ -3950,7 +4441,7 @@ function createCacheSessionRepository(refreshTokenMaxAge) {
3950
4441
  if (!record)
3951
4442
  return ok(null);
3952
4443
  const session = restoreSession(record.session);
3953
- return ok(session.expiresAt.getTime() <= Date.now() ? null : session);
4444
+ return ok(session.expiresAt.getTime() <= Date.now() || policyExpired(session) ? null : session);
3954
4445
  },
3955
4446
  async getByRefreshToken(token) {
3956
4447
  const scope = getTokenSessionScope(token);
@@ -3962,7 +4453,8 @@ function createCacheSessionRepository(refreshTokenMaxAge) {
3962
4453
  const record = Object.values(result.data?.sessions ?? {}).find((entry) => entry.refreshToken === token && entry.refreshExpiresAt > Date.now());
3963
4454
  if (!record)
3964
4455
  return ok(null);
3965
- return ok(restoreSession(record.session));
4456
+ const session = restoreSession(record.session);
4457
+ return ok(policyExpired(session) ? null : session);
3966
4458
  },
3967
4459
  async updateByAccessToken(token, patch, expectedVersion) {
3968
4460
  const scope = getTokenSessionScope(token);
@@ -3970,7 +4462,7 @@ function createCacheSessionRepository(refreshTokenMaxAge) {
3970
4462
  return err(HaiIamError.SESSION_NOT_FOUND, iamM("iam_sessionNotExist"));
3971
4463
  return mutate(scope, (state) => {
3972
4464
  const entry = state.sessions[token];
3973
- if (!entry || new Date(entry.session.expiresAt).getTime() <= Date.now())
4465
+ if (!entry || new Date(entry.session.expiresAt).getTime() <= Date.now() || policyExpired(entry.session))
3974
4466
  return err(HaiIamError.SESSION_NOT_FOUND, iamM("iam_sessionNotExist"));
3975
4467
  if (!expectedVersion || entry.session.version !== expectedVersion)
3976
4468
  return err(HaiIamError.CONFLICT, iamM("iam_sessionStateConflict"));
@@ -3987,7 +4479,7 @@ function createCacheSessionRepository(refreshTokenMaxAge) {
3987
4479
  return err(HaiIamError.SESSION_NOT_FOUND, iamM("iam_sessionNotExist"));
3988
4480
  return mutate(scope, (state) => {
3989
4481
  const entry = state.sessions[token];
3990
- if (!entry || new Date(entry.session.expiresAt).getTime() <= Date.now())
4482
+ if (!entry || new Date(entry.session.expiresAt).getTime() <= Date.now() || policyExpired(entry.session))
3991
4483
  return err(HaiIamError.SESSION_NOT_FOUND, iamM("iam_sessionNotExist"));
3992
4484
  if (entry.session.version !== expectedVersion)
3993
4485
  return err(HaiIamError.CONFLICT, iamM("iam_sessionStateConflict"));
@@ -3995,7 +4487,7 @@ function createCacheSessionRepository(refreshTokenMaxAge) {
3995
4487
  const next = {
3996
4488
  ...current,
3997
4489
  lastActiveAt: new Date(Math.max(current.lastActiveAt.getTime(), now.getTime())),
3998
- expiresAt: new Date(Math.max(current.expiresAt.getTime(), now.getTime() + current.maxAge * 1e3))
4490
+ expiresAt: policy.sliding === false ? current.expiresAt : new Date(Math.max(current.expiresAt.getTime(), now.getTime() + current.maxAge * 1e3))
3999
4491
  };
4000
4492
  entry.session = next;
4001
4493
  return ok(next);
@@ -4053,26 +4545,28 @@ function stripInternalSessionFields(session) {
4053
4545
  const hasExtraKeys = Object.keys(rest).length > 0;
4054
4546
  return { ...session, data: hasExtraKeys ? rest : void 0 };
4055
4547
  }
4056
- var logger8 = core.logger.child({ module: "iam", scope: "session" });
4548
+ var logger11 = core.logger.child({ module: "iam", scope: "session" });
4057
4549
  async function createSessionOperations(deps) {
4058
4550
  try {
4059
4551
  const { config } = deps;
4060
4552
  const sessionConfig = SessionConfigSchema.parse(config.session ?? {});
4061
4553
  const sessionRepository = createCacheSessionRepository(
4062
- sessionConfig.refreshTokenMaxAge ?? 604800
4554
+ sessionConfig.refreshTokenMaxAge ?? 604800,
4555
+ sessionConfig
4063
4556
  );
4064
4557
  const functions = buildSessionFunctions({
4065
4558
  readCredential: deps.readCredential,
4066
4559
  maxAge: sessionConfig.maxAge,
4067
4560
  sliding: sessionConfig.sliding,
4068
4561
  singleDevice: sessionConfig.singleDevice,
4562
+ idleTimeout: sessionConfig.idleTimeout,
4069
4563
  sessionRepository,
4070
4564
  apiKeyRepository: config.login.apikey ? await createDbApiKeyRepository() : void 0
4071
4565
  });
4072
- logger8.info("Session sub-feature initialized");
4566
+ logger11.info("Session sub-feature initialized");
4073
4567
  return ok(functions);
4074
4568
  } catch (error) {
4075
- logger8.error("Session sub-feature initialization failed", { error });
4569
+ logger11.error("Session sub-feature initialization failed", { error });
4076
4570
  return err(
4077
4571
  HaiIamError.CONFIG_ERROR,
4078
4572
  iamM("iam_initComponentFailed"),
@@ -4141,7 +4635,7 @@ function buildSessionFunctions(config) {
4141
4635
  if (!saveResult.success) {
4142
4636
  return saveResult;
4143
4637
  }
4144
- logger8.debug("Session created", { userId: options.userId });
4638
+ logger11.debug("Session created", { userId: options.userId });
4145
4639
  return ok(session);
4146
4640
  } catch (error) {
4147
4641
  return err(
@@ -4176,7 +4670,7 @@ function buildSessionFunctions(config) {
4176
4670
  if (!validated.success)
4177
4671
  return validated;
4178
4672
  let output = validated.data;
4179
- if (sliding) {
4673
+ if (sliding || (config.idleTimeout ?? 0) > 0) {
4180
4674
  const updateResult = await repo.touchByAccessToken(accessToken, session.version, /* @__PURE__ */ new Date());
4181
4675
  if (!updateResult.success) {
4182
4676
  return updateResult;
@@ -4202,7 +4696,7 @@ function buildSessionFunctions(config) {
4202
4696
  return repo.updateByAccessToken(accessToken, data, expectedVersion);
4203
4697
  },
4204
4698
  async delete(accessToken) {
4205
- logger8.debug("Session delete requested");
4699
+ logger11.debug("Session delete requested");
4206
4700
  return repo.removeByAccessToken(accessToken);
4207
4701
  },
4208
4702
  async deleteByUserId(userId) {
@@ -4255,7 +4749,7 @@ function buildSessionFunctions(config) {
4255
4749
  const current = await credential(oldSession.userId, oldSession.credentialVersion);
4256
4750
  if (!current.success)
4257
4751
  return current;
4258
- logger8.debug("Token refreshed", { userId: oldSession.userId });
4752
+ logger11.debug("Token refreshed", { userId: oldSession.userId });
4259
4753
  return ok(next.data.tokens);
4260
4754
  } catch (error) {
4261
4755
  return err(
@@ -4273,63 +4767,7 @@ function buildSessionFunctions(config) {
4273
4767
  }
4274
4768
  };
4275
4769
  }
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" });
4770
+ var logger12 = core.logger.child({ module: "iam", scope: "user" });
4333
4771
  var USER_SORT_COLUMNS = {
4334
4772
  /** 用户名列。 */
4335
4773
  username: "username",
@@ -4354,10 +4792,10 @@ async function createUserOperations(deps) {
4354
4792
  config,
4355
4793
  onPasswordResetRequest
4356
4794
  });
4357
- logger10.info("User sub-feature initialized");
4795
+ logger12.info("User sub-feature initialized");
4358
4796
  return ok(functions);
4359
4797
  } catch (error) {
4360
- logger10.error("User sub-feature initialization failed", { error });
4798
+ logger12.error("User sub-feature initialization failed", { error });
4361
4799
  return err(
4362
4800
  HaiIamError.CONFIG_ERROR,
4363
4801
  iamM("iam_initComponentFailed"),
@@ -4420,10 +4858,26 @@ async function validateUniqueFieldsForUpdate(userRepository, userId, data) {
4420
4858
  }
4421
4859
  return ok(void 0);
4422
4860
  }
4861
+ function validatePasswordReplacement(ctx, user, password) {
4862
+ const strength = ctx.validatePassword(password);
4863
+ if (!strength.success)
4864
+ return strength;
4865
+ if (user.passwordHash && (user.mustChangePassword && ctx.config.password?.requireInitialChange || ctx.config.password?.preventSamePassword || (ctx.config.password?.expirationDays ?? 0) > 0)) {
4866
+ const same = crypto$1.password.verify(password, user.passwordHash);
4867
+ if (!same.success)
4868
+ return same;
4869
+ if (same.data)
4870
+ return err(HaiIamError.PASSWORD_POLICY_VIOLATION, iamM("iam_passwordMustDiffer"));
4871
+ }
4872
+ return ok(void 0);
4873
+ }
4423
4874
  function buildRegistrationOps(ctx) {
4424
4875
  const { userRepository, authzFunctions, config, registerConfig, agreementConfig } = ctx;
4425
4876
  const { validatePassword, hashPassword } = ctx;
4426
4877
  async function validateRegisterPreconditions(options) {
4878
+ const accountPolicy = validateAccountPolicy(options);
4879
+ if (!accountPolicy.success)
4880
+ return accountPolicy;
4427
4881
  const validateResult = validatePassword(options.password);
4428
4882
  if (!validateResult.success)
4429
4883
  return validateResult;
@@ -4459,7 +4913,7 @@ function buildRegistrationOps(ctx) {
4459
4913
  return err(HaiIamError.ROLE_NOT_FOUND, iamM("iam_roleNotExist"));
4460
4914
  return authzFunctions.assignRole(userId, roleResult.data.id);
4461
4915
  }
4462
- async function createUser(options, enabled) {
4916
+ async function createUser(options, enabled, managed = false) {
4463
4917
  const preResult = await validateRegisterPreconditions(options);
4464
4918
  if (!preResult.success)
4465
4919
  return preResult;
@@ -4477,6 +4931,10 @@ function buildRegistrationOps(ctx) {
4477
4931
  phone: options.phone,
4478
4932
  displayName: options.displayName,
4479
4933
  enabled,
4934
+ accountType: options.accountType ?? "permanent",
4935
+ accountStatus: options.accountStatus ?? "active",
4936
+ accountExpiresAt: options.accountExpiresAt,
4937
+ mustChangePassword: managed && config.password?.requireInitialChange === true,
4480
4938
  emailVerified: false,
4481
4939
  phoneVerified: false,
4482
4940
  passwordHash: hashResult.data,
@@ -4506,15 +4964,18 @@ function buildRegistrationOps(ctx) {
4506
4964
  await userRepository.deleteById(createdUser.id);
4507
4965
  return assigned;
4508
4966
  }
4509
- logger10.info("User registered", { userId: createdUser.id, username: options.username });
4967
+ logger12.info("User registered", { userId: createdUser.id, username: options.username });
4510
4968
  return ok({
4511
4969
  user: toUser(createdUser),
4512
4970
  agreements: buildAgreementDisplay(agreementConfig, agreementConfig.showOnRegister)
4513
4971
  });
4514
4972
  }
4515
4973
  return {
4516
- createUser: (options) => createUser(options, true),
4517
- register(options) {
4974
+ createUser: (options) => createUser(options, true, true),
4975
+ async register(options) {
4976
+ const captcha = await verifyIamCaptcha(ctx.config.captcha, "register", options.captcha);
4977
+ if (!captcha.success)
4978
+ return captcha;
4518
4979
  if (!registerConfig.enabled)
4519
4980
  return Promise.resolve(err(HaiIamError.REGISTER_DISABLED, iamM("iam_registerDisabled")));
4520
4981
  return createUser(options, registerConfig.defaultEnabled);
@@ -4693,6 +5154,18 @@ function buildUserMutationOps(ctx) {
4693
5154
  return ok(toUser(updatedResult.data));
4694
5155
  },
4695
5156
  async updateUser(userId, data) {
5157
+ const currentAccount = await userRepository.findById(userId);
5158
+ if (!currentAccount.success)
5159
+ return currentAccount;
5160
+ if (!currentAccount.data)
5161
+ return err(HaiIamError.USER_NOT_FOUND, iamM("iam_userNotExist"));
5162
+ const accountPolicy = validateAccountPolicy({
5163
+ accountType: data.accountType === void 0 ? currentAccount.data.accountType : data.accountType,
5164
+ accountStatus: data.accountStatus === void 0 ? currentAccount.data.accountStatus : data.accountStatus,
5165
+ accountExpiresAt: data.accountExpiresAt === void 0 ? currentAccount.data.accountExpiresAt : data.accountExpiresAt
5166
+ });
5167
+ if (!accountPolicy.success)
5168
+ return accountPolicy;
4696
5169
  if (!hasUpdateFields(data)) {
4697
5170
  const currentResult = await userRepository.findById(userId);
4698
5171
  if (!currentResult.success) {
@@ -4710,18 +5183,37 @@ function buildUserMutationOps(ctx) {
4710
5183
  if (!uniqueResult.success) {
4711
5184
  return uniqueResult;
4712
5185
  }
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
- );
5186
+ const changesSecurity = data.enabled !== void 0 || data.accountStatus !== void 0 || data.accountType !== void 0 || data.accountExpiresAt !== void 0;
5187
+ const transaction = changesSecurity ? await reldb.tx.begin() : void 0;
5188
+ if (transaction && !transaction.success)
5189
+ return transaction;
5190
+ const tx = transaction?.success ? transaction.data : void 0;
5191
+ let transactionCommitted = false;
5192
+ try {
5193
+ const updateResult = await userRepository.updateById(userId, data, tx);
5194
+ if (!updateResult.success)
5195
+ return mapUpdateErrorAsDomainError(updateResult.error);
5196
+ if (updateResult.data.changes === 0)
5197
+ return err(HaiIamError.USER_NOT_FOUND, iamM("iam_userNotExist"));
5198
+ if (tx) {
5199
+ 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 ?? ""]);
5200
+ if (!advanced.success || advanced.data.changes !== 1)
5201
+ return advanced.success ? err(HaiIamError.CONFLICT, iamM("iam_sessionStateConflict")) : advanced;
5202
+ const committed = await tx.commit();
5203
+ if (!committed.success)
5204
+ return committed;
5205
+ transactionCommitted = true;
5206
+ }
5207
+ } catch (cause) {
5208
+ return err(HaiIamError.REPOSITORY_ERROR, iamM("iam_accountUpdateFailed"), cause);
5209
+ } finally {
5210
+ if (tx && !transactionCommitted)
5211
+ await tx.rollback();
4722
5212
  }
4723
- if (data.enabled === false) {
4724
- await sessionFunctions.deleteByUserId(userId);
5213
+ if (tx) {
5214
+ const revoked = await sessionFunctions.deleteByUserId(userId);
5215
+ if (!revoked.success)
5216
+ logger12.warn("Account credentials revoked; session cache cleanup failed", { userId, error: revoked.error });
4725
5217
  }
4726
5218
  const updatedResult = await userRepository.findById(userId);
4727
5219
  if (!updatedResult.success) {
@@ -4736,7 +5228,7 @@ function buildUserMutationOps(ctx) {
4736
5228
  return ok(toUser(updatedResult.data));
4737
5229
  },
4738
5230
  async deleteUser(userId) {
4739
- logger10.debug("Deleting user", { userId });
5231
+ logger12.debug("Deleting user", { userId });
4740
5232
  const userResult = await userRepository.findById(userId);
4741
5233
  if (!userResult.success) {
4742
5234
  return mapRepositoryError("iam_queryUserFailed", userResult.error.message);
@@ -4776,17 +5268,17 @@ function buildUserMutationOps(ctx) {
4776
5268
  );
4777
5269
  }
4778
5270
  await sessionFunctions.deleteByUserId(userId);
4779
- logger10.info("User deleted", { userId });
5271
+ logger12.info("User deleted", { userId });
4780
5272
  return ok(void 0);
4781
5273
  }
4782
5274
  };
4783
5275
  }
4784
5276
  function buildPasswordChangeOps(ctx) {
4785
5277
  const { userRepository, sessionFunctions } = ctx;
4786
- const { validatePassword, hashPassword } = ctx;
5278
+ const { hashPassword } = ctx;
4787
5279
  return {
4788
5280
  async adminResetPassword(userId, newPassword) {
4789
- logger10.debug("Admin resetting user password", { userId });
5281
+ logger12.debug("Admin resetting user password", { userId });
4790
5282
  const userResult = await userRepository.findById(userId);
4791
5283
  if (!userResult.success) {
4792
5284
  return mapRepositoryError("iam_queryUserFailed", userResult.error.message);
@@ -4797,13 +5289,13 @@ function buildPasswordChangeOps(ctx) {
4797
5289
  iamM("iam_userNotExist")
4798
5290
  );
4799
5291
  }
4800
- const validateResult = validatePassword(newPassword);
5292
+ const validateResult = validatePasswordReplacement(ctx, userResult.data, newPassword);
4801
5293
  if (!validateResult.success)
4802
5294
  return validateResult;
4803
5295
  const hashResult = hashPassword(newPassword);
4804
5296
  if (!hashResult.success)
4805
5297
  return hashResult;
4806
- const updateResult = await userRepository.setPassword({ userId, expectedVersion: userResult.data.credentialVersion ?? "", passwordHash: hashResult.data });
5298
+ const updateResult = await userRepository.setPassword({ userId, expectedVersion: userResult.data.credentialVersion ?? "", passwordHash: hashResult.data, mustChangePassword: ctx.config.password?.requireInitialChange === true });
4807
5299
  if (!updateResult.success)
4808
5300
  return updateResult;
4809
5301
  if (!updateResult.data)
@@ -4811,7 +5303,7 @@ function buildPasswordChangeOps(ctx) {
4811
5303
  const revoked = await sessionFunctions.deleteByUserId(userId);
4812
5304
  if (!revoked.success)
4813
5305
  return revoked;
4814
- logger10.info("Admin reset password", { userId });
5306
+ logger12.info("Admin reset password", { userId });
4815
5307
  return ok(void 0);
4816
5308
  },
4817
5309
  async changePassword(userId, oldPassword, newPassword) {
@@ -4826,10 +5318,10 @@ function buildPasswordChangeOps(ctx) {
4826
5318
  );
4827
5319
  }
4828
5320
  const user = userResult.data;
4829
- if (!user.enabled) {
5321
+ if (!isUserActive(user)) {
4830
5322
  return err(HaiIamError.USER_DISABLED, iamM("iam_accountDisabled"));
4831
5323
  }
4832
- if (isAccountLocked(user)) {
5324
+ if (isAccountLocked(user, ctx.loginConfig)) {
4833
5325
  return err(HaiIamError.USER_LOCKED, iamM("iam_accountLocked"));
4834
5326
  }
4835
5327
  if (!user.passwordHash) {
@@ -4840,10 +5332,7 @@ function buildPasswordChangeOps(ctx) {
4840
5332
  }
4841
5333
  const verifyResult = crypto$1.password.verify(oldPassword, user.passwordHash);
4842
5334
  if (!verifyResult.success || !verifyResult.data) {
4843
- const failureResult = await recordLoginFailure(userRepository, user, {
4844
- maxLoginAttempts: ctx.loginConfig.maxLoginAttempts,
4845
- lockoutDuration: ctx.loginConfig.lockoutDuration
4846
- });
5335
+ const failureResult = await recordLoginFailure(userRepository, user, ctx.loginConfig);
4847
5336
  if (!failureResult.success)
4848
5337
  return failureResult;
4849
5338
  return err(
@@ -4851,10 +5340,10 @@ function buildPasswordChangeOps(ctx) {
4851
5340
  iamM("iam_originalPasswordWrong")
4852
5341
  );
4853
5342
  }
4854
- const resetResult = await resetLoginFailures(userRepository, user);
5343
+ const resetResult = await resetLoginFailures(userRepository, user, ctx.loginConfig);
4855
5344
  if (!resetResult.success)
4856
5345
  return resetResult;
4857
- const validateResult = validatePassword(newPassword);
5346
+ const validateResult = validatePasswordReplacement(ctx, user, newPassword);
4858
5347
  if (!validateResult.success)
4859
5348
  return validateResult;
4860
5349
  const hashResult = hashPassword(newPassword);
@@ -4868,7 +5357,7 @@ function buildPasswordChangeOps(ctx) {
4868
5357
  const revoked = await sessionFunctions.deleteByUserId(userId);
4869
5358
  if (!revoked.success)
4870
5359
  return revoked;
4871
- logger10.info("Password changed", { userId });
5360
+ logger12.info("Password changed", { userId });
4872
5361
  return ok(void 0);
4873
5362
  },
4874
5363
  /**
@@ -4888,48 +5377,58 @@ function buildPasswordChangeOps(ctx) {
4888
5377
  }
4889
5378
  };
4890
5379
  }
5380
+ async function issuePasswordReset(ctx, stored) {
5381
+ const { userRepository, onPasswordResetRequest, resetConfig } = ctx;
5382
+ if (!onPasswordResetRequest)
5383
+ return err(HaiIamError.PASSWORD_RESET_DISABLED, iamM("iam_passwordResetDisabled"));
5384
+ const user = toUser(stored);
5385
+ const token = generateToken();
5386
+ const expiresAt = new Date(Date.now() + resetConfig.tokenExpiresIn * 1e3);
5387
+ const tokenHash = crypto$1.hash.hash(token);
5388
+ if (!tokenHash.success)
5389
+ return tokenHash;
5390
+ const saveResult = await userRepository.publishPasswordReset({ user: stored, tokenHash: tokenHash.data, expiresAt });
5391
+ if (!saveResult.success)
5392
+ return saveResult;
5393
+ if (!saveResult.data)
5394
+ return err(HaiIamError.CONFLICT, iamM("iam_securityStateFailed"));
5395
+ try {
5396
+ await onPasswordResetRequest(user, token, { expiresAt, idempotencyKey: `iam-reset:${tokenHash.data}` });
5397
+ } catch {
5398
+ logger12.error("Password reset callback failed", { userId: user.id });
5399
+ return err(HaiIamError.INTERNAL_ERROR, iamM("iam_notificationDeliveryUnknown"));
5400
+ }
5401
+ logger12.info("Password reset token generated", { userId: user.id });
5402
+ return ok(void 0);
5403
+ }
4891
5404
  function buildPasswordResetOps(ctx) {
4892
5405
  const { userRepository, sessionFunctions, onPasswordResetRequest } = ctx;
4893
- const { validatePassword, hashPassword } = ctx;
5406
+ const { hashPassword } = ctx;
4894
5407
  return {
4895
- async requestPasswordReset(identifier) {
4896
- logger10.debug("Password reset requested", { identifier });
5408
+ async requestPasswordReset(identifier, options) {
5409
+ const captcha = await verifyIamCaptcha(ctx.config.captcha, "passwordReset", options?.captcha);
5410
+ if (!captcha.success)
5411
+ return captcha;
5412
+ logger12.debug("Password reset requested", { identifier });
4897
5413
  const resetConfig = ctx.resetConfig;
4898
5414
  if (!resetConfig.enabled || !onPasswordResetRequest)
4899
5415
  return err(HaiIamError.PASSWORD_RESET_DISABLED, iamM("iam_passwordResetDisabled"));
4900
5416
  const userResult = await userRepository.findByIdentifier(identifier);
4901
5417
  if (!userResult.success) {
4902
- logger10.warn("Failed to look up user for password reset", { identifier });
5418
+ logger12.warn("Failed to look up user for password reset", { identifier });
4903
5419
  return ok(void 0);
4904
5420
  }
4905
5421
  if (!userResult.data) {
4906
- logger10.debug("User not found for password reset, returning ok to prevent enumeration", { identifier });
5422
+ logger12.debug("User not found for password reset, returning ok to prevent enumeration", { identifier });
4907
5423
  return ok(void 0);
4908
5424
  }
4909
5425
  const user = toUser(userResult.data);
4910
5426
  if (!user.enabled)
4911
5427
  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);
5428
+ return issuePasswordReset(ctx, userResult.data);
4930
5429
  },
4931
5430
  async confirmPasswordReset(token, newPassword) {
4932
- logger10.debug("Confirming password reset");
5431
+ logger12.debug("Confirming password reset");
4933
5432
  const resetConfig = ctx.resetConfig;
4934
5433
  if (!resetConfig.enabled)
4935
5434
  return err(HaiIamError.PASSWORD_RESET_DISABLED, iamM("iam_passwordResetDisabled"));
@@ -4939,7 +5438,7 @@ function buildPasswordResetOps(ctx) {
4939
5438
  const claimed = await userRepository.claimPasswordReset(tokenHash.data, resetConfig.maxAttempts);
4940
5439
  if (!claimed.success)
4941
5440
  return claimed;
4942
- const validateResult = validatePassword(newPassword);
5441
+ const validateResult = validatePasswordReplacement(ctx, claimed.data, newPassword);
4943
5442
  if (!validateResult.success)
4944
5443
  return validateResult;
4945
5444
  const hashResult = hashPassword(newPassword);
@@ -4953,7 +5452,7 @@ function buildPasswordResetOps(ctx) {
4953
5452
  const revoked = await sessionFunctions.deleteByUserId(claimed.data.id);
4954
5453
  if (!revoked.success)
4955
5454
  return revoked;
4956
- logger10.info("Password reset confirmed", { userId: claimed.data.id });
5455
+ logger12.info("Password reset confirmed", { userId: claimed.data.id });
4957
5456
  return ok(void 0);
4958
5457
  }
4959
5458
  };
@@ -4969,17 +5468,56 @@ function buildUserFunctions(deps) {
4969
5468
  resetConfig: PasswordResetConfigSchema.parse(deps.config.passwordReset ?? {}),
4970
5469
  loginConfig: SecurityConfigSchema.parse(deps.config.security ?? {})
4971
5470
  };
5471
+ const passwordChanges = buildPasswordChangeOps(ctx);
5472
+ const registration = buildRegistrationOps(ctx);
4972
5473
  return {
4973
- ...buildRegistrationOps(ctx),
5474
+ ...registration,
4974
5475
  ...buildUserQueryOps(ctx),
4975
5476
  ...buildUserMutationOps(ctx),
4976
- ...buildPasswordChangeOps(ctx),
4977
- ...buildPasswordResetOps(ctx)
5477
+ ...passwordChanges,
5478
+ ...buildPasswordResetOps(ctx),
5479
+ async inviteUser(options) {
5480
+ const destination = ctx.resetConfig.mode === "phone" ? options.phone : options.email;
5481
+ if (!deps.config.initialCredentials?.enabled || !ctx.resetConfig.enabled || !ctx.onPasswordResetRequest || !destination)
5482
+ return err(HaiIamError.INVITATION_DISABLED, iamM("iam_invitationDisabled"));
5483
+ const policy = deps.passwordStrategyResult.passwordConfig;
5484
+ const length = Math.min(policy.maxLength, Math.max(32, policy.minLength));
5485
+ const password = `Aa1!${Array.from({ length: Math.ceil(length / 32) }, () => generateToken()).join("")}`.slice(0, length);
5486
+ const created = await registration.createUser({ ...options, password });
5487
+ if (!created.success)
5488
+ return created;
5489
+ const stored = await deps.userRepository.findById(created.data.user.id);
5490
+ if (!stored.success || !stored.data)
5491
+ return err(HaiIamError.INVITATION_DELIVERY_UNKNOWN, iamM("iam_invitationDeliveryUnknown"));
5492
+ const delivered = await issuePasswordReset(ctx, stored.data);
5493
+ if (!delivered.success)
5494
+ return err(HaiIamError.INVITATION_DELIVERY_UNKNOWN, iamM("iam_invitationDeliveryUnknown"), delivered.error);
5495
+ return created;
5496
+ },
5497
+ async renewPassword(input) {
5498
+ const captcha = await verifyIamCaptcha(deps.config.captcha, "renewPassword", input.captcha);
5499
+ if (!captcha.success)
5500
+ return captcha;
5501
+ const found = await deps.userRepository.findByIdentifier(input.identifier);
5502
+ if (!found.success)
5503
+ return found;
5504
+ if (!found.data) {
5505
+ crypto$1.password.hash("dummy-password-to-prevent-timing-leak");
5506
+ return err(HaiIamError.INVALID_CREDENTIALS, iamM("iam_passwordWrong"));
5507
+ }
5508
+ return passwordChanges.changePassword(found.data.id, input.password, input.newPassword);
5509
+ },
5510
+ async scanInactiveAccounts() {
5511
+ const days = deps.config.accounts?.dormantAfterDays ?? 0;
5512
+ if (days === 0)
5513
+ return ok(0);
5514
+ return deps.userRepository.suspendInactive(new Date(Date.now() - days * 864e5));
5515
+ }
4978
5516
  };
4979
5517
  }
4980
5518
 
4981
5519
  // src/iam-main.ts
4982
- var logger11 = core.logger.child({ module: "iam", scope: "main" });
5520
+ var logger13 = core.logger.child({ module: "iam", scope: "main" });
4983
5521
  var initInProgress = false;
4984
5522
  var currentConfig = null;
4985
5523
  var currentPublicConfig = null;
@@ -4989,6 +5527,8 @@ var currentAuthz = null;
4989
5527
  var currentSession = null;
4990
5528
  var currentApiKey = null;
4991
5529
  var currentTicket = null;
5530
+ var accountScanTimer;
5531
+ var accountScanRunning;
4992
5532
  var notInitialized = core.module.createNotInitializedKit(
4993
5533
  HaiIamError.NOT_INITIALIZED,
4994
5534
  () => iamM("iam_notInitialized")
@@ -5008,7 +5548,7 @@ var notInitializedUser = new Proxy({}, {
5008
5548
  var iam = {
5009
5549
  async init(config) {
5010
5550
  if (initInProgress) {
5011
- logger11.warn("IAM init already in progress, skipping concurrent call");
5551
+ logger13.warn("IAM init already in progress, skipping concurrent call");
5012
5552
  return err(
5013
5553
  HaiIamError.CONFIG_ERROR,
5014
5554
  iamM("iam_initInProgress")
@@ -5017,11 +5557,11 @@ var iam = {
5017
5557
  initInProgress = true;
5018
5558
  try {
5019
5559
  if (currentConfig !== null) {
5020
- logger11.warn("IAM module is already initialized, reinitializing");
5560
+ logger13.warn("IAM module is already initialized, reinitializing");
5021
5561
  await iam.close();
5022
5562
  }
5023
5563
  const { ldapClientFactory, ldapSyncUser } = config;
5024
- logger11.info("Initializing IAM module");
5564
+ logger13.info("Initializing IAM module");
5025
5565
  if (!reldb.isInitialized) {
5026
5566
  return err(
5027
5567
  HaiIamError.CONFIG_ERROR,
@@ -5046,7 +5586,7 @@ var iam = {
5046
5586
  }
5047
5587
  const parseResult = IamConfigSchema.safeParse(config);
5048
5588
  if (!parseResult.success) {
5049
- logger11.error("IAM config validation failed", { error: parseResult.error.message });
5589
+ logger13.error("IAM config validation failed", { error: parseResult.error.message });
5050
5590
  return err(
5051
5591
  HaiIamError.CONFIG_ERROR,
5052
5592
  iamM("iam_configError", { params: { error: parseResult.error.message } }),
@@ -5055,7 +5595,7 @@ var iam = {
5055
5595
  }
5056
5596
  const parsed = parseResult.data;
5057
5597
  const { onPasswordResetRequest, onOtpSendEmail, onOtpSendSms } = await resolveIamNotifications(config);
5058
- logger11.info("IAM notification channels resolved", { email: Boolean(onOtpSendEmail), sms: Boolean(onOtpSendSms), passwordReset: Boolean(onPasswordResetRequest) });
5598
+ logger13.info("IAM notification channels resolved", { email: Boolean(onOtpSendEmail), sms: Boolean(onOtpSendSms), passwordReset: Boolean(onPasswordResetRequest) });
5059
5599
  const migration = await migrateIam();
5060
5600
  if (!migration.success)
5061
5601
  return migration;
@@ -5066,20 +5606,27 @@ var iam = {
5066
5606
  const found = await credentialUsers.findById(userId);
5067
5607
  if (!found.success)
5068
5608
  return found;
5069
- return ok(found.data ? { version: found.data.credentialVersion ?? "", enabled: found.data.enabled } : null);
5609
+ if (found.data) {
5610
+ const passwordState = checkPasswordState(found.data, parsed.password);
5611
+ if (!passwordState.success)
5612
+ return passwordState;
5613
+ }
5614
+ return ok(found.data ? { version: found.data.credentialVersion ?? "", enabled: isUserActive(found.data) } : null);
5070
5615
  }
5071
5616
  });
5072
5617
  if (!sessionResult.success) {
5073
5618
  return sessionResult;
5074
5619
  }
5075
- const authzResult = await createAuthzOperations({ config: parsed, session: sessionResult.data });
5620
+ const sessions = observeSessions(sessionResult.data);
5621
+ const authzResult = await createAuthzOperations({ config: parsed, session: sessions });
5076
5622
  if (!authzResult.success) {
5077
5623
  return authzResult;
5078
5624
  }
5625
+ const authorization = observeAuthorization(authzResult.data);
5079
5626
  const authnResult = await createAuthnOperations({
5080
5627
  config: parsed,
5081
- sessionFunctions: sessionResult.data,
5082
- authzFunctions: authzResult.data,
5628
+ sessionFunctions: sessions,
5629
+ authzFunctions: authorization,
5083
5630
  ldapClientFactory,
5084
5631
  ldapSyncUser,
5085
5632
  onOtpSendEmail,
@@ -5091,41 +5638,57 @@ var iam = {
5091
5638
  const userResult = await createUserOperations({
5092
5639
  config: parsed,
5093
5640
  passwordStrategyResult: authnResult.data.passwordStrategyResult,
5094
- sessionFunctions: sessionResult.data,
5095
- authzFunctions: authzResult.data,
5641
+ sessionFunctions: sessions,
5642
+ authzFunctions: authorization,
5096
5643
  onPasswordResetRequest
5097
5644
  });
5098
5645
  if (!userResult.success) {
5099
5646
  return userResult;
5100
5647
  }
5648
+ const users = observeUsers(userResult.data);
5101
5649
  if (parsed.seedDefaultData) {
5102
- const seedResult = await seedIamData(authzResult.data);
5650
+ const seedResult = await seedIamData(authorization);
5103
5651
  if (!seedResult.success) {
5104
5652
  return seedResult;
5105
5653
  }
5106
5654
  }
5107
- currentSession = sessionResult.data;
5108
- currentAuthz = authzResult.data;
5109
- currentUser = userResult.data;
5655
+ currentSession = sessions;
5656
+ currentAuthz = authorization;
5657
+ currentUser = users;
5110
5658
  currentConfig = parsed;
5111
5659
  currentPublicConfig = buildIamPublicConfig(parsed, { ...config, onPasswordResetRequest, onOtpSendEmail, onOtpSendSms });
5112
- currentApiKey = authnResult.data.apiKeyFunctions;
5660
+ currentApiKey = authnResult.data.apiKeyFunctions ? observeApiKeys(authnResult.data.apiKeyFunctions) : null;
5113
5661
  currentTicket = createTicketOperations();
5114
5662
  const authn = authnResult.data.authn;
5115
- currentAuth = {
5663
+ currentAuth = observeAuth({
5116
5664
  ...authn,
5117
- async registerAndLogin(options) {
5665
+ async registerAndLogin(options, context) {
5118
5666
  const regResult = await currentUser.register(options);
5119
5667
  if (!regResult.success) {
5120
5668
  return regResult;
5121
5669
  }
5122
- return authnResult.data.loginRegisteredUser(regResult.data.user);
5670
+ return authnResult.data.loginRegisteredUser(regResult.data.user, context);
5123
5671
  }
5124
- };
5125
- logger11.info("IAM module initialized");
5672
+ });
5673
+ if ((parsed.accounts?.dormantAfterDays ?? 0) > 0) {
5674
+ accountScanTimer = setInterval(() => {
5675
+ if (accountScanRunning)
5676
+ return;
5677
+ accountScanRunning = users.scanInactiveAccounts().then((result) => {
5678
+ if (!result.success)
5679
+ logger13.error("Inactive account scan failed", { error: result.error });
5680
+ }).catch((error) => {
5681
+ logger13.error("Inactive account scan failed", { error });
5682
+ }).finally(() => {
5683
+ accountScanRunning = void 0;
5684
+ });
5685
+ }, (parsed.accounts?.scanIntervalSeconds ?? 3600) * 1e3);
5686
+ accountScanTimer.unref();
5687
+ }
5688
+ logger13.info("IAM module initialized");
5126
5689
  return ok(void 0);
5127
5690
  } catch (error) {
5128
- logger11.error("IAM module initialization failed", { error });
5691
+ logger13.error("IAM module initialization failed", { error });
5129
5692
  return err(
5130
5693
  HaiIamError.CONFIG_ERROR,
5131
5694
  iamM("iam_initFailed"),
@@ -5167,10 +5730,13 @@ var iam = {
5167
5730
  },
5168
5731
  async close() {
5169
5732
  if (currentConfig === null && currentAuth === null && currentUser === null && currentAuthz === null && currentSession === null) {
5170
- logger11.info("IAM module already closed, skipping");
5733
+ logger13.info("IAM module already closed, skipping");
5171
5734
  return;
5172
5735
  }
5173
- logger11.info("Closing IAM module");
5736
+ logger13.info("Closing IAM module");
5737
+ clearInterval(accountScanTimer);
5738
+ accountScanTimer = void 0;
5739
+ await accountScanRunning;
5174
5740
  currentAuth = null;
5175
5741
  currentUser = null;
5176
5742
  currentAuthz = null;
@@ -5184,7 +5750,7 @@ var iam = {
5184
5750
  resetUserRepoSingleton();
5185
5751
  resetRoleRepoSingleton();
5186
5752
  resetPermissionRepoSingleton();
5187
- logger11.info("IAM module closed");
5753
+ logger13.info("IAM module closed");
5188
5754
  }
5189
5755
  };
5190
5756