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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
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';
1
+ import { HaiIamError, IamConfigSchema, SessionConfigSchema, SecurityConfigSchema, LoginConfigSchema, OtpConfigSchema, ApiKeyConfigSchema, AuthUiConfigSchema, PasswordConfigSchema, RbacConfigSchema, AgreementConfigSchema, PasswordResetConfigSchema, RegisterConfigSchema } from './chunk-KPG3M34Q.js';
2
+ export { AccountConfigSchema, AgreementConfigSchema, ApiKeyConfigSchema, AuthStrategyTypeSchema, AuthUiConfigSchema, CaptchaConfigSchema, HaiIamError, IamConfigSchema, LdapConfigSchema, LoginAccessConfigSchema, LoginConfigSchema, OtpConfigSchema, PasswordConfigSchema, PasswordResetConfigSchema, RbacConfigSchema, RegisterConfigSchema, SecurityConfigSchema, SessionConfigSchema } from './chunk-KPG3M34Q.js';
3
3
  import { cache } from '@h-ai/cache';
4
4
  import { core, err, ok } from '@h-ai/core';
5
5
  import { crypto as crypto$1 } from '@h-ai/crypto';
6
6
  import { migrateSchema, reldb, schemaColumns, BaseReldbCrudRepository, HaiReldbError } from '@h-ai/reldb';
7
- import { createHash, randomBytes, randomInt } from 'crypto';
7
+ import { randomUUID, createHash, randomBytes, randomInt } from 'crypto';
8
8
  import { isIP, BlockList } from 'net';
9
9
  import { verifySolution, pbkdf2, createChallenge } from 'altcha/lib';
10
10
  import { z } from 'zod';
@@ -213,7 +213,9 @@ var en_US_default = {
213
213
  iam_captchaRequired: "Complete the security check first",
214
214
  iam_captchaInvalid: "Security check is invalid or expired; try again",
215
215
  iam_captchaDisabled: "Security challenges are disabled",
216
- iam_captchaUnavailable: "Security check is temporarily unavailable"
216
+ iam_captchaUnavailable: "Security check is temporarily unavailable",
217
+ iam_sensitiveOperationsInvalid: "Invalid sensitive operation IDs: use at most 1000 entries of 1\u2013128 characters each.",
218
+ iam_terminalTypeInvalid: "Invalid terminal type: use web, app, desktop or api."
217
219
  };
218
220
 
219
221
  // messages/ja-JP.json
@@ -419,7 +421,9 @@ var ja_JP_default = {
419
421
  iam_captchaRequired: "\u5148\u306B\u30BB\u30AD\u30E5\u30EA\u30C6\u30A3\u78BA\u8A8D\u3092\u5B8C\u4E86\u3057\u3066\u304F\u3060\u3055\u3044",
420
422
  iam_captchaInvalid: "\u30BB\u30AD\u30E5\u30EA\u30C6\u30A3\u78BA\u8A8D\u304C\u7121\u52B9\u307E\u305F\u306F\u671F\u9650\u5207\u308C\u3067\u3059\u3002\u518D\u8A66\u884C\u3057\u3066\u304F\u3060\u3055\u3044",
421
423
  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"
424
+ iam_captchaUnavailable: "\u30BB\u30AD\u30E5\u30EA\u30C6\u30A3\u78BA\u8A8D\u3092\u4E00\u6642\u7684\u306B\u5229\u7528\u3067\u304D\u307E\u305B\u3093",
425
+ iam_sensitiveOperationsInvalid: "\u91CD\u8981\u64CD\u4F5C ID \u306F\u6700\u59271000\u4EF6\u3001\u54041\uFF5E128\u6587\u5B57\u3067\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
426
+ iam_terminalTypeInvalid: "\u7AEF\u672B\u7A2E\u5225\u306F web\u3001app\u3001desktop\u3001api \u306E\u3044\u305A\u308C\u304B\u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
423
427
  };
424
428
 
425
429
  // messages/zh-CN.json
@@ -625,7 +629,9 @@ var zh_CN_default = {
625
629
  iam_captchaRequired: "\u8BF7\u5148\u5B8C\u6210\u5B89\u5168\u9A8C\u8BC1",
626
630
  iam_captchaInvalid: "\u5B89\u5168\u9A8C\u8BC1\u65E0\u6548\u6216\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u8BD5",
627
631
  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"
632
+ iam_captchaUnavailable: "\u5B89\u5168\u9A8C\u8BC1\u6682\u4E0D\u53EF\u7528\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5",
633
+ iam_sensitiveOperationsInvalid: "\u654F\u611F\u64CD\u4F5C ID \u5217\u8868\u65E0\u6548\uFF1A\u6700\u591A 1000 \u9879\uFF0C\u6BCF\u9879\u4E3A 1\u2013128 \u5B57\u7B26\u3002",
634
+ iam_terminalTypeInvalid: "\u7EC8\u7AEF\u7C7B\u578B\u65E0\u6548\uFF0C\u8BF7\u4F7F\u7528 web\u3001app\u3001desktop \u6216 api\u3002"
629
635
  };
630
636
 
631
637
  // src/iam-i18n.ts
@@ -635,6 +641,118 @@ var iamM = core.i18n.createMessageGetter({
635
641
  "ja-JP": ja_JP_default
636
642
  });
637
643
 
644
+ // src/iam-cache.ts
645
+ var revisionKey = "hai:iam:read_cache:revision";
646
+ var transactions = /* @__PURE__ */ new WeakMap();
647
+ var logger = core.logger.child({ module: "iam", scope: "read-cache" });
648
+ var degraded = false;
649
+ function failure() {
650
+ return err(HaiIamError.REPOSITORY_ERROR, iamM("iam_sessionExpired"));
651
+ }
652
+ async function revision() {
653
+ const current = await cache.kv.get(revisionKey);
654
+ if (!current.success)
655
+ return current;
656
+ if (current.data)
657
+ return ok(current.data);
658
+ const created = await cache.kv.set(revisionKey, { epoch: randomUUID(), writers: 0 }, { nx: true });
659
+ if (!created.success)
660
+ return created;
661
+ const loaded = await cache.kv.get(revisionKey);
662
+ return loaded.success && loaded.data ? ok(loaded.data) : failure();
663
+ }
664
+ async function change(delta) {
665
+ for (let attempt = 0; attempt < 32; attempt++) {
666
+ const current = await revision();
667
+ if (!current.success)
668
+ return current;
669
+ const written = await cache.kv.compareAndSwap(revisionKey, current.data, { epoch: randomUUID(), writers: Math.max(0, current.data.writers + delta) });
670
+ if (!written.success)
671
+ return written;
672
+ if (written.data)
673
+ return ok(void 0);
674
+ }
675
+ return failure();
676
+ }
677
+ async function readIamCache(key, load) {
678
+ const before = await revision();
679
+ if (!before.success || before.data.writers)
680
+ return load();
681
+ const address = `hai:iam:read_cache:${before.data.epoch}:${key}`;
682
+ const cached = await cache.kv.get(address);
683
+ if (cached.success && cached.data) {
684
+ const after2 = await revision();
685
+ if (after2.success && !after2.data.writers && after2.data.epoch === before.data.epoch)
686
+ return ok(cached.data.value);
687
+ return load();
688
+ }
689
+ const result = await load();
690
+ if (!result.success)
691
+ return result;
692
+ const after = await revision();
693
+ if (after.success && !after.data.writers && after.data.epoch === before.data.epoch)
694
+ await cache.kv.set(address, { value: result.data }, { ex: 60 });
695
+ return result;
696
+ }
697
+ async function finish() {
698
+ const completed = await change(-1);
699
+ if (!completed.success && !degraded) {
700
+ degraded = true;
701
+ logger.warn("IAM cache invalidation incomplete; authorization reads use database", { code: completed.error.code });
702
+ }
703
+ if (completed.success)
704
+ degraded = false;
705
+ }
706
+ async function enroll(tx) {
707
+ const existing = transactions.get(tx);
708
+ if (existing)
709
+ return existing;
710
+ const pending = (async () => {
711
+ const started = await change(1);
712
+ if (!started.success)
713
+ return started;
714
+ const commit = tx.commit.bind(tx);
715
+ const rollback = tx.rollback.bind(tx);
716
+ tx.commit = async () => {
717
+ const result = await commit();
718
+ if (result.success)
719
+ await finish();
720
+ return result;
721
+ };
722
+ tx.rollback = async () => {
723
+ const result = await rollback();
724
+ if (result.success)
725
+ await finish();
726
+ return result;
727
+ };
728
+ return ok(void 0);
729
+ })();
730
+ transactions.set(tx, pending);
731
+ return pending;
732
+ }
733
+ function guardIamMutation(operation, txIndex) {
734
+ return async (...args) => {
735
+ const candidate = txIndex === void 0 ? void 0 : args[txIndex];
736
+ const tx = candidate;
737
+ let started;
738
+ try {
739
+ started = tx ? await enroll(tx) : await change(1);
740
+ } catch {
741
+ return failure();
742
+ }
743
+ if (!started.success)
744
+ return started;
745
+ try {
746
+ return await operation(...args);
747
+ } catch {
748
+ return failure();
749
+ } finally {
750
+ if (!tx)
751
+ await finish();
752
+ }
753
+ };
754
+ }
755
+
638
756
  // src/authn/apikey/iam-authn-apikey-repository.ts
639
757
  var TABLE_NAME = "hai_iam_api_keys";
