@h-ai/iam 0.1.0-alpha.59 → 0.1.0-alpha.61

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
@@ -7,12 +7,13 @@ import { randomBytes } from 'crypto';
7
7
  import { audit } from '@h-ai/audit';
8
8
 
9
9
  // src/iam-config.ts
10
+ var WebUrlSchema = z.url().refine((value) => ["http:", "https:"].includes(new URL(value).protocol));
10
11
  var AuthStrategyTypeSchema = z.enum(["password", "otp", "ldap", "apikey"]);
11
12
  var PasswordConfigSchema = z.object({
12
13
  /** 最小长度(默认 8) */
13
14
  minLength: z.number().int().min(1).default(8),
14
15
  /** 最大长度(默认 128) */
15
- maxLength: z.number().int().max(256).default(128),
16
+ maxLength: z.number().int().min(1).max(256).default(128),
16
17
  /** 需要大写字母 */
17
18
  requireUppercase: z.boolean().default(true),
18
19
  /** 需要小写字母 */
@@ -23,8 +24,11 @@ var PasswordConfigSchema = z.object({
23
24
  requireSpecialChar: z.boolean().default(false),
24
25
  /** 密码过期天数(0 表示不过期) */
25
26
  expirationDays: z.number().int().min(0).default(0)
26
- });
27
+ }).refine((config) => config.maxLength >= config.minLength, { message: "maxLength must be >= minLength", path: ["maxLength"] });
27
28
  var OtpConfigSchema = z.object({
29
+ /** 分别启用邮件、短信验证码 */
30
+ email: z.boolean().default(true),
31
+ sms: z.boolean().default(true),
28
32
  /** 验证码长度(默认 6) */
29
33
  length: z.number().int().min(4).max(8).default(6),
30
34
  /** 验证码过期时间(秒,默认 300) */
@@ -35,6 +39,12 @@ var OtpConfigSchema = z.object({
35
39
  resendInterval: z.number().int().min(30).default(60)
36
40
  });
37
41
  var PasswordResetConfigSchema = z.object({
42
+ /** 重置通知渠道,phone 使用 password_reset_sms 模板。 */
43
+ mode: z.enum(["email", "phone"]).default("email"),
44
+ /** 重置页面的绝对地址,通知模板可使用 resetUrl。 */
45
+ resetUrl: WebUrlSchema.optional(),
46
+ /** 是否启用密码重置 */
47
+ enabled: z.boolean().default(true),
38
48
  /** 重置令牌有效期(秒,默认 3600 = 1小时) */
39
49
  tokenExpiresIn: z.number().int().min(300).default(3600),
40
50
  /** 最大验证尝试次数(默认 3) */
@@ -42,7 +52,7 @@ var PasswordResetConfigSchema = z.object({
42
52
  });
43
53
  var LdapConfigSchema = z.object({
44
54
  /** LDAP 服务器 URL */
45
- url: z.string().url(),
55
+ url: z.url().refine((value) => ["ldap:", "ldaps:"].includes(new URL(value).protocol)),
46
56
  /** 绑定 DN */
47
57
  bindDn: z.string(),
48
58
  /** 绑定密码 */
@@ -75,8 +85,8 @@ var LoginConfigSchema = z.object({
75
85
  password: z.boolean().default(true),
76
86
  /** 是否启用 OTP 登录 */
77
87
  otp: z.boolean().default(true),
78
- /** 是否启用 LDAP 登录 */
79
- ldap: z.boolean().default(true),
88
+ /** 是否启用 LDAP 登录;需要显式开启并提供 ldap 配置。 */
89
+ ldap: z.boolean().default(false),
80
90
  /** 是否启用 API Key 登录 */
81
91
  apikey: z.boolean().default(false)
82
92
  });
@@ -93,15 +103,29 @@ var SecurityConfigSchema = z.object({
93
103
  lockoutDuration: z.number().int().min(60).default(900)
94
104
  });
95
105
  var AgreementConfigSchema = z.object({
106
+ /** 用户协议 Markdown 正文;同时配置 URL 时优先显示正文。 */
107
+ userAgreementMarkdown: z.string().min(1).optional(),
108
+ /** 隐私协议 Markdown 正文;YAML 使用 | 保留段落和换行。 */
109
+ privacyPolicyMarkdown: z.string().min(1).optional(),
96
110
  /** 用户协议 URL */
97
- userAgreementUrl: z.url().optional(),
111
+ userAgreementUrl: WebUrlSchema.optional(),
98
112
  /** 隐私协议 URL */
99
- privacyPolicyUrl: z.url().optional(),
113
+ privacyPolicyUrl: WebUrlSchema.optional(),
100
114
  /** 注册时展示协议 */
101
115
  showOnRegister: z.boolean().default(true),
102
116
  /** 登录时展示协议 */
103
117
  showOnLogin: z.boolean().default(false)
104
118
  });
119
+ var AuthImageSchema = z.string().refine((value) => /^https?:\/\//.test(value) || value.startsWith("/") && !value.startsWith("//"));
120
+ var AuthUiConfigSchema = z.object({
121
+ variant: z.enum(["card", "split"]).default("card"),
122
+ title: z.string().optional(),
123
+ subtitle: z.string().optional(),
124
+ brandTitle: z.string().optional(),
125
+ brandText: z.string().optional(),
126
+ logoUrl: AuthImageSchema.optional(),
127
+ illustrationUrl: AuthImageSchema.optional()
128
+ });
105
129
  var SessionConfigSchema = z.object({
106
130
  /** 会话超时时间(秒,默认 86400 = 24小时) */
107
131
  maxAge: z.number().int().min(60).default(86400),
@@ -121,6 +145,8 @@ var RbacConfigSchema = z.object({
121
145
  defaultRole: z.string().default("user")
122
146
  });
123
147
  var IamConfigSchema = z.object({
148
+ /** 认证页外观 */
149
+ ui: AuthUiConfigSchema.optional(),
124
150
  // ─── 认证策略配置 ───
125
151
  /** 密码配置 */
126
152
  password: PasswordConfigSchema.optional(),
@@ -136,7 +162,7 @@ var IamConfigSchema = z.object({
136
162
  login: LoginConfigSchema.default({
137
163
  password: true,
138
164
  otp: true,
139
- ldap: true,
165
+ ldap: false,
140
166
  apikey: false
141
167
  }),
142
168
  /** 注册配置 */
@@ -159,7 +185,7 @@ var IamConfigSchema = z.object({
159
185
  session: SessionConfigSchema.optional(),
160
186
  // ─── 授权配置 ───
161
187
  /** RBAC 配置 */
162
- rbac: RbacConfigSchema.optional(),
188
+ rbac: RbacConfigSchema.default(() => RbacConfigSchema.parse({})),
163
189
  // ─── 运行时选项 ───
164
190
  /** 是否初始化默认角色和权限(默认 true) */
165
191
  seedDefaultData: z.boolean().default(true)
@@ -168,6 +194,10 @@ var IamConfigSchema = z.object({
168
194
  // messages/en-US.json
169
195
  var en_US_default = {
170
196
  $schema: "https://inlang.com/schema/inlang-message-format",
197
+ iam_securityStateFailed: "Unable to update authentication security state. Please try again later.",
198
+ iam_securityBusy: "Too many authentication requests. Please try again later.",
199
+ iam_passwordResetDisabled: "Password reset is disabled or delivery is not configured.",
200
+ iam_passwordResetDeliveryFailed: "Unable to deliver the reset notification. Please try again later.",
171
201
  iam_passwordMinLength: "Password must be at least {minLength} characters",
172
202
  iam_passwordMaxLength: "Password cannot exceed {maxLength} characters",
173
203
  iam_passwordNeedUppercase: "Password must contain uppercase letters",
@@ -290,7 +320,7 @@ var en_US_default = {
290
320
  iam_queryOtpFailed: "Failed to query OTP: {message}",
291
321
  iam_updateOtpAttemptsFailed: "Failed to update OTP attempts: {message}",
292
322
  iam_deleteOtpFailed: "Failed to delete OTP: {message}",
293
- iam_otpSendFailed: "Failed to send OTP: {message}",
323
+ iam_otpSendFailed: "Failed to send verification code. Please try again later.",
294
324
  iam_resetTokenInvalid: "Invalid or expired password reset token",
295
325
  iam_resetTokenMaxAttempts: "Too many verification attempts, please request a new reset token",
296
326
  iam_saveResetTokenFailed: "Failed to save password reset token: {message}",
@@ -351,6 +381,10 @@ var en_US_default = {
351
381
  // messages/zh-CN.json
352
382
  var zh_CN_default = {
353
383
  $schema: "https://inlang.com/schema/inlang-message-format",
384
+ iam_securityStateFailed: "\u65E0\u6CD5\u66F4\u65B0\u8BA4\u8BC1\u5B89\u5168\u72B6\u6001\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5",
385
+ iam_securityBusy: "\u8BA4\u8BC1\u8BF7\u6C42\u8FC7\u4E8E\u9891\u7E41\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5",
386
+ iam_passwordResetDisabled: "\u5BC6\u7801\u91CD\u7F6E\u672A\u542F\u7528\u6216\u901A\u77E5\u6E20\u9053\u672A\u914D\u7F6E",
387
+ iam_passwordResetDeliveryFailed: "\u91CD\u7F6E\u901A\u77E5\u53D1\u9001\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5",
354
388
  iam_passwordMinLength: "\u5BC6\u7801\u957F\u5EA6\u81F3\u5C11\u4E3A {minLength} \u4E2A\u5B57\u7B26",
355
389
  iam_passwordMaxLength: "\u5BC6\u7801\u957F\u5EA6\u4E0D\u80FD\u8D85\u8FC7 {maxLength} \u4E2A\u5B57\u7B26",
356
390
  iam_passwordNeedUppercase: "\u5BC6\u7801\u5FC5\u987B\u5305\u542B\u5927\u5199\u5B57\u6BCD",
@@ -473,7 +507,7 @@ var zh_CN_default = {
473
507
  iam_queryOtpFailed: "\u67E5\u8BE2 OTP \u5931\u8D25: {message}",
474
508
  iam_updateOtpAttemptsFailed: "\u66F4\u65B0 OTP \u5C1D\u8BD5\u6B21\u6570\u5931\u8D25: {message}",
475
509
  iam_deleteOtpFailed: "\u5220\u9664 OTP \u5931\u8D25: {message}",
476
- iam_otpSendFailed: "\u9A8C\u8BC1\u7801\u53D1\u9001\u5931\u8D25: {message}",
510
+ iam_otpSendFailed: "\u9A8C\u8BC1\u7801\u53D1\u9001\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5",
477
511
  iam_resetTokenInvalid: "\u5BC6\u7801\u91CD\u7F6E\u4EE4\u724C\u65E0\u6548\u6216\u5DF2\u8FC7\u671F",
478
512
  iam_resetTokenMaxAttempts: "\u9A8C\u8BC1\u6B21\u6570\u8FC7\u591A\uFF0C\u8BF7\u91CD\u65B0\u7533\u8BF7\u91CD\u7F6E\u4EE4\u724C",
479
513
  iam_saveResetTokenFailed: "\u4FDD\u5B58\u5BC6\u7801\u91CD\u7F6E\u4EE4\u724C\u5931\u8D25: {message}",
@@ -559,6 +593,8 @@ var IamErrorInfo = {
559
593
  RESET_TOKEN_EXPIRED: "020:400",
560
594
  RESET_TOKEN_MAX_ATTEMPTS: "021:429",
561
595
  OTP_EXPIRED: "022:400",
596
+ SECURITY_BUSY: "023:429",
597
+ PASSWORD_RESET_DISABLED: "024:403",
562
598
  SESSION_NOT_FOUND: "101:401",
563
599
  SESSION_EXPIRED: "102:401",
564
600
  SESSION_INVALID: "103:401",
@@ -792,9 +828,11 @@ var DbApiKeyRepository = class extends BaseReldbCrudRepository {
792
828
  function buildAgreementDisplay(config, enabled) {
793
829
  if (!enabled)
794
830
  return void 0;
795
- if (!config.userAgreementUrl && !config.privacyPolicyUrl)
831
+ if (!config.userAgreementMarkdown && !config.privacyPolicyMarkdown && !config.userAgreementUrl && !config.privacyPolicyUrl)
796
832
  return void 0;
797
833
  return {
834
+ userAgreementMarkdown: config.userAgreementMarkdown,
835
+ privacyPolicyMarkdown: config.privacyPolicyMarkdown,
798
836
  userAgreementUrl: config.userAgreementUrl,
799
837
  privacyPolicyUrl: config.privacyPolicyUrl,
800
838
  showOnRegister: config.showOnRegister,
@@ -962,6 +1000,40 @@ var DbUserRepository = class extends BaseReldbCrudRepository {
962
1000
  fields: USER_FIELDS
963
1001
  });
964
1002
  }
1003
+ async recordLoginFailure(userId, policy) {
1004
+ const sqlite = reldb.config?.type === "sqlite";
1005
+ const now = sqlite ? Date.now() : /* @__PURE__ */ new Date();
1006
+ const deadline = Date.now() + policy.lockoutDuration * 1e3;
1007
+ const lockedUntil = sqlite ? deadline : new Date(deadline);
1008
+ 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]);
1009
+ if (!expired.success)
1010
+ return this.buildQueryError(expired.error, expired.error);
1011
+ const result = await reldb.sql.execute(
1012
+ `UPDATE ${TABLE_NAME2} SET locked_until = CASE WHEN COALESCE(login_failed_count, 0) + 1 >= ? THEN ? ELSE locked_until END, login_failed_count = COALESCE(login_failed_count, 0) + 1, last_login_failed_at = ? WHERE id = ? AND locked_until IS NULL`,
1013
+ [policy.maxLoginAttempts, lockedUntil, now, userId]
1014
+ );
1015
+ if (!result.success)
1016
+ return this.buildQueryError(result.error, result.error);
1017
+ return ok(void 0);
1018
+ }
1019
+ async resetLoginFailures(userId) {
1020
+ const result = await reldb.sql.execute(
1021
+ `UPDATE ${TABLE_NAME2} SET login_failed_count = 0, last_login_failed_at = NULL, locked_until = NULL WHERE id = ? AND enabled = ? AND (locked_until IS NULL OR locked_until <= ?)`,
1022
+ [userId, reldb.config?.type === "postgresql" ? true : 1, reldb.config?.type === "sqlite" ? Date.now() : /* @__PURE__ */ new Date()]
1023
+ );
1024
+ if (!result.success)
1025
+ return this.buildQueryError(result.error, result.error);
1026
+ if (result.data.changes === 0) {
1027
+ const user = await this.findById(userId);
1028
+ if (!user.success)
1029
+ return user;
1030
+ if (!user.data?.enabled)
1031
+ return err(HaiIamError.USER_DISABLED, iamM("iam_accountDisabled"));
1032
+ if (user.data.lockedUntil && user.data.lockedUntil > /* @__PURE__ */ new Date())
1033
+ return err(HaiIamError.USER_LOCKED, iamM("iam_accountLocked"));
1034
+ }
1035
+ return ok(void 0);
1036
+ }
965
1037
  /** 根据用户名查找用户 */
966
1038
  async findByUsername(username, tx) {
967
1039
  return this.findOneBy("username = ?", [username], tx);
@@ -1064,24 +1136,10 @@ function isAccountLocked(user) {
1064
1136
  return /* @__PURE__ */ new Date() < user.lockedUntil;
1065
1137
  }
1066
1138
  async function recordLoginFailure(userRepository, user, policy) {
1067
- const failedCount = (user.loginFailedCount || 0) + 1;
1068
- const updateData = {
1069
- loginFailedCount: failedCount,
1070
- lastLoginFailedAt: /* @__PURE__ */ new Date()
1071
- };
1072
- if (failedCount >= policy.maxLoginAttempts) {
1073
- updateData.lockedUntil = new Date(Date.now() + policy.lockoutDuration * 1e3);
1074
- }
1075
- await userRepository.updateById(user.id, updateData);
1139
+ return userRepository.recordLoginFailure(user.id, policy);
1076
1140
  }
1077
1141
  async function resetLoginFailures(userRepository, user) {
1078
- if (user.loginFailedCount && user.loginFailedCount > 0) {
1079
- await userRepository.updateById(user.id, {
1080
- loginFailedCount: 0,
1081
- lastLoginFailedAt: void 0,
1082
- lockedUntil: void 0
1083
- });
1084
- }
1142
+ return userRepository.resetLoginFailures(user.id);
1085
1143
  }
1086
1144
 
1087
1145
  // src/authn/apikey/iam-authn-apikey-strategy.ts
@@ -1228,6 +1286,60 @@ function createApiKeyStrategy(config) {
1228
1286
  };
1229
1287
  return { strategy, apiKeyFunctions };
1230
1288
  }
1289
+ async function loadLdapClientFactory() {
1290
+ const { Client } = await import('ldapts');
1291
+ return async (config) => {
1292
+ const client = new Client({ url: config.url, connectTimeout: config.connectTimeout, timeout: config.connectTimeout });
1293
+ try {
1294
+ if (config.useTls && config.url.startsWith("ldap://"))
1295
+ await client.startTLS({ minVersion: "TLSv1.2" });
1296
+ return ok({
1297
+ async bind(dn, password) {
1298
+ if (!password)
1299
+ return err(HaiIamError.INVALID_CREDENTIALS, iamM("iam_passwordWrong"));
1300
+ try {
1301
+ await client.bind(dn, password);
1302
+ return ok(void 0);
1303
+ } catch (error) {
1304
+ return err(HaiIamError.LDAP_BIND_FAILED, iamM("iam_passwordWrong"), error);
1305
+ }
1306
+ },
1307
+ async search(base, filter, attributes) {
1308
+ try {
1309
+ const result = await client.search(base, { scope: "sub", filter, attributes });
1310
+ return ok(result.searchEntries.map((entry) => {
1311
+ const values = {};
1312
+ for (const attribute of attributes) {
1313
+ const value = entry[attribute];
1314
+ if (typeof value === "string")
1315
+ values[attribute] = value;
1316
+ else if (Array.isArray(value))
1317
+ values[attribute] = value.map((item) => String(item));
1318
+ else if (value !== void 0)
1319
+ values[attribute] = String(value);
1320
+ }
1321
+ return { dn: entry.dn, attributes: values };
1322
+ }));
1323
+ } catch (error) {
1324
+ return err(HaiIamError.LDAP_SEARCH_FAILED, iamM("iam_ldapSearchFailed"), error);
1325
+ }
1326
+ },
1327
+ async unbind() {
1328
+ try {
1329
+ await client.unbind();
1330
+ return ok(void 0);
1331
+ } catch (error) {
1332
+ return err(HaiIamError.LDAP_CONNECTION_FAILED, iamM("iam_ldapConnectionFailed"), error);
1333
+ }
1334
+ }
1335
+ });
1336
+ } catch (error) {
1337
+ await client.unbind().catch(() => {
1338
+ });
1339
+ return err(HaiIamError.LDAP_CONNECTION_FAILED, iamM("iam_ldapConnectionFailed"), error);
1340
+ }
1341
+ };
1342
+ }
1231
1343
  var logger2 = core.logger.child({ module: "iam", scope: "ldap-strategy" });
1232
1344
  function escapeLdapFilterValue(value) {
1233
1345
  return value.replace(/[\\*()/\0]/g, (ch) => {
@@ -1276,6 +1388,8 @@ function createLdapStrategy(config) {
1276
1388
  return credentialResult;
1277
1389
  }
1278
1390
  const { username, password } = credentialResult.data;
1391
+ if (!username.trim() || !password)
1392
+ return err(HaiIamError.INVALID_CREDENTIALS, iamM("iam_passwordWrong"));
1279
1393
  const clientResult = await ldapClientFactory(ldapConfig);
1280
1394
  if (!clientResult.success) {
1281
1395
  return err(
@@ -1340,7 +1454,9 @@ function createLdapStrategy(config) {
1340
1454
  const userBindResult = await client.bind(entry.dn, password);
1341
1455
  if (!userBindResult.success) {
1342
1456
  if (storedUser) {
1343
- await recordLoginFailure(userRepository, storedUser, { maxLoginAttempts, lockoutDuration });
1457
+ const failureResult = await recordLoginFailure(userRepository, storedUser, { maxLoginAttempts, lockoutDuration });
1458
+ if (!failureResult.success)
1459
+ return failureResult;
1344
1460
  }
1345
1461
  logger2.warn("LDAP authentication failed", { username });
1346
1462
  return err(
@@ -1353,7 +1469,7 @@ function createLdapStrategy(config) {
1353
1469
  username: ldapUsername,
1354
1470
  email: ldapEmail,
1355
1471
  displayName: ldapDisplayName,
1356
- enabled: true,
1472
+ enabled: config.defaultEnabled ?? true,
1357
1473
  emailVerified: !!ldapEmail,
1358
1474
  metadata: {
1359
1475
  ldapDn: entry.dn,
@@ -1374,7 +1490,11 @@ function createLdapStrategy(config) {
1374
1490
  storedUser = createdResult.data;
1375
1491
  if (storedUser && config.onUserAutoRegistered) {
1376
1492
  try {
1377
- await config.onUserAutoRegistered(storedUser.id);
1493
+ const assigned = await config.onUserAutoRegistered(storedUser.id);
1494
+ if (!assigned.success) {
1495
+ await userRepository.deleteById(storedUser.id);
1496
+ return assigned;
1497
+ }
1378
1498
  } catch (callbackError) {
1379
1499
  logger2.warn("onUserAutoRegistered callback failed", { userId: storedUser?.id, error: callbackError });
1380
1500
  }
@@ -1400,7 +1520,9 @@ function createLdapStrategy(config) {
1400
1520
  const ldapUser = buildLdapUser(entry, ldapUsername, ldapEmail, ldapDisplayName);
1401
1521
  return ok(ldapUser);
1402
1522
  }
1403
- await resetLoginFailures(userRepository, storedUser);
1523
+ const resetResult = await resetLoginFailures(userRepository, storedUser);
1524
+ if (!resetResult.success)
1525
+ return resetResult;
1404
1526
  logger2.info("LDAP authentication succeeded", { userId: storedUser.id });
1405
1527
  return ok(toUser(storedUser));
1406
1528
  } finally {
@@ -1455,7 +1577,9 @@ function createCacheOtpRepository() {
1455
1577
  result.error
1456
1578
  );
1457
1579
  }
1458
- await cache.kv.del(buildOtpAttemptsKey(identifier));
1580
+ const reset = await cache.kv.del(buildOtpAttemptsKey(identifier));
1581
+ if (!reset.success)
1582
+ return err(HaiIamError.REPOSITORY_ERROR, iamM("iam_securityStateFailed"), reset.error);
1459
1583
  return ok(void 0);
1460
1584
  },
1461
1585
  async fetchOtp(identifier) {
@@ -1471,6 +1595,8 @@ function createCacheOtpRepository() {
1471
1595
  return ok(null);
1472
1596
  }
1473
1597
  const attemptsResult = await cache.kv.get(buildOtpAttemptsKey(identifier));
1598
+ if (!attemptsResult.success)
1599
+ return err(HaiIamError.REPOSITORY_ERROR, iamM("iam_securityStateFailed"), attemptsResult.error);
1474
1600
  const attempts = attemptsResult.success && typeof attemptsResult.data === "number" ? attemptsResult.data : 0;
1475
1601
  const restored = restoreOtpDates(result.data);
1476
1602
  return ok({ ...restored, attempts });
@@ -1498,8 +1624,12 @@ function createCacheOtpRepository() {
1498
1624
  );
1499
1625
  }
1500
1626
  const ttlResult = await cache.kv.ttl(otpKey);
1627
+ if (!ttlResult.success)
1628
+ return err(HaiIamError.REPOSITORY_ERROR, iamM("iam_securityStateFailed"), ttlResult.error);
1501
1629
  const ttl = ttlResult.success && ttlResult.data > 0 ? ttlResult.data : 1;
1502
- await cache.kv.expire(attemptsKey, ttl);
1630
+ const expire = await cache.kv.expire(attemptsKey, ttl);
1631
+ if (!expire.success)
1632
+ return err(HaiIamError.REPOSITORY_ERROR, iamM("iam_securityStateFailed"), expire.error);
1503
1633
  return ok(incrResult.data);
1504
1634
  },
1505
1635
  async removeOtp(identifier) {
@@ -1511,13 +1641,108 @@ function createCacheOtpRepository() {
1511
1641
  result.error
1512
1642
  );
1513
1643
  }
1514
- await cache.kv.del(buildOtpAttemptsKey(identifier));
1644
+ const removed = await cache.kv.del(buildOtpAttemptsKey(identifier));
1645
+ if (!removed.success)
1646
+ return err(HaiIamError.REPOSITORY_ERROR, iamM("iam_securityStateFailed"), removed.error);
1515
1647
  return ok(void 0);
1516
1648
  }
1517
1649
  };
1518
1650
  otpRepoInstance = repo;
1519
1651
  return repo;
1520
1652
  }
1653
+
1654
+ // src/session/iam-session-utils.ts
1655
+ var BASE64_PLUS_REGEX = /\+/g;
1656
+ var BASE64_SLASH_REGEX = /\//g;
1657
+ var BASE64_TRAILING_PADDING_REGEX = /=+$/;
1658
+ function generateToken() {
1659
+ const bytes = new Uint8Array(32);
1660
+ crypto.getRandomValues(bytes);
1661
+ const base64 = btoa(String.fromCharCode(...bytes));
1662
+ return base64.replace(BASE64_PLUS_REGEX, "-").replace(BASE64_SLASH_REGEX, "_").replace(BASE64_TRAILING_PADDING_REGEX, "");
1663
+ }
1664
+ function buildSession(options, now, sessionTtl, accessToken) {
1665
+ return {
1666
+ userId: options.userId,
1667
+ username: options.username,
1668
+ displayName: options.displayName,
1669
+ avatarUrl: options.avatarUrl,
1670
+ roles: options.roles ?? [],
1671
+ permissions: options.permissions ?? [],
1672
+ source: options.source,
1673
+ accessToken,
1674
+ createdAt: now,
1675
+ lastActiveAt: now,
1676
+ expiresAt: new Date(now.getTime() + sessionTtl * 1e3),
1677
+ data: options.data
1678
+ };
1679
+ }
1680
+ function getSessionTtl(session, now = Date.now()) {
1681
+ return Math.max(0, Math.floor((session.expiresAt.getTime() - now) / 1e3));
1682
+ }
1683
+ function applySessionPatch(session, patch) {
1684
+ const nextSession = { ...session };
1685
+ if (patch.data !== void 0) {
1686
+ nextSession.data = { ...nextSession.data, ...patch.data };
1687
+ }
1688
+ if (patch.roles !== void 0) {
1689
+ nextSession.roles = patch.roles;
1690
+ }
1691
+ if (patch.permissions !== void 0) {
1692
+ nextSession.permissions = patch.permissions;
1693
+ }
1694
+ if (patch.username !== void 0) {
1695
+ nextSession.username = patch.username;
1696
+ }
1697
+ if (patch.displayName !== void 0) {
1698
+ nextSession.displayName = patch.displayName;
1699
+ }
1700
+ if (patch.avatarUrl !== void 0) {
1701
+ nextSession.avatarUrl = patch.avatarUrl;
1702
+ }
1703
+ if (patch.source !== void 0) {
1704
+ nextSession.source = patch.source;
1705
+ }
1706
+ nextSession.lastActiveAt = /* @__PURE__ */ new Date();
1707
+ return nextSession;
1708
+ }
1709
+
1710
+ // src/iam-security.ts
1711
+ async function withIamLock(key, operation) {
1712
+ const owner = generateToken();
1713
+ const lockKey = `hai:iam:security:${key}`;
1714
+ for (let attempt = 0; attempt < 500; attempt++) {
1715
+ const acquired = await cache.lock.acquire(lockKey, { owner, ttl: 30 });
1716
+ if (!acquired.success)
1717
+ return err(HaiIamError.REPOSITORY_ERROR, iamM("iam_securityStateFailed"), acquired.error);
1718
+ if (acquired.data) {
1719
+ let leaseValid = true;
1720
+ const renewal = setInterval(() => {
1721
+ void cache.lock.extend(lockKey, 30, owner).then((result) => {
1722
+ if (!result.success || !result.data)
1723
+ leaseValid = false;
1724
+ }).catch(() => {
1725
+ leaseValid = false;
1726
+ });
1727
+ }, 1e4);
1728
+ try {
1729
+ const result = await operation();
1730
+ if (!leaseValid)
1731
+ return err(HaiIamError.REPOSITORY_ERROR, iamM("iam_securityStateFailed"));
1732
+ return result;
1733
+ } catch (error) {
1734
+ return err(HaiIamError.INTERNAL_ERROR, iamM("iam_securityStateFailed"), error);
1735
+ } finally {
1736
+ clearInterval(renewal);
1737
+ await cache.lock.release(lockKey, owner);
1738
+ }
1739
+ }
1740
+ await new Promise((resolve) => setTimeout(resolve, 10));
1741
+ }
1742
+ return err(HaiIamError.SECURITY_BUSY, iamM("iam_securityBusy"));
1743
+ }
1744
+
1745
+ // src/authn/otp/iam-authn-otp-strategy.ts
1521
1746
  var logger3 = core.logger.child({ module: "iam", scope: "otp-strategy" });
1522
1747
  var PHONE_IDENTIFIER_REGEX = /^\+?\d{8,}$/;
1523
1748
  var PHONE_SEPARATOR_REGEX = /[\s-]/g;
@@ -1559,98 +1784,136 @@ function createOtpStrategy(config) {
1559
1784
  if (!credentialResult.success) {
1560
1785
  return credentialResult;
1561
1786
  }
1562
- const { identifier, code } = credentialResult.data;
1563
- const userResult = await config.userRepository.findByIdentifier(identifier);
1564
- if (!userResult.success) {
1565
- return userResult;
1566
- }
1567
- let storedUser = userResult.data;
1568
- if (storedUser) {
1569
- if (!storedUser.enabled) {
1570
- return err(HaiIamError.USER_DISABLED, iamM("iam_accountDisabled"));
1571
- }
1572
- if (isAccountLocked(storedUser)) {
1573
- return err(HaiIamError.USER_LOCKED, iamM("iam_accountLocked"));
1787
+ const identifier = normalizeOtpIdentifier(credentialResult.data.identifier);
1788
+ const { code } = credentialResult.data;
1789
+ if (!isChannelEnabled(identifier))
1790
+ return err(HaiIamError.LOGIN_DISABLED, iamM("iam_identifierTypeNotSupported"));
1791
+ return withIamLock(`otp:${identifier}`, async () => {
1792
+ const userResult = await config.userRepository.findByIdentifier(identifier);
1793
+ if (!userResult.success) {
1794
+ return userResult;
1574
1795
  }
1575
- }
1576
- const storedOtpResult = await config.otpRepository.fetchOtp(identifier);
1577
- if (!storedOtpResult.success) {
1578
- return storedOtpResult;
1579
- }
1580
- const storedOtp = storedOtpResult.data;
1581
- if (!storedOtp) {
1582
- return err(HaiIamError.OTP_INVALID, iamM("iam_otpNotExistOrExpired"));
1583
- }
1584
- if (storedOtp.attempts >= otpConfig.maxAttempts) {
1585
- await config.otpRepository.removeOtp(identifier);
1796
+ let storedUser = userResult.data;
1586
1797
  if (storedUser) {
1587
- await recordLoginFailure(config.userRepository, storedUser, { maxLoginAttempts, lockoutDuration });
1798
+ if (!storedUser.enabled) {
1799
+ return err(HaiIamError.USER_DISABLED, iamM("iam_accountDisabled"));
1800
+ }
1801
+ if (isAccountLocked(storedUser)) {
1802
+ return err(HaiIamError.USER_LOCKED, iamM("iam_accountLocked"));
1803
+ }
1588
1804
  }
1589
- return err(HaiIamError.OTP_INVALID, iamM("iam_otpInvalid"));
1590
- }
1591
- if (!core.string.constantTimeEqual(storedOtp.code, code)) {
1592
- await config.otpRepository.incrementOtpAttempts(identifier);
1593
- if (storedUser) {
1594
- await recordLoginFailure(config.userRepository, storedUser, { maxLoginAttempts, lockoutDuration });
1805
+ const storedOtpResult = await config.otpRepository.fetchOtp(identifier);
1806
+ if (!storedOtpResult.success) {
1807
+ return storedOtpResult;
1595
1808
  }
1596
- return err(HaiIamError.OTP_INVALID, iamM("iam_otpWrong"));
1597
- }
1598
- await config.otpRepository.removeOtp(identifier);
1599
- if (!storedUser && allowAutoRegister) {
1600
- const type = identifierType(identifier);
1601
- const createResult = await config.userRepository.create({
1602
- username: identifier,
1603
- email: type === "email" ? identifier : void 0,
1604
- phone: type === "phone" ? identifier : void 0,
1605
- enabled: defaultEnabled,
1606
- emailVerified: type === "email",
1607
- phoneVerified: type === "phone"
1608
- });
1609
- if (!createResult.success) {
1610
- return err(
1611
- HaiIamError.REPOSITORY_ERROR,
1612
- iamM("iam_createUserFailed", { params: { message: createResult.error.message } }),
1613
- createResult.error
1614
- );
1809
+ const storedOtp = storedOtpResult.data;
1810
+ if (!storedOtp) {
1811
+ return err(HaiIamError.OTP_INVALID, iamM("iam_otpNotExistOrExpired"));
1615
1812
  }
1616
- const createdResult = await config.userRepository.findByIdentifier(identifier);
1617
- if (!createdResult.success) {
1618
- return createdResult;
1813
+ if (storedOtp.attempts >= otpConfig.maxAttempts) {
1814
+ const removed2 = await config.otpRepository.removeOtp(identifier);
1815
+ if (!removed2.success)
1816
+ return removed2;
1817
+ if (storedUser) {
1818
+ const failureResult = await recordLoginFailure(config.userRepository, storedUser, { maxLoginAttempts, lockoutDuration });
1819
+ if (!failureResult.success)
1820
+ return failureResult;
1821
+ }
1822
+ return err(HaiIamError.OTP_INVALID, iamM("iam_otpInvalid"));
1619
1823
  }
1620
- storedUser = createdResult.data;
1621
- if (storedUser && config.onUserAutoRegistered) {
1622
- try {
1623
- await config.onUserAutoRegistered(storedUser.id);
1624
- } catch (callbackError) {
1625
- logger3.warn("onUserAutoRegistered callback failed", { userId: storedUser.id, error: callbackError });
1824
+ if (!core.string.constantTimeEqual(storedOtp.code, code)) {
1825
+ const attemptResult = await config.otpRepository.incrementOtpAttempts(identifier);
1826
+ if (!attemptResult.success)
1827
+ return attemptResult;
1828
+ if (attemptResult.data >= otpConfig.maxAttempts) {
1829
+ const removed2 = await config.otpRepository.removeOtp(identifier);
1830
+ if (!removed2.success)
1831
+ return removed2;
1832
+ }
1833
+ if (storedUser) {
1834
+ const failureResult = await recordLoginFailure(config.userRepository, storedUser, { maxLoginAttempts, lockoutDuration });
1835
+ if (!failureResult.success)
1836
+ return failureResult;
1626
1837
  }
1838
+ return err(HaiIamError.OTP_INVALID, iamM("iam_otpWrong"));
1627
1839
  }
1628
- }
1629
- if (!storedUser) {
1630
- return err(HaiIamError.USER_NOT_FOUND, iamM("iam_userNotExist"));
1631
- }
1632
- await resetLoginFailures(config.userRepository, storedUser);
1633
- logger3.info("OTP authentication succeeded", { userId: storedUser.id });
1634
- return ok(toUser(storedUser));
1840
+ const removed = await config.otpRepository.removeOtp(identifier);
1841
+ if (!removed.success)
1842
+ return removed;
1843
+ if (!storedUser && allowAutoRegister) {
1844
+ const type = identifierType(identifier);
1845
+ const createResult = await config.userRepository.create({
1846
+ username: identifier,
1847
+ email: type === "email" ? identifier : void 0,
1848
+ phone: type === "phone" ? identifier : void 0,
1849
+ enabled: defaultEnabled,
1850
+ emailVerified: type === "email",
1851
+ phoneVerified: type === "phone"
1852
+ });
1853
+ if (!createResult.success) {
1854
+ return err(
1855
+ HaiIamError.REPOSITORY_ERROR,
1856
+ iamM("iam_createUserFailed", { params: { message: createResult.error.message } }),
1857
+ createResult.error
1858
+ );
1859
+ }
1860
+ const createdResult = await config.userRepository.findByIdentifier(identifier);
1861
+ if (!createdResult.success) {
1862
+ return createdResult;
1863
+ }
1864
+ storedUser = createdResult.data;
1865
+ if (storedUser && config.onUserAutoRegistered) {
1866
+ try {
1867
+ const assigned = await config.onUserAutoRegistered(storedUser.id);
1868
+ if (!assigned.success) {
1869
+ await config.userRepository.deleteById(storedUser.id);
1870
+ return assigned;
1871
+ }
1872
+ } catch (callbackError) {
1873
+ logger3.warn("onUserAutoRegistered callback failed", { userId: storedUser.id, error: callbackError });
1874
+ }
1875
+ }
1876
+ }
1877
+ if (!storedUser) {
1878
+ return err(HaiIamError.USER_NOT_FOUND, iamM("iam_userNotExist"));
1879
+ }
1880
+ if (!storedUser.enabled)
1881
+ return err(HaiIamError.USER_DISABLED, iamM("iam_accountDisabled"));
1882
+ const resetResult = await resetLoginFailures(config.userRepository, storedUser);
1883
+ if (!resetResult.success)
1884
+ return resetResult;
1885
+ logger3.info("OTP authentication succeeded", { userId: storedUser.id });
1886
+ return ok(toUser(storedUser));
1887
+ });
1635
1888
  }
1636
1889
  };
1890
+ function isChannelEnabled(identifier) {
1891
+ const type = identifierType(identifier);
1892
+ return type === "email" && otpConfig.email && !!config.onOtpSendEmail || type === "phone" && otpConfig.sms && !!config.onOtpSendSms;
1893
+ }
1637
1894
  async function challenge(identifier) {
1638
- const existingResult = await config.otpRepository.fetchOtp(identifier);
1639
- if (existingResult.success && existingResult.data) {
1640
- const elapsedSeconds = Math.floor((Date.now() - existingResult.data.createdAt.getTime()) / 1e3);
1641
- if (elapsedSeconds < otpConfig.resendInterval) {
1642
- return err(
1643
- HaiIamError.OTP_RESEND_TOO_FAST,
1644
- iamM("iam_otpResendTooFast", { params: { seconds: otpConfig.resendInterval - elapsedSeconds } })
1645
- );
1646
- }
1647
- }
1895
+ identifier = normalizeOtpIdentifier(identifier);
1896
+ if (!isChannelEnabled(identifier))
1897
+ return err(HaiIamError.LOGIN_DISABLED, iamM("iam_identifierTypeNotSupported"));
1648
1898
  const code = generateOtpCode(otpConfig.length);
1649
1899
  const expiresAt = new Date(Date.now() + otpConfig.expiresIn * 1e3);
1650
- const storeResult = await config.otpRepository.saveOtp(identifier, code, otpConfig.expiresIn);
1651
- if (!storeResult.success) {
1652
- return storeResult;
1653
- }
1900
+ const saved = await withIamLock(`otp:${identifier}`, async () => {
1901
+ const cooldownKey = `hai:iam:otp:cooldown:${identifier}`;
1902
+ const cooldown = await cache.kv.get(cooldownKey);
1903
+ if (!cooldown.success)
1904
+ return err(HaiIamError.REPOSITORY_ERROR, iamM("iam_securityStateFailed"), cooldown.error);
1905
+ if (cooldown.data && cooldown.data > Date.now())
1906
+ return err(HaiIamError.OTP_RESEND_TOO_FAST, iamM("iam_otpResendTooFast", { params: { seconds: Math.ceil((cooldown.data - Date.now()) / 1e3) } }));
1907
+ const storeResult = await config.otpRepository.saveOtp(identifier, code, otpConfig.expiresIn);
1908
+ if (!storeResult.success)
1909
+ return storeResult;
1910
+ const limited = await cache.kv.set(cooldownKey, Date.now() + otpConfig.resendInterval * 1e3, { ex: otpConfig.resendInterval });
1911
+ if (!limited.success)
1912
+ return err(HaiIamError.REPOSITORY_ERROR, iamM("iam_securityStateFailed"), limited.error);
1913
+ return ok(void 0);
1914
+ });
1915
+ if (!saved.success)
1916
+ return saved;
1654
1917
  const type = identifierType(identifier);
1655
1918
  if (type === "email" && config.onOtpSendEmail) {
1656
1919
  try {
@@ -1658,7 +1921,7 @@ function createOtpStrategy(config) {
1658
1921
  } catch (error) {
1659
1922
  return err(
1660
1923
  HaiIamError.INTERNAL_ERROR,
1661
- iamM("iam_otpSendFailed", { params: { message: String(error) } }),
1924
+ iamM("iam_otpSendFailed"),
1662
1925
  error
1663
1926
  );
1664
1927
  }
@@ -1668,7 +1931,7 @@ function createOtpStrategy(config) {
1668
1931
  } catch (error) {
1669
1932
  return err(
1670
1933
  HaiIamError.INTERNAL_ERROR,
1671
- iamM("iam_otpSendFailed", { params: { message: String(error) } }),
1934
+ iamM("iam_otpSendFailed"),
1672
1935
  error
1673
1936
  );
1674
1937
  }
@@ -1765,7 +2028,9 @@ function createPasswordStrategy(config) {
1765
2028
  return mapPasswordError(verifyResult.error.message);
1766
2029
  }
1767
2030
  if (!verifyResult.data) {
1768
- await recordLoginFailure(config.userRepository, user, { maxLoginAttempts, lockoutDuration });
2031
+ const failureResult = await recordLoginFailure(config.userRepository, user, { maxLoginAttempts, lockoutDuration });
2032
+ if (!failureResult.success)
2033
+ return failureResult;
1769
2034
  logger4.warn("Password verification failed", { userId: user.id });
1770
2035
  return err(HaiIamError.INVALID_CREDENTIALS, iamM("iam_passwordWrong"));
1771
2036
  }
@@ -1797,7 +2062,9 @@ function createPasswordStrategy(config) {
1797
2062
  if (isPasswordExpired(storedUser)) {
1798
2063
  return err(HaiIamError.PASSWORD_EXPIRED, iamM("iam_passwordExpired"));
1799
2064
  }
1800
- await resetLoginFailures(config.userRepository, storedUser);
2065
+ const resetResult = await resetLoginFailures(config.userRepository, storedUser);
2066
+ if (!resetResult.success)
2067
+ return resetResult;
1801
2068
  logger4.info("Password authentication succeeded", { userId: storedUser.id });
1802
2069
  return ok(toUser(storedUser));
1803
2070
  }
@@ -1826,12 +2093,14 @@ async function createAuthnOperations(deps) {
1826
2093
  const securityConfig = SecurityConfigSchema.parse(config.security ?? {});
1827
2094
  const loginConfig = LoginConfigSchema.parse(config.login ?? {});
1828
2095
  async function onUserAutoRegistered(userId) {
1829
- if (!config.rbac?.defaultRole)
1830
- return;
2096
+ if (!config.rbac?.enabled || !config.rbac.defaultRole)
2097
+ return ok(void 0);
1831
2098
  const roleResult = await authzFunctions.getRoleByCode(config.rbac.defaultRole);
1832
- if (roleResult.success && roleResult.data) {
1833
- await authzFunctions.assignRole(userId, roleResult.data.id);
1834
- }
2099
+ if (!roleResult.success)
2100
+ return roleResult;
2101
+ if (!roleResult.data)
2102
+ return err(HaiIamError.ROLE_NOT_FOUND, iamM("iam_roleNotExist"));
2103
+ return authzFunctions.assignRole(userId, roleResult.data.id);
1835
2104
  }
1836
2105
  const passwordResult = createPasswordStrategy({
1837
2106
  passwordConfig: config.password,
@@ -1857,12 +2126,14 @@ async function createAuthnOperations(deps) {
1857
2126
  });
1858
2127
  }
1859
2128
  let ldapStrategy;
1860
- if (loginConfig.ldap && config.ldap && ldapClientFactory) {
2129
+ if (loginConfig.ldap && config.ldap) {
2130
+ const clientFactory = ldapClientFactory ?? await loadLdapClientFactory();
1861
2131
  ldapStrategy = createLdapStrategy({
1862
2132
  ldapConfig: config.ldap,
1863
2133
  userRepository,
1864
- ldapClientFactory,
2134
+ ldapClientFactory: clientFactory,
1865
2135
  syncUser: ldapSyncUser ?? true,
2136
+ defaultEnabled: config.register.defaultEnabled,
1866
2137
  maxLoginAttempts: securityConfig.maxLoginAttempts,
1867
2138
  lockoutDuration: securityConfig.lockoutDuration,
1868
2139
  onUserAutoRegistered
@@ -1892,7 +2163,7 @@ async function createAuthnOperations(deps) {
1892
2163
  config
1893
2164
  });
1894
2165
  logger5.info("Authn sub-feature initialized");
1895
- return ok({ authn: operations, passwordStrategyResult: passwordResult, apiKeyFunctions });
2166
+ return ok({ authn: operations.authn, loginRegisteredUser: operations.loginRegisteredUser, passwordStrategyResult: passwordResult, apiKeyFunctions });
1896
2167
  } catch (error) {
1897
2168
  logger5.error("Authn sub-feature initialization failed", { error });
1898
2169
  return err(
@@ -1960,6 +2231,8 @@ function buildAuthnOperations(deps) {
1960
2231
  );
1961
2232
  }
1962
2233
  async function buildAuthResult(user) {
2234
+ if (!user.enabled)
2235
+ return err(HaiIamError.USER_DISABLED, iamM("iam_accountDisabled"));
1963
2236
  const [roleCodesResult, permCodesResult] = await Promise.all([
1964
2237
  resolveUserRoleCodes(user.id),
1965
2238
  resolveUserPermissionCodes(user.id)
@@ -2015,7 +2288,7 @@ function buildAuthnOperations(deps) {
2015
2288
  }
2016
2289
  return result;
2017
2290
  }
2018
- return {
2291
+ return { loginRegisteredUser: buildAuthResult, authn: {
2019
2292
  async login(credentials) {
2020
2293
  return loginWithStrategy("password", credentials, passwordStrategy);
2021
2294
  },
@@ -2056,7 +2329,7 @@ function buildAuthnOperations(deps) {
2056
2329
  }
2057
2330
  return otpChallenge(identifier);
2058
2331
  }
2059
- };
2332
+ } };
2060
2333
  }
2061
2334
  var TABLE_NAME3 = "hai_iam_permissions";
2062
2335
  var PERMISSION_FIELDS = [
@@ -3461,6 +3734,62 @@ function createRbacManager(config) {
3461
3734
  }
3462
3735
  };
3463
3736
  }
3737
+
3738
+ // src/iam-notifications.ts
3739
+ async function resolveIamNotifications(input) {
3740
+ const callbacks = { onOtpSendEmail: input.onOtpSendEmail, onOtpSendSms: input.onOtpSendSms, onPasswordResetRequest: input.onPasswordResetRequest };
3741
+ const reach = input.reach;
3742
+ if (!reach?.isInitialized)
3743
+ return callbacks;
3744
+ const needsEmail = input.login?.otp !== false && !!input.otp && input.otp.email !== false && !callbacks.onOtpSendEmail;
3745
+ const needsSms = input.login?.otp !== false && !!input.otp && input.otp.sms !== false && !callbacks.onOtpSendSms;
3746
+ const needsReset = input.passwordReset?.enabled !== false && !callbacks.onPasswordResetRequest;
3747
+ if (!needsEmail && !needsSms && !needsReset)
3748
+ return callbacks;
3749
+ const templates = await reach.template.list();
3750
+ if (!templates.success)
3751
+ throw new Error(iamM("iam_securityStateFailed"));
3752
+ const configured = new Map(templates.data.map((template) => [template.name, template.provider]));
3753
+ async function send(template, to, vars) {
3754
+ const provider = configured.get(template);
3755
+ if (!reach?.isInitialized || !provider || !to)
3756
+ throw new Error(iamM("iam_passwordResetDeliveryFailed"));
3757
+ const result = await reach.send({ provider, to, template, vars });
3758
+ if (!result.success || !result.data.success)
3759
+ throw new Error(iamM("iam_passwordResetDeliveryFailed"));
3760
+ }
3761
+ if (needsEmail && configured.has("otp_email"))
3762
+ callbacks.onOtpSendEmail = (email, code) => send("otp_email", email, { code });
3763
+ if (needsSms && configured.has("otp_sms"))
3764
+ callbacks.onOtpSendSms = (phone, code) => send("otp_sms", phone, { code });
3765
+ const resetTemplate = input.passwordReset?.mode === "phone" ? "password_reset_sms" : "password_reset";
3766
+ if (needsReset && configured.has(resetTemplate)) {
3767
+ callbacks.onPasswordResetRequest = (user, token, expiresAt) => {
3768
+ const url = input.passwordReset?.resetUrl ? new URL(input.passwordReset.resetUrl) : null;
3769
+ url?.searchParams.set("token", token);
3770
+ const vars = { token, expiresAt: expiresAt.toISOString(), resetUrl: url?.toString() ?? token };
3771
+ return send(resetTemplate, input.passwordReset?.mode === "phone" ? user.phone ?? "" : user.email ?? "", vars);
3772
+ };
3773
+ }
3774
+ return callbacks;
3775
+ }
3776
+
3777
+ // src/iam-public-config.ts
3778
+ function buildIamPublicConfig(config, runtime) {
3779
+ const otp = OtpConfigSchema.parse(config.otp ?? {});
3780
+ const email = config.login.otp && !!config.otp && otp.email && !!runtime.onOtpSendEmail;
3781
+ const sms = config.login.otp && !!config.otp && otp.sms && !!runtime.onOtpSendSms;
3782
+ return {
3783
+ password: PasswordConfigSchema.parse(config.password ?? {}),
3784
+ register: { enabled: config.register.enabled },
3785
+ login: { password: config.login.password, otp: email || sms, ldap: config.login.ldap && !!config.ldap, email, sms },
3786
+ passwordReset: { enabled: config.passwordReset?.enabled !== false && !!runtime.onPasswordResetRequest, mode: config.passwordReset?.mode ?? "email" },
3787
+ otp: { length: otp.length, resendInterval: otp.resendInterval },
3788
+ agreements: { ...config.agreements },
3789
+ ui: AuthUiConfigSchema.parse(config.ui ?? {}),
3790
+ session: { maxAge: SessionConfigSchema.parse(config.session ?? {}).maxAge }
3791
+ };
3792
+ }
3464
3793
  var logger7 = core.logger.child({ module: "iam", scope: "seed" });
3465
3794
  var DEFAULT_ROLES = [
3466
3795
  { code: "admin", name: () => iamM("iam_seedRoleAdminName"), description: () => iamM("iam_seedRoleAdminDesc"), isSystem: true },
@@ -3572,64 +3901,6 @@ async function seedIamData(authz) {
3572
3901
  );
3573
3902
  }
3574
3903
  }
3575
-
3576
- // src/session/iam-session-utils.ts
3577
- var BASE64_PLUS_REGEX = /\+/g;
3578
- var BASE64_SLASH_REGEX = /\//g;
3579
- var BASE64_TRAILING_PADDING_REGEX = /=+$/;
3580
- function generateToken() {
3581
- const bytes = new Uint8Array(32);
3582
- crypto.getRandomValues(bytes);
3583
- const base64 = btoa(String.fromCharCode(...bytes));
3584
- return base64.replace(BASE64_PLUS_REGEX, "-").replace(BASE64_SLASH_REGEX, "_").replace(BASE64_TRAILING_PADDING_REGEX, "");
3585
- }
3586
- function buildSession(options, now, sessionTtl, accessToken) {
3587
- return {
3588
- userId: options.userId,
3589
- username: options.username,
3590
- displayName: options.displayName,
3591
- avatarUrl: options.avatarUrl,
3592
- roles: options.roles ?? [],
3593
- permissions: options.permissions ?? [],
3594
- source: options.source,
3595
- accessToken,
3596
- createdAt: now,
3597
- lastActiveAt: now,
3598
- expiresAt: new Date(now.getTime() + sessionTtl * 1e3),
3599
- data: options.data
3600
- };
3601
- }
3602
- function getSessionTtl(session, now = Date.now()) {
3603
- return Math.max(0, Math.floor((session.expiresAt.getTime() - now) / 1e3));
3604
- }
3605
- function applySessionPatch(session, patch) {
3606
- const nextSession = { ...session };
3607
- if (patch.data !== void 0) {
3608
- nextSession.data = { ...nextSession.data, ...patch.data };
3609
- }
3610
- if (patch.roles !== void 0) {
3611
- nextSession.roles = patch.roles;
3612
- }
3613
- if (patch.permissions !== void 0) {
3614
- nextSession.permissions = patch.permissions;
3615
- }
3616
- if (patch.username !== void 0) {
3617
- nextSession.username = patch.username;
3618
- }
3619
- if (patch.displayName !== void 0) {
3620
- nextSession.displayName = patch.displayName;
3621
- }
3622
- if (patch.avatarUrl !== void 0) {
3623
- nextSession.avatarUrl = patch.avatarUrl;
3624
- }
3625
- if (patch.source !== void 0) {
3626
- nextSession.source = patch.source;
3627
- }
3628
- nextSession.lastActiveAt = /* @__PURE__ */ new Date();
3629
- return nextSession;
3630
- }
3631
-
3632
- // src/session/iam-session-repository-cache.ts
3633
3904
  var TOKEN_KEY_PREFIX = "hai:iam:token:";
3634
3905
  var USER_TOKENS_KEY_PREFIX = "hai:iam:user:";
3635
3906
  var REFRESH_TOKEN_PREFIX = "hai:iam:refresh:";
@@ -4203,61 +4474,82 @@ function createCacheResetTokenRepository() {
4203
4474
  }
4204
4475
  const repo = {
4205
4476
  async saveToken(token, userId, expiresAt) {
4206
- const hashResult = hashResetToken(token);
4207
- if (!hashResult.success) {
4208
- return hashResult;
4209
- }
4210
- const hashedToken = hashResult.data;
4211
- const ttlSeconds = Math.max(1, Math.ceil((expiresAt.getTime() - Date.now()) / 1e3));
4212
- const userTokenKey = buildUserTokenKey(userId);
4213
- const existingTokenResult = await cache.kv.get(userTokenKey);
4214
- if (!existingTokenResult.success) {
4215
- return buildResetTokenRepositoryError(
4216
- "iam_queryResetTokenFailed",
4217
- existingTokenResult.error.message,
4218
- existingTokenResult.error
4219
- );
4220
- }
4221
- if (existingTokenResult.data && existingTokenResult.data !== hashedToken) {
4222
- const removeExistingTokenResult = await cache.kv.del(buildResetTokenKey(existingTokenResult.data));
4223
- if (!removeExistingTokenResult.success) {
4477
+ return withIamLock(`reset:${userId}`, async () => {
4478
+ const hashResult = hashResetToken(token);
4479
+ if (!hashResult.success) {
4480
+ return hashResult;
4481
+ }
4482
+ const hashedToken = hashResult.data;
4483
+ const ttlSeconds = Math.max(1, Math.ceil((expiresAt.getTime() - Date.now()) / 1e3));
4484
+ const userTokenKey = buildUserTokenKey(userId);
4485
+ const existingTokenResult = await cache.kv.get(userTokenKey);
4486
+ if (!existingTokenResult.success) {
4487
+ return buildResetTokenRepositoryError(
4488
+ "iam_queryResetTokenFailed",
4489
+ existingTokenResult.error.message,
4490
+ existingTokenResult.error
4491
+ );
4492
+ }
4493
+ if (existingTokenResult.data && existingTokenResult.data !== hashedToken) {
4494
+ const removeExistingTokenResult = await cache.kv.del(buildResetTokenKey(existingTokenResult.data));
4495
+ if (!removeExistingTokenResult.success) {
4496
+ return buildResetTokenRepositoryError(
4497
+ "iam_saveResetTokenFailed",
4498
+ removeExistingTokenResult.error.message,
4499
+ removeExistingTokenResult.error
4500
+ );
4501
+ }
4502
+ }
4503
+ const setResult = await cache.kv.set(buildResetTokenKey(hashedToken), userId, { ex: ttlSeconds });
4504
+ if (!setResult.success) {
4505
+ return err(
4506
+ HaiIamError.REPOSITORY_ERROR,
4507
+ iamM("iam_saveResetTokenFailed", { params: { message: setResult.error.message } }),
4508
+ setResult.error
4509
+ );
4510
+ }
4511
+ const saveUserTokenResult = await cache.kv.set(userTokenKey, hashedToken, { ex: ttlSeconds });
4512
+ if (!saveUserTokenResult.success) {
4513
+ await cache.kv.del(buildResetTokenKey(hashedToken));
4224
4514
  return buildResetTokenRepositoryError(
4225
4515
  "iam_saveResetTokenFailed",
4226
- removeExistingTokenResult.error.message,
4227
- removeExistingTokenResult.error
4516
+ saveUserTokenResult.error.message,
4517
+ saveUserTokenResult.error
4228
4518
  );
4229
4519
  }
4230
- }
4231
- const setResult = await cache.kv.set(buildResetTokenKey(hashedToken), userId, { ex: ttlSeconds });
4232
- if (!setResult.success) {
4233
- return err(
4234
- HaiIamError.REPOSITORY_ERROR,
4235
- iamM("iam_saveResetTokenFailed", { params: { message: setResult.error.message } }),
4236
- setResult.error
4237
- );
4238
- }
4239
- const saveUserTokenResult = await cache.kv.set(userTokenKey, hashedToken, { ex: ttlSeconds });
4240
- if (!saveUserTokenResult.success) {
4241
- await cache.kv.del(buildResetTokenKey(hashedToken));
4242
- return buildResetTokenRepositoryError(
4243
- "iam_saveResetTokenFailed",
4244
- saveUserTokenResult.error.message,
4245
- saveUserTokenResult.error
4246
- );
4247
- }
4248
- const attemptsResult = await cache.kv.set(buildAttemptsKey(userId), 0, { ex: ttlSeconds });
4249
- if (!attemptsResult.success) {
4250
- await Promise.all([
4251
- cache.kv.del(buildResetTokenKey(hashedToken)),
4252
- cache.kv.del(userTokenKey)
4253
- ]);
4254
- return buildResetTokenRepositoryError(
4255
- "iam_saveResetTokenFailed",
4256
- attemptsResult.error.message,
4257
- attemptsResult.error
4258
- );
4259
- }
4260
- return ok(void 0);
4520
+ const attemptsResult = await cache.kv.set(buildAttemptsKey(userId), 0, { ex: ttlSeconds });
4521
+ if (!attemptsResult.success) {
4522
+ await Promise.all([
4523
+ cache.kv.del(buildResetTokenKey(hashedToken)),
4524
+ cache.kv.del(userTokenKey)
4525
+ ]);
4526
+ return buildResetTokenRepositoryError(
4527
+ "iam_saveResetTokenFailed",
4528
+ attemptsResult.error.message,
4529
+ attemptsResult.error
4530
+ );
4531
+ }
4532
+ return ok(void 0);
4533
+ });
4534
+ },
4535
+ async useToken(token, maxAttempts, operation) {
4536
+ const hash = hashResetToken(token);
4537
+ if (!hash.success)
4538
+ return hash;
4539
+ const user = await cache.kv.get(buildResetTokenKey(hash.data));
4540
+ if (!user.success)
4541
+ return buildResetTokenRepositoryError("iam_queryResetTokenFailed", user.error.message, user.error);
4542
+ if (!user.data)
4543
+ return err(HaiIamError.RESET_TOKEN_INVALID, iamM("iam_resetTokenInvalid"));
4544
+ return withIamLock(`reset:${user.data}`, async () => {
4545
+ const verified = await repo.tryGetUserByToken(token, maxAttempts);
4546
+ if (!verified.success)
4547
+ return verified;
4548
+ const result = await operation(verified.data);
4549
+ if (!result.success)
4550
+ return result;
4551
+ return repo.removeToken(token);
4552
+ });
4261
4553
  },
4262
4554
  async tryGetUserByToken(token, maxAttempts) {
4263
4555
  const hashResult = hashResetToken(token);
@@ -4281,6 +4573,11 @@ function createCacheResetTokenRepository() {
4281
4573
  );
4282
4574
  }
4283
4575
  const userId = result.data;
4576
+ const current = await cache.kv.get(buildUserTokenKey(userId));
4577
+ if (!current.success)
4578
+ return buildResetTokenRepositoryError("iam_queryResetTokenFailed", current.error.message, current.error);
4579
+ if (current.data !== hashedToken)
4580
+ return err(HaiIamError.RESET_TOKEN_INVALID, iamM("iam_resetTokenInvalid"));
4284
4581
  const attemptsKey = buildAttemptsKey(userId);
4285
4582
  const incrResult = await cache.kv.incr(attemptsKey);
4286
4583
  if (!incrResult.success) {
@@ -4427,16 +4724,12 @@ function buildRegistrationOps(ctx) {
4427
4724
  const { userRepository, authzFunctions, config, registerConfig, agreementConfig } = ctx;
4428
4725
  const { validatePassword, hashPassword } = ctx;
4429
4726
  async function validateRegisterPreconditions(options) {
4430
- if (!registerConfig.enabled) {
4431
- return err(
4432
- HaiIamError.REGISTER_DISABLED,
4433
- iamM("iam_registerDisabled")
4434
- );
4435
- }
4436
4727
  const validateResult = validatePassword(options.password);
4437
4728
  if (!validateResult.success)
4438
4729
  return validateResult;
4439
4730
  const existsResult = await userRepository.existsByUsername(options.username);
4731
+ if (!existsResult.success)
4732
+ return existsResult;
4440
4733
  if (existsResult.success && existsResult.data) {
4441
4734
  return err(
4442
4735
  HaiIamError.USER_ALREADY_EXISTS,
@@ -4445,6 +4738,8 @@ function buildRegistrationOps(ctx) {
4445
4738
  }
4446
4739
  if (options.email) {
4447
4740
  const emailExistsResult = await userRepository.existsByEmail(options.email);
4741
+ if (!emailExistsResult.success)
4742
+ return emailExistsResult;
4448
4743
  if (emailExistsResult.success && emailExistsResult.data) {
4449
4744
  return err(
4450
4745
  HaiIamError.USER_ALREADY_EXISTS,
@@ -4455,61 +4750,74 @@ function buildRegistrationOps(ctx) {
4455
4750
  return ok(void 0);
4456
4751
  }
4457
4752
  async function assignDefaultRole(userId) {
4458
- if (!config.rbac?.defaultRole)
4459
- return;
4753
+ if (!config.rbac?.enabled || !config.rbac.defaultRole)
4754
+ return ok(void 0);
4460
4755
  const roleResult = await authzFunctions.getRoleByCode(config.rbac.defaultRole);
4461
- if (roleResult.success && roleResult.data) {
4462
- await authzFunctions.assignRole(userId, roleResult.data.id);
4756
+ if (!roleResult.success)
4757
+ return roleResult;
4758
+ if (!roleResult.data)
4759
+ return err(HaiIamError.ROLE_NOT_FOUND, iamM("iam_roleNotExist"));
4760
+ return authzFunctions.assignRole(userId, roleResult.data.id);
4761
+ }
4762
+ async function createUser(options, enabled) {
4763
+ const preResult = await validateRegisterPreconditions(options);
4764
+ if (!preResult.success)
4765
+ return preResult;
4766
+ const hashResult = hashPassword(options.password);
4767
+ if (!hashResult.success)
4768
+ return hashResult;
4769
+ const txResult = await reldb.tx.begin();
4770
+ if (!txResult.success) {
4771
+ return mapRepositoryError("iam_createUserFailed", txResult.error.message);
4463
4772
  }
4773
+ const tx = txResult.data;
4774
+ const createResult = await userRepository.create({
4775
+ username: options.username,
4776
+ email: options.email,
4777
+ phone: options.phone,
4778
+ displayName: options.displayName,
4779
+ enabled,
4780
+ emailVerified: false,
4781
+ phoneVerified: false,
4782
+ passwordHash: hashResult.data,
4783
+ passwordUpdatedAt: /* @__PURE__ */ new Date(),
4784
+ metadata: options.metadata
4785
+ }, tx);
4786
+ if (!createResult.success) {
4787
+ await tx.rollback();
4788
+ return mapUpdateErrorAsDomainError(createResult.error.message);
4789
+ }
4790
+ const createdUserResult = await userRepository.findByUsername(options.username, tx);
4791
+ if (!createdUserResult.success || !createdUserResult.data) {
4792
+ await tx.rollback();
4793
+ return err(
4794
+ HaiIamError.USER_NOT_FOUND,
4795
+ iamM("iam_userNotExist")
4796
+ );
4797
+ }
4798
+ const commitResult = await tx.commit();
4799
+ if (!commitResult.success) {
4800
+ return mapRepositoryError("iam_createUserFailed", commitResult.error.message);
4801
+ }
4802
+ const createdUser = createdUserResult.data;
4803
+ const assigned = await assignDefaultRole(createdUser.id);
4804
+ if (!assigned.success) {
4805
+ await authzFunctions.syncRoles(createdUser.id, []);
4806
+ await userRepository.deleteById(createdUser.id);
4807
+ return assigned;
4808
+ }
4809
+ logger10.info("User registered", { userId: createdUser.id, username: options.username });
4810
+ return ok({
4811
+ user: toUser(createdUser),
4812
+ agreements: buildAgreementDisplay(agreementConfig, agreementConfig.showOnRegister)
4813
+ });
4464
4814
  }
4465
4815
  return {
4466
- async register(options) {
4467
- const preResult = await validateRegisterPreconditions(options);
4468
- if (!preResult.success)
4469
- return preResult;
4470
- const hashResult = hashPassword(options.password);
4471
- if (!hashResult.success)
4472
- return hashResult;
4473
- const txResult = await reldb.tx.begin();
4474
- if (!txResult.success) {
4475
- return mapRepositoryError("iam_createUserFailed", txResult.error.message);
4476
- }
4477
- const tx = txResult.data;
4478
- const createResult = await userRepository.create({
4479
- username: options.username,
4480
- email: options.email,
4481
- phone: options.phone,
4482
- displayName: options.displayName,
4483
- enabled: registerConfig.defaultEnabled,
4484
- emailVerified: false,
4485
- phoneVerified: false,
4486
- passwordHash: hashResult.data,
4487
- passwordUpdatedAt: /* @__PURE__ */ new Date(),
4488
- metadata: options.metadata
4489
- }, tx);
4490
- if (!createResult.success) {
4491
- await tx.rollback();
4492
- return mapUpdateErrorAsDomainError(createResult.error.message);
4493
- }
4494
- const createdUserResult = await userRepository.findByUsername(options.username, tx);
4495
- if (!createdUserResult.success || !createdUserResult.data) {
4496
- await tx.rollback();
4497
- return err(
4498
- HaiIamError.USER_NOT_FOUND,
4499
- iamM("iam_userNotExist")
4500
- );
4501
- }
4502
- const commitResult = await tx.commit();
4503
- if (!commitResult.success) {
4504
- return mapRepositoryError("iam_createUserFailed", commitResult.error.message);
4505
- }
4506
- const createdUser = createdUserResult.data;
4507
- await assignDefaultRole(createdUser.id);
4508
- logger10.info("User registered", { userId: createdUser.id, username: options.username });
4509
- return ok({
4510
- user: toUser(createdUser),
4511
- agreements: buildAgreementDisplay(agreementConfig, agreementConfig.showOnRegister)
4512
- });
4816
+ createUser: (options) => createUser(options, true),
4817
+ register(options) {
4818
+ if (!registerConfig.enabled)
4819
+ return Promise.resolve(err(HaiIamError.REGISTER_DISABLED, iamM("iam_registerDisabled")));
4820
+ return createUser(options, registerConfig.defaultEnabled);
4513
4821
  },
4514
4822
  validatePassword(password) {
4515
4823
  return validatePassword(password);
@@ -4806,16 +5114,20 @@ function buildPasswordChangeOps(ctx) {
4806
5114
  }
4807
5115
  const verifyResult = crypto$1.password.verify(oldPassword, user.passwordHash);
4808
5116
  if (!verifyResult.success || !verifyResult.data) {
4809
- await recordLoginFailure(userRepository, user, {
5117
+ const failureResult = await recordLoginFailure(userRepository, user, {
4810
5118
  maxLoginAttempts: ctx.loginConfig.maxLoginAttempts,
4811
5119
  lockoutDuration: ctx.loginConfig.lockoutDuration
4812
5120
  });
5121
+ if (!failureResult.success)
5122
+ return failureResult;
4813
5123
  return err(
4814
5124
  HaiIamError.INVALID_CREDENTIALS,
4815
5125
  iamM("iam_originalPasswordWrong")
4816
5126
  );
4817
5127
  }
4818
- await resetLoginFailures(userRepository, user);
5128
+ const resetResult = await resetLoginFailures(userRepository, user);
5129
+ if (!resetResult.success)
5130
+ return resetResult;
4819
5131
  const validateResult = validatePassword(newPassword);
4820
5132
  if (!validateResult.success)
4821
5133
  return validateResult;
@@ -4857,6 +5169,8 @@ function buildPasswordResetOps(ctx) {
4857
5169
  async requestPasswordReset(identifier) {
4858
5170
  logger10.debug("Password reset requested", { identifier });
4859
5171
  const resetConfig = ctx.resetConfig;
5172
+ if (!resetConfig.enabled || !onPasswordResetRequest)
5173
+ return err(HaiIamError.PASSWORD_RESET_DISABLED, iamM("iam_passwordResetDisabled"));
4860
5174
  const userResult = await userRepository.findByIdentifier(identifier);
4861
5175
  if (!userResult.success) {
4862
5176
  logger10.warn("Failed to look up user for password reset", { identifier });
@@ -4867,20 +5181,19 @@ function buildPasswordResetOps(ctx) {
4867
5181
  return ok(void 0);
4868
5182
  }
4869
5183
  const user = toUser(userResult.data);
5184
+ if (!user.enabled)
5185
+ return ok(void 0);
4870
5186
  const token = generateToken();
4871
5187
  const expiresAt = new Date(Date.now() + resetConfig.tokenExpiresIn * 1e3);
4872
5188
  const saveResult = await resetTokenRepository.saveToken(token, user.id, expiresAt);
4873
5189
  if (!saveResult.success) {
4874
5190
  return saveResult;
4875
5191
  }
4876
- if (onPasswordResetRequest) {
4877
- try {
4878
- await onPasswordResetRequest(user, token, expiresAt);
4879
- } catch (callbackError) {
4880
- logger10.error("Password reset callback failed", { userId: user.id, error: callbackError });
4881
- }
4882
- } else {
4883
- logger10.warn("No password reset callback configured, token will not be delivered to user", { userId: user.id });
5192
+ try {
5193
+ await onPasswordResetRequest(user, token, expiresAt);
5194
+ } catch {
5195
+ logger10.error("Password reset callback failed", { userId: user.id });
5196
+ return err(HaiIamError.INTERNAL_ERROR, iamM("iam_passwordResetDeliveryFailed"));
4884
5197
  }
4885
5198
  logger10.info("Password reset token generated", { userId: user.id });
4886
5199
  return ok(void 0);
@@ -4888,38 +5201,43 @@ function buildPasswordResetOps(ctx) {
4888
5201
  async confirmPasswordReset(token, newPassword) {
4889
5202
  logger10.debug("Confirming password reset");
4890
5203
  const resetConfig = ctx.resetConfig;
4891
- const validateResult = validatePassword(newPassword);
4892
- if (!validateResult.success)
4893
- return validateResult;
4894
- const tokenResult = await resetTokenRepository.tryGetUserByToken(token, resetConfig.maxAttempts);
4895
- if (!tokenResult.success) {
4896
- return tokenResult;
4897
- }
4898
- const userId = tokenResult.data;
4899
- const userResult = await userRepository.findById(userId);
4900
- if (!userResult.success) {
4901
- return mapRepositoryError("iam_queryUserFailed", userResult.error.message);
4902
- }
4903
- if (!userResult.data) {
4904
- return err(
4905
- HaiIamError.USER_NOT_FOUND,
4906
- iamM("iam_userNotExist")
4907
- );
4908
- }
4909
- const hashResult = hashPassword(newPassword);
4910
- if (!hashResult.success)
4911
- return hashResult;
4912
- const updateResult = await userRepository.updateById(userId, {
4913
- passwordHash: hashResult.data,
4914
- passwordUpdatedAt: /* @__PURE__ */ new Date()
5204
+ if (!resetConfig.enabled)
5205
+ return err(HaiIamError.PASSWORD_RESET_DISABLED, iamM("iam_passwordResetDisabled"));
5206
+ return resetTokenRepository.useToken(token, resetConfig.maxAttempts, async (userId) => {
5207
+ const validateResult = validatePassword(newPassword);
5208
+ if (!validateResult.success)
5209
+ return validateResult;
5210
+ const userResult = await userRepository.findById(userId);
5211
+ if (!userResult.success) {
5212
+ return mapRepositoryError("iam_queryUserFailed", userResult.error.message);
5213
+ }
5214
+ if (!userResult.data) {
5215
+ return err(
5216
+ HaiIamError.USER_NOT_FOUND,
5217
+ iamM("iam_userNotExist")
5218
+ );
5219
+ }
5220
+ if (!userResult.data.enabled)
5221
+ return err(HaiIamError.USER_DISABLED, iamM("iam_accountDisabled"));
5222
+ const hashResult = hashPassword(newPassword);
5223
+ if (!hashResult.success)
5224
+ return hashResult;
5225
+ const updateResult = await userRepository.updateById(userId, {
5226
+ passwordHash: hashResult.data,
5227
+ passwordUpdatedAt: /* @__PURE__ */ new Date(),
5228
+ loginFailedCount: 0,
5229
+ lockedUntil: null,
5230
+ lastLoginFailedAt: null
5231
+ });
5232
+ if (!updateResult.success) {
5233
+ return mapRepositoryError("iam_updateUserFailed", updateResult.error.message);
5234
+ }
5235
+ const revoked = await sessionFunctions.deleteByUserId(userId);
5236
+ if (!revoked.success)
5237
+ return revoked;
5238
+ logger10.info("Password reset confirmed", { userId });
5239
+ return ok(void 0);
4915
5240
  });
4916
- if (!updateResult.success) {
4917
- return mapRepositoryError("iam_updateUserFailed", updateResult.error.message);
4918
- }
4919
- await resetTokenRepository.removeToken(token);
4920
- await sessionFunctions.deleteByUserId(userId);
4921
- logger10.info("Password reset confirmed", { userId });
4922
- return ok(void 0);
4923
5241
  }
4924
5242
  };
4925
5243
  }
@@ -4947,6 +5265,7 @@ function buildUserFunctions(deps) {
4947
5265
  var logger11 = core.logger.child({ module: "iam", scope: "main" });
4948
5266
  var initInProgress = false;
4949
5267
  var currentConfig = null;
5268
+ var currentPublicConfig = null;
4950
5269
  var currentAuth = null;
4951
5270
  var currentUser = null;
4952
5271
  var currentAuthz = null;
@@ -4984,7 +5303,7 @@ var iam = {
4984
5303
  logger11.warn("IAM module is already initialized, reinitializing");
4985
5304
  await iam.close();
4986
5305
  }
4987
- const { ldapClientFactory, ldapSyncUser, onPasswordResetRequest, onOtpSendEmail, onOtpSendSms, ...settingsInput } = config;
5306
+ const { ldapClientFactory, ldapSyncUser } = config;
4988
5307
  logger11.info("Initializing IAM module");
4989
5308
  if (!reldb.isInitialized) {
4990
5309
  return err(
@@ -5008,7 +5327,7 @@ var iam = {
5008
5327
  );
5009
5328
  }
5010
5329
  }
5011
- const parseResult = IamConfigSchema.safeParse(settingsInput);
5330
+ const parseResult = IamConfigSchema.safeParse(config);
5012
5331
  if (!parseResult.success) {
5013
5332
  logger11.error("IAM config validation failed", { error: parseResult.error.message });
5014
5333
  return err(
@@ -5018,6 +5337,8 @@ var iam = {
5018
5337
  );
5019
5338
  }
5020
5339
  const parsed = parseResult.data;
5340
+ const { onPasswordResetRequest, onOtpSendEmail, onOtpSendSms } = await resolveIamNotifications(config);
5341
+ logger11.info("IAM notification channels resolved", { email: Boolean(onOtpSendEmail), sms: Boolean(onOtpSendSms), passwordReset: Boolean(onPasswordResetRequest) });
5021
5342
  const sessionResult = await createSessionOperations({ config: parsed });
5022
5343
  if (!sessionResult.success) {
5023
5344
  return sessionResult;
@@ -5058,6 +5379,7 @@ var iam = {
5058
5379
  currentAuthz = authzResult.data;
5059
5380
  currentUser = userResult.data;
5060
5381
  currentConfig = parsed;
5382
+ currentPublicConfig = buildIamPublicConfig(parsed, { ...config, onPasswordResetRequest, onOtpSendEmail, onOtpSendSms });
5061
5383
  currentApiKey = authnResult.data.apiKeyFunctions;
5062
5384
  currentTicket = createTicketOperations();
5063
5385
  const authn = authnResult.data.authn;
@@ -5068,7 +5390,7 @@ var iam = {
5068
5390
  if (!regResult.success) {
5069
5391
  return regResult;
5070
5392
  }
5071
- return authn.login({ identifier: options.username, password: options.password });
5393
+ return authnResult.data.loginRegisteredUser(regResult.data.user);
5072
5394
  }
5073
5395
  };
5074
5396
  logger11.info("IAM module initialized");
@@ -5105,6 +5427,9 @@ var iam = {
5105
5427
  get config() {
5106
5428
  return currentConfig ? core.sanitize.sanitizeSensitiveFields(currentConfig) : null;
5107
5429
  },
5430
+ get publicConfig() {
5431
+ return currentPublicConfig ? structuredClone(currentPublicConfig) : null;
5432
+ },
5108
5433
  get isInitialized() {
5109
5434
  return currentConfig !== null;
5110
5435
  },
@@ -5124,6 +5449,7 @@ var iam = {
5124
5449
  currentApiKey = null;
5125
5450
  currentTicket = null;
5126
5451
  currentConfig = null;
5452
+ currentPublicConfig = null;
5127
5453
  resetApiKeyRepoSingleton();
5128
5454
  resetOtpRepoSingleton();
5129
5455
  resetUserRepoSingleton();
@@ -5134,6 +5460,6 @@ var iam = {
5134
5460
  }
5135
5461
  };
5136
5462
 
5137
- export { AgreementConfigSchema, ApiKeyConfigSchema, AuthStrategyTypeSchema, HaiIamError, IamConfigSchema, LdapConfigSchema, LoginConfigSchema, OtpConfigSchema, PasswordConfigSchema, PasswordResetConfigSchema, RbacConfigSchema, RegisterConfigSchema, SecurityConfigSchema, SessionConfigSchema, iam };
5463
+ export { AgreementConfigSchema, ApiKeyConfigSchema, AuthStrategyTypeSchema, AuthUiConfigSchema, HaiIamError, IamConfigSchema, LdapConfigSchema, LoginConfigSchema, OtpConfigSchema, PasswordConfigSchema, PasswordResetConfigSchema, RbacConfigSchema, RegisterConfigSchema, SecurityConfigSchema, SessionConfigSchema, iam };
5138
5464
  //# sourceMappingURL=index.js.map
5139
5465
  //# sourceMappingURL=index.js.map