640
758
  var API_KEY_SORT_COLUMNS = {
@@ -736,6 +854,10 @@ async function createDbApiKeyRepository() {
736
854
  return apiKeyRepoInstance;
737
855
  const repo = new DbApiKeyRepository();
738
856
  await repo.count();
857
+ repo.removeById = guardIamMutation(repo.removeById.bind(repo), 1);
858
+ const update = repo.updateFields.bind(repo);
859
+ const protectedUpdate = guardIamMutation(update, 2);
860
+ repo.updateFields = (id, data, tx) => Object.keys(data).every((key) => key === "lastUsedAt") ? update(id, data, tx) : protectedUpdate(id, data, tx);
739
861
  apiKeyRepoInstance = repo;
740
862
  apiKeyRepoSqlOps = reldb.sql;
741
863
  return repo;
@@ -882,6 +1004,7 @@ function buildSession(options, now, sessionTtl, accessToken, credentialVersion)
882
1004
  roles: options.roles ?? [],
883
1005
  permissions: options.permissions ?? [],
884
1006
  source: options.source,
1007
+ terminalType: options.terminalType ?? "web",
885
1008
  accessToken,
886
1009
  createdAt: now,
887
1010
  startedAt: now,
@@ -921,7 +1044,7 @@ function applySessionPatch(session, patch) {
921
1044
  }
922
1045
 
923
1046
  // src/ticket/iam-ticket-functions.ts
924
- var logger = core.logger.child({ module: "iam", scope: "ticket" });
1047
+ var logger2 = core.logger.child({ module: "iam", scope: "ticket" });
925
1048
  var TICKET_KEY_PREFIX = "hai:iam:ticket:";
926
1049
  var DEFAULT_TICKET_TTL_MS = 3e4;
927
1050
  function buildTicketKey(ticket) {
@@ -941,7 +1064,7 @@ function createTicketOperations() {
941
1064
  };
942
1065
  const saved = await cache.kv.set(buildTicketKey(ticket), record, { px: ttlMs, nx: true });
943
1066
  if (!saved.success || !saved.data) {
944
- logger.warn("Failed to issue ticket", { purpose: options.purpose });
1067
+ logger2.warn("Failed to issue ticket", { purpose: options.purpose });
945
1068
  return err(HaiIamError.TICKET_ISSUE_FAILED, iamM("iam_ticketIssueFailed"));
946
1069
  }
947
1070
  return ok({ ticket, expiresAt });
@@ -1145,6 +1268,12 @@ async function createDbUserRepository() {
1145
1268
  return userRepoInstance;
1146
1269
  const repo = new DbUserRepository();
1147
1270
  await repo.count();
1271
+ repo.create = guardIamMutation(repo.create.bind(repo), 1);
1272
+ repo.updateById = guardIamMutation(repo.updateById.bind(repo), 2);
1273
+ repo.deleteById = guardIamMutation(repo.deleteById.bind(repo), 1);
1274
+ repo.suspendInactive = guardIamMutation(repo.suspendInactive.bind(repo));
1275
+ repo.setPassword = guardIamMutation(repo.setPassword.bind(repo));
1276
+ repo.commitPasswordReset = guardIamMutation(repo.commitPasswordReset.bind(repo));
1148
1277
  userRepoInstance = repo;
1149
1278
  userRepoSqlOps = reldb.sql;
1150
1279
  return repo;
@@ -1415,7 +1544,7 @@ async function resetLoginFailures(userRepository, user, policy) {
1415
1544
  }
1416
1545
 
1417
1546
  // src/authn/apikey/iam-authn-apikey-strategy.ts
1418
- var logger2 = core.logger.child({ module: "iam", scope: "apikey-strategy" });
1547
+ var logger3 = core.logger.child({ module: "iam", scope: "apikey-strategy" });
1419
1548
  var KEY_PREFIX_LENGTH = 12;
1420
1549
  function generateRawKey(prefix2) {
1421
1550
  const hex = randomBytes(32).toString("hex");
@@ -1441,7 +1570,7 @@ function createApiKeyStrategy(config) {
1441
1570
  const apikeyConfig = config.apikeyConfig ? ApiKeyConfigSchema.parse(config.apikeyConfig) : ApiKeyConfigSchema.parse({});
1442
1571
  const { userRepository, apiKeyRepository } = config;
1443
1572
  const passwordOps = crypto$1.password;
1444
- async function verifyRawKey(rawKey) {
1573
+ async function loadRawKey(rawKey) {
1445
1574
  const prefix2 = extractKeyPrefix(rawKey);
1446
1575
  const candidatesResult = await apiKeyRepository.findByKeyPrefix(prefix2);
1447
1576
  if (!candidatesResult.success) {
@@ -1460,12 +1589,26 @@ function createApiKeyStrategy(config) {
1460
1589
  return err(HaiIamError.APIKEY_EXPIRED, iamM("iam_apikeyExpired"));
1461
1590
  }
1462
1591
  apiKeyRepository.updateFields(candidate.id, { lastUsedAt: /* @__PURE__ */ new Date() }).catch(() => {
1463
- logger2.warn("Failed to update API Key lastUsedAt", { keyId: candidate.id });
1592
+ logger3.warn("Failed to update API Key lastUsedAt", { keyId: candidate.id });
1464
1593
  });
1465
1594
  return ok(toApiKey(candidate));
1466
1595
  }
1467
1596
  return err(HaiIamError.APIKEY_INVALID, iamM("iam_apikeyInvalid"));
1468
1597
  }
1598
+ async function verifyRawKey(rawKey) {
1599
+ const fingerprint = createHash("sha256").update(rawKey).digest("hex");
1600
+ const result = await readIamCache(`verified_apikey:${fingerprint}`, async () => {
1601
+ const verified = await loadRawKey(rawKey);
1602
+ return verified.success ? ok({ ...verified.data, expiresAt: verified.data.expiresAt?.getTime() ?? null, createdAt: verified.data.createdAt.getTime(), lastUsedAt: verified.data.lastUsedAt?.getTime() ?? null }) : verified;
1603
+ });
1604
+ if (!result.success)
1605
+ return result;
1606
+ if (!result.data.enabled)
1607
+ return err(HaiIamError.APIKEY_DISABLED, iamM("iam_apikeyDisabled"));
1608
+ if (result.data.expiresAt !== null && result.data.expiresAt <= Date.now())
1609
+ return err(HaiIamError.APIKEY_EXPIRED, iamM("iam_apikeyExpired"));
1610
+ return ok({ ...result.data, expiresAt: result.data.expiresAt === null ? null : new Date(result.data.expiresAt), createdAt: new Date(result.data.createdAt), lastUsedAt: result.data.lastUsedAt === null ? null : new Date(result.data.lastUsedAt) });
1611
+ }
1469
1612
  const strategy = {
1470
1613
  type: "apikey",
1471
1614
  name: "apikey-strategy",
@@ -1490,7 +1633,7 @@ function createApiKeyStrategy(config) {
1490
1633
  if (!isUserActive(userResult.data)) {
1491
1634
  return err(HaiIamError.USER_DISABLED, iamM("iam_accountDisabled"));
1492
1635
  }
1493
- logger2.info("API Key authentication succeeded", { userId: userResult.data.id, keyId: apiKey.id });
1636
+ logger3.debug("API Key authentication succeeded", { userId: userResult.data.id, keyId: apiKey.id });
1494
1637
  return ok({ user: { ...toUser(userResult.data), credentialVersion: userResult.data.credentialVersion ?? "" }, apiKey });
1495
1638
  }
1496
1639
  };
@@ -1527,7 +1670,7 @@ function createApiKeyStrategy(config) {
1527
1670
  const insertResult = await apiKeyRepository.insert(storedApiKey);
1528
1671
  if (!insertResult.success)
1529
1672
  return insertResult;
1530
- logger2.info("API Key created", { userId, keyId: id, name: options.name });
1673
+ logger3.info("API Key created", { userId, keyId: id, name: options.name });
1531
1674
  return ok({ apiKey: toApiKey(storedApiKey), rawKey });
1532
1675
  } catch (error) {
1533
1676
  return err(HaiIamError.INTERNAL_ERROR, iamM("iam_apikeyCreateFailed", { params: { message: String(error) } }), error);
@@ -1549,7 +1692,7 @@ function createApiKeyStrategy(config) {
1549
1692
  const result = await apiKeyRepository.removeById(keyId);
1550
1693
  if (!result.success)
1551
1694
  return result;
1552
- logger2.info("API Key revoked", { keyId });
1695
+ logger3.info("API Key revoked", { keyId });
1553
1696
  return ok(void 0);
1554
1697
  },
1555
1698
  async verifyApiKey(rawKey) {
@@ -1600,7 +1743,7 @@ function createLoginAccessCheck(policy) {
1600
1743
  return ok(void 0);
1601
1744
  };
1602
1745
  }
1603
- var logger3 = core.logger.child({ module: "iam", scope: "captcha" });
1746
+ var logger4 = core.logger.child({ module: "iam", scope: "captcha" });
1604
1747
  var prefix = "hai:iam:captcha:";
1605
1748
  var operationSchema = z.enum(["login", "register", "sendOtp", "passwordReset", "renewPassword"]);
1606
1749
  var proofSchema = z.object({
@@ -1632,7 +1775,7 @@ async function createIamCaptcha(config, input) {
1632
1775
  return err(HaiIamError.CAPTCHA_UNAVAILABLE, iamM("iam_captchaUnavailable"));
1633
1776
  return ok({ id, challenge });
1634
1777
  } catch (error) {
1635
- logger3.error("Captcha challenge generation failed", { operation: operation.data, error });
1778
+ logger4.error("Captcha challenge generation failed", { operation: operation.data, error });
1636
1779
  return err(HaiIamError.CAPTCHA_UNAVAILABLE, iamM("iam_captchaUnavailable"));
1637
1780
  }
1638
1781
  }
@@ -1668,7 +1811,7 @@ async function verifyIamCaptcha(config, operation, proof) {
1668
1811
  return err(HaiIamError.CAPTCHA_UNAVAILABLE, iamM("iam_captchaUnavailable"));
1669
1812
  return removed.data === 1 ? ok(void 0) : err(HaiIamError.CAPTCHA_INVALID, iamM("iam_captchaInvalid"));
1670
1813
  } catch (error) {
1671
- logger3.error("Captcha verification failed", { operation, error });
1814
+ logger4.error("Captcha verification failed", { operation, error });
1672
1815
  return err(HaiIamError.CAPTCHA_UNAVAILABLE, iamM("iam_captchaUnavailable"));
1673
1816
  }
1674
1817
  }
@@ -1726,7 +1869,7 @@ async function loadLdapClientFactory() {
1726
1869
  }
1727
1870
  };
1728
1871
  }
1729
- var logger4 = core.logger.child({ module: "iam", scope: "ldap-strategy" });
1872
+ var logger5 = core.logger.child({ module: "iam", scope: "ldap-strategy" });
1730
1873
  function escapeLdapFilterValue(value) {
1731
1874
  return value.replace(/[\\*()/\0]/g, (ch) => {
1732
1875
  const hex = ch.charCodeAt(0).toString(16).padStart(2, "0");
@@ -1829,7 +1972,7 @@ function createLdapStrategy(config) {
1829
1972
  if (!failureResult.success)
1830
1973
  return failureResult;
1831
1974
  }
1832
- logger4.warn("LDAP authentication failed", { username });
1975
+ logger5.warn("LDAP authentication failed", { username });
1833
1976
  return err(
1834
1977
  HaiIamError.INVALID_CREDENTIALS,
1835
1978
  iamM("iam_passwordWrong")
@@ -1867,7 +2010,7 @@ function createLdapStrategy(config) {
1867
2010
  return assigned;
1868
2011
  }
1869
2012
  } catch (callbackError) {
1870
- logger4.warn("onUserAutoRegistered callback failed", { userId: storedUser?.id, error: callbackError });
2013
+ logger5.warn("onUserAutoRegistered callback failed", { userId: storedUser?.id, error: callbackError });
1871
2014
  }
1872
2015
  }
1873
2016
  } else if (storedUser && syncUser) {
@@ -1892,7 +2035,7 @@ function createLdapStrategy(config) {
1892
2035
  const resetResult = await resetLoginFailures(userRepository, storedUser, failurePolicy);
1893
2036
  if (!resetResult.success)
1894
2037
  return resetResult;
1895
- logger4.info("LDAP authentication succeeded", { userId: storedUser.id });
2038
+ logger5.debug("LDAP authentication succeeded", { userId: storedUser.id });
1896
2039
  return ok({ ...toUser(storedUser), credentialVersion: storedUser.credentialVersion ?? "" });
1897
2040
  } finally {
1898
2041
  await client.unbind();
@@ -1912,7 +2055,7 @@ function createCacheOtpRepository() {
1912
2055
  if (otpRepoInstance)
1913
2056
  return otpRepoInstance;
1914
2057
  const keyFor = (identifier) => `hai:iam:otp:${normalizeOtpIdentifier(identifier)}`;
1915
- const failure = () => err(HaiIamError.REPOSITORY_ERROR, iamM("iam_securityStateFailed"));
2058
+ const failure2 = () => err(HaiIamError.REPOSITORY_ERROR, iamM("iam_securityStateFailed"));
1916
2059
  const invalid = () => err(HaiIamError.OTP_INVALID, iamM("iam_otpNotExistOrExpired"));
1917
2060
  otpRepoInstance = {
1918
2061
  async issueOtp(input) {
@@ -1927,7 +2070,7 @@ function createCacheOtpRepository() {
1927
2070
  const key = keyFor(input.identifier);
1928
2071
  const current = await cache.kv.get(key);
1929
2072
  if (!current.success)
1930
- return failure();
2073
+ return failure2();
1931
2074
  if (current.data && current.data.cooldownUntil > Date.now()) {
1932
2075
  return err(HaiIamError.OTP_RESEND_TOO_FAST, iamM("iam_otpResendTooFast", { params: { seconds: Math.ceil((current.data.cooldownUntil - Date.now()) / 1e3) } }));
1933
2076
  }
@@ -1935,7 +2078,7 @@ function createCacheOtpRepository() {
1935
2078
  return invalid();
1936
2079
  const published = await cache.kv.compareAndSwap(key, current.data ?? void 0, state, { px: Math.max(state.expiresAt, state.cooldownUntil) - Date.now() });
1937
2080
  if (!published.success)
1938
- return failure();
2081
+ return failure2();
1939
2082
  if (!published.data)
1940
2083
  return err(HaiIamError.SECURITY_BUSY, iamM("iam_securityBusy"));
1941
2084
  if (Date.now() >= state.expiresAt)
@@ -1948,7 +2091,7 @@ function createCacheOtpRepository() {
1948
2091
  for (let attempt = 0; attempt < 16; attempt++) {
1949
2092
  const current = await cache.kv.get(key);
1950
2093
  if (!current.success)
1951
- return failure();
2094
+ return failure2();
1952
2095
  const state = current.data;
1953
2096
  if (!state || state.code === null || state.expiresAt <= Date.now() || state.attempts >= maxAttempts || version && state.version !== version)
1954
2097
  return invalid();
@@ -1958,7 +2101,7 @@ function createCacheOtpRepository() {
1958
2101
  const next = { ...state, attempts, code: correct || attempts >= maxAttempts ? null : state.code };
1959
2102
  const committed = await cache.kv.compareAndSwap(key, state, next, { keepTtl: true });
1960
2103
  if (!committed.success)
1961
- return failure();
2104
+ return failure2();
1962
2105
  if (!committed.data)
1963
2106
  continue;
1964
2107
  if (!correct || state.expiresAt <= Date.now())
@@ -1970,7 +2113,7 @@ function createCacheOtpRepository() {
1970
2113
  };
1971
2114
  return otpRepoInstance;
1972
2115
  }
1973
- var logger5 = core.logger.child({ module: "iam", scope: "otp-strategy" });
2116
+ var logger6 = core.logger.child({ module: "iam", scope: "otp-strategy" });
1974
2117
  var PHONE_IDENTIFIER_REGEX = /^\+?\d{8,}$/;
1975
2118
  var PHONE_SEPARATOR_REGEX = /[\s-]/g;
1976
2119
  function generateOtpCode(length) {
@@ -2068,7 +2211,7 @@ function createOtpStrategy(config) {
2068
2211
  return assigned;
2069
2212
  }
2070
2213
  } catch (callbackError) {
2071
- logger5.warn("onUserAutoRegistered callback failed", { userId: storedUser.id, error: callbackError });
2214
+ logger6.warn("onUserAutoRegistered callback failed", { userId: storedUser.id, error: callbackError });
2072
2215
  }
2073
2216
  }
2074
2217
  }
@@ -2080,7 +2223,7 @@ function createOtpStrategy(config) {
2080
2223
  const resetResult = await resetLoginFailures(config.userRepository, storedUser, failurePolicy);
2081
2224
  if (!resetResult.success)
2082
2225
  return resetResult;
2083
- logger5.info("OTP authentication succeeded", { userId: storedUser.id });
2226
+ logger6.debug("OTP authentication succeeded", { userId: storedUser.id });
2084
2227
  return ok({ ...toUser(storedUser), credentialVersion: storedUser.credentialVersion ?? "" });
2085
2228
  }
2086
2229
  };
@@ -2124,12 +2267,12 @@ function createOtpStrategy(config) {
2124
2267
  );
2125
2268
  }
2126
2269
  const result = { expiresAt };
2127
- logger5.debug("OTP challenge sent", { identifier, expiresAt });
2270
+ logger6.debug("OTP challenge sent", { identifier, expiresAt });
2128
2271
  return ok(result);
2129
2272
  }
2130
2273
  return { strategy, challenge };
2131
2274
  }
2132
- var logger6 = core.logger.child({ module: "iam", scope: "password-strategy" });
2275
+ var logger7 = core.logger.child({ module: "iam", scope: "password-strategy" });
2133
2276
  var PASSWORD_UPPERCASE_REGEX = /[A-Z]/;
2134
2277
  var PASSWORD_LOWERCASE_REGEX = /[a-z]/;
2135
2278
  var PASSWORD_DIGIT_REGEX = /\d/;
@@ -2209,7 +2352,7 @@ function createPasswordStrategy(config) {
2209
2352
  const failureResult = await recordLoginFailure(config.userRepository, user, failurePolicy);
2210
2353
  if (!failureResult.success)
2211
2354
  return failureResult;
2212
- logger6.warn("Password verification failed", { userId: user.id });
2355
+ logger7.warn("Password verification failed", { userId: user.id });
2213
2356
  return err(HaiIamError.INVALID_CREDENTIALS, iamM("iam_passwordWrong"));
2214
2357
  }
2215
2358
  return ok(void 0);
@@ -2243,7 +2386,7 @@ function createPasswordStrategy(config) {
2243
2386
  const resetResult = await resetLoginFailures(config.userRepository, storedUser, failurePolicy);
2244
2387
  if (!resetResult.success)
2245
2388
  return resetResult;
2246
- logger6.info("Password authentication succeeded", { userId: storedUser.id });
2389
+ logger7.debug("Password authentication succeeded", { userId: storedUser.id });
2247
2390
  return ok({ ...toUser(storedUser), credentialVersion: storedUser.credentialVersion ?? "" });
2248
2391
  }
2249
2392
  };
@@ -2263,7 +2406,7 @@ function createPasswordStrategy(config) {
2263
2406
  }
2264
2407
 
2265
2408
  // src/authn/iam-authn-functions.ts
2266
- var logger7 = core.logger.child({ module: "iam", scope: "authn" });
2409
+ var logger8 = core.logger.child({ module: "iam", scope: "authn" });
2267
2410
  async function createAuthnOperations(deps) {
2268
2411
  try {
2269
2412
  const { config, sessionFunctions, authzFunctions, ldapClientFactory, ldapSyncUser, onOtpSendEmail, onOtpSendSms } = deps;
@@ -2347,10 +2490,10 @@ async function createAuthnOperations(deps) {
2347
2490
  userRepository,
2348
2491
  config
2349
2492
  });
2350
- logger7.info("Authn sub-feature initialized");
2493
+ logger8.info("Authn sub-feature initialized");
2351
2494
  return ok({ authn: operations.authn, loginRegisteredUser: operations.loginRegisteredUser, passwordStrategyResult: passwordResult, apiKeyFunctions });
2352
2495
  } catch (error) {
2353
- logger7.error("Authn sub-feature initialization failed", { error });
2496
+ logger8.error("Authn sub-feature initialization failed", { error });
2354
2497
  return err(
2355
2498
  HaiIamError.CONFIG_ERROR,
2356
2499
  iamM("iam_initComponentFailed"),
@@ -2430,7 +2573,8 @@ function buildAuthnOperations(deps) {
2430
2573
  avatarUrl: user.avatarUrl,
2431
2574
  ...effective,
2432
2575
  apiKeyId: apiKey?.id,
2433
- apiKeyScopes: apiKey?.scopes
2576
+ apiKeyScopes: apiKey?.scopes,
2577
+ terminalType: context?.terminalType ?? (apiKey ? "api" : "web")
2434
2578
  });
2435
2579
  if (!sessionResult.success) {
2436
2580
  return sessionResult;
@@ -2444,7 +2588,7 @@ function buildAuthnOperations(deps) {
2444
2588
  if (!recorded.success) {
2445
2589
  const revoked = await sessionFunctions.delete(session.accessToken);
2446
2590
  if (!revoked.success)
2447
- logger7.error("Failed to revoke session after login timestamp write failure", { userId: user.id, error: revoked.error });
2591
+ logger8.error("Failed to revoke session after login timestamp write failure", { userId: user.id, error: revoked.error });
2448
2592
  return recorded;
2449
2593
  }
2450
2594
  const { credentialVersion, ...publicUser } = user;
@@ -2473,12 +2617,14 @@ function buildAuthnOperations(deps) {
2473
2617
  ...credentials
2474
2618
  });
2475
2619
  if (!authResult.success) {
2476
- logger7.warn("Login failed", { type, reason: authResult.error.code });
2620
+ logger8.warn("Login failed", { type, reason: authResult.error.code });
2477
2621
  return authResult;
2478
2622
  }
2623
+ if (credentials.expectedUserId && credentials.expectedUserId !== authResult.data.id)
2624
+ return err(HaiIamError.INVALID_CREDENTIALS, iamM("iam_passwordWrong"));
2479
2625
  const result = await buildAuthResult(authResult.data, void 0, context);
2480
2626
  if (result.success) {
2481
- logger7.info("Login succeeded", { type, userId: authResult.data.id });
2627
+ logger8.info("Login succeeded", { type, userId: authResult.data.id });
2482
2628
  }
2483
2629
  return result;
2484
2630
  }
@@ -2486,24 +2632,24 @@ function buildAuthnOperations(deps) {
2486
2632
  const session = await sessionFunctions.verifyToken(accessToken);
2487
2633
  if (!session.success)
2488
2634
  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"));
2635
+ const authorization = await authzFunctions.getUserAuthorization(session.data.userId);
2499
2636
  if (!authorization.success)
2500
2637
  return authorization;
2501
- const current = { ...session.data, username: user.data.username, displayName: user.data.displayName, avatarUrl: user.data.avatarUrl, ...authorization.data };
2638
+ const current = { ...session.data, ...authorization.data };
2502
2639
  return ok(current.apiKeyId ? applyApiKeyScopes(current, current.apiKeyScopes ?? []) : current);
2503
2640
  }
2504
2641
  const tickets = createTicketOperations();
2505
2642
  const sensitive = config.reauthentication;
2506
- const requiresProof = (operation) => sensitive?.enabled && sensitive.operations.includes(operation);
2643
+ async function requiresProof(operation) {
2644
+ if (!sensitive?.enabled)
2645
+ return ok(false);
2646
+ if (sensitive.operations.includes(operation))
2647
+ return ok(true);
2648
+ const configured = await authzFunctions.getSensitiveOperations(operation);
2649
+ if (!configured.success)
2650
+ return configured;
2651
+ return ok(sensitive.operations.includes(operation) || configured.data.includes(operation));
2652
+ }
2507
2653
  return { loginRegisteredUser: async (user, context) => {
2508
2654
  const stored = await userRepository.findById(user.id);
2509
2655
  if (!stored.success)
@@ -2514,7 +2660,10 @@ function buildAuthnOperations(deps) {
2514
2660
  }, authn: {
2515
2661
  createCaptcha: (input) => createIamCaptcha(config.captcha, input),
2516
2662
  async reauthenticate(accessToken, password, operation) {
2517
- if (!requiresProof(operation))
2663
+ const required = await requiresProof(operation);
2664
+ if (!required.success)
2665
+ return required;
2666
+ if (!required.data)
2518
2667
  return err(HaiIamError.PERMISSION_DENIED, iamM("iam_reauthenticationRequired"));
2519
2668
  const current = await verifyCurrentSession(accessToken);
2520
2669
  if (!current.success)
@@ -2535,7 +2684,10 @@ function buildAuthnOperations(deps) {
2535
2684
  const current = await verifyCurrentSession(accessToken);
2536
2685
  if (!current.success)
2537
2686
  return current;
2538
- if (!requiresProof(operation))
2687
+ const required = await requiresProof(operation);
2688
+ if (!required.success)
2689
+ return required;
2690
+ if (!required.data)
2539
2691
  return ok(void 0);
2540
2692
  if (!ticket)
2541
2693
  return err(HaiIamError.REAUTHENTICATION_REQUIRED, iamM("iam_reauthenticationRequired"));
@@ -2590,6 +2742,7 @@ function buildAuthnOperations(deps) {
2590
2742
  }
2591
2743
  var TABLE_NAME3 = "hai_iam_permissions";
2592
2744
  var PERMISSION_FIELDS = [
2745
+ { fieldName: "sensitiveOperations", columnName: "sensitive_operations", def: { type: "JSON" }, select: true, create: true, update: true },
2593
2746
  {
2594
2747
  fieldName: "id",
2595
2748
  columnName: "id",
@@ -2679,6 +2832,32 @@ async function createDbPermissionRepository() {
2679
2832
  return repo;
2680
2833
  }
2681
2834
  var DbPermissionRepository = class extends BaseReldbCrudRepository {
2835
+ async getSensitiveOperations(operation) {
2836
+ if (operation !== void 0 && (typeof operation !== "string" || !operation.trim() || operation.length > 128))
2837
+ return err(HaiIamError.INVALID_ARGUMENT, iamM("iam_sensitiveOperationsInvalid"));
2838
+ const match = reldb.config?.type === "postgresql" ? "sensitive_operations::jsonb @> CAST(? AS jsonb)" : reldb.config?.type === "mysql" ? "JSON_CONTAINS(sensitive_operations, JSON_QUOTE(?))" : "EXISTS (SELECT 1 FROM json_each(sensitive_operations) WHERE type = 'text' AND value = ?)";
2839
+ const params = operation === void 0 ? [] : [reldb.config?.type === "postgresql" ? JSON.stringify([operation]) : operation];
2840
+ const result = await reldb.sql.query(`SELECT sensitive_operations FROM hai_iam_permissions WHERE sensitive_operations IS NOT NULL${operation === void 0 ? "" : ` AND ${match} LIMIT 1`}`, params);
2841
+ if (!result.success)
2842
+ return result;
2843
+ const operations = /* @__PURE__ */ new Set();
2844
+ try {
2845
+ for (const row of result.data) {
2846
+ const values = typeof row.sensitive_operations === "string" ? JSON.parse(row.sensitive_operations) : row.sensitive_operations;
2847
+ if (!Array.isArray(values) || values.length > 1e3)
2848
+ return err(HaiIamError.REPOSITORY_ERROR, iamM("iam_reauthenticationRequired"));
2849
+ for (const value of values) {
2850
+ if (typeof value !== "string" || !value.trim() || value.length > 128)
2851
+ return err(HaiIamError.REPOSITORY_ERROR, iamM("iam_reauthenticationRequired"));
2852
+ if (operation === void 0 || value.trim() === operation)
2853
+ operations.add(value.trim());
2854
+ }
2855
+ }
2856
+ return ok([...operations].sort());
2857
+ } catch {
2858
+ return err(HaiIamError.REPOSITORY_ERROR, iamM("iam_reauthenticationRequired"));
2859
+ }
2860
+ }
2682
2861
  constructor() {
2683
2862
  super(reldb, {
2684
2863
  table: TABLE_NAME3,
@@ -3247,7 +3426,7 @@ var DbRoleRepository = class extends BaseReldbCrudRepository {
3247
3426
  };
3248
3427
 
3249
3428
  // src/authz/iam-authz-functions.ts
3250
- var logger8 = core.logger.child({ module: "iam", scope: "authz" });
3429
+ var logger9 = core.logger.child({ module: "iam", scope: "authz" });
3251
3430
  async function createAuthzOperations(deps) {
3252
3431
  try {
3253
3432
  const { config, session } = deps;
@@ -3269,10 +3448,23 @@ async function createAuthzOperations(deps) {
3269
3448
  userRoleRepository: userRoleResult.data,
3270
3449
  session
3271
3450
  });
3272
- logger8.info("Authz sub-feature initialized");
3273
- return ok(manager);
3451
+ logger9.info("Authz sub-feature initialized");
3452
+ return ok({
3453
+ ...manager,
3454
+ assignRole: guardIamMutation(manager.assignRole, 2),
3455
+ removeRole: guardIamMutation(manager.removeRole, 2),
3456
+ syncRoles: guardIamMutation(manager.syncRoles, 2),
3457
+ createRole: guardIamMutation(manager.createRole, 1),
3458
+ updateRole: guardIamMutation(manager.updateRole, 2),
3459
+ deleteRole: guardIamMutation(manager.deleteRole, 1),
3460
+ createPermission: guardIamMutation(manager.createPermission, 1),
3461
+ deletePermission: guardIamMutation(manager.deletePermission, 1),
3462
+ assignPermissionToRole: guardIamMutation(manager.assignPermissionToRole, 2),
3463
+ removePermissionFromRole: guardIamMutation(manager.removePermissionFromRole, 2),
3464
+ setPermissionSensitiveOperations: guardIamMutation(manager.setPermissionSensitiveOperations)
3465
+ });
3274
3466
  } catch (error) {
3275
- logger8.error("Authz sub-feature initialization failed", { error });
3467
+ logger9.error("Authz sub-feature initialization failed", { error });
3276
3468
  return err(
3277
3469
  HaiIamError.CONFIG_ERROR,
3278
3470
  iamM("iam_initComponentFailed"),
@@ -3289,18 +3481,6 @@ function createRbacManager(config) {
3289
3481
  userRoleRepository,
3290
3482
  session
3291
3483
  } = config;
3292
- let superAdminRoleId;
3293
- async function resolveSuperAdminRoleId() {
3294
- if (superAdminRoleId !== void 0) {
3295
- return ok(superAdminRoleId);
3296
- }
3297
- const roleResult = await roleRepository.findByCode(rbacConfig.superAdminRole);
3298
- if (!roleResult.success) {
3299
- return mapRepositoryError2("iam_queryRoleFailed", roleResult.error.message);
3300
- }
3301
- superAdminRoleId = roleResult.data?.id ?? null;
3302
- return ok(superAdminRoleId);
3303
- }
3304
3484
  function mapRepositoryError2(messageKey, message) {
3305
3485
  return err(
3306
3486
  HaiIamError.REPOSITORY_ERROR,
@@ -3317,16 +3497,6 @@ function createRbacManager(config) {
3317
3497
  }
3318
3498
  return false;
3319
3499
  }
3320
- async function hasPermissionInRoles(roleIds, permission) {
3321
- const codesResult = await rolePermissionRepository.getPermissionCodesForRoles(roleIds);
3322
- if (!codesResult.success)
3323
- return codesResult;
3324
- for (const code of codesResult.data) {
3325
- if (matchesPermission(permission, code))
3326
- return ok(true);
3327
- }
3328
- return ok(false);
3329
- }
3330
3500
  async function getUserPermissionsInternal(userId) {
3331
3501
  const roleIdsResult = await userRoleRepository.getRoleIds(userId);
3332
3502
  if (!roleIdsResult.success)
@@ -3368,19 +3538,19 @@ function createRbacManager(config) {
3368
3538
  async function syncSessionPermissionsForRole(roleId) {
3369
3539
  const userIdsResult = await userRoleRepository.getUserIdsByRoleId(roleId);
3370
3540
  if (!userIdsResult.success) {
3371
- logger8.error("Failed to query users for permission sync", { roleId, error: userIdsResult.error.message });
3541
+ logger9.error("Failed to query users for permission sync", { roleId, error: userIdsResult.error.message });
3372
3542
  return;
3373
3543
  }
3374
3544
  await Promise.allSettled(
3375
3545
  userIdsResult.data.map(async (userId) => {
3376
3546
  const permResult = await resolveUserPermissionCodes(userId);
3377
3547
  if (!permResult.success) {
3378
- logger8.error("Failed to resolve permissions for session sync", { userId, roleId, error: permResult.error.message });
3548
+ logger9.error("Failed to resolve permissions for session sync", { userId, roleId, error: permResult.error.message });
3379
3549
  return;
3380
3550
  }
3381
3551
  const syncResult = await session.patchUserSessions(userId, { permissions: permResult.data });
3382
3552
  if (!syncResult.success) {
3383
- logger8.error("Failed to sync session permissions", { userId, roleId, error: syncResult.error.message });
3553
+ logger9.error("Failed to sync session permissions", { userId, roleId, error: syncResult.error.message });
3384
3554
  }
3385
3555
  })
3386
3556
  );
@@ -3394,17 +3564,17 @@ function createRbacManager(config) {
3394
3564
  if (roleCodesResult.success) {
3395
3565
  updates.roles = roleCodesResult.data;
3396
3566
  } else {
3397
- logger8.error("Failed to resolve roles for session sync", { userId, error: roleCodesResult.error.message });
3567
+ logger9.error("Failed to resolve roles for session sync", { userId, error: roleCodesResult.error.message });
3398
3568
  }
3399
3569
  if (permCodesResult.success) {
3400
3570
  updates.permissions = permCodesResult.data;
3401
3571
  } else {
3402
- logger8.error("Failed to resolve permissions for session sync", { userId, error: permCodesResult.error.message });
3572
+ logger9.error("Failed to resolve permissions for session sync", { userId, error: permCodesResult.error.message });
3403
3573
  }
3404
3574
  if (updates.roles !== void 0 || updates.permissions !== void 0) {
3405
3575
  const syncResult = await session.patchUserSessions(userId, updates);
3406
3576
  if (!syncResult.success) {
3407
- logger8.error("Failed to patch user sessions", { userId, error: syncResult.error.message });
3577
+ logger9.error("Failed to patch user sessions", { userId, error: syncResult.error.message });
3408
3578
  }
3409
3579
  }
3410
3580
  }
@@ -3427,38 +3597,42 @@ function createRbacManager(config) {
3427
3597
  }
3428
3598
  return ok(void 0);
3429
3599
  }
3600
+ async function getUserAuthorization(userId) {
3601
+ return readIamCache(`authorization:${userId}`, async () => {
3602
+ const rows = await reldb.sql.query("SELECT r.code AS role_code, p.code AS permission_code FROM hai_iam_user_roles ur JOIN hai_iam_roles r ON r.id = ur.role_id LEFT JOIN hai_iam_role_permissions rp ON rp.role_id = r.id LEFT JOIN hai_iam_permissions p ON p.id = rp.permission_id WHERE ur.user_id = ?", [userId]);
3603
+ return rows.success ? ok({ roles: [...new Set(rows.data.map((row) => row.role_code))], permissions: [...new Set(rows.data.flatMap((row) => row.permission_code ? [row.permission_code] : []))] }) : rows;
3604
+ });
3605
+ }
3430
3606
  return {
3607
+ getSensitiveOperations: (operation) => rbacConfig.enabled ? readIamCache(`sensitive:${operation ?? "*"}`, () => permissionRepository.getSensitiveOperations(operation)) : Promise.resolve(ok([])),
3608
+ async setPermissionSensitiveOperations(permissionId, operations) {
3609
+ if (!Array.isArray(operations) || operations.length > 1e3 || operations.some((value) => typeof value !== "string" || !value.trim() || value.length > 128))
3610
+ return err(HaiIamError.INVALID_ARGUMENT, iamM("iam_sensitiveOperationsInvalid"));
3611
+ const updated = await permissionRepository.updateById(permissionId, { sensitiveOperations: [...new Set(operations.map((value) => value.trim()))] });
3612
+ if (!updated.success)
3613
+ return updated;
3614
+ if (updated.data.changes === 0) {
3615
+ const existing = await permissionRepository.findById(permissionId);
3616
+ if (!existing.success)
3617
+ return existing;
3618
+ if (!existing.data)
3619
+ return err(HaiIamError.PERMISSION_NOT_FOUND, iamM("iam_permissionNotExist"));
3620
+ }
3621
+ return ok(void 0);
3622
+ },
3431
3623
  async checkPermission(userId, permission) {
3432
3624
  if (!rbacConfig.enabled) {
3433
3625
  return ok(true);
3434
3626
  }
3435
- const roleIdsResult = await userRoleRepository.getRoleIds(userId);
3436
- if (!roleIdsResult.success)
3437
- return roleIdsResult;
3438
- const roleIds = roleIdsResult.data;
3439
- if (roleIds.length === 0) {
3440
- return ok(false);
3441
- }
3442
- const superAdminResult = await resolveSuperAdminRoleId();
3443
- if (!superAdminResult.success)
3444
- return superAdminResult;
3445
- if (superAdminResult.data && roleIds.includes(superAdminResult.data)) {
3446
- return ok(true);
3447
- }
3448
- return hasPermissionInRoles(roleIds, permission);
3627
+ const authorization = await getUserAuthorization(userId);
3628
+ if (!authorization.success)
3629
+ return authorization;
3630
+ return ok(authorization.data.roles.includes(rbacConfig.superAdminRole) || authorization.data.permissions.some((code) => matchesPermission(permission, code)));
3449
3631
  },
3450
3632
  async getUserPermissions(userId) {
3451
3633
  return getUserPermissionsInternal(userId);
3452
3634
  },
3453
- async getUserAuthorization(userId) {
3454
- const roles = await userRoleRepository.getRoleSummaries(userId);
3455
- if (!roles.success)
3456
- return roles;
3457
- const permissions = roles.data.length ? await rolePermissionRepository.getPermissionCodesForRoles(roles.data.map((role) => role.id)) : ok([]);
3458
- if (!permissions.success)
3459
- return permissions;
3460
- return ok({ roles: roles.data.map((role) => role.code), permissions: permissions.data });
3461
- },
3635
+ getUserAuthorization,
3462
3636
  async getUserRoles(userId) {
3463
3637
  return userRoleRepository.getRoles(userId);
3464
3638
  },
@@ -3472,7 +3646,7 @@ function createRbacManager(config) {
3472
3646
  }
3473
3647
  const result = await userRoleRepository.assign(userId, roleId, tx);
3474
3648
  if (result.success) {
3475
- logger8.info("Role assigned to user", { userId, roleId });
3649
+ logger9.info("Role assigned to user", { userId, roleId });
3476
3650
  if (!tx) {
3477
3651
  await syncUserSessionAfterRoleChange(userId);
3478
3652
  }
@@ -3482,7 +3656,7 @@ function createRbacManager(config) {
3482
3656
  async removeRole(userId, roleId, tx) {
3483
3657
  const result = await userRoleRepository.remove(userId, roleId, tx);
3484
3658
  if (result.success) {
3485
- logger8.info("Role removed from user", { userId, roleId });
3659
+ logger9.info("Role removed from user", { userId, roleId });
3486
3660
  if (!tx) {
3487
3661
  await syncUserSessionAfterRoleChange(userId);
3488
3662
  }
@@ -3550,7 +3724,7 @@ function createRbacManager(config) {
3550
3724
  error
3551
3725
  );
3552
3726
  }
3553
- logger8.info("Roles synced for user", { userId, added: toAdd.length, removed: toRemove.length });
3727
+ logger9.info("Roles synced for user", { userId, added: toAdd.length, removed: toRemove.length });
3554
3728
  if (ownTx) {
3555
3729
  await syncUserSessionAfterRoleChange(userId);
3556
3730
  }
@@ -3602,10 +3776,7 @@ function createRbacManager(config) {
3602
3776
  return mapRepositoryError2("iam_createRoleFailed", commitResult.error.message);
3603
3777
  }
3604
3778
  }
3605
- logger8.info("Role created", { roleId: createdResult.data.id, code: role.code });
3606
- if (role.code === rbacConfig.superAdminRole) {
3607
- superAdminRoleId = void 0;
3608
- }
3779
+ logger9.info("Role created", { roleId: createdResult.data.id, code: role.code });
3609
3780
  return ok(createdResult.data);
3610
3781
  } catch (error) {
3611
3782
  if (ownTx)
@@ -3691,7 +3862,6 @@ function createRbacManager(config) {
3691
3862
  return mapRepositoryError2("iam_updateRoleFailed", commitResult.error.message);
3692
3863
  }
3693
3864
  }
3694
- superAdminRoleId = void 0;
3695
3865
  if (ownTx) {
3696
3866
  const affectedUsersResult = await userRoleRepository.getUserIdsByRoleId(roleId);
3697
3867
  if (affectedUsersResult.success) {
@@ -3699,7 +3869,7 @@ function createRbacManager(config) {
3699
3869
  affectedUsersResult.data.map((userId) => syncUserSessionAfterRoleChange(userId))
3700
3870
  );
3701
3871
  } else {
3702
- logger8.error("Failed to query affected users after updateRole", { roleId, error: affectedUsersResult.error.message });
3872
+ logger9.error("Failed to query affected users after updateRole", { roleId, error: affectedUsersResult.error.message });
3703
3873
  }
3704
3874
  }
3705
3875
  return ok(updatedResult.data);
@@ -3773,12 +3943,15 @@ function createRbacManager(config) {
3773
3943
  affectedUserIds.map((userId) => syncUserSessionAfterRoleChange(userId))
3774
3944
  );
3775
3945
  }
3776
- superAdminRoleId = void 0;
3777
- logger8.info("Role deleted", { roleId });
3946
+ logger9.info("Role deleted", { roleId });
3778
3947
  return ok(void 0);
3779
3948
  },
3780
3949
  // ─── 权限管理 ───
3781
3950
  async createPermission(permission, tx) {
3951
+ const operations = permission.sensitiveOperations ?? [];
3952
+ if (!Array.isArray(operations) || operations.length > 1e3 || operations.some((value) => typeof value !== "string" || !value.trim() || value.length > 128))
3953
+ return err(HaiIamError.INVALID_ARGUMENT, iamM("iam_sensitiveOperationsInvalid"));
3954
+ permission = { ...permission, sensitiveOperations: [...new Set(operations.map((value) => value.trim()))] };
3782
3955
  const ownTx = !tx;
3783
3956
  if (!tx) {
3784
3957
  const txResult = await reldb.tx.begin();
@@ -3814,7 +3987,7 @@ function createRbacManager(config) {
3814
3987
  return mapRepositoryError2("iam_createPermissionFailed", commitResult.error.message);
3815
3988
  }
3816
3989
  }
3817
- logger8.info("Permission created", { permissionId: createdResult.data.id, code: permission.code });
3990
+ logger9.info("Permission created", { permissionId: createdResult.data.id, code: permission.code });
3818
3991
  return ok(createdResult.data);
3819
3992
  } catch (error) {
3820
3993
  if (ownTx)
@@ -3918,7 +4091,7 @@ function createRbacManager(config) {
3918
4091
  affectedRoleIds.map((roleId) => syncSessionPermissionsForRole(roleId))
3919
4092
  );
3920
4093
  }
3921
- logger8.info("Permission deleted", { permissionId });
4094
+ logger9.info("Permission deleted", { permissionId });
3922
4095
  return ok(void 0);
3923
4096
  },
3924
4097
  async assignPermissionToRole(roleId, permissionId, tx) {
@@ -3940,7 +4113,7 @@ function createRbacManager(config) {
3940
4113
  }
3941
4114
  const assignResult = await rolePermissionRepository.assign(roleId, permissionId, tx);
3942
4115
  if (assignResult.success) {
3943
- logger8.info("Permission assigned to role", { roleId, permissionId });
4116
+ logger9.info("Permission assigned to role", { roleId, permissionId });
3944
4117
  if (!tx) {
3945
4118
  await syncSessionPermissionsForRole(roleId);
3946
4119
  }
@@ -3969,7 +4142,7 @@ function createRbacManager(config) {
3969
4142
  }
3970
4143
  const removeResult = await rolePermissionRepository.remove(roleId, permissionId, tx);
3971
4144
  if (removeResult.success) {
3972
- logger8.info("Permission removed from role", { roleId, permissionId });
4145
+ logger9.info("Permission removed from role", { roleId, permissionId });
3973
4146
  if (!tx) {
3974
4147
  await syncSessionPermissionsForRole(roleId);
3975
4148
  }
@@ -3987,14 +4160,16 @@ function createRbacManager(config) {
3987
4160
  }
3988
4161
  };
3989
4162
  }
3990
- var logger9 = core.logger.child({ module: "iam", scope: "audit" });
4163
+ var logger10 = core.logger.child({ module: "iam", scope: "audit" });
3991
4164
  async function observe(operation, event, tx) {
3992
4165
  const result = await operation();
4166
+ if (!audit.isInitialized)
4167
+ return result;
3993
4168
  const fields = typeof event === "function" ? event(result) : event;
3994
4169
  if (!fields)
3995
4170
  return result;
3996
4171
  if (tx && !result.success) {
3997
- logger9.warn("IAM transactional operation failed", { action: fields.action, resource: fields.resource, reasonCode: result.error.code });
4172
+ logger10.warn("IAM transactional operation failed", { action: fields.action, resource: fields.resource, reasonCode: result.error.code });
3998
4173
  return result;
3999
4174
  }
4000
4175
  const recorded = await audit.capture({
@@ -4004,7 +4179,7 @@ async function observe(operation, event, tx) {
4004
4179
  details: { ...fields.details, ...!result.success ? { reasonCode: result.error.code } : {} }
4005
4180
  }, { tx });
4006
4181
  if (!recorded.success) {
4007
- logger9.error("IAM event audit failed", { action: fields.action, resource: fields.resource, error: recorded.error });
4182
+ logger10.error("IAM event audit failed", { action: fields.action, resource: fields.resource, error: recorded.error });
4008
4183
  if (tx)
4009
4184
  return recorded;
4010
4185
  }
@@ -4065,6 +4240,7 @@ function observeAuthorization(operations) {
4065
4240
  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
4241
  deleteRole: (id, tx) => observe(() => operations.deleteRole(id, tx), { action: "role.delete", resource: "iam.roles", resourceId: id }, tx),
4067
4242
  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),
4243
+ setPermissionSensitiveOperations: (id, operationIds) => observe(() => operations.setPermissionSensitiveOperations(id, operationIds), { action: "permission.sensitive-operations", resource: "iam.permissions", resourceId: id, details: { operations: operationIds } }),
4068
4244
  deletePermission: (id, tx) => observe(() => operations.deletePermission(id, tx), { action: "permission.delete", resource: "iam.permissions", resourceId: id }, tx),
4069
4245
  assignPermissionToRole: (id, permissionId, tx) => observe(() => operations.assignPermissionToRole(id, permissionId, tx), { action: "permission.assign", resource: "iam.role-permissions", resourceId: id, details: { permissionId } }, tx),
4070
4246
  removePermissionFromRole: (id, permissionId, tx) => observe(() => operations.removePermissionFromRole(id, permissionId, tx), { action: "permission.remove", resource: "iam.role-permissions", resourceId: id, details: { permissionId } }, tx)
@@ -4151,6 +4327,17 @@ var steps = [{
4151
4327
  const added = await sql.execute("ALTER TABLE hai_iam_users ADD COLUMN first_login_failed_at TIMESTAMP");
4152
4328
  return added.success ? ok(void 0) : added;
4153
4329
  }
4330
+ }, {
4331
+ version: 5,
4332
+ name: "permission-sensitive-operations",
4333
+ async needed(sql) {
4334
+ const found = await schemaColumns(reldb, "hai_iam_permissions", sql);
4335
+ return found.success ? ok(found.data.length > 0 && !found.data.includes("sensitive_operations")) : found;
4336
+ },
4337
+ async apply(sql) {
4338
+ const added = await sql.execute("ALTER TABLE hai_iam_permissions ADD COLUMN sensitive_operations JSON");
4339
+ return added.success ? ok(void 0) : added;
4340
+ }
4154
4341
  }];
4155
4342
  function migrateIam(options = {}) {
4156
4343
  return migrateSchema(reldb, "iam", steps, options);
@@ -4215,7 +4402,29 @@ function buildIamPublicConfig(config, runtime) {
4215
4402
  initialCredentials: { enabled: Boolean(config.initialCredentials?.enabled && config.passwordReset?.enabled !== false && runtime.onPasswordResetRequest) }
4216
4403
  };
4217
4404
  }
4218
- var logger10 = core.logger.child({ module: "iam", scope: "seed" });
4405
+ async function readIamUserSecurity(repository, userId) {
4406
+ const found = await readIamCache(`user:${userId}`, async () => {
4407
+ const row = await repository.findById(userId);
4408
+ if (!row.success)
4409
+ return row;
4410
+ const user = row.data;
4411
+ return ok(user ? {
4412
+ username: user.username,
4413
+ displayName: user.displayName,
4414
+ avatarUrl: user.avatarUrl,
4415
+ enabled: user.enabled,
4416
+ accountType: user.accountType,
4417
+ accountStatus: user.accountStatus,
4418
+ accountExpiresAt: user.accountExpiresAt ? new Date(user.accountExpiresAt).getTime() : null,
4419
+ credentialVersion: user.credentialVersion ?? "",
4420
+ mustChangePassword: user.mustChangePassword,
4421
+ hasPassword: !!user.passwordHash,
4422
+ passwordUpdatedAt: user.passwordUpdatedAt?.getTime() ?? null
4423
+ } : null);
4424
+ });
4425
+ return found.success ? ok(found.data ? { ...found.data, accountExpiresAt: found.data.accountExpiresAt === null ? null : new Date(found.data.accountExpiresAt), passwordUpdatedAt: found.data.passwordUpdatedAt === null ? void 0 : new Date(found.data.passwordUpdatedAt), passwordHash: found.data.hasPassword ? "present" : void 0 } : null) : found;
4426
+ }
4427
+ var logger11 = core.logger.child({ module: "iam", scope: "seed" });
4219
4428
  var DEFAULT_ROLES = [
4220
4429
  { code: "admin", name: () => iamM("iam_seedRoleAdminName"), description: () => iamM("iam_seedRoleAdminDesc"), isSystem: true },
4221
4430
  { code: "user", name: () => iamM("iam_seedRoleUserName"), description: () => iamM("iam_seedRoleUserDesc"), isSystem: true },
@@ -4259,12 +4468,11 @@ var USER_ROLE_PERMISSIONS = ["dashboard:view", "profile:read"];
4259
4468
  async function seedIamData(authz) {
4260
4469
  try {
4261
4470
  const existingRoles = await authz.getAllRoles({ page: 1, pageSize: 1e3 });
4471
+ if (!existingRoles.success)
4472
+ return existingRoles;
4262
4473
  const existingRoleMap = /* @__PURE__ */ new Map();
4263
- if (existingRoles.success) {
4264
- for (const role of existingRoles.data.items) {
4265
- existingRoleMap.set(role.code, role.id);
4266
- }
4267
- }
4474
+ for (const role of existingRoles.data.items)
4475
+ existingRoleMap.set(role.code, role.id);
4268
4476
  const roleMap = /* @__PURE__ */ new Map();
4269
4477
  for (const role of DEFAULT_ROLES) {
4270
4478
  const existingId = existingRoleMap.get(role.code);
@@ -4275,17 +4483,23 @@ async function seedIamData(authz) {
4275
4483
  const result = await authz.createRole({ code: role.code, name: role.name(), description: role.description(), isSystem: role.isSystem });
4276
4484
  if (result.success) {
4277
4485
  roleMap.set(role.code, result.data.id);
4486
+ } else if (result.error.code === HaiIamError.ROLE_ALREADY_EXISTS.code) {
4487
+ const concurrent = await authz.getRoleByCode(role.code);
4488
+ if (!concurrent.success)
4489
+ return concurrent;
4490
+ if (!concurrent.data)
4491
+ return result;
4492
+ roleMap.set(role.code, concurrent.data.id);
4278
4493
  } else {
4279
4494
  return result;
4280
4495
  }
4281
4496
  }
4282
4497
  const existingPerms = await authz.getAllPermissions({ page: 1, pageSize: 1e3 });
4498
+ if (!existingPerms.success)
4499
+ return existingPerms;
4283
4500
  const existingPermMap = /* @__PURE__ */ new Map();
4284
- if (existingPerms.success) {
4285
- for (const perm of existingPerms.data.items) {
4286
- existingPermMap.set(perm.code, perm.id);
4287
- }
4288
- }
4501
+ for (const perm of existingPerms.data.items)
4502
+ existingPermMap.set(perm.code, perm.id);
4289
4503
  const permMap = /* @__PURE__ */ new Map();
4290
4504
  for (const perm of DEFAULT_PERMISSIONS) {
4291
4505
  const existingId = existingPermMap.get(perm.code);
@@ -4296,6 +4510,13 @@ async function seedIamData(authz) {
4296
4510
  const result = await authz.createPermission({ code: perm.code, name: perm.name(), type: perm.type, resource: perm.resource, action: perm.action });
4297
4511
  if (result.success) {
4298
4512
  permMap.set(perm.code, result.data.id);
4513
+ } else if (result.error.code === HaiIamError.PERMISSION_ALREADY_EXISTS.code) {
4514
+ const concurrent = await authz.getPermissionByCode(perm.code);
4515
+ if (!concurrent.success)
4516
+ return concurrent;
4517
+ if (!concurrent.data)
4518
+ return result;
4519
+ permMap.set(perm.code, concurrent.data.id);
4299
4520
  } else {
4300
4521
  return result;
4301
4522
  }
@@ -4303,7 +4524,9 @@ async function seedIamData(authz) {
4303
4524
  const adminRoleId = roleMap.get("admin");
4304
4525
  if (adminRoleId) {
4305
4526
  for (const [, permId] of permMap) {
4306
- await authz.assignPermissionToRole(adminRoleId, permId);
4527
+ const assigned = await authz.assignPermissionToRole(adminRoleId, permId);
4528
+ if (!assigned.success)
4529
+ return assigned;
4307
4530
  }
4308
4531
  }
4309
4532
  const userRoleId = roleMap.get("user");
@@ -4311,14 +4534,16 @@ async function seedIamData(authz) {
4311
4534
  for (const permCode of USER_ROLE_PERMISSIONS) {
4312
4535
  const permId = permMap.get(permCode);
4313
4536
  if (permId) {
4314
- await authz.assignPermissionToRole(userRoleId, permId);
4537
+ const assigned = await authz.assignPermissionToRole(userRoleId, permId);
4538
+ if (!assigned.success)
4539
+ return assigned;
4315
4540
  }
4316
4541
  }
4317
4542
  }
4318
- logger10.info("IAM seed data initialized");
4543
+ logger11.info("IAM seed data initialized");
4319
4544
  return ok(void 0);
4320
4545
  } catch (error) {
4321
- logger10.error("Failed to seed IAM data", { error });
4546
+ logger11.error("Failed to seed IAM data", { error });
4322
4547
  return err(
4323
4548
  HaiIamError.REPOSITORY_ERROR,
4324
4549
  iamM("iam_initSeedDataFailed"),
@@ -4331,7 +4556,7 @@ function retainedUntil(entry) {
4331
4556
  }
4332
4557
  function restoreSession(session) {
4333
4558
  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()))) {
4559
+ if (result.terminalType !== void 0 && !["web", "app", "desktop", "api"].includes(result.terminalType) || typeof result.credentialVersion !== "string" || !result.version || !Number.isSafeInteger(result.maxAge) || result.maxAge <= 0 || !Array.isArray(result.roles) || !result.roles.every((role) => typeof role === "string") || !Array.isArray(result.permissions) || !result.permissions.every((permission) => typeof permission === "string") || ![result.createdAt, result.startedAt, result.lastActiveAt, result.expiresAt].every((date) => Number.isFinite(date.getTime()))) {
4335
4560
  throw new TypeError("Invalid session state");
4336
4561
  }
4337
4562
  return result;
@@ -4410,12 +4635,29 @@ function createCacheSessionRepository(refreshTokenMaxAge, policy = {}) {
4410
4635
  }
4411
4636
  return {
4412
4637
  save: (session, tokens, singleDevice = false) => mutate(getUserSessionScope(session.userId), (state) => {
4413
- const count = Object.values(state.sessions).filter((entry) => entry.session.credentialVersion === session.credentialVersion).length;
4414
- if (singleDevice && policy.singleDeviceMode !== "reject") {
4415
- state.sessions = {};
4416
- } else if (singleDevice && count > 0 || maxSessions > 0 && count >= maxSessions) {
4638
+ const active = Object.entries(state.sessions).filter(([, entry]) => entry.session.credentialVersion === session.credentialVersion);
4639
+ const terminal = session.terminalType ?? "web";
4640
+ const limits = policy.terminalLimits ?? {};
4641
+ const cap = limits[terminal];
4642
+ if (Object.keys(limits).length && cap === void 0)
4417
4643
  return err(HaiIamError.SESSION_LIMIT_EXCEEDED, iamM("iam_sessionLimitExceeded"));
4644
+ const removed = /* @__PURE__ */ new Set();
4645
+ if (singleDevice) {
4646
+ if (policy.singleDeviceMode === "reject" && active.length)
4647
+ return err(HaiIamError.SESSION_LIMIT_EXCEEDED, iamM("iam_sessionLimitExceeded"));
4648
+ if (policy.singleDeviceMode !== "reject")
4649
+ Object.keys(state.sessions).forEach((token) => removed.add(token));
4650
+ } else if (cap) {
4651
+ const same = active.filter(([, entry]) => (entry.session.terminalType ?? "web") === terminal).sort(([firstToken, first], [secondToken, second]) => new Date(first.session.startedAt ?? first.session.createdAt).getTime() - new Date(second.session.startedAt ?? second.session.createdAt).getTime() || firstToken.localeCompare(secondToken));
4652
+ if (same.length >= cap) {
4653
+ if (policy.terminalLimitMode !== "replace")
4654
+ return err(HaiIamError.SESSION_LIMIT_EXCEEDED, iamM("iam_sessionLimitExceeded"));
4655
+ same.slice(0, same.length - cap + 1).forEach(([token]) => removed.add(token));
4656
+ }
4418
4657
  }
4658
+ if (maxSessions > 0 && active.filter(([token]) => !removed.has(token)).length >= maxSessions)
4659
+ return err(HaiIamError.SESSION_LIMIT_EXCEEDED, iamM("iam_sessionLimitExceeded"));
4660
+ removed.forEach((token) => delete state.sessions[token]);
4419
4661
  return storeSession(state, session, tokens);
4420
4662
  }),
4421
4663
  rotate: (previous, refreshToken, session, tokens) => mutate(getUserSessionScope(previous.userId), (state) => {
@@ -4466,6 +4708,8 @@ function createCacheSessionRepository(refreshTokenMaxAge, policy = {}) {
4466
4708
  return err(HaiIamError.SESSION_NOT_FOUND, iamM("iam_sessionNotExist"));
4467
4709
  if (!expectedVersion || entry.session.version !== expectedVersion)
4468
4710
  return err(HaiIamError.CONFLICT, iamM("iam_sessionStateConflict"));
4711
+ if (patch.terminalType !== void 0 && patch.terminalType !== (entry.session.terminalType ?? "web"))
4712
+ return err(HaiIamError.CONFLICT, iamM("iam_sessionStateConflict"));
4469
4713
  const updated = applySessionPatch(restoreSession(entry.session), patch);
4470
4714
  if (![updated.expiresAt, updated.lastActiveAt].every((date) => Number.isFinite(date.getTime())))
4471
4715
  return err(HaiIamError.INVALID_ARGUMENT, iamM("iam_sessionMaxAgeInvalid"));
@@ -4545,7 +4789,7 @@ function stripInternalSessionFields(session) {
4545
4789
  const hasExtraKeys = Object.keys(rest).length > 0;
4546
4790
  return { ...session, data: hasExtraKeys ? rest : void 0 };
4547
4791
  }
4548
- var logger11 = core.logger.child({ module: "iam", scope: "session" });
4792
+ var logger12 = core.logger.child({ module: "iam", scope: "session" });
4549
4793
  async function createSessionOperations(deps) {
4550
4794
  try {
4551
4795
  const { config } = deps;
@@ -4563,10 +4807,10 @@ async function createSessionOperations(deps) {
4563
4807
  sessionRepository,
4564
4808
  apiKeyRepository: config.login.apikey ? await createDbApiKeyRepository() : void 0
4565
4809
  });
4566
- logger11.info("Session sub-feature initialized");
4810
+ logger12.info("Session sub-feature initialized");
4567
4811
  return ok(functions);
4568
4812
  } catch (error) {
4569
- logger11.error("Session sub-feature initialization failed", { error });
4813
+ logger12.error("Session sub-feature initialization failed", { error });
4570
4814
  return err(
4571
4815
  HaiIamError.CONFIG_ERROR,
4572
4816
  iamM("iam_initComponentFailed"),
@@ -4580,6 +4824,8 @@ function buildSessionFunctions(config) {
4580
4824
  const singleDevice = config.singleDevice ?? false;
4581
4825
  const repo = config.sessionRepository;
4582
4826
  function candidate(options, credentialVersion) {
4827
+ if (options.terminalType !== void 0 && !["web", "app", "desktop", "api"].includes(options.terminalType))
4828
+ return err(HaiIamError.INVALID_ARGUMENT, iamM("iam_terminalTypeInvalid"));
4583
4829
  const now = /* @__PURE__ */ new Date();
4584
4830
  const ttl = options.maxAge ?? maxAge;
4585
4831
  if (!Number.isSafeInteger(ttl) || ttl <= 0 || !Number.isFinite(new Date(now.getTime() + ttl * 1e3).getTime()))
@@ -4599,7 +4845,7 @@ function buildSessionFunctions(config) {
4599
4845
  return err(HaiIamError.USER_DISABLED, iamM("iam_accountDisabled"));
4600
4846
  if (typeof current.data.version !== "string" || expected !== void 0 && current.data.version !== expected)
4601
4847
  return err(HaiIamError.SESSION_INVALID, iamM("iam_sessionExpired"));
4602
- return ok(current.data.version);
4848
+ return ok(current.data);
4603
4849
  } catch {
4604
4850
  return err(HaiIamError.REPOSITORY_ERROR, iamM("iam_sessionExpired"));
4605
4851
  }
@@ -4609,7 +4855,10 @@ function buildSessionFunctions(config) {
4609
4855
  return ok(session);
4610
4856
  if (!config.apiKeyRepository)
4611
4857
  return err(HaiIamError.APIKEY_INVALID, iamM("iam_apikeyInvalid"));
4612
- const found = await config.apiKeyRepository.findOneById(session.apiKeyId);
4858
+ const found = await readIamCache(`apikey:${session.apiKeyId}`, async () => {
4859
+ const row = await config.apiKeyRepository.findOneById(session.apiKeyId);
4860
+ return row.success ? ok(row.data ? { userId: row.data.userId, enabled: row.data.enabled, expiresAt: row.data.expiresAt?.getTime() ?? null, scopes: row.data.scopes } : null) : row;
4861
+ });
4613
4862
  if (!found.success)
4614
4863
  return found;
4615
4864
  const key = found.data;
@@ -4617,7 +4866,7 @@ function buildSessionFunctions(config) {
4617
4866
  return err(HaiIamError.APIKEY_INVALID, iamM("iam_apikeyInvalid"));
4618
4867
  if (!key.enabled)
4619
4868
  return err(HaiIamError.APIKEY_DISABLED, iamM("iam_apikeyDisabled"));
4620
- if (key.expiresAt && /* @__PURE__ */ new Date() >= key.expiresAt)
4869
+ if (key.expiresAt && Date.now() >= key.expiresAt)
4621
4870
  return err(HaiIamError.APIKEY_EXPIRED, iamM("iam_apikeyExpired"));
4622
4871
  return ok(applyApiKeyScopes({ ...session, apiKeyScopes: key.scopes }, key.scopes));
4623
4872
  }
@@ -4627,7 +4876,7 @@ function buildSessionFunctions(config) {
4627
4876
  const bound = await credential(options.userId, options.expectedCredentialVersion);
4628
4877
  if (!bound.success)
4629
4878
  return bound;
4630
- const built = candidate(options, bound.data);
4879
+ const built = candidate(options, bound.data.version);
4631
4880
  if (!built.success)
4632
4881
  return built;
4633
4882
  const { session, tokens } = built.data;
@@ -4635,7 +4884,7 @@ function buildSessionFunctions(config) {
4635
4884
  if (!saveResult.success) {
4636
4885
  return saveResult;
4637
4886
  }
4638
- logger11.debug("Session created", { userId: options.userId });
4887
+ logger12.debug("Session created", { userId: options.userId });
4639
4888
  return ok(session);
4640
4889
  } catch (error) {
4641
4890
  return err(
@@ -4666,7 +4915,7 @@ function buildSessionFunctions(config) {
4666
4915
  const bound = await credential(session.userId, session.credentialVersion);
4667
4916
  if (!bound.success)
4668
4917
  return bound;
4669
- const validated = await validateApiKey(session);
4918
+ const validated = await validateApiKey({ ...session, ...bound.data.profile });
4670
4919
  if (!validated.success)
4671
4920
  return validated;
4672
4921
  let output = validated.data;
@@ -4696,7 +4945,7 @@ function buildSessionFunctions(config) {
4696
4945
  return repo.updateByAccessToken(accessToken, data, expectedVersion);
4697
4946
  },
4698
4947
  async delete(accessToken) {
4699
- logger11.debug("Session delete requested");
4948
+ logger12.debug("Session delete requested");
4700
4949
  return repo.removeByAccessToken(accessToken);
4701
4950
  },
4702
4951
  async deleteByUserId(userId) {
@@ -4738,6 +4987,7 @@ function buildSessionFunctions(config) {
4738
4987
  apiKeyId: effectiveSession.apiKeyId,
4739
4988
  apiKeyScopes: effectiveSession.apiKeyScopes,
4740
4989
  source: oldSession.source,
4990
+ terminalType: oldSession.terminalType ?? "web",
4741
4991
  maxAge: oldSession.maxAge,
4742
4992
  data: oldSession.data ? { ...oldSession.data, _tokenPair: void 0 } : void 0
4743
4993
  }, oldSession.credentialVersion);
@@ -4749,7 +4999,7 @@ function buildSessionFunctions(config) {
4749
4999
  const current = await credential(oldSession.userId, oldSession.credentialVersion);
4750
5000
  if (!current.success)
4751
5001
  return current;
4752
- logger11.debug("Token refreshed", { userId: oldSession.userId });
5002
+ logger12.debug("Token refreshed", { userId: oldSession.userId });
4753
5003
  return ok(next.data.tokens);
4754
5004
  } catch (error) {
4755
5005
  return err(
@@ -4767,7 +5017,7 @@ function buildSessionFunctions(config) {
4767
5017
  }
4768
5018
  };
4769
5019
  }
4770
- var logger12 = core.logger.child({ module: "iam", scope: "user" });
5020
+ var logger13 = core.logger.child({ module: "iam", scope: "user" });
4771
5021
  var USER_SORT_COLUMNS = {
4772
5022
  /** 用户名列。 */
4773
5023
  username: "username",
@@ -4792,10 +5042,10 @@ async function createUserOperations(deps) {
4792
5042
  config,
4793
5043
  onPasswordResetRequest
4794
5044
  });
4795
- logger12.info("User sub-feature initialized");
5045
+ logger13.info("User sub-feature initialized");
4796
5046
  return ok(functions);
4797
5047
  } catch (error) {
4798
- logger12.error("User sub-feature initialization failed", { error });
5048
+ logger13.error("User sub-feature initialization failed", { error });
4799
5049
  return err(
4800
5050
  HaiIamError.CONFIG_ERROR,
4801
5051
  iamM("iam_initComponentFailed"),
@@ -4964,7 +5214,7 @@ function buildRegistrationOps(ctx) {
4964
5214
  await userRepository.deleteById(createdUser.id);
4965
5215
  return assigned;
4966
5216
  }
4967
- logger12.info("User registered", { userId: createdUser.id, username: options.username });
5217
+ logger13.info("User registered", { userId: createdUser.id, username: options.username });
4968
5218
  return ok({
4969
5219
  user: toUser(createdUser),
4970
5220
  agreements: buildAgreementDisplay(agreementConfig, agreementConfig.showOnRegister)
@@ -5213,7 +5463,7 @@ function buildUserMutationOps(ctx) {
5213
5463
  if (tx) {
5214
5464
  const revoked = await sessionFunctions.deleteByUserId(userId);
5215
5465
  if (!revoked.success)
5216
- logger12.warn("Account credentials revoked; session cache cleanup failed", { userId, error: revoked.error });
5466
+ logger13.warn("Account credentials revoked; session cache cleanup failed", { userId, error: revoked.error });
5217
5467
  }
5218
5468
  const updatedResult = await userRepository.findById(userId);
5219
5469
  if (!updatedResult.success) {
@@ -5228,7 +5478,7 @@ function buildUserMutationOps(ctx) {
5228
5478
  return ok(toUser(updatedResult.data));
5229
5479
  },
5230
5480
  async deleteUser(userId) {
5231
- logger12.debug("Deleting user", { userId });
5481
+ logger13.debug("Deleting user", { userId });
5232
5482
  const userResult = await userRepository.findById(userId);
5233
5483
  if (!userResult.success) {
5234
5484
  return mapRepositoryError("iam_queryUserFailed", userResult.error.message);
@@ -5268,7 +5518,7 @@ function buildUserMutationOps(ctx) {
5268
5518
  );
5269
5519
  }
5270
5520
  await sessionFunctions.deleteByUserId(userId);
5271
- logger12.info("User deleted", { userId });
5521
+ logger13.info("User deleted", { userId });
5272
5522
  return ok(void 0);
5273
5523
  }
5274
5524
  };
@@ -5278,7 +5528,7 @@ function buildPasswordChangeOps(ctx) {
5278
5528
  const { hashPassword } = ctx;
5279
5529
  return {
5280
5530
  async adminResetPassword(userId, newPassword) {
5281
- logger12.debug("Admin resetting user password", { userId });
5531
+ logger13.debug("Admin resetting user password", { userId });
5282
5532
  const userResult = await userRepository.findById(userId);
5283
5533
  if (!userResult.success) {
5284
5534
  return mapRepositoryError("iam_queryUserFailed", userResult.error.message);
@@ -5303,7 +5553,7 @@ function buildPasswordChangeOps(ctx) {
5303
5553
  const revoked = await sessionFunctions.deleteByUserId(userId);
5304
5554
  if (!revoked.success)
5305
5555
  return revoked;
5306
- logger12.info("Admin reset password", { userId });
5556
+ logger13.info("Admin reset password", { userId });
5307
5557
  return ok(void 0);
5308
5558
  },
5309
5559
  async changePassword(userId, oldPassword, newPassword) {
@@ -5357,7 +5607,7 @@ function buildPasswordChangeOps(ctx) {
5357
5607
  const revoked = await sessionFunctions.deleteByUserId(userId);
5358
5608
  if (!revoked.success)
5359
5609
  return revoked;
5360
- logger12.info("Password changed", { userId });
5610
+ logger13.info("Password changed", { userId });
5361
5611
  return ok(void 0);
5362
5612
  },
5363
5613
  /**
@@ -5395,10 +5645,10 @@ async function issuePasswordReset(ctx, stored) {
5395
5645
  try {
5396
5646
  await onPasswordResetRequest(user, token, { expiresAt, idempotencyKey: `iam-reset:${tokenHash.data}` });
5397
5647
  } catch {
5398
- logger12.error("Password reset callback failed", { userId: user.id });
5648
+ logger13.error("Password reset callback failed", { userId: user.id });
5399
5649
  return err(HaiIamError.INTERNAL_ERROR, iamM("iam_notificationDeliveryUnknown"));
5400
5650
  }
5401
- logger12.info("Password reset token generated", { userId: user.id });
5651
+ logger13.info("Password reset token generated", { userId: user.id });
5402
5652
  return ok(void 0);
5403
5653
  }
5404
5654
  function buildPasswordResetOps(ctx) {
@@ -5409,17 +5659,17 @@ function buildPasswordResetOps(ctx) {
5409
5659
  const captcha = await verifyIamCaptcha(ctx.config.captcha, "passwordReset", options?.captcha);
5410
5660
  if (!captcha.success)
5411
5661
  return captcha;
5412
- logger12.debug("Password reset requested", { identifier });
5662
+ logger13.debug("Password reset requested", { identifier });
5413
5663
  const resetConfig = ctx.resetConfig;
5414
5664
  if (!resetConfig.enabled || !onPasswordResetRequest)
5415
5665
  return err(HaiIamError.PASSWORD_RESET_DISABLED, iamM("iam_passwordResetDisabled"));
5416
5666
  const userResult = await userRepository.findByIdentifier(identifier);
5417
5667
  if (!userResult.success) {
5418
- logger12.warn("Failed to look up user for password reset", { identifier });
5668
+ logger13.warn("Failed to look up user for password reset", { identifier });
5419
5669
  return ok(void 0);
5420
5670
  }
5421
5671
  if (!userResult.data) {
5422
- logger12.debug("User not found for password reset, returning ok to prevent enumeration", { identifier });
5672
+ logger13.debug("User not found for password reset, returning ok to prevent enumeration", { identifier });
5423
5673
  return ok(void 0);
5424
5674
  }
5425
5675
  const user = toUser(userResult.data);
@@ -5428,7 +5678,7 @@ function buildPasswordResetOps(ctx) {
5428
5678
  return issuePasswordReset(ctx, userResult.data);
5429
5679
  },
5430
5680
  async confirmPasswordReset(token, newPassword) {
5431
- logger12.debug("Confirming password reset");
5681
+ logger13.debug("Confirming password reset");
5432
5682
  const resetConfig = ctx.resetConfig;
5433
5683
  if (!resetConfig.enabled)
5434
5684
  return err(HaiIamError.PASSWORD_RESET_DISABLED, iamM("iam_passwordResetDisabled"));
@@ -5452,7 +5702,7 @@ function buildPasswordResetOps(ctx) {
5452
5702
  const revoked = await sessionFunctions.deleteByUserId(claimed.data.id);
5453
5703
  if (!revoked.success)
5454
5704
  return revoked;
5455
- logger12.info("Password reset confirmed", { userId: claimed.data.id });
5705
+ logger13.info("Password reset confirmed", { userId: claimed.data.id });
5456
5706
  return ok(void 0);
5457
5707
  }
5458
5708
  };
@@ -5517,7 +5767,7 @@ function buildUserFunctions(deps) {
5517
5767
  }
5518
5768
 
5519
5769
  // src/iam-main.ts
5520
- var logger13 = core.logger.child({ module: "iam", scope: "main" });
5770
+ var logger14 = core.logger.child({ module: "iam", scope: "main" });
5521
5771
  var initInProgress = false;
5522
5772
  var currentConfig = null;
5523
5773
  var currentPublicConfig = null;
@@ -5548,7 +5798,7 @@ var notInitializedUser = new Proxy({}, {
5548
5798
  var iam = {
5549
5799
  async init(config) {
5550
5800
  if (initInProgress) {
5551
- logger13.warn("IAM init already in progress, skipping concurrent call");
5801
+ logger14.warn("IAM init already in progress, skipping concurrent call");
5552
5802
  return err(
5553
5803
  HaiIamError.CONFIG_ERROR,
5554
5804
  iamM("iam_initInProgress")
@@ -5557,11 +5807,11 @@ var iam = {
5557
5807
  initInProgress = true;
5558
5808
  try {
5559
5809
  if (currentConfig !== null) {
5560
- logger13.warn("IAM module is already initialized, reinitializing");
5810
+ logger14.warn("IAM module is already initialized, reinitializing");
5561
5811
  await iam.close();
5562
5812
  }
5563
5813
  const { ldapClientFactory, ldapSyncUser } = config;
5564
- logger13.info("Initializing IAM module");
5814
+ logger14.info("Initializing IAM module");
5565
5815
  if (!reldb.isInitialized) {
5566
5816
  return err(
5567
5817
  HaiIamError.CONFIG_ERROR,
@@ -5586,7 +5836,7 @@ var iam = {
5586
5836
  }
5587
5837
  const parseResult = IamConfigSchema.safeParse(config);
5588
5838
  if (!parseResult.success) {
5589
- logger13.error("IAM config validation failed", { error: parseResult.error.message });
5839
+ logger14.error("IAM config validation failed", { error: parseResult.error.message });
5590
5840
  return err(
5591
5841
  HaiIamError.CONFIG_ERROR,
5592
5842
  iamM("iam_configError", { params: { error: parseResult.error.message } }),
@@ -5595,7 +5845,7 @@ var iam = {
5595
5845
  }
5596
5846
  const parsed = parseResult.data;
5597
5847
  const { onPasswordResetRequest, onOtpSendEmail, onOtpSendSms } = await resolveIamNotifications(config);
5598
- logger13.info("IAM notification channels resolved", { email: Boolean(onOtpSendEmail), sms: Boolean(onOtpSendSms), passwordReset: Boolean(onPasswordResetRequest) });
5848
+ logger14.info("IAM notification channels resolved", { email: Boolean(onOtpSendEmail), sms: Boolean(onOtpSendSms), passwordReset: Boolean(onPasswordResetRequest) });
5599
5849
  const migration = await migrateIam();
5600
5850
  if (!migration.success)
5601
5851
  return migration;
@@ -5603,7 +5853,7 @@ var iam = {
5603
5853
  const sessionResult = await createSessionOperations({
5604
5854
  config: parsed,
5605
5855
  readCredential: async (userId) => {
5606
- const found = await credentialUsers.findById(userId);
5856
+ const found = await readIamUserSecurity(credentialUsers, userId);
5607
5857
  if (!found.success)
5608
5858
  return found;
5609
5859
  if (found.data) {
@@ -5611,7 +5861,7 @@ var iam = {
5611
5861
  if (!passwordState.success)
5612
5862
  return passwordState;
5613
5863
  }
5614
- return ok(found.data ? { version: found.data.credentialVersion ?? "", enabled: isUserActive(found.data) } : null);
5864
+ return ok(found.data ? { version: found.data.credentialVersion ?? "", enabled: isUserActive(found.data), profile: { username: found.data.username, displayName: found.data.displayName, avatarUrl: found.data.avatarUrl } } : null);
5615
5865
  }
5616
5866
  });
5617
5867
  if (!sessionResult.success) {
@@ -5676,19 +5926,19 @@ var iam = {
5676
5926
  return;
5677
5927
  accountScanRunning = users.scanInactiveAccounts().then((result) => {
5678
5928
  if (!result.success)
5679
- logger13.error("Inactive account scan failed", { error: result.error });
5929
+ logger14.error("Inactive account scan failed", { error: result.error });
5680
5930
  }).catch((error) => {
5681
- logger13.error("Inactive account scan failed", { error });
5931
+ logger14.error("Inactive account scan failed", { error });
5682
5932
  }).finally(() => {
5683
5933
  accountScanRunning = void 0;
5684
5934
  });
5685
5935
  }, (parsed.accounts?.scanIntervalSeconds ?? 3600) * 1e3);
5686
5936
  accountScanTimer.unref();
5687
5937
  }
5688
- logger13.info("IAM module initialized");
5938
+ logger14.info("IAM module initialized");
5689
5939
  return ok(void 0);
5690
5940
  } catch (error) {
5691
- logger13.error("IAM module initialization failed", { error });
5941
+ logger14.error("IAM module initialization failed", { error });
5692
5942
  return err(
5693
5943
  HaiIamError.CONFIG_ERROR,
5694
5944
  iamM("iam_initFailed"),
@@ -5701,6 +5951,18 @@ var iam = {
5701
5951
  get auth() {
5702
5952
  return currentAuth ?? notInitializedAuth;
5703
5953
  },
5954
+ async getPublicConfig() {
5955
+ const snapshot = currentPublicConfig ? structuredClone(currentPublicConfig) : null;
5956
+ const authz = currentAuthz;
5957
+ if (!snapshot || !authz)
5958
+ return notInitialized.result();
5959
+ if (!snapshot.reauthentication.enabled)
5960
+ return ok(snapshot);
5961
+ const configured = await authz.getSensitiveOperations();
5962
+ if (!configured.success)
5963
+ return configured;
5964
+ return ok({ ...snapshot, reauthentication: { ...snapshot.reauthentication, operations: [.../* @__PURE__ */ new Set([...snapshot.reauthentication.operations, ...configured.data])].sort() } });
5965
+ },
5704
5966
  get user() {
5705
5967
  return currentUser ?? notInitializedUser;
5706
5968
  },
@@ -5730,10 +5992,10 @@ var iam = {
5730
5992
  },
5731
5993
  async close() {
5732
5994
  if (currentConfig === null && currentAuth === null && currentUser === null && currentAuthz === null && currentSession === null) {
5733
- logger13.info("IAM module already closed, skipping");
5995
+ logger14.info("IAM module already closed, skipping");
5734
5996
  return;
5735
5997
  }
5736
- logger13.info("Closing IAM module");
5998
+ logger14.info("Closing IAM module");
5737
5999
  clearInterval(accountScanTimer);
5738
6000
  accountScanTimer = void 0;
5739
6001
  await accountScanRunning;
@@ -5750,7 +6012,7 @@ var iam = {
5750
6012
  resetUserRepoSingleton();
5751
6013
  resetRoleRepoSingleton();
5752
6014
  resetPermissionRepoSingleton();
5753
- logger13.info("IAM module closed");
6015
+ logger14.info("IAM module closed");
5754
6016
  }
5755
6017
  };
5756
6018