@h-ai/iam 0.1.0-alpha.67 → 0.1.0-alpha.68

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,196 +1,12 @@
1
- import { z } from 'zod';
1
+ import { HaiIamError, IamConfigSchema, SessionConfigSchema, SecurityConfigSchema, LoginConfigSchema, OtpConfigSchema, ApiKeyConfigSchema, AuthUiConfigSchema, PasswordConfigSchema, RbacConfigSchema, AgreementConfigSchema, PasswordResetConfigSchema, RegisterConfigSchema } from './chunk-VCE74BFW.js';
2
+ export { AgreementConfigSchema, ApiKeyConfigSchema, AuthStrategyTypeSchema, AuthUiConfigSchema, HaiIamError, IamConfigSchema, LdapConfigSchema, LoginConfigSchema, OtpConfigSchema, PasswordConfigSchema, PasswordResetConfigSchema, RbacConfigSchema, RegisterConfigSchema, SecurityConfigSchema, SessionConfigSchema } from './chunk-VCE74BFW.js';
2
3
  import { cache } from '@h-ai/cache';
3
4
  import { core, err, ok } from '@h-ai/core';
4
5
  import { crypto as crypto$1 } from '@h-ai/crypto';
5
6
  import { reldb, BaseReldbCrudRepository } from '@h-ai/reldb';
6
- import { randomBytes } from 'crypto';
7
+ import { createHash, randomBytes } from 'crypto';
7
8
  import { audit } from '@h-ai/audit';
8
9
 
9
- // src/iam-config.ts
10
- var WebUrlSchema = z.url().refine((value) => ["http:", "https:"].includes(new URL(value).protocol));
11
- var AuthStrategyTypeSchema = z.enum(["password", "otp", "ldap", "apikey"]);
12
- var PasswordConfigSchema = z.object({
13
- /** 最小长度(默认 8) */
14
- minLength: z.number().int().min(1).default(8),
15
- /** 最大长度(默认 128) */
16
- maxLength: z.number().int().min(1).max(256).default(128),
17
- /** 需要大写字母 */
18
- requireUppercase: z.boolean().default(true),
19
- /** 需要小写字母 */
20
- requireLowercase: z.boolean().default(true),
21
- /** 需要数字 */
22
- requireNumber: z.boolean().default(true),
23
- /** 需要特殊字符 */
24
- requireSpecialChar: z.boolean().default(false),
25
- /** 密码过期天数(0 表示不过期) */
26
- expirationDays: z.number().int().min(0).default(0)
27
- }).refine((config) => config.maxLength >= config.minLength, { message: "maxLength must be >= minLength", path: ["maxLength"] });
28
- var OtpConfigSchema = z.object({
29
- /** 分别启用邮件、短信验证码 */
30
- email: z.boolean().default(true),
31
- sms: z.boolean().default(true),
32
- /** 验证码长度(默认 6) */
33
- length: z.number().int().min(4).max(8).default(6),
34
- /** 验证码过期时间(秒,默认 300) */
35
- expiresIn: z.number().int().min(60).default(300),
36
- /** 最大重试次数(默认 3) */
37
- maxAttempts: z.number().int().min(1).default(3),
38
- /** 发送间隔(秒,默认 60) */
39
- resendInterval: z.number().int().min(30).default(60)
40
- });
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),
48
- /** 重置令牌有效期(秒,默认 3600 = 1小时) */
49
- tokenExpiresIn: z.number().int().min(300).default(3600),
50
- /** 最大验证尝试次数(默认 3) */
51
- maxAttempts: z.number().int().min(1).default(3)
52
- });
53
- var LdapConfigSchema = z.object({
54
- /** LDAP 服务器 URL */
55
- url: z.url().refine((value) => ["ldap:", "ldaps:"].includes(new URL(value).protocol)),
56
- /** 绑定 DN */
57
- bindDn: z.string(),
58
- /** 绑定密码 */
59
- bindPassword: z.string(),
60
- /** 搜索基础 DN */
61
- searchBase: z.string(),
62
- /** 搜索过滤器(默认使用 uid) */
63
- searchFilter: z.string().default("(uid={{username}})"),
64
- /** 用户名属性 */
65
- usernameAttribute: z.string().default("uid"),
66
- /** 邮箱属性 */
67
- emailAttribute: z.string().default("mail"),
68
- /** 显示名称属性 */
69
- displayNameAttribute: z.string().default("cn"),
70
- /** 启用 TLS */
71
- useTls: z.boolean().default(false),
72
- /** 连接超时(毫秒) */
73
- connectTimeout: z.number().int().min(1e3).default(5e3)
74
- });
75
- var ApiKeyConfigSchema = z.object({
76
- /** 单用户最大 API Key 数量(默认 10) */
77
- maxKeysPerUser: z.number().int().min(1).default(10),
78
- /** API Key 默认有效期(天,0 表示永不过期,默认 0) */
79
- defaultExpirationDays: z.number().int().min(0).default(0),
80
- /** API Key 前缀(默认 'hai_') */
81
- prefix: z.string().default("hai_")
82
- });
83
- var LoginConfigSchema = z.object({
84
- /** 是否启用密码登录 */
85
- password: z.boolean().default(true),
86
- /** 是否启用 OTP 登录 */
87
- otp: z.boolean().default(true),
88
- /** 是否启用 LDAP 登录;需要显式开启并提供 ldap 配置。 */
89
- ldap: z.boolean().default(false),
90
- /** 是否启用 API Key 登录 */
91
- apikey: z.boolean().default(false)
92
- });
93
- var RegisterConfigSchema = z.object({
94
- /** 是否启用注册 */
95
- enabled: z.boolean().default(true),
96
- /** 新注册用户是否默认启用 */
97
- defaultEnabled: z.boolean().default(true)
98
- });
99
- var SecurityConfigSchema = z.object({
100
- /** 最大登录失败次数(默认 5) */
101
- maxLoginAttempts: z.number().int().min(1).default(5),
102
- /** 锁定时长(秒,默认 900) */
103
- lockoutDuration: z.number().int().min(60).default(900)
104
- });
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(),
110
- /** 用户协议 URL */
111
- userAgreementUrl: WebUrlSchema.optional(),
112
- /** 隐私协议 URL */
113
- privacyPolicyUrl: WebUrlSchema.optional(),
114
- /** 注册时展示协议 */
115
- showOnRegister: z.boolean().default(true),
116
- /** 登录时展示协议 */
117
- showOnLogin: z.boolean().default(false)
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
- });
129
- var SessionConfigSchema = z.object({
130
- /** 会话超时时间(秒,默认 86400 = 24小时) */
131
- maxAge: z.number().int().min(60).default(86400),
132
- /** 是否滑动窗口(每次访问刷新过期时间) */
133
- sliding: z.boolean().default(true),
134
- /** 单设备登录(踢掉其他设备) */
135
- singleDevice: z.boolean().default(false),
136
- /** refreshToken 过期时间(秒,默认 604800 = 7天) */
137
- refreshTokenMaxAge: z.number().int().min(3600).default(604800)
138
- });
139
- var RbacConfigSchema = z.object({
140
- /** 是否启用 RBAC */
141
- enabled: z.boolean().default(true),
142
- /** 超级管理员角色代码 */
143
- superAdminRole: z.string().default("super_admin"),
144
- /** 默认用户角色 */
145
- defaultRole: z.string().default("user")
146
- });
147
- var IamConfigSchema = z.object({
148
- /** 认证页外观 */
149
- ui: AuthUiConfigSchema.optional(),
150
- // ─── 认证策略配置 ───
151
- /** 密码配置 */
152
- password: PasswordConfigSchema.optional(),
153
- /** OTP 配置 */
154
- otp: OtpConfigSchema.optional(),
155
- /** LDAP 配置 */
156
- ldap: LdapConfigSchema.optional(),
157
- /** API Key 配置 */
158
- apikey: ApiKeyConfigSchema.optional(),
159
- /** 密码重置配置 */
160
- passwordReset: PasswordResetConfigSchema.optional(),
161
- /** 登录启用配置 */
162
- login: LoginConfigSchema.default({
163
- password: true,
164
- otp: true,
165
- ldap: false,
166
- apikey: false
167
- }),
168
- /** 注册配置 */
169
- register: RegisterConfigSchema.default({
170
- enabled: true,
171
- defaultEnabled: true
172
- }),
173
- /** 协议展示配置 */
174
- agreements: AgreementConfigSchema.default({
175
- showOnRegister: true,
176
- showOnLogin: false
177
- }),
178
- /** 安全策略配置 */
179
- security: SecurityConfigSchema.default({
180
- maxLoginAttempts: 5,
181
- lockoutDuration: 900
182
- }),
183
- // ─── 会话配置 ───
184
- /** 会话配置 */
185
- session: SessionConfigSchema.optional(),
186
- // ─── 授权配置 ───
187
- /** RBAC 配置 */
188
- rbac: RbacConfigSchema.default(() => RbacConfigSchema.parse({})),
189
- // ─── 运行时选项 ───
190
- /** 是否初始化默认角色和权限(默认 true) */
191
- seedDefaultData: z.boolean().default(true)
192
- });
193
-
194
10
  // messages/en-US.json
195
11
  var en_US_default = {
196
12
  iam_batchUserIdsInvalid: "Provide at most 1000 non-empty user IDs",
@@ -376,7 +192,203 @@ var en_US_default = {
376
192
  iam_apikeyUpdateFailed: "Failed to update API Key: {message}",
377
193
  iam_apikeyDeleteFailed: "Failed to delete API Key: {message}",
378
194
  iam_apikeyQueryFailed: "Failed to query API Key: {message}",
379
- iam_apikeyStrategyRequired: "API Key authentication requires API Key strategy configuration"
195
+ iam_apikeyStrategyRequired: "API Key authentication requires API Key strategy configuration",
196
+ iam_sessionStateFailed: "Shared session state operation failed",
197
+ iam_sessionStateConflict: "Session state changed; retry the operation",
198
+ iam_sessionMaxAgeInvalid: "Invalid session lifetime or timestamp",
199
+ iam_notificationDeliveryUnknown: "Notification delivery is unknown and may have succeeded; do not resend automatically."
200
+ };
201
+
202
+ // messages/ja-JP.json
203
+ var ja_JP_default = {
204
+ iam_batchUserIdsInvalid: "\u7A7A\u3067\u306A\u3044\u30E6\u30FC\u30B6\u30FCID\u30921000\u4EF6\u4EE5\u5185\u3067\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044",
205
+ $schema: "https://inlang.com/schema/inlang-message-format",
206
+ iam_securityStateFailed: "\u8A8D\u8A3C\u30BB\u30AD\u30E5\u30EA\u30C6\u30A3\u72B6\u614B\u3092\u66F4\u65B0\u3067\u304D\u307E\u305B\u3093\u3002\u3057\u3070\u3089\u304F\u3057\u3066\u304B\u3089\u518D\u8A66\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
207
+ iam_securityBusy: "\u8A8D\u8A3C\u30EA\u30AF\u30A8\u30B9\u30C8\u304C\u591A\u3059\u304E\u307E\u3059\u3002\u3057\u3070\u3089\u304F\u3057\u3066\u304B\u3089\u518D\u8A66\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
208
+ iam_passwordResetDisabled: "\u30D1\u30B9\u30EF\u30FC\u30C9\u306E\u30EA\u30BB\u30C3\u30C8\u304C\u7121\u52B9\u304B\u3001\u914D\u4FE1\u304C\u8A2D\u5B9A\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002",
209
+ iam_passwordResetDeliveryFailed: "\u30EA\u30BB\u30C3\u30C8\u901A\u77E5\u3092\u914D\u4FE1\u3067\u304D\u307E\u305B\u3093\u3002\u3057\u3070\u3089\u304F\u3057\u3066\u304B\u3089\u518D\u8A66\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
210
+ iam_passwordMinLength: "\u30D1\u30B9\u30EF\u30FC\u30C9\u306F{minLength}\u6587\u5B57\u4EE5\u4E0A\u3067\u5165\u529B\u3057\u3066\u304F\u3060\u3055\u3044",
211
+ iam_passwordMaxLength: "\u30D1\u30B9\u30EF\u30FC\u30C9\u306F{maxLength}\u6587\u5B57\u4EE5\u5185\u3067\u5165\u529B\u3057\u3066\u304F\u3060\u3055\u3044",
212
+ iam_passwordNeedUppercase: "\u30D1\u30B9\u30EF\u30FC\u30C9\u306B\u306F\u82F1\u5927\u6587\u5B57\u3092\u542B\u3081\u3066\u304F\u3060\u3055\u3044",
213
+ iam_passwordNeedLowercase: "\u30D1\u30B9\u30EF\u30FC\u30C9\u306B\u306F\u82F1\u5C0F\u6587\u5B57\u3092\u542B\u3081\u3066\u304F\u3060\u3055\u3044",
214
+ iam_passwordNeedNumber: "\u30D1\u30B9\u30EF\u30FC\u30C9\u306B\u306F\u6570\u5B57\u3092\u542B\u3081\u3066\u304F\u3060\u3055\u3044",
215
+ iam_passwordNeedSpecialChar: "\u30D1\u30B9\u30EF\u30FC\u30C9\u306B\u306F\u7279\u6B8A\u6587\u5B57\u3092\u542B\u3081\u3066\u304F\u3060\u3055\u3044",
216
+ iam_credentialTypeMismatch: "\u8A8D\u8A3C\u60C5\u5831\u306E\u7A2E\u985E\u304C\u4E00\u81F4\u3057\u307E\u305B\u3093",
217
+ iam_userNotExist: "\u30E6\u30FC\u30B6\u30FC\u304C\u5B58\u5728\u3057\u307E\u305B\u3093",
218
+ iam_accountDisabled: "\u30A2\u30AB\u30A6\u30F3\u30C8\u304C\u7121\u52B9\u3067\u3059",
219
+ iam_accountLocked: "\u30A2\u30AB\u30A6\u30F3\u30C8\u304C\u30ED\u30C3\u30AF\u3055\u308C\u3066\u3044\u307E\u3059\u3002\u3057\u3070\u3089\u304F\u3057\u3066\u304B\u3089\u518D\u8A66\u884C\u3057\u3066\u304F\u3060\u3055\u3044",
220
+ iam_accountNoPassword: "\u30D1\u30B9\u30EF\u30FC\u30C9\u304C\u8A2D\u5B9A\u3055\u308C\u3066\u3044\u307E\u305B\u3093",
221
+ iam_passwordWrong: "\u30D1\u30B9\u30EF\u30FC\u30C9\u304C\u6B63\u3057\u304F\u3042\u308A\u307E\u305B\u3093",
222
+ iam_passwordExpired: "\u30D1\u30B9\u30EF\u30FC\u30C9\u306E\u6709\u52B9\u671F\u9650\u304C\u5207\u308C\u3066\u3044\u307E\u3059\u3002\u5909\u66F4\u3057\u3066\u304F\u3060\u3055\u3044",
223
+ iam_otpNotExistOrExpired: "\u8A8D\u8A3C\u30B3\u30FC\u30C9\u304C\u5B58\u5728\u3057\u306A\u3044\u304B\u3001\u6709\u52B9\u671F\u9650\u304C\u5207\u308C\u3066\u3044\u307E\u3059",
224
+ iam_otpInvalid: "\u8A8D\u8A3C\u30B3\u30FC\u30C9\u304C\u7121\u52B9\u3067\u3059\u3002\u65B0\u3057\u3044\u30B3\u30FC\u30C9\u3092\u53D6\u5F97\u3057\u3066\u304F\u3060\u3055\u3044",
225
+ iam_otpWrong: "\u8A8D\u8A3C\u30B3\u30FC\u30C9\u304C\u6B63\u3057\u304F\u3042\u308A\u307E\u305B\u3093",
226
+ iam_otpResendTooFast: "\u6B21\u306E\u30B3\u30FC\u30C9\u3092\u53D6\u5F97\u3059\u308B\u307E\u3067{seconds}\u79D2\u304A\u5F85\u3061\u304F\u3060\u3055\u3044",
227
+ iam_identifierTypeNotSupported: "\u672A\u5BFE\u5FDC\u306E\u8B58\u5225\u5B50\u306E\u7A2E\u985E\u304B\u3001\u914D\u4FE1\u65B9\u6CD5\u304C\u8A2D\u5B9A\u3055\u308C\u3066\u3044\u307E\u305B\u3093",
228
+ iam_loginDisabled: "{type}\u306B\u3088\u308B\u30ED\u30B0\u30A4\u30F3\u306F\u7121\u52B9\u3067\u3059",
229
+ iam_registerDisabled: "\u30E6\u30FC\u30B6\u30FC\u767B\u9332\u306F\u7121\u52B9\u3067\u3059",
230
+ iam_otpStrategyRequired: "OTP\u8A8D\u8A3C\u306B\u306FOTP\u6226\u7565\u306E\u8A2D\u5B9A\u304C\u5FC5\u8981\u3067\u3059",
231
+ iam_otpStrategyRequiredForSend: "OTP\u306B\u306FOTP\u6226\u7565\u306E\u8A2D\u5B9A\u304C\u5FC5\u8981\u3067\u3059",
232
+ iam_userNotExistNoAutoRegister: "\u30E6\u30FC\u30B6\u30FC\u304C\u5B58\u5728\u305B\u305A\u3001\u81EA\u52D5\u767B\u9332\u3082\u6709\u52B9\u3067\u306F\u3042\u308A\u307E\u305B\u3093",
233
+ iam_refreshTokenFailed: "\u30C8\u30FC\u30AF\u30F3\u306E\u66F4\u65B0\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
234
+ iam_refreshTokenExpired: "\u66F4\u65B0\u30C8\u30FC\u30AF\u30F3\u306E\u6709\u52B9\u671F\u9650\u304C\u5207\u308C\u3066\u3044\u307E\u3059",
235
+ iam_refreshTokenRequestFailed: "\u30C8\u30FC\u30AF\u30F3\u66F4\u65B0\u30EA\u30AF\u30A8\u30B9\u30C8\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
236
+ iam_ldapConnectionFailed: "LDAP\u30B5\u30FC\u30D0\u30FC\u306B\u63A5\u7D9A\u3067\u304D\u307E\u305B\u3093",
237
+ iam_ldapAdminBindFailed: "LDAP\u7BA1\u7406\u8005\u306E\u30D0\u30A4\u30F3\u30C9\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
238
+ iam_ldapSearchFailed: "LDAP\u691C\u7D22\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
239
+ iam_ldapStrategyRequired: "LDAP\u8A8D\u8A3C\u306B\u306FLDAP\u6226\u7565\u306E\u8A2D\u5B9A\u304C\u5FC5\u8981\u3067\u3059",
240
+ iam_tokenExpired: "\u30C8\u30FC\u30AF\u30F3\u306E\u6709\u52B9\u671F\u9650\u304C\u5207\u308C\u3066\u3044\u307E\u3059",
241
+ iam_tokenInvalid: "\u30C8\u30FC\u30AF\u30F3\u304C\u7121\u52B9\u3067\u3059",
242
+ iam_ticketInvalid: "\u30C1\u30B1\u30C3\u30C8\u304C\u7121\u52B9\u3001\u4F7F\u7528\u6E08\u307F\u3001\u307E\u305F\u306F\u4E0D\u660E\u3067\u3059",
243
+ iam_ticketExpired: "\u30C1\u30B1\u30C3\u30C8\u306E\u6709\u52B9\u671F\u9650\u304C\u5207\u308C\u3066\u3044\u307E\u3059",
244
+ iam_ticketIssueFailed: "\u30C1\u30B1\u30C3\u30C8\u306E\u767A\u884C\u307E\u305F\u306F\u8AAD\u307F\u53D6\u308A\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
245
+ iam_createSessionFailed: "\u30BB\u30C3\u30B7\u30E7\u30F3\u306E\u4F5C\u6210\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
246
+ iam_sessionExpired: "\u30BB\u30C3\u30B7\u30E7\u30F3\u306E\u6709\u52B9\u671F\u9650\u304C\u5207\u308C\u3066\u3044\u307E\u3059",
247
+ iam_invalidRefreshToken: "\u66F4\u65B0\u30C8\u30FC\u30AF\u30F3\u304C\u7121\u52B9\u3067\u3059",
248
+ iam_sessionNotExist: "\u30BB\u30C3\u30B7\u30E7\u30F3\u304C\u5B58\u5728\u3057\u307E\u305B\u3093",
249
+ iam_usernameAlreadyExist: "\u30E6\u30FC\u30B6\u30FC\u540D\u306F\u65E2\u306B\u5B58\u5728\u3057\u307E\u3059",
250
+ iam_emailAlreadyUsed: "\u30E1\u30FC\u30EB\u30A2\u30C9\u30EC\u30B9\u306F\u65E2\u306B\u4F7F\u7528\u3055\u308C\u3066\u3044\u307E\u3059",
251
+ iam_originalPasswordWrong: "\u73FE\u5728\u306E\u30D1\u30B9\u30EF\u30FC\u30C9\u304C\u6B63\u3057\u304F\u3042\u308A\u307E\u305B\u3093",
252
+ iam_featureNotImplemented: "\u3053\u306E\u6A5F\u80FD\u306F\u5B9F\u88C5\u3055\u308C\u3066\u3044\u307E\u305B\u3093",
253
+ iam_userAlreadyExist: "\u30E6\u30FC\u30B6\u30FC\u306F\u65E2\u306B\u5B58\u5728\u3057\u307E\u3059",
254
+ iam_parseSessionDataFailed: "\u30BB\u30C3\u30B7\u30E7\u30F3\u30C7\u30FC\u30BF\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
255
+ iam_roleAlreadyExist: "\u30ED\u30FC\u30EB\u306F\u65E2\u306B\u5B58\u5728\u3057\u307E\u3059",
256
+ iam_roleNotExist: "\u30ED\u30FC\u30EB\u304C\u5B58\u5728\u3057\u307E\u305B\u3093",
257
+ iam_cannotDeleteSystemRole: "\u30B7\u30B9\u30C6\u30E0\u30ED\u30FC\u30EB\u306F\u524A\u9664\u3067\u304D\u307E\u305B\u3093",
258
+ iam_permissionAlreadyExist: "\u6A29\u9650\u306F\u65E2\u306B\u5B58\u5728\u3057\u307E\u3059",
259
+ iam_permissionNotExist: "\u6A29\u9650\u304C\u5B58\u5728\u3057\u307E\u305B\u3093",
260
+ iam_notInitialized: "\u521D\u671F\u5316\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002\u5148\u306Biam.init()\u3092\u547C\u3073\u51FA\u3057\u3066\u304F\u3060\u3055\u3044",
261
+ iam_initFailed: "\u521D\u671F\u5316\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
262
+ iam_depsNotInitialized: "\u4F9D\u5B58\u30E2\u30B8\u30E5\u30FC\u30EB{dep}\u304C\u521D\u671F\u5316\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002iam.init()\u306E\u524D\u306B{dep}.init()\u3092\u547C\u3073\u51FA\u3057\u3066\u304F\u3060\u3055\u3044",
263
+ iam_initInProgress: "IAM\u306E\u521D\u671F\u5316\u306F\u65E2\u306B\u5B9F\u884C\u4E2D\u3067\u3059",
264
+ iam_cacheRequired: "\u30BB\u30C3\u30B7\u30E7\u30F3\u7BA1\u7406\u306B\u306F\u30AD\u30E3\u30C3\u30B7\u30E5\u30B5\u30FC\u30D3\u30B9\u304C\u5FC5\u8981\u3067\u3059",
265
+ iam_initComponentFailed: "\u30B3\u30F3\u30DD\u30FC\u30CD\u30F3\u30C8\u306E\u521D\u671F\u5316\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
266
+ iam_initSeedDataFailed: "\u521D\u671F\u30C7\u30FC\u30BF\u306E\u8A2D\u5B9A\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
267
+ iam_createUserTableFailed: "\u30E6\u30FC\u30B6\u30FC\u30C6\u30FC\u30D6\u30EB\u306E\u4F5C\u6210\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {message}",
268
+ iam_queryUserFailed: "\u30E6\u30FC\u30B6\u30FC\u306E\u53D6\u5F97\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {message}",
269
+ iam_createUserFailed: "\u30E6\u30FC\u30B6\u30FC\u306E\u4F5C\u6210\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {message}",
270
+ iam_queryUserListFailed: "\u30E6\u30FC\u30B6\u30FC\u4E00\u89A7\u306E\u53D6\u5F97\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {message}",
271
+ iam_updateUserFailed: "\u30E6\u30FC\u30B6\u30FC\u306E\u66F4\u65B0\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {message}",
272
+ iam_deleteUserFailed: "\u30E6\u30FC\u30B6\u30FC\u306E\u524A\u9664\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {message}",
273
+ iam_createSessionTableFailed: "\u30BB\u30C3\u30B7\u30E7\u30F3\u30C6\u30FC\u30D6\u30EB\u306E\u4F5C\u6210\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {message}",
274
+ iam_createSessionIndexFailed: "\u30BB\u30C3\u30B7\u30E7\u30F3\u30A4\u30F3\u30C7\u30C3\u30AF\u30B9\u306E\u4F5C\u6210\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {message}",
275
+ iam_createSessionRecordFailed: "\u30BB\u30C3\u30B7\u30E7\u30F3\u30EC\u30B3\u30FC\u30C9\u306E\u4F5C\u6210\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {message}",
276
+ iam_querySessionFailed: "\u30BB\u30C3\u30B7\u30E7\u30F3\u306E\u53D6\u5F97\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {message}",
277
+ iam_updateSessionFailed: "\u30BB\u30C3\u30B7\u30E7\u30F3\u306E\u66F4\u65B0\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {message}",
278
+ iam_deleteSessionFailed: "\u30BB\u30C3\u30B7\u30E7\u30F3\u306E\u524A\u9664\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {message}",
279
+ iam_cleanupSessionFailed: "\u30BB\u30C3\u30B7\u30E7\u30F3\u306E\u30AF\u30EA\u30FC\u30F3\u30A2\u30C3\u30D7\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {message}",
280
+ iam_createSessionStoreTableFailed: "\u30BB\u30C3\u30B7\u30E7\u30F3\u30B9\u30C8\u30A2\u30C6\u30FC\u30D6\u30EB\u306E\u4F5C\u6210\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {message}",
281
+ iam_createTokenMappingTableFailed: "\u30C8\u30FC\u30AF\u30F3\u5BFE\u5FDC\u30C6\u30FC\u30D6\u30EB\u306E\u4F5C\u6210\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {message}",
282
+ iam_createUserSessionTableFailed: "\u30E6\u30FC\u30B6\u30FC\u30BB\u30C3\u30B7\u30E7\u30F3\u30C6\u30FC\u30D6\u30EB\u306E\u4F5C\u6210\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {message}",
283
+ iam_createSessionStoreIndexFailed: "\u30BB\u30C3\u30B7\u30E7\u30F3\u30A4\u30F3\u30C7\u30C3\u30AF\u30B9\u306E\u4F5C\u6210\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {message}",
284
+ iam_saveSessionFailed: "\u30BB\u30C3\u30B7\u30E7\u30F3\u306E\u4FDD\u5B58\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {message}",
285
+ iam_queryTokenMappingFailed: "\u30C8\u30FC\u30AF\u30F3\u5BFE\u5FDC\u60C5\u5831\u306E\u53D6\u5F97\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {message}",
286
+ iam_saveTokenMappingFailed: "\u30C8\u30FC\u30AF\u30F3\u5BFE\u5FDC\u60C5\u5831\u306E\u4FDD\u5B58\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {message}",
287
+ iam_deleteTokenMappingFailed: "\u30C8\u30FC\u30AF\u30F3\u5BFE\u5FDC\u60C5\u5831\u306E\u524A\u9664\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {message}",
288
+ iam_deleteUserSessionMappingFailed: "\u30E6\u30FC\u30B6\u30FC\u30BB\u30C3\u30B7\u30E7\u30F3\u5BFE\u5FDC\u60C5\u5831\u306E\u524A\u9664\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {message}",
289
+ iam_queryUserSessionFailed: "\u30E6\u30FC\u30B6\u30FC\u30BB\u30C3\u30B7\u30E7\u30F3\u306E\u53D6\u5F97\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {message}",
290
+ iam_addUserSessionMappingFailed: "\u30E6\u30FC\u30B6\u30FC\u30BB\u30C3\u30B7\u30E7\u30F3\u5BFE\u5FDC\u60C5\u5831\u306E\u8FFD\u52A0\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {message}",
291
+ iam_createRoleTableFailed: "\u30ED\u30FC\u30EB\u30C6\u30FC\u30D6\u30EB\u306E\u4F5C\u6210\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {message}",
292
+ iam_queryRoleFailed: "\u30ED\u30FC\u30EB\u306E\u53D6\u5F97\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {message}",
293
+ iam_queryRoleListFailed: "\u30ED\u30FC\u30EB\u4E00\u89A7\u306E\u53D6\u5F97\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {message}",
294
+ iam_createRoleFailed: "\u30ED\u30FC\u30EB\u306E\u4F5C\u6210\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {message}",
295
+ iam_updateRoleFailed: "\u30ED\u30FC\u30EB\u306E\u66F4\u65B0\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {message}",
296
+ iam_deleteRoleFailed: "\u30ED\u30FC\u30EB\u306E\u524A\u9664\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {message}",
297
+ iam_createRolePermissionTableFailed: "\u30ED\u30FC\u30EB\u6A29\u9650\u30C6\u30FC\u30D6\u30EB\u306E\u4F5C\u6210\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {message}",
298
+ iam_createRolePermissionIndexFailed: "\u30ED\u30FC\u30EB\u6A29\u9650\u30A4\u30F3\u30C7\u30C3\u30AF\u30B9\u306E\u4F5C\u6210\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {message}",
299
+ iam_assignPermissionFailed: "\u6A29\u9650\u306E\u5272\u308A\u5F53\u3066\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {message}",
300
+ iam_removePermissionFailed: "\u6A29\u9650\u306E\u89E3\u9664\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {message}",
301
+ iam_createUserRoleTableFailed: "\u30E6\u30FC\u30B6\u30FC\u30ED\u30FC\u30EB\u30C6\u30FC\u30D6\u30EB\u306E\u4F5C\u6210\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {message}",
302
+ iam_createUserRoleIndexFailed: "\u30E6\u30FC\u30B6\u30FC\u30ED\u30FC\u30EB\u30A4\u30F3\u30C7\u30C3\u30AF\u30B9\u306E\u4F5C\u6210\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {message}",
303
+ iam_assignRoleFailed: "\u30ED\u30FC\u30EB\u306E\u5272\u308A\u5F53\u3066\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {message}",
304
+ iam_removeRoleFailed: "\u30ED\u30FC\u30EB\u306E\u89E3\u9664\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {message}",
305
+ iam_syncRolesFailed: "\u30ED\u30FC\u30EB\u306E\u540C\u671F\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {message}",
306
+ iam_createPermissionTableFailed: "\u6A29\u9650\u30C6\u30FC\u30D6\u30EB\u306E\u4F5C\u6210\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {message}",
307
+ iam_createPermissionFailed: "\u6A29\u9650\u306E\u4F5C\u6210\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {message}",
308
+ iam_queryPermissionFailed: "\u6A29\u9650\u306E\u53D6\u5F97\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {message}",
309
+ iam_queryPermissionListFailed: "\u6A29\u9650\u4E00\u89A7\u306E\u53D6\u5F97\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {message}",
310
+ iam_deletePermissionFailed: "\u6A29\u9650\u306E\u524A\u9664\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {message}",
311
+ iam_queryPermissionCacheFailed: "\u6A29\u9650\u30AD\u30E3\u30C3\u30B7\u30E5\u306E\u53D6\u5F97\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {message}",
312
+ iam_setPermissionCacheFailed: "\u6A29\u9650\u30AD\u30E3\u30C3\u30B7\u30E5\u306E\u4FDD\u5B58\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {message}",
313
+ iam_clearPermissionCacheFailed: "\u6A29\u9650\u30AD\u30E3\u30C3\u30B7\u30E5\u306E\u30AF\u30EA\u30A2\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {message}",
314
+ iam_queryOtpCacheFailed: "OTP\u30AD\u30E3\u30C3\u30B7\u30E5\u306E\u53D6\u5F97\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {message}",
315
+ iam_saveOtpCacheFailed: "OTP\u30AD\u30E3\u30C3\u30B7\u30E5\u306E\u4FDD\u5B58\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {message}",
316
+ iam_updateOtpCacheFailed: "OTP\u30AD\u30E3\u30C3\u30B7\u30E5\u306E\u66F4\u65B0\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {message}",
317
+ iam_deleteOtpCacheFailed: "OTP\u30AD\u30E3\u30C3\u30B7\u30E5\u306E\u524A\u9664\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {message}",
318
+ iam_querySessionMappingCacheFailed: "\u30BB\u30C3\u30B7\u30E7\u30F3\u5BFE\u5FDC\u30AD\u30E3\u30C3\u30B7\u30E5\u306E\u53D6\u5F97\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {message}",
319
+ iam_saveSessionMappingCacheFailed: "\u30BB\u30C3\u30B7\u30E7\u30F3\u5BFE\u5FDC\u30AD\u30E3\u30C3\u30B7\u30E5\u306E\u4FDD\u5B58\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {message}",
320
+ iam_deleteSessionMappingCacheFailed: "\u30BB\u30C3\u30B7\u30E7\u30F3\u5BFE\u5FDC\u30AD\u30E3\u30C3\u30B7\u30E5\u306E\u524A\u9664\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {message}",
321
+ iam_queryTokenMappingCacheFailed: "\u30C8\u30FC\u30AF\u30F3\u5BFE\u5FDC\u30AD\u30E3\u30C3\u30B7\u30E5\u306E\u53D6\u5F97\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {message}",
322
+ iam_saveTokenMappingCacheFailed: "\u30C8\u30FC\u30AF\u30F3\u5BFE\u5FDC\u30AD\u30E3\u30C3\u30B7\u30E5\u306E\u4FDD\u5B58\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {message}",
323
+ iam_deleteTokenMappingCacheFailed: "\u30C8\u30FC\u30AF\u30F3\u5BFE\u5FDC\u30AD\u30E3\u30C3\u30B7\u30E5\u306E\u524A\u9664\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {message}",
324
+ iam_queryUserSessionCacheFailed: "\u30E6\u30FC\u30B6\u30FC\u30BB\u30C3\u30B7\u30E7\u30F3\u30AD\u30E3\u30C3\u30B7\u30E5\u306E\u53D6\u5F97\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {message}",
325
+ iam_saveUserSessionCacheFailed: "\u30E6\u30FC\u30B6\u30FC\u30BB\u30C3\u30B7\u30E7\u30F3\u30AD\u30E3\u30C3\u30B7\u30E5\u306E\u4FDD\u5B58\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {message}",
326
+ iam_deleteUserSessionCacheFailed: "\u30E6\u30FC\u30B6\u30FC\u30BB\u30C3\u30B7\u30E7\u30F3\u30AD\u30E3\u30C3\u30B7\u30E5\u306E\u524A\u9664\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {message}",
327
+ iam_createOtpTableFailed: "OTP\u30C6\u30FC\u30D6\u30EB\u306E\u4F5C\u6210\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {message}",
328
+ iam_saveOtpFailed: "OTP\u306E\u4FDD\u5B58\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {message}",
329
+ iam_queryOtpFailed: "OTP\u306E\u53D6\u5F97\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {message}",
330
+ iam_updateOtpAttemptsFailed: "OTP\u8A66\u884C\u56DE\u6570\u306E\u66F4\u65B0\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {message}",
331
+ iam_deleteOtpFailed: "OTP\u306E\u524A\u9664\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {message}",
332
+ iam_otpSendFailed: "\u8A8D\u8A3C\u30B3\u30FC\u30C9\u306E\u9001\u4FE1\u306B\u5931\u6557\u3057\u307E\u3057\u305F\u3002\u3057\u3070\u3089\u304F\u3057\u3066\u304B\u3089\u518D\u8A66\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
333
+ iam_resetTokenInvalid: "\u30D1\u30B9\u30EF\u30FC\u30C9\u30EA\u30BB\u30C3\u30C8\u7528\u30C8\u30FC\u30AF\u30F3\u304C\u7121\u52B9\u304B\u3001\u6709\u52B9\u671F\u9650\u304C\u5207\u308C\u3066\u3044\u307E\u3059",
334
+ iam_resetTokenMaxAttempts: "\u8A8D\u8A3C\u306E\u8A66\u884C\u56DE\u6570\u304C\u591A\u3059\u304E\u307E\u3059\u3002\u65B0\u3057\u3044\u30EA\u30BB\u30C3\u30C8\u30C8\u30FC\u30AF\u30F3\u3092\u53D6\u5F97\u3057\u3066\u304F\u3060\u3055\u3044",
335
+ iam_saveResetTokenFailed: "\u30D1\u30B9\u30EF\u30FC\u30C9\u30EA\u30BB\u30C3\u30C8\u7528\u30C8\u30FC\u30AF\u30F3\u306E\u4FDD\u5B58\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {message}",
336
+ iam_hashResetTokenFailed: "\u30EA\u30BB\u30C3\u30C8\u30C8\u30FC\u30AF\u30F3\u306E\u30CF\u30C3\u30B7\u30E5\u5316\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {message}",
337
+ iam_queryResetTokenFailed: "\u30D1\u30B9\u30EF\u30FC\u30C9\u30EA\u30BB\u30C3\u30C8\u7528\u30C8\u30FC\u30AF\u30F3\u306E\u53D6\u5F97\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {message}",
338
+ iam_passwordResetNotConfigured: "\u30D1\u30B9\u30EF\u30FC\u30C9\u306E\u30EA\u30BB\u30C3\u30C8\u304C\u8A2D\u5B9A\u3055\u308C\u3066\u3044\u307E\u305B\u3093",
339
+ iam_clientFetchNotAvailable: "\u73FE\u5728\u306E\u74B0\u5883\u3067\u306FFetch\u3092\u5229\u7528\u3067\u304D\u307E\u305B\u3093",
340
+ iam_clientRequestFailed: "\u30EA\u30AF\u30A8\u30B9\u30C8\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {status}",
341
+ iam_clientNetworkError: "\u30CD\u30C3\u30C8\u30EF\u30FC\u30AF\u30EA\u30AF\u30A8\u30B9\u30C8\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
342
+ iam_clientNetworkErrorWithDetail: "\u30CD\u30C3\u30C8\u30EF\u30FC\u30AF\u30EA\u30AF\u30A8\u30B9\u30C8\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {message}",
343
+ iam_seedRoleAdminName: "\u7BA1\u7406\u8005",
344
+ iam_seedRoleAdminDesc: "\u3059\u3079\u3066\u306E\u6A29\u9650\u3092\u6301\u3064\u30B7\u30B9\u30C6\u30E0\u7BA1\u7406\u8005",
345
+ iam_seedRoleUserName: "\u30E6\u30FC\u30B6\u30FC",
346
+ iam_seedRoleUserDesc: "\u4E00\u822C\u30E6\u30FC\u30B6\u30FC",
347
+ iam_seedRoleGuestName: "\u30B2\u30B9\u30C8",
348
+ iam_seedRoleGuestDesc: "\u8AAD\u307F\u53D6\u308A\u5C02\u7528\u306E\u30B2\u30B9\u30C8",
349
+ iam_seedPermUserRead: "\u30E6\u30FC\u30B6\u30FC\u306E\u95B2\u89A7",
350
+ iam_seedPermUserCreate: "\u30E6\u30FC\u30B6\u30FC\u306E\u4F5C\u6210",
351
+ iam_seedPermUserUpdate: "\u30E6\u30FC\u30B6\u30FC\u306E\u66F4\u65B0",
352
+ iam_seedPermUserDelete: "\u30E6\u30FC\u30B6\u30FC\u306E\u524A\u9664",
353
+ iam_seedPermUserList: "\u30E6\u30FC\u30B6\u30FC\u4E00\u89A7API",
354
+ iam_seedPermUserApiCreate: "\u30E6\u30FC\u30B6\u30FC\u4F5C\u6210API",
355
+ iam_seedPermUserApiUpdate: "\u30E6\u30FC\u30B6\u30FC\u66F4\u65B0API",
356
+ iam_seedPermUserApiDelete: "\u30E6\u30FC\u30B6\u30FC\u524A\u9664API",
357
+ iam_seedPermRoleRead: "\u30ED\u30FC\u30EB\u306E\u95B2\u89A7",
358
+ iam_seedPermRoleCreate: "\u30ED\u30FC\u30EB\u306E\u4F5C\u6210",
359
+ iam_seedPermRoleUpdate: "\u30ED\u30FC\u30EB\u306E\u66F4\u65B0",
360
+ iam_seedPermRoleDelete: "\u30ED\u30FC\u30EB\u306E\u524A\u9664",
361
+ iam_seedPermRoleList: "\u30ED\u30FC\u30EB\u4E00\u89A7API",
362
+ iam_seedPermRoleApiCreate: "\u30ED\u30FC\u30EB\u4F5C\u6210API",
363
+ iam_seedPermRoleApiUpdate: "\u30ED\u30FC\u30EB\u66F4\u65B0API",
364
+ iam_seedPermRoleApiDelete: "\u30ED\u30FC\u30EB\u524A\u9664API",
365
+ iam_seedPermPermRead: "\u6A29\u9650\u306E\u95B2\u89A7",
366
+ iam_seedPermPermManage: "\u6A29\u9650\u306E\u7BA1\u7406",
367
+ iam_seedPermPermCreate: "\u6A29\u9650\u306E\u4F5C\u6210",
368
+ iam_seedPermPermDelete: "\u6A29\u9650\u306E\u524A\u9664",
369
+ iam_seedPermPermList: "\u6A29\u9650\u4E00\u89A7API",
370
+ iam_seedPermPermApiCreate: "\u6A29\u9650\u4F5C\u6210API",
371
+ iam_seedPermPermApiDelete: "\u6A29\u9650\u524A\u9664API",
372
+ iam_seedPermSystemSettings: "\u30B7\u30B9\u30C6\u30E0\u8A2D\u5B9A",
373
+ iam_seedPermSystemLogs: "\u30ED\u30B0\u306E\u95B2\u89A7",
374
+ iam_seedPermSystemModules: "\u30E2\u30B8\u30E5\u30FC\u30EB\u7BA1\u7406",
375
+ iam_seedPermDashboardView: "\u30C0\u30C3\u30B7\u30E5\u30DC\u30FC\u30C9",
376
+ iam_seedPermProfileRead: "\u30D7\u30ED\u30D5\u30A3\u30FC\u30EB",
377
+ iam_seedPermAuditRead: "\u76E3\u67FB\u30ED\u30B0",
378
+ iam_configError: "IAM\u8A2D\u5B9A\u306E\u691C\u8A3C\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {error}",
379
+ iam_apikeyInvalid: "API\u30AD\u30FC\u304C\u7121\u52B9\u3067\u3059",
380
+ iam_apikeyExpired: "API\u30AD\u30FC\u306E\u6709\u52B9\u671F\u9650\u304C\u5207\u308C\u3066\u3044\u307E\u3059",
381
+ iam_apikeyDisabled: "API\u30AD\u30FC\u304C\u7121\u52B9\u5316\u3055\u308C\u3066\u3044\u307E\u3059",
382
+ iam_apikeyMaxKeysReached: "API\u30AD\u30FC\u306E\u4E0A\u9650\uFF08{max}\u4EF6\uFF09\u306B\u9054\u3057\u307E\u3057\u305F",
383
+ iam_apikeyCreateFailed: "API\u30AD\u30FC\u306E\u4F5C\u6210\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {message}",
384
+ iam_apikeyUpdateFailed: "API\u30AD\u30FC\u306E\u66F4\u65B0\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {message}",
385
+ iam_apikeyDeleteFailed: "API\u30AD\u30FC\u306E\u524A\u9664\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {message}",
386
+ iam_apikeyQueryFailed: "API\u30AD\u30FC\u306E\u53D6\u5F97\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {message}",
387
+ iam_apikeyStrategyRequired: "API\u30AD\u30FC\u8A8D\u8A3C\u306B\u306FAPI\u30AD\u30FC\u6226\u7565\u306E\u8A2D\u5B9A\u304C\u5FC5\u8981\u3067\u3059",
388
+ iam_sessionStateFailed: "\u5171\u6709\u30BB\u30C3\u30B7\u30E7\u30F3\u72B6\u614B\u306E\u64CD\u4F5C\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
389
+ iam_sessionStateConflict: "\u30BB\u30C3\u30B7\u30E7\u30F3\u72B6\u614B\u304C\u5909\u66F4\u3055\u308C\u307E\u3057\u305F\u3002\u64CD\u4F5C\u3092\u518D\u8A66\u884C\u3057\u3066\u304F\u3060\u3055\u3044",
390
+ iam_sessionMaxAgeInvalid: "\u30BB\u30C3\u30B7\u30E7\u30F3\u306E\u6709\u52B9\u671F\u9593\u307E\u305F\u306F\u30BF\u30A4\u30E0\u30B9\u30BF\u30F3\u30D7\u304C\u7121\u52B9\u3067\u3059",
391
+ iam_notificationDeliveryUnknown: "\u901A\u77E5\u306E\u914D\u4FE1\u7D50\u679C\u306F\u4E0D\u660E\u3067\u3001\u6210\u529F\u3057\u305F\u53EF\u80FD\u6027\u304C\u3042\u308A\u307E\u3059\u3002\u81EA\u52D5\u3067\u518D\u9001\u3057\u306A\u3044\u3067\u304F\u3060\u3055\u3044\u3002"
380
392
  };
381
393
 
382
394
  // messages/zh-CN.json
@@ -564,66 +576,19 @@ var zh_CN_default = {
564
576
  iam_apikeyUpdateFailed: "\u66F4\u65B0 API Key \u5931\u8D25\uFF1A{message}",
565
577
  iam_apikeyDeleteFailed: "\u5220\u9664 API Key \u5931\u8D25\uFF1A{message}",
566
578
  iam_apikeyQueryFailed: "\u67E5\u8BE2 API Key \u5931\u8D25\uFF1A{message}",
567
- iam_apikeyStrategyRequired: "API Key \u8BA4\u8BC1\u9700\u8981 API Key \u7B56\u7565\u914D\u7F6E"
579
+ iam_apikeyStrategyRequired: "API Key \u8BA4\u8BC1\u9700\u8981 API Key \u7B56\u7565\u914D\u7F6E",
580
+ iam_sessionStateFailed: "\u4F1A\u8BDD\u5171\u4EAB\u72B6\u6001\u64CD\u4F5C\u5931\u8D25",
581
+ iam_sessionStateConflict: "\u4F1A\u8BDD\u72B6\u6001\u5DF2\u53D8\u5316\uFF0C\u8BF7\u91CD\u8BD5",
582
+ iam_sessionMaxAgeInvalid: "\u4F1A\u8BDD\u6709\u6548\u671F\u6216\u65F6\u95F4\u5B57\u6BB5\u65E0\u6548",
583
+ iam_notificationDeliveryUnknown: "\u901A\u77E5\u53EF\u80FD\u5DF2\u9001\u8FBE\uFF0C\u7ED3\u679C\u672A\u77E5\uFF1B\u8BF7\u52FF\u81EA\u52A8\u91CD\u53D1\u3002"
568
584
  };
569
585
 
570
586
  // src/iam-i18n.ts
571
587
  var iamM = core.i18n.createMessageGetter({
572
588
  "zh-CN": zh_CN_default,
573
- "en-US": en_US_default
589
+ "en-US": en_US_default,
590
+ "ja-JP": ja_JP_default
574
591
  });
575
- var IamErrorInfo = {
576
- AUTH_FAILED: "001:401",
577
- INVALID_CREDENTIALS: "002:401",
578
- USER_NOT_FOUND: "003:404",
579
- USER_DISABLED: "004:403",
580
- USER_LOCKED: "005:403",
581
- USER_ALREADY_EXISTS: "006:409",
582
- PASSWORD_EXPIRED: "007:401",
583
- PASSWORD_POLICY_VIOLATION: "008:400",
584
- OTP_INVALID: "009:400",
585
- NOT_INITIALIZED: "010:500",
586
- OTP_RESEND_TOO_FAST: "011:429",
587
- LOGIN_DISABLED: "012:400",
588
- REGISTER_DISABLED: "013:403",
589
- STRATEGY_NOT_SUPPORTED: "014:400",
590
- APIKEY_INVALID: "015:401",
591
- APIKEY_EXPIRED: "016:401",
592
- APIKEY_DISABLED: "017:403",
593
- APIKEY_NOT_FOUND: "018:404",
594
- RESET_TOKEN_INVALID: "019:400",
595
- RESET_TOKEN_EXPIRED: "020:400",
596
- RESET_TOKEN_MAX_ATTEMPTS: "021:429",
597
- OTP_EXPIRED: "022:400",
598
- SECURITY_BUSY: "023:429",
599
- PASSWORD_RESET_DISABLED: "024:403",
600
- SESSION_NOT_FOUND: "101:401",
601
- SESSION_EXPIRED: "102:401",
602
- SESSION_INVALID: "103:401",
603
- SESSION_CREATE_FAILED: "104:500",
604
- TOKEN_EXPIRED: "105:401",
605
- TOKEN_INVALID: "106:401",
606
- TOKEN_REFRESH_FAILED: "107:401",
607
- PERMISSION_DENIED: "201:403",
608
- ROLE_NOT_FOUND: "202:404",
609
- PERMISSION_NOT_FOUND: "203:404",
610
- ROLE_ALREADY_EXISTS: "204:409",
611
- PERMISSION_ALREADY_EXISTS: "205:409",
612
- LDAP_CONNECTION_FAILED: "301:500",
613
- LDAP_BIND_FAILED: "302:401",
614
- LDAP_SEARCH_FAILED: "303:500",
615
- TICKET_INVALID: "110:401",
616
- TICKET_EXPIRED: "111:401",
617
- TICKET_ISSUE_FAILED: "112:500",
618
- REPOSITORY_ERROR: "401:500",
619
- NOT_FOUND: "402:404",
620
- CONFLICT: "403:409",
621
- FORBIDDEN: "501:403",
622
- INVALID_ARGUMENT: "502:400",
623
- CONFIG_ERROR: "901:500",
624
- INTERNAL_ERROR: "999:500"
625
- };
626
- var HaiIamError = core.error.buildHaiErrorsDef("iam", IamErrorInfo);
627
592
 
628
593
  // src/authn/apikey/iam-authn-apikey-repository.ts
629
594
  var TABLE_NAME = "hai_iam_api_keys";
@@ -843,6 +808,11 @@ function buildAgreementDisplay(config, enabled) {
843
808
  }
844
809
  var TABLE_NAME2 = "hai_iam_users";
845
810
  var USER_FIELDS = [
811
+ { fieldName: "credentialVersion", columnName: "credential_version", def: { type: "TEXT", notNull: true, defaultValue: "" }, select: true, create: false, update: false },
812
+ { fieldName: "resetVersion", columnName: "reset_version", def: { type: "TEXT", notNull: true, defaultValue: "" }, select: true, create: false, update: false },
813
+ { fieldName: "resetHash", columnName: "reset_hash", def: { type: "TEXT" }, select: true, create: false, update: false },
814
+ { fieldName: "resetExpiresAt", columnName: "reset_expires_at", def: { type: "BIGINT" }, select: true, create: false, update: false },
815
+ { fieldName: "resetAttempts", columnName: "reset_attempts", def: { type: "INTEGER", notNull: true, defaultValue: 0 }, select: true, create: false, update: false },
846
816
  {
847
817
  fieldName: "id",
848
818
  columnName: "id",
@@ -1002,6 +972,64 @@ var DbUserRepository = class extends BaseReldbCrudRepository {
1002
972
  fields: USER_FIELDS
1003
973
  });
1004
974
  }
975
+ /** 数据库时钟在执行最终 SQL 时核验有效期,暂停进程不能复用旧的 Date.now()。 */
976
+ databaseNow() {
977
+ if (reldb.config?.type === "postgresql")
978
+ return "(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)";
979
+ if (reldb.config?.type === "mysql")
980
+ return "(UNIX_TIMESTAMP(CURRENT_TIMESTAMP(3)) * 1000)";
981
+ return "CAST((julianday('now') - 2440587.5) * 86400000 AS INTEGER)";
982
+ }
983
+ /** 改密推进凭据代际;会话存储清理失败也不能让旧凭据恢复授权。 */
984
+ async setPassword(input) {
985
+ const result = await reldb.sql.execute(
986
+ `UPDATE ${TABLE_NAME2} SET password_hash = ?, password_updated_at = ?, credential_version = ?, reset_version = ?, reset_hash = NULL, reset_expires_at = NULL, reset_attempts = 0 WHERE id = ? AND credential_version = ? AND enabled = ?`,
987
+ [input.passwordHash, reldb.config?.type === "sqlite" ? Date.now() : /* @__PURE__ */ new Date(), crypto.randomUUID(), crypto.randomUUID(), input.userId, input.expectedVersion, reldb.config?.type === "postgresql" ? true : 1]
988
+ );
989
+ return result.success ? ok(result.data.changes === 1) : this.buildQueryError(result.error, result.error);
990
+ }
991
+ /** 发布只替换读取到的重置代际;旧发送者暂停后不能覆盖接管节点的新令牌。 */
992
+ async publishPasswordReset(input) {
993
+ const expiry = input.expiresAt.getTime();
994
+ if (!Number.isSafeInteger(expiry))
995
+ return err(HaiIamError.INVALID_ARGUMENT, iamM("iam_resetTokenInvalid"));
996
+ const result = await reldb.sql.execute(
997
+ `UPDATE ${TABLE_NAME2} SET reset_hash = ?, reset_expires_at = ?, reset_attempts = 0, reset_version = ? WHERE id = ? AND credential_version = ? AND reset_version = ? AND enabled = ? AND ? > ${this.databaseNow()}`,
998
+ [input.tokenHash, expiry, crypto.randomUUID(), input.user.id, input.user.credentialVersion ?? "", input.user.resetVersion ?? "", reldb.config?.type === "postgresql" ? true : 1, expiry]
999
+ );
1000
+ return result.success ? ok(result.data.changes === 1) : this.buildQueryError(result.error, result.error);
1001
+ }
1002
+ /** 条件计次返回新代际;业务策略失败也消耗尝试次数。 */
1003
+ async claimPasswordReset(tokenHash, maxAttempts) {
1004
+ const found = await this.findOneBy("reset_hash = ?", [tokenHash]);
1005
+ if (!found.success)
1006
+ return found;
1007
+ const user = found.data;
1008
+ if (!user)
1009
+ return err(HaiIamError.RESET_TOKEN_INVALID, iamM("iam_resetTokenInvalid"));
1010
+ if (!user.enabled)
1011
+ return err(HaiIamError.USER_DISABLED, iamM("iam_accountDisabled"));
1012
+ if ((user.resetAttempts ?? 0) >= maxAttempts)
1013
+ return err(HaiIamError.RESET_TOKEN_MAX_ATTEMPTS, iamM("iam_resetTokenMaxAttempts"));
1014
+ const version = crypto.randomUUID();
1015
+ const result = await reldb.sql.execute(
1016
+ `UPDATE ${TABLE_NAME2} SET reset_attempts = reset_attempts + 1, reset_version = ? WHERE id = ? AND reset_hash = ? AND reset_version = ? AND credential_version = ? AND reset_attempts < ? AND reset_expires_at > ${this.databaseNow()} AND enabled = ?`,
1017
+ [version, user.id, tokenHash, user.resetVersion ?? "", user.credentialVersion ?? "", maxAttempts, reldb.config?.type === "postgresql" ? true : 1]
1018
+ );
1019
+ if (!result.success)
1020
+ return this.buildQueryError(result.error, result.error);
1021
+ if (result.data.changes !== 1)
1022
+ return err(HaiIamError.RESET_TOKEN_INVALID, iamM("iam_resetTokenInvalid"));
1023
+ return ok({ ...user, resetVersion: version, resetAttempts: (user.resetAttempts ?? 0) + 1 });
1024
+ }
1025
+ /** 密码与令牌消费同一行原子提交,禁止先消费 Redis 凭据再无条件写数据库。 */
1026
+ async commitPasswordReset(input) {
1027
+ const result = await reldb.sql.execute(
1028
+ `UPDATE ${TABLE_NAME2} SET password_hash = ?, password_updated_at = ?, credential_version = ?, reset_version = ?, reset_hash = NULL, reset_expires_at = NULL, reset_attempts = 0, login_failed_count = 0, locked_until = NULL, last_login_failed_at = NULL WHERE id = ? AND reset_hash = ? AND reset_version = ? AND credential_version = ? AND reset_expires_at > ${this.databaseNow()} AND enabled = ?`,
1029
+ [input.passwordHash, reldb.config?.type === "sqlite" ? Date.now() : /* @__PURE__ */ new Date(), crypto.randomUUID(), crypto.randomUUID(), input.user.id, input.tokenHash, input.user.resetVersion ?? "", input.user.credentialVersion ?? "", reldb.config?.type === "postgresql" ? true : 1]
1030
+ );
1031
+ return result.success ? ok(result.data.changes === 1) : this.buildQueryError(result.error, result.error);
1032
+ }
1005
1033
  async recordLoginFailure(userId, policy) {
1006
1034
  const sqlite = reldb.config?.type === "sqlite";
1007
1035
  const now = sqlite ? Date.now() : /* @__PURE__ */ new Date();
@@ -1018,10 +1046,10 @@ var DbUserRepository = class extends BaseReldbCrudRepository {
1018
1046
  return this.buildQueryError(result.error, result.error);
1019
1047
  return ok(void 0);
1020
1048
  }
1021
- async resetLoginFailures(userId) {
1049
+ async resetLoginFailures(userId, expectedCredentialVersion) {
1022
1050
  const result = await reldb.sql.execute(
1023
- `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 <= ?)`,
1024
- [userId, reldb.config?.type === "postgresql" ? true : 1, reldb.config?.type === "sqlite" ? Date.now() : /* @__PURE__ */ new Date()]
1051
+ `UPDATE ${TABLE_NAME2} SET login_failed_count = 0, last_login_failed_at = NULL, locked_until = NULL WHERE id = ? AND credential_version = ? AND enabled = ? AND (locked_until IS NULL OR locked_until <= ?)`,
1052
+ [userId, expectedCredentialVersion, reldb.config?.type === "postgresql" ? true : 1, reldb.config?.type === "sqlite" ? Date.now() : /* @__PURE__ */ new Date()]
1025
1053
  );
1026
1054
  if (!result.success)
1027
1055
  return this.buildQueryError(result.error, result.error);
@@ -1031,6 +1059,8 @@ var DbUserRepository = class extends BaseReldbCrudRepository {
1031
1059
  return user;
1032
1060
  if (!user.data?.enabled)
1033
1061
  return err(HaiIamError.USER_DISABLED, iamM("iam_accountDisabled"));
1062
+ if ((user.data.credentialVersion ?? "") !== expectedCredentialVersion)
1063
+ return err(HaiIamError.SESSION_INVALID, iamM("iam_sessionExpired"));
1034
1064
  if (user.data.lockedUntil && user.data.lockedUntil > /* @__PURE__ */ new Date())
1035
1065
  return err(HaiIamError.USER_LOCKED, iamM("iam_accountLocked"));
1036
1066
  }
@@ -1158,7 +1188,7 @@ async function recordLoginFailure(userRepository, user, policy) {
1158
1188
  return userRepository.recordLoginFailure(user.id, policy);
1159
1189
  }
1160
1190
  async function resetLoginFailures(userRepository, user) {
1161
- return userRepository.resetLoginFailures(user.id);
1191
+ return userRepository.resetLoginFailures(user.id, user.credentialVersion ?? "");
1162
1192
  }
1163
1193
 
1164
1194
  // src/authn/apikey/iam-authn-apikey-strategy.ts
@@ -1203,7 +1233,7 @@ function createApiKeyStrategy(config) {
1203
1233
  if (!candidate.enabled) {
1204
1234
  return err(HaiIamError.APIKEY_DISABLED, iamM("iam_apikeyDisabled"));
1205
1235
  }
1206
- if (candidate.expiresAt && /* @__PURE__ */ new Date() > candidate.expiresAt) {
1236
+ if (candidate.expiresAt && /* @__PURE__ */ new Date() >= candidate.expiresAt) {
1207
1237
  return err(HaiIamError.APIKEY_EXPIRED, iamM("iam_apikeyExpired"));
1208
1238
  }
1209
1239
  apiKeyRepository.updateFields(candidate.id, { lastUsedAt: /* @__PURE__ */ new Date() }).catch(() => {
@@ -1238,7 +1268,7 @@ function createApiKeyStrategy(config) {
1238
1268
  return err(HaiIamError.USER_DISABLED, iamM("iam_accountDisabled"));
1239
1269
  }
1240
1270
  logger.info("API Key authentication succeeded", { userId: userResult.data.id, keyId: apiKey.id });
1241
- return ok({ user: toUser(userResult.data), apiKey });
1271
+ return ok({ user: { ...toUser(userResult.data), credentialVersion: userResult.data.credentialVersion ?? "" }, apiKey });
1242
1272
  }
1243
1273
  };
1244
1274
  const apiKeyFunctions = {
@@ -1382,22 +1412,6 @@ function createLdapStrategy(config) {
1382
1412
  }
1383
1413
  return value;
1384
1414
  }
1385
- function buildLdapUser(entry, ldapUsername, ldapEmail, ldapDisplayName) {
1386
- const now = /* @__PURE__ */ new Date();
1387
- return {
1388
- id: entry.dn,
1389
- username: ldapUsername,
1390
- email: ldapEmail,
1391
- displayName: ldapDisplayName,
1392
- enabled: true,
1393
- createdAt: now,
1394
- updatedAt: now,
1395
- metadata: {
1396
- ldapDn: entry.dn,
1397
- authSource: "ldap"
1398
- }
1399
- };
1400
- }
1401
1415
  return {
1402
1416
  type: "ldap",
1403
1417
  name: "ldap-strategy",
@@ -1535,41 +1549,22 @@ function createLdapStrategy(config) {
1535
1549
  }
1536
1550
  }
1537
1551
  }
1538
- if (!storedUser) {
1539
- const ldapUser = buildLdapUser(entry, ldapUsername, ldapEmail, ldapDisplayName);
1540
- return ok(ldapUser);
1541
- }
1552
+ if (!storedUser)
1553
+ return err(HaiIamError.USER_NOT_FOUND, iamM("iam_userNotExist"));
1542
1554
  const resetResult = await resetLoginFailures(userRepository, storedUser);
1543
1555
  if (!resetResult.success)
1544
1556
  return resetResult;
1545
1557
  logger2.info("LDAP authentication succeeded", { userId: storedUser.id });
1546
- return ok(toUser(storedUser));
1558
+ return ok({ ...toUser(storedUser), credentialVersion: storedUser.credentialVersion ?? "" });
1547
1559
  } finally {
1548
1560
  await client.unbind();
1549
1561
  }
1550
1562
  }
1551
1563
  };
1552
1564
  }
1553
- var OTP_KEY_PREFIX = "hai:iam:otp:";
1554
- var OTP_ATTEMPTS_KEY_PREFIX = "hai:iam:otp:attempts:";
1555
1565
  function normalizeOtpIdentifier(identifier) {
1556
1566
  const trimmed = identifier.trim();
1557
- if (trimmed.includes("@"))
1558
- return trimmed.toLowerCase();
1559
- return trimmed.replace(/[\s-]/g, "");
1560
- }
1561
- function buildOtpKey(identifier) {
1562
- return `${OTP_KEY_PREFIX}${normalizeOtpIdentifier(identifier)}`;
1563
- }
1564
- function buildOtpAttemptsKey(identifier) {
1565
- return `${OTP_ATTEMPTS_KEY_PREFIX}${normalizeOtpIdentifier(identifier)}`;
1566
- }
1567
- function restoreOtpDates(record) {
1568
- return {
1569
- ...record,
1570
- expiresAt: record.expiresAt instanceof Date ? record.expiresAt : new Date(record.expiresAt),
1571
- createdAt: record.createdAt instanceof Date ? record.createdAt : new Date(record.createdAt)
1572
- };
1567
+ return trimmed.includes("@") ? trimmed.toLowerCase() : trimmed.replace(/[\s-]/g, "");
1573
1568
  }
1574
1569
  var otpRepoInstance = null;
1575
1570
  function resetOtpRepoSingleton() {
@@ -1578,192 +1573,65 @@ function resetOtpRepoSingleton() {
1578
1573
  function createCacheOtpRepository() {
1579
1574
  if (otpRepoInstance)
1580
1575
  return otpRepoInstance;
1581
- const repo = {
1582
- async saveOtp(identifier, code, expiresIn) {
1576
+ const keyFor = (identifier) => `hai:iam:otp:${normalizeOtpIdentifier(identifier)}`;
1577
+ const failure = () => err(HaiIamError.REPOSITORY_ERROR, iamM("iam_securityStateFailed"));
1578
+ const invalid = () => err(HaiIamError.OTP_INVALID, iamM("iam_otpNotExistOrExpired"));
1579
+ otpRepoInstance = {
1580
+ async issueOtp(input) {
1583
1581
  const now = Date.now();
1584
- const record = {
1585
- identifier: normalizeOtpIdentifier(identifier),
1586
- code,
1587
- expiresAt: new Date(now + expiresIn * 1e3),
1582
+ const state = {
1583
+ version: crypto.randomUUID(),
1584
+ code: input.code,
1588
1585
  attempts: 0,
1589
- createdAt: new Date(now)
1586
+ expiresAt: now + input.expiresIn * 1e3,
1587
+ cooldownUntil: now + input.resendInterval * 1e3
1590
1588
  };
1591
- const result = await cache.kv.set(buildOtpKey(identifier), record, { ex: expiresIn });
1592
- if (!result.success) {
1593
- return err(
1594
- HaiIamError.REPOSITORY_ERROR,
1595
- iamM("iam_saveOtpFailed", { params: { message: result.error.message } }),
1596
- result.error
1597
- );
1598
- }
1599
- const reset = await cache.kv.del(buildOtpAttemptsKey(identifier));
1600
- if (!reset.success)
1601
- return err(HaiIamError.REPOSITORY_ERROR, iamM("iam_securityStateFailed"), reset.error);
1602
- return ok(void 0);
1603
- },
1604
- async fetchOtp(identifier) {
1605
- const result = await cache.kv.get(buildOtpKey(identifier));
1606
- if (!result.success) {
1607
- return err(
1608
- HaiIamError.REPOSITORY_ERROR,
1609
- iamM("iam_queryOtpFailed", { params: { message: result.error.message } }),
1610
- result.error
1611
- );
1612
- }
1613
- if (!result.data) {
1614
- return ok(null);
1615
- }
1616
- const attemptsResult = await cache.kv.get(buildOtpAttemptsKey(identifier));
1617
- if (!attemptsResult.success)
1618
- return err(HaiIamError.REPOSITORY_ERROR, iamM("iam_securityStateFailed"), attemptsResult.error);
1619
- const attempts = attemptsResult.success && typeof attemptsResult.data === "number" ? attemptsResult.data : 0;
1620
- const restored = restoreOtpDates(result.data);
1621
- return ok({ ...restored, attempts });
1622
- },
1623
- async incrementOtpAttempts(identifier) {
1624
- const otpKey = buildOtpKey(identifier);
1625
- const attemptsKey = buildOtpAttemptsKey(identifier);
1626
- const current = await cache.kv.get(otpKey);
1627
- if (!current.success) {
1628
- return err(
1629
- HaiIamError.REPOSITORY_ERROR,
1630
- iamM("iam_queryOtpFailed", { params: { message: current.error.message } }),
1631
- current.error
1632
- );
1633
- }
1634
- if (!current.data) {
1635
- return ok(0);
1636
- }
1637
- const incrResult = await cache.kv.incr(attemptsKey);
1638
- if (!incrResult.success) {
1639
- return err(
1640
- HaiIamError.REPOSITORY_ERROR,
1641
- iamM("iam_updateOtpAttemptsFailed", { params: { message: incrResult.error.message } }),
1642
- incrResult.error
1643
- );
1644
- }
1645
- const ttlResult = await cache.kv.ttl(otpKey);
1646
- if (!ttlResult.success)
1647
- return err(HaiIamError.REPOSITORY_ERROR, iamM("iam_securityStateFailed"), ttlResult.error);
1648
- const ttl = ttlResult.success && ttlResult.data > 0 ? ttlResult.data : 1;
1649
- const expire = await cache.kv.expire(attemptsKey, ttl);
1650
- if (!expire.success)
1651
- return err(HaiIamError.REPOSITORY_ERROR, iamM("iam_securityStateFailed"), expire.error);
1652
- return ok(incrResult.data);
1589
+ const key = keyFor(input.identifier);
1590
+ const current = await cache.kv.get(key);
1591
+ if (!current.success)
1592
+ return failure();
1593
+ if (current.data && current.data.cooldownUntil > Date.now()) {
1594
+ return err(HaiIamError.OTP_RESEND_TOO_FAST, iamM("iam_otpResendTooFast", { params: { seconds: Math.ceil((current.data.cooldownUntil - Date.now()) / 1e3) } }));
1595
+ }
1596
+ if (Date.now() >= state.expiresAt)
1597
+ return invalid();
1598
+ const published = await cache.kv.compareAndSwap(key, current.data ?? void 0, state, { px: Math.max(state.expiresAt, state.cooldownUntil) - Date.now() });
1599
+ if (!published.success)
1600
+ return failure();
1601
+ if (!published.data)
1602
+ return err(HaiIamError.SECURITY_BUSY, iamM("iam_securityBusy"));
1603
+ if (Date.now() >= state.expiresAt)
1604
+ return invalid();
1605
+ return ok({ idempotencyKey: `iam-otp:${state.version}`, expiresAt: new Date(state.expiresAt) });
1653
1606
  },
1654
- async removeOtp(identifier) {
1655
- const result = await cache.kv.del(buildOtpKey(identifier));
1656
- if (!result.success) {
1657
- return err(
1658
- HaiIamError.REPOSITORY_ERROR,
1659
- iamM("iam_deleteOtpFailed", { params: { message: result.error.message } }),
1660
- result.error
1661
- );
1607
+ async consumeOtp(identifier, code, maxAttempts) {
1608
+ const key = keyFor(identifier);
1609
+ let version;
1610
+ for (let attempt = 0; attempt < 16; attempt++) {
1611
+ const current = await cache.kv.get(key);
1612
+ if (!current.success)
1613
+ return failure();
1614
+ const state = current.data;
1615
+ if (!state || state.code === null || state.expiresAt <= Date.now() || state.attempts >= maxAttempts || version && state.version !== version)
1616
+ return invalid();
1617
+ version = state.version;
1618
+ const correct = core.string.constantTimeEqual(state.code, code);
1619
+ const attempts = state.attempts + 1;
1620
+ const next = { ...state, attempts, code: correct || attempts >= maxAttempts ? null : state.code };
1621
+ const committed = await cache.kv.compareAndSwap(key, state, next, { keepTtl: true });
1622
+ if (!committed.success)
1623
+ return failure();
1624
+ if (!committed.data)
1625
+ continue;
1626
+ if (!correct || state.expiresAt <= Date.now())
1627
+ return err(HaiIamError.OTP_INVALID, iamM("iam_otpWrong"));
1628
+ return ok(void 0);
1662
1629
  }
1663
- const removed = await cache.kv.del(buildOtpAttemptsKey(identifier));
1664
- if (!removed.success)
1665
- return err(HaiIamError.REPOSITORY_ERROR, iamM("iam_securityStateFailed"), removed.error);
1666
- return ok(void 0);
1630
+ return err(HaiIamError.SECURITY_BUSY, iamM("iam_securityBusy"));
1667
1631
  }
1668
1632
  };
1669
- otpRepoInstance = repo;
1670
- return repo;
1671
- }
1672
-
1673
- // src/session/iam-session-utils.ts
1674
- var BASE64_PLUS_REGEX = /\+/g;
1675
- var BASE64_SLASH_REGEX = /\//g;
1676
- var BASE64_TRAILING_PADDING_REGEX = /=+$/;
1677
- function generateToken() {
1678
- const bytes = new Uint8Array(32);
1679
- crypto.getRandomValues(bytes);
1680
- const base64 = btoa(String.fromCharCode(...bytes));
1681
- return base64.replace(BASE64_PLUS_REGEX, "-").replace(BASE64_SLASH_REGEX, "_").replace(BASE64_TRAILING_PADDING_REGEX, "");
1682
- }
1683
- function buildSession(options, now, sessionTtl, accessToken) {
1684
- return {
1685
- apiKeyId: options.apiKeyId,
1686
- apiKeyScopes: options.apiKeyScopes,
1687
- userId: options.userId,
1688
- username: options.username,
1689
- displayName: options.displayName,
1690
- avatarUrl: options.avatarUrl,
1691
- roles: options.roles ?? [],
1692
- permissions: options.permissions ?? [],
1693
- source: options.source,
1694
- accessToken,
1695
- createdAt: now,
1696
- lastActiveAt: now,
1697
- expiresAt: new Date(now.getTime() + sessionTtl * 1e3),
1698
- data: options.data
1699
- };
1700
- }
1701
- function getSessionTtl(session, now = Date.now()) {
1702
- return Math.max(0, Math.floor((session.expiresAt.getTime() - now) / 1e3));
1703
- }
1704
- function applySessionPatch(session, patch) {
1705
- const nextSession = { ...session };
1706
- if (patch.data !== void 0) {
1707
- nextSession.data = { ...nextSession.data, ...patch.data };
1708
- }
1709
- if (patch.roles !== void 0) {
1710
- nextSession.roles = patch.roles;
1711
- }
1712
- if (patch.permissions !== void 0) {
1713
- nextSession.permissions = patch.permissions;
1714
- }
1715
- if (patch.username !== void 0) {
1716
- nextSession.username = patch.username;
1717
- }
1718
- if (patch.displayName !== void 0) {
1719
- nextSession.displayName = patch.displayName;
1720
- }
1721
- if (patch.avatarUrl !== void 0) {
1722
- nextSession.avatarUrl = patch.avatarUrl;
1723
- }
1724
- if (patch.source !== void 0) {
1725
- nextSession.source = patch.source;
1726
- }
1727
- nextSession.lastActiveAt = /* @__PURE__ */ new Date();
1728
- return nextSession;
1729
- }
1730
-
1731
- // src/iam-security.ts
1732
- async function withIamLock(key, operation) {
1733
- const owner = generateToken();
1734
- const lockKey = `hai:iam:security:${key}`;
1735
- for (let attempt = 0; attempt < 500; attempt++) {
1736
- const acquired = await cache.lock.acquire(lockKey, { owner, ttl: 30 });
1737
- if (!acquired.success)
1738
- return err(HaiIamError.REPOSITORY_ERROR, iamM("iam_securityStateFailed"), acquired.error);
1739
- if (acquired.data) {
1740
- let leaseValid = true;
1741
- const renewal = setInterval(() => {
1742
- void cache.lock.extend(lockKey, 30, owner).then((result) => {
1743
- if (!result.success || !result.data)
1744
- leaseValid = false;
1745
- }).catch(() => {
1746
- leaseValid = false;
1747
- });
1748
- }, 1e4);
1749
- try {
1750
- const result = await operation();
1751
- if (!leaseValid)
1752
- return err(HaiIamError.REPOSITORY_ERROR, iamM("iam_securityStateFailed"));
1753
- return result;
1754
- } catch (error) {
1755
- return err(HaiIamError.INTERNAL_ERROR, iamM("iam_securityStateFailed"), error);
1756
- } finally {
1757
- clearInterval(renewal);
1758
- await cache.lock.release(lockKey, owner);
1759
- }
1760
- }
1761
- await new Promise((resolve) => setTimeout(resolve, 10));
1762
- }
1763
- return err(HaiIamError.SECURITY_BUSY, iamM("iam_securityBusy"));
1633
+ return otpRepoInstance;
1764
1634
  }
1765
-
1766
- // src/authn/otp/iam-authn-otp-strategy.ts
1767
1635
  var logger3 = core.logger.child({ module: "iam", scope: "otp-strategy" });
1768
1636
  var PHONE_IDENTIFIER_REGEX = /^\+?\d{8,}$/;
1769
1637
  var PHONE_SEPARATOR_REGEX = /[\s-]/g;
@@ -1809,103 +1677,72 @@ function createOtpStrategy(config) {
1809
1677
  const { code } = credentialResult.data;
1810
1678
  if (!isChannelEnabled(identifier))
1811
1679
  return err(HaiIamError.LOGIN_DISABLED, iamM("iam_identifierTypeNotSupported"));
1812
- return withIamLock(`otp:${identifier}`, async () => {
1813
- const userResult = await config.userRepository.findByIdentifier(identifier);
1814
- if (!userResult.success) {
1815
- return userResult;
1816
- }
1817
- let storedUser = userResult.data;
1818
- if (storedUser) {
1819
- if (!storedUser.enabled) {
1820
- return err(HaiIamError.USER_DISABLED, iamM("iam_accountDisabled"));
1821
- }
1822
- if (isAccountLocked(storedUser)) {
1823
- return err(HaiIamError.USER_LOCKED, iamM("iam_accountLocked"));
1824
- }
1680
+ const userResult = await config.userRepository.findByIdentifier(identifier);
1681
+ if (!userResult.success) {
1682
+ return userResult;
1683
+ }
1684
+ let storedUser = userResult.data;
1685
+ if (storedUser) {
1686
+ if (!storedUser.enabled) {
1687
+ return err(HaiIamError.USER_DISABLED, iamM("iam_accountDisabled"));
1825
1688
  }
1826
- const storedOtpResult = await config.otpRepository.fetchOtp(identifier);
1827
- if (!storedOtpResult.success) {
1828
- return storedOtpResult;
1689
+ if (isAccountLocked(storedUser)) {
1690
+ return err(HaiIamError.USER_LOCKED, iamM("iam_accountLocked"));
1829
1691
  }
1830
- const storedOtp = storedOtpResult.data;
1831
- if (!storedOtp) {
1832
- return err(HaiIamError.OTP_INVALID, iamM("iam_otpNotExistOrExpired"));
1692
+ }
1693
+ const consumed = await config.otpRepository.consumeOtp(identifier, code, otpConfig.maxAttempts);
1694
+ if (!consumed.success) {
1695
+ if (storedUser && consumed.error.code === HaiIamError.OTP_INVALID.code) {
1696
+ const failed = await recordLoginFailure(config.userRepository, storedUser, { maxLoginAttempts, lockoutDuration });
1697
+ if (!failed.success)
1698
+ return failed;
1833
1699
  }
1834
- if (storedOtp.attempts >= otpConfig.maxAttempts) {
1835
- const removed2 = await config.otpRepository.removeOtp(identifier);
1836
- if (!removed2.success)
1837
- return removed2;
1838
- if (storedUser) {
1839
- const failureResult = await recordLoginFailure(config.userRepository, storedUser, { maxLoginAttempts, lockoutDuration });
1840
- if (!failureResult.success)
1841
- return failureResult;
1842
- }
1843
- return err(HaiIamError.OTP_INVALID, iamM("iam_otpInvalid"));
1700
+ return consumed;
1701
+ }
1702
+ if (!storedUser && allowAutoRegister) {
1703
+ const type = identifierType(identifier);
1704
+ const createResult = await config.userRepository.create({
1705
+ username: identifier,
1706
+ email: type === "email" ? identifier : void 0,
1707
+ phone: type === "phone" ? identifier : void 0,
1708
+ enabled: defaultEnabled,
1709
+ emailVerified: type === "email",
1710
+ phoneVerified: type === "phone"
1711
+ });
1712
+ if (!createResult.success) {
1713
+ return err(
1714
+ HaiIamError.REPOSITORY_ERROR,
1715
+ iamM("iam_createUserFailed", { params: { message: createResult.error.message } }),
1716
+ createResult.error
1717
+ );
1844
1718
  }
1845
- if (!core.string.constantTimeEqual(storedOtp.code, code)) {
1846
- const attemptResult = await config.otpRepository.incrementOtpAttempts(identifier);
1847
- if (!attemptResult.success)
1848
- return attemptResult;
1849
- if (attemptResult.data >= otpConfig.maxAttempts) {
1850
- const removed2 = await config.otpRepository.removeOtp(identifier);
1851
- if (!removed2.success)
1852
- return removed2;
1853
- }
1854
- if (storedUser) {
1855
- const failureResult = await recordLoginFailure(config.userRepository, storedUser, { maxLoginAttempts, lockoutDuration });
1856
- if (!failureResult.success)
1857
- return failureResult;
1858
- }
1859
- return err(HaiIamError.OTP_INVALID, iamM("iam_otpWrong"));
1719
+ const createdResult = await config.userRepository.findByIdentifier(identifier);
1720
+ if (!createdResult.success) {
1721
+ return createdResult;
1860
1722
  }
1861
- const removed = await config.otpRepository.removeOtp(identifier);
1862
- if (!removed.success)
1863
- return removed;
1864
- if (!storedUser && allowAutoRegister) {
1865
- const type = identifierType(identifier);
1866
- const createResult = await config.userRepository.create({
1867
- username: identifier,
1868
- email: type === "email" ? identifier : void 0,
1869
- phone: type === "phone" ? identifier : void 0,
1870
- enabled: defaultEnabled,
1871
- emailVerified: type === "email",
1872
- phoneVerified: type === "phone"
1873
- });
1874
- if (!createResult.success) {
1875
- return err(
1876
- HaiIamError.REPOSITORY_ERROR,
1877
- iamM("iam_createUserFailed", { params: { message: createResult.error.message } }),
1878
- createResult.error
1879
- );
1880
- }
1881
- const createdResult = await config.userRepository.findByIdentifier(identifier);
1882
- if (!createdResult.success) {
1883
- return createdResult;
1884
- }
1885
- storedUser = createdResult.data;
1886
- if (storedUser && config.onUserAutoRegistered) {
1887
- try {
1888
- const assigned = await config.onUserAutoRegistered(storedUser.id);
1889
- if (!assigned.success) {
1890
- await config.userRepository.deleteById(storedUser.id);
1891
- return assigned;
1892
- }
1893
- } catch (callbackError) {
1894
- logger3.warn("onUserAutoRegistered callback failed", { userId: storedUser.id, error: callbackError });
1723
+ storedUser = createdResult.data;
1724
+ if (storedUser && config.onUserAutoRegistered) {
1725
+ try {
1726
+ const assigned = await config.onUserAutoRegistered(storedUser.id);
1727
+ if (!assigned.success) {
1728
+ await config.userRepository.deleteById(storedUser.id);
1729
+ return assigned;
1895
1730
  }
1731
+ } catch (callbackError) {
1732
+ logger3.warn("onUserAutoRegistered callback failed", { userId: storedUser.id, error: callbackError });
1896
1733
  }
1897
1734
  }
1898
- if (!storedUser) {
1899
- return err(HaiIamError.USER_NOT_FOUND, iamM("iam_userNotExist"));
1900
- }
1901
- if (!storedUser.enabled)
1902
- return err(HaiIamError.USER_DISABLED, iamM("iam_accountDisabled"));
1903
- const resetResult = await resetLoginFailures(config.userRepository, storedUser);
1904
- if (!resetResult.success)
1905
- return resetResult;
1906
- logger3.info("OTP authentication succeeded", { userId: storedUser.id });
1907
- return ok(toUser(storedUser));
1908
- });
1735
+ }
1736
+ if (!storedUser) {
1737
+ return err(HaiIamError.USER_NOT_FOUND, iamM("iam_userNotExist"));
1738
+ }
1739
+ if (!storedUser.enabled)
1740
+ return err(HaiIamError.USER_DISABLED, iamM("iam_accountDisabled"));
1741
+ const resetResult = await resetLoginFailures(config.userRepository, storedUser);
1742
+ if (!resetResult.success)
1743
+ return resetResult;
1744
+ logger3.info("OTP authentication succeeded", { userId: storedUser.id });
1745
+ return ok({ ...toUser(storedUser), credentialVersion: storedUser.credentialVersion ?? "" });
1909
1746
  }
1910
1747
  };
1911
1748
  function isChannelEnabled(identifier) {
@@ -1917,43 +1754,28 @@ function createOtpStrategy(config) {
1917
1754
  if (!isChannelEnabled(identifier))
1918
1755
  return err(HaiIamError.LOGIN_DISABLED, iamM("iam_identifierTypeNotSupported"));
1919
1756
  const code = generateOtpCode(otpConfig.length);
1920
- const expiresAt = new Date(Date.now() + otpConfig.expiresIn * 1e3);
1921
- const saved = await withIamLock(`otp:${identifier}`, async () => {
1922
- const cooldownKey = `hai:iam:otp:cooldown:${identifier}`;
1923
- const cooldown = await cache.kv.get(cooldownKey);
1924
- if (!cooldown.success)
1925
- return err(HaiIamError.REPOSITORY_ERROR, iamM("iam_securityStateFailed"), cooldown.error);
1926
- if (cooldown.data && cooldown.data > Date.now())
1927
- return err(HaiIamError.OTP_RESEND_TOO_FAST, iamM("iam_otpResendTooFast", { params: { seconds: Math.ceil((cooldown.data - Date.now()) / 1e3) } }));
1928
- const storeResult = await config.otpRepository.saveOtp(identifier, code, otpConfig.expiresIn);
1929
- if (!storeResult.success)
1930
- return storeResult;
1931
- const limited = await cache.kv.set(cooldownKey, Date.now() + otpConfig.resendInterval * 1e3, { ex: otpConfig.resendInterval });
1932
- if (!limited.success)
1933
- return err(HaiIamError.REPOSITORY_ERROR, iamM("iam_securityStateFailed"), limited.error);
1934
- return ok(void 0);
1935
- });
1757
+ const saved = await config.otpRepository.issueOtp({ identifier, code, expiresIn: otpConfig.expiresIn, resendInterval: otpConfig.resendInterval });
1936
1758
  if (!saved.success)
1937
1759
  return saved;
1760
+ const context = saved.data;
1761
+ const expiresAt = context.expiresAt;
1938
1762
  const type = identifierType(identifier);
1939
1763
  if (type === "email" && config.onOtpSendEmail) {
1940
1764
  try {
1941
- await config.onOtpSendEmail(identifier, code);
1942
- } catch (error) {
1765
+ await config.onOtpSendEmail(identifier, code, context);
1766
+ } catch {
1943
1767
  return err(
1944
1768
  HaiIamError.INTERNAL_ERROR,
1945
- iamM("iam_otpSendFailed"),
1946
- error
1769
+ iamM("iam_notificationDeliveryUnknown")
1947
1770
  );
1948
1771
  }
1949
1772
  } else if (type === "phone" && config.onOtpSendSms) {
1950
1773
  try {
1951
- await config.onOtpSendSms(identifier, code);
1952
- } catch (error) {
1774
+ await config.onOtpSendSms(identifier, code, context);
1775
+ } catch {
1953
1776
  return err(
1954
1777
  HaiIamError.INTERNAL_ERROR,
1955
- iamM("iam_otpSendFailed"),
1956
- error
1778
+ iamM("iam_notificationDeliveryUnknown")
1957
1779
  );
1958
1780
  }
1959
1781
  } else {
@@ -2087,7 +1909,7 @@ function createPasswordStrategy(config) {
2087
1909
  if (!resetResult.success)
2088
1910
  return resetResult;
2089
1911
  logger4.info("Password authentication succeeded", { userId: storedUser.id });
2090
- return ok(toUser(storedUser));
1912
+ return ok({ ...toUser(storedUser), credentialVersion: storedUser.credentialVersion ?? "" });
2091
1913
  }
2092
1914
  };
2093
1915
  function hashPassword(password) {
@@ -2249,6 +2071,7 @@ function buildAuthnOperations(deps) {
2249
2071
  const effective = apiKey ? applyApiKeyScopes(access, apiKey.scopes) : access;
2250
2072
  const sessionResult = await sessionFunctions.create({
2251
2073
  userId: user.id,
2074
+ expectedCredentialVersion: user.credentialVersion,
2252
2075
  username: user.username,
2253
2076
  displayName: user.displayName,
2254
2077
  avatarUrl: user.avatarUrl,
@@ -2264,8 +2087,9 @@ function buildAuthnOperations(deps) {
2264
2087
  if (!tokenPair) {
2265
2088
  return err(HaiIamError.SESSION_CREATE_FAILED, iamM("iam_createSessionFailed"));
2266
2089
  }
2090
+ const { credentialVersion, ...publicUser } = user;
2267
2091
  return ok({
2268
- user,
2092
+ user: publicUser,
2269
2093
  tokens: tokenPair,
2270
2094
  ...effective,
2271
2095
  agreements: buildAgreementDisplay(agreementConfig, agreementConfig.showOnLogin)
@@ -2292,7 +2116,14 @@ function buildAuthnOperations(deps) {
2292
2116
  }
2293
2117
  return result;
2294
2118
  }
2295
- return { loginRegisteredUser: buildAuthResult, authn: {
2119
+ return { loginRegisteredUser: async (user) => {
2120
+ const stored = await userRepository.findById(user.id);
2121
+ if (!stored.success)
2122
+ return stored;
2123
+ if (!stored.data?.enabled)
2124
+ return err(HaiIamError.USER_DISABLED, iamM("iam_accountDisabled"));
2125
+ return buildAuthResult({ ...user, credentialVersion: stored.data.credentialVersion ?? "" });
2126
+ }, authn: {
2296
2127
  async login(credentials) {
2297
2128
  return loginWithStrategy("password", credentials, passwordStrategy);
2298
2129
  },
@@ -2314,16 +2145,7 @@ function buildAuthnOperations(deps) {
2314
2145
  return buildAuthResult(authenticated.data.user, authenticated.data.apiKey);
2315
2146
  },
2316
2147
  async logout(accessToken) {
2317
- const sessionResult = await sessionFunctions.get(accessToken);
2318
- if (sessionResult.success && sessionResult.data) {
2319
- const tokenPair = sessionResult.data.data?._tokenPair;
2320
- if (tokenPair?.refreshToken) {
2321
- await sessionFunctions.revokeRefresh(tokenPair.refreshToken);
2322
- }
2323
- await sessionFunctions.delete(sessionResult.data.accessToken);
2324
- logger5.info("User logged out", { userId: sessionResult.data.userId });
2325
- }
2326
- return ok(void 0);
2148
+ return sessionFunctions.delete(accessToken);
2327
2149
  },
2328
2150
  async verifyToken(accessToken) {
2329
2151
  const session = await sessionFunctions.verifyToken(accessToken);
@@ -2337,6 +2159,8 @@ function buildAuthnOperations(deps) {
2337
2159
  return user;
2338
2160
  if (!user.data?.enabled)
2339
2161
  return err(HaiIamError.USER_DISABLED, iamM("iam_accountDisabled"));
2162
+ if (session.data.credentialVersion !== (user.data.credentialVersion ?? ""))
2163
+ return err(HaiIamError.SESSION_INVALID, iamM("iam_sessionExpired"));
2340
2164
  if (!authorization.success)
2341
2165
  return authorization;
2342
2166
  const current = { ...session.data, username: user.data.username, displayName: user.data.displayName, avatarUrl: user.data.avatarUrl, ...authorization.data };
@@ -3792,25 +3616,25 @@ async function resolveIamNotifications(input) {
3792
3616
  if (!templates.success)
3793
3617
  throw new Error(iamM("iam_securityStateFailed"));
3794
3618
  const configured = new Map(templates.data.map((template) => [template.name, template.provider]));
3795
- async function send(template, to, vars) {
3619
+ async function send(template, to, vars, context) {
3796
3620
  const provider = configured.get(template);
3797
3621
  if (!reach?.isInitialized || !provider || !to)
3798
3622
  throw new Error(iamM("iam_passwordResetDeliveryFailed"));
3799
- const result = await reach.send({ provider, to, template, vars });
3623
+ const result = await reach.send({ provider, to, template, vars, idempotencyKey: context.idempotencyKey });
3800
3624
  if (!result.success || !result.data.success)
3801
3625
  throw new Error(iamM("iam_passwordResetDeliveryFailed"));
3802
3626
  }
3803
3627
  if (needsEmail && configured.has("otp_email"))
3804
- callbacks.onOtpSendEmail = (email, code) => send("otp_email", email, { code });
3628
+ callbacks.onOtpSendEmail = (email, code, context) => send("otp_email", email, { code }, context);
3805
3629
  if (needsSms && configured.has("otp_sms"))
3806
- callbacks.onOtpSendSms = (phone, code) => send("otp_sms", phone, { code });
3630
+ callbacks.onOtpSendSms = (phone, code, context) => send("otp_sms", phone, { code }, context);
3807
3631
  const resetTemplate = input.passwordReset?.mode === "phone" ? "password_reset_sms" : "password_reset";
3808
3632
  if (needsReset && configured.has(resetTemplate)) {
3809
- callbacks.onPasswordResetRequest = (user, token, expiresAt) => {
3633
+ callbacks.onPasswordResetRequest = (user, token, context) => {
3810
3634
  const url = input.passwordReset?.resetUrl ? new URL(input.passwordReset.resetUrl) : null;
3811
3635
  url?.searchParams.set("token", token);
3812
- const vars = { token, expiresAt: expiresAt.toISOString(), resetUrl: url?.toString() ?? token };
3813
- return send(resetTemplate, input.passwordReset?.mode === "phone" ? user.phone ?? "" : user.email ?? "", vars);
3636
+ const vars = { token, expiresAt: context.expiresAt.toISOString(), resetUrl: url?.toString() ?? token };
3637
+ return send(resetTemplate, input.passwordReset?.mode === "phone" ? user.phone ?? "" : user.email ?? "", vars, context);
3814
3638
  };
3815
3639
  }
3816
3640
  return callbacks;
@@ -3943,283 +3767,267 @@ async function seedIamData(authz) {
3943
3767
  );
3944
3768
  }
3945
3769
  }
3946
- var TOKEN_KEY_PREFIX = "hai:iam:token:";
3947
- var USER_TOKENS_KEY_PREFIX = "hai:iam:user:";
3948
- var REFRESH_TOKEN_PREFIX = "hai:iam:refresh:";
3949
- function buildTokenKey(token) {
3950
- return `${TOKEN_KEY_PREFIX}${token}`;
3770
+ var BASE64_PLUS_REGEX = /\+/g;
3771
+ var BASE64_SLASH_REGEX = /\//g;
3772
+ var BASE64_TRAILING_PADDING_REGEX = /=+$/;
3773
+ function generateToken() {
3774
+ const bytes = new Uint8Array(32);
3775
+ crypto.getRandomValues(bytes);
3776
+ const base64 = btoa(String.fromCharCode(...bytes));
3777
+ return base64.replace(BASE64_PLUS_REGEX, "-").replace(BASE64_SLASH_REGEX, "_").replace(BASE64_TRAILING_PADDING_REGEX, "");
3778
+ }
3779
+ function getUserSessionScope(userId) {
3780
+ return createHash("sha256").update(userId).digest("hex");
3951
3781
  }
3952
- function buildUserTokensKey(userId) {
3953
- return `${USER_TOKENS_KEY_PREFIX}${userId}:tokens`;
3782
+ function generateSessionToken(userId) {
3783
+ return `s1.${getUserSessionScope(userId)}.${generateToken()}`;
3954
3784
  }
3955
- function buildRefreshKey(refreshToken) {
3956
- return `${REFRESH_TOKEN_PREFIX}${refreshToken}`;
3785
+ function getTokenSessionScope(token) {
3786
+ return /^s1\.([a-f0-9]{64})\.[\w-]{43}$/.exec(token)?.[1];
3957
3787
  }
3958
- function restoreSessionDates(session) {
3788
+ function buildSession(options, now, sessionTtl, accessToken, credentialVersion) {
3959
3789
  return {
3960
- ...session,
3961
- createdAt: session.createdAt instanceof Date ? session.createdAt : new Date(session.createdAt),
3962
- lastActiveAt: session.lastActiveAt instanceof Date ? session.lastActiveAt : new Date(session.lastActiveAt),
3963
- expiresAt: session.expiresAt instanceof Date ? session.expiresAt : new Date(session.expiresAt)
3790
+ version: crypto.randomUUID(),
3791
+ credentialVersion,
3792
+ apiKeyId: options.apiKeyId,
3793
+ apiKeyScopes: options.apiKeyScopes,
3794
+ userId: options.userId,
3795
+ username: options.username,
3796
+ displayName: options.displayName,
3797
+ avatarUrl: options.avatarUrl,
3798
+ roles: options.roles ?? [],
3799
+ permissions: options.permissions ?? [],
3800
+ source: options.source,
3801
+ accessToken,
3802
+ createdAt: now,
3803
+ lastActiveAt: now,
3804
+ expiresAt: new Date(now.getTime() + sessionTtl * 1e3),
3805
+ maxAge: sessionTtl,
3806
+ data: options.data
3964
3807
  };
3965
3808
  }
3966
- function buildRepositoryError(messageKey, message, cause) {
3967
- return err(
3968
- HaiIamError.REPOSITORY_ERROR,
3969
- iamM(messageKey, { params: { message } }),
3970
- cause
3971
- );
3972
- }
3973
- function createCacheSessionRepository(sessionMaxAge, refreshTokenMaxAge) {
3974
- const repo = {
3975
- async save(session, tokenPair) {
3976
- const accessToken = session.accessToken;
3977
- const userId = session.userId;
3978
- const ttl = getSessionTtl(session);
3979
- const setResult = await cache.kv.set(buildTokenKey(accessToken), session, { ex: ttl });
3980
- if (!setResult.success) {
3981
- return err(
3982
- HaiIamError.REPOSITORY_ERROR,
3983
- iamM("iam_saveSessionMappingCacheFailed", { params: { message: setResult.error.message } }),
3984
- setResult.error
3985
- );
3986
- }
3987
- const saddResult = await cache.set_.sadd(buildUserTokensKey(userId), accessToken);
3988
- if (!saddResult.success) {
3989
- await cache.kv.del(buildTokenKey(accessToken));
3990
- return err(
3991
- HaiIamError.REPOSITORY_ERROR,
3992
- iamM("iam_saveUserSessionCacheFailed", { params: { message: saddResult.error.message } }),
3993
- saddResult.error
3994
- );
3995
- }
3996
- await cache.kv.expire(buildUserTokensKey(userId), sessionMaxAge * 2);
3997
- const refreshResult = await cache.kv.set(
3998
- buildRefreshKey(tokenPair.refreshToken),
3999
- { userId, accessToken },
4000
- { ex: refreshTokenMaxAge }
4001
- );
4002
- if (!refreshResult.success) {
4003
- await cache.kv.del(buildTokenKey(accessToken));
4004
- await cache.set_.srem(buildUserTokensKey(userId), accessToken);
4005
- return err(
4006
- HaiIamError.REPOSITORY_ERROR,
4007
- iamM("iam_saveSessionMappingCacheFailed", { params: { message: refreshResult.error.message } }),
4008
- refreshResult.error
4009
- );
4010
- }
4011
- return ok(void 0);
4012
- },
4013
- async getByAccessToken(accessToken) {
4014
- const result = await cache.kv.get(buildTokenKey(accessToken));
4015
- if (!result.success) {
4016
- return err(
4017
- HaiIamError.REPOSITORY_ERROR,
4018
- iamM("iam_querySessionMappingCacheFailed", { params: { message: result.error.message } }),
4019
- result.error
4020
- );
4021
- }
4022
- if (!result.data) {
4023
- return ok(null);
4024
- }
4025
- return ok(restoreSessionDates(result.data));
4026
- },
4027
- async updateByAccessToken(accessToken, data) {
4028
- const sessionResult = await repo.getByAccessToken(accessToken);
4029
- if (!sessionResult.success) {
4030
- return sessionResult;
4031
- }
4032
- if (!sessionResult.data) {
4033
- return err(
4034
- HaiIamError.SESSION_NOT_FOUND,
4035
- iamM("iam_sessionNotExist")
4036
- );
4037
- }
4038
- const nextSession = applySessionPatch(sessionResult.data, data);
4039
- const ttl = getSessionTtl(nextSession);
4040
- const setResult = await cache.kv.set(buildTokenKey(accessToken), nextSession, { ex: ttl });
4041
- if (!setResult.success) {
4042
- return err(
4043
- HaiIamError.REPOSITORY_ERROR,
4044
- iamM("iam_saveSessionMappingCacheFailed", { params: { message: setResult.error.message } }),
4045
- setResult.error
4046
- );
4047
- }
4048
- return ok(void 0);
4049
- },
4050
- async removeByAccessToken(accessToken) {
4051
- const tokenKey = buildTokenKey(accessToken);
4052
- const sessionResult = await cache.kv.get(tokenKey);
4053
- if (!sessionResult.success) {
4054
- return buildRepositoryError(
4055
- "iam_querySessionMappingCacheFailed",
4056
- sessionResult.error.message,
4057
- sessionResult.error
4058
- );
4059
- }
4060
- if (sessionResult.data) {
4061
- const session = sessionResult.data;
4062
- const removeUserTokenResult = await cache.set_.srem(buildUserTokensKey(session.userId), accessToken);
4063
- if (!removeUserTokenResult.success) {
4064
- return buildRepositoryError(
4065
- "iam_deleteUserSessionCacheFailed",
4066
- removeUserTokenResult.error.message,
4067
- removeUserTokenResult.error
4068
- );
4069
- }
4070
- const tokenPair = session.data?._tokenPair;
4071
- if (tokenPair?.refreshToken) {
4072
- const removeRefreshTokenResult = await cache.kv.del(buildRefreshKey(tokenPair.refreshToken));
4073
- if (!removeRefreshTokenResult.success) {
4074
- return buildRepositoryError(
4075
- "iam_deleteSessionMappingCacheFailed",
4076
- removeRefreshTokenResult.error.message,
4077
- removeRefreshTokenResult.error
4078
- );
4079
- }
3809
+ function applySessionPatch(session, patch) {
3810
+ const nextSession = { ...session };
3811
+ if (patch.data !== void 0) {
3812
+ nextSession.data = { ...nextSession.data, ...patch.data };
3813
+ }
3814
+ if (patch.roles !== void 0) {
3815
+ nextSession.roles = patch.roles;
3816
+ }
3817
+ if (patch.permissions !== void 0) {
3818
+ nextSession.permissions = patch.permissions;
3819
+ }
3820
+ if (patch.username !== void 0) {
3821
+ nextSession.username = patch.username;
3822
+ }
3823
+ if (patch.displayName !== void 0) {
3824
+ nextSession.displayName = patch.displayName;
3825
+ }
3826
+ if (patch.avatarUrl !== void 0) {
3827
+ nextSession.avatarUrl = patch.avatarUrl;
3828
+ }
3829
+ if (patch.source !== void 0) {
3830
+ nextSession.source = patch.source;
3831
+ }
3832
+ if (patch.expiresAt !== void 0)
3833
+ nextSession.expiresAt = new Date(patch.expiresAt);
3834
+ nextSession.lastActiveAt = patch.lastActiveAt !== void 0 ? new Date(patch.lastActiveAt) : /* @__PURE__ */ new Date();
3835
+ return nextSession;
3836
+ }
3837
+
3838
+ // src/session/iam-session-repository-cache.ts
3839
+ function retainedUntil(entry) {
3840
+ return Math.max(new Date(entry.session.expiresAt).getTime(), entry.refreshToken ? entry.refreshExpiresAt : 0);
3841
+ }
3842
+ function restoreSession(session) {
3843
+ const result = { ...session, createdAt: new Date(session.createdAt), lastActiveAt: new Date(session.lastActiveAt), expiresAt: new Date(session.expiresAt) };
3844
+ if (typeof result.credentialVersion !== "string" || !result.version || !Number.isSafeInteger(result.maxAge) || result.maxAge <= 0 || !Array.isArray(result.roles) || !result.roles.every((role) => typeof role === "string") || !Array.isArray(result.permissions) || !result.permissions.every((permission) => typeof permission === "string") || ![result.createdAt, result.lastActiveAt, result.expiresAt].every((date) => Number.isFinite(date.getTime()))) {
3845
+ throw new TypeError("Invalid session state");
3846
+ }
3847
+ return result;
3848
+ }
3849
+ function stateKey(scope) {
3850
+ return `hai:iam:sessions:{${scope}}`;
3851
+ }
3852
+ function repositoryFailure(cause) {
3853
+ return err(HaiIamError.REPOSITORY_ERROR, iamM("iam_sessionStateFailed"), cause);
3854
+ }
3855
+ function createCacheSessionRepository(refreshTokenMaxAge) {
3856
+ async function read(scope) {
3857
+ try {
3858
+ const result = await cache.kv.get(stateKey(scope));
3859
+ if (!result.success)
3860
+ return repositoryFailure(result.error);
3861
+ if (result.data === null)
3862
+ return ok(void 0);
3863
+ const state = result.data;
3864
+ if (typeof state.revision !== "string" || !state.revision || !state.sessions || typeof state.sessions !== "object" || Array.isArray(state.sessions))
3865
+ throw new TypeError("Invalid session state");
3866
+ for (const [token, entry] of Object.entries(state.sessions)) {
3867
+ const session = restoreSession(entry.session);
3868
+ if (getTokenSessionScope(token) !== scope || token !== session.accessToken || getUserSessionScope(session.userId) !== scope || !Number.isSafeInteger(entry.refreshExpiresAt) || entry.refreshToken !== void 0 && getTokenSessionScope(entry.refreshToken) !== scope) {
3869
+ throw new TypeError("Invalid session state");
4080
3870
  }
4081
3871
  }
4082
- const removeSessionResult = await cache.kv.del(tokenKey);
4083
- if (!removeSessionResult.success) {
4084
- return buildRepositoryError(
4085
- "iam_deleteSessionMappingCacheFailed",
4086
- removeSessionResult.error.message,
4087
- removeSessionResult.error
4088
- );
4089
- }
4090
- return ok(void 0);
4091
- },
4092
- async removeByUserId(userId) {
4093
- const tokensResult = await cache.set_.smembers(buildUserTokensKey(userId));
4094
- if (!tokensResult.success) {
4095
- return buildRepositoryError(
4096
- "iam_queryUserSessionCacheFailed",
4097
- tokensResult.error.message,
4098
- tokensResult.error
4099
- );
4100
- }
4101
- let removed = 0;
4102
- if (tokensResult.data.length > 0) {
4103
- const results = await Promise.allSettled(
4104
- tokensResult.data.map((token) => repo.removeByAccessToken(token))
4105
- );
4106
- for (const r of results) {
4107
- if (r.status === "rejected") {
4108
- return buildRepositoryError(
4109
- "iam_deleteSessionFailed",
4110
- String(r.reason),
4111
- r.reason
4112
- );
4113
- }
4114
- if (!r.value.success)
4115
- return r.value;
4116
- removed += 1;
3872
+ return ok(state);
3873
+ } catch (cause) {
3874
+ return repositoryFailure(cause);
3875
+ }
3876
+ }
3877
+ async function mutate(scope, operation) {
3878
+ try {
3879
+ for (let attempt = 0; attempt < 16; attempt++) {
3880
+ const current = await read(scope);
3881
+ if (!current.success)
3882
+ return current;
3883
+ const next = current.data ? JSON.parse(JSON.stringify(current.data)) : { revision: "", sessions: {} };
3884
+ for (const [token, entry] of Object.entries(next.sessions)) {
3885
+ if (retainedUntil(entry) <= Date.now())
3886
+ delete next.sessions[token];
4117
3887
  }
4118
- }
4119
- const removeUserTokensKeyResult = await cache.kv.del(buildUserTokensKey(userId));
4120
- if (!removeUserTokensKeyResult.success) {
4121
- return buildRepositoryError(
4122
- "iam_deleteUserSessionCacheFailed",
4123
- removeUserTokensKeyResult.error.message,
4124
- removeUserTokensKeyResult.error
4125
- );
4126
- }
4127
- return ok(removed);
3888
+ const outcome = operation(next);
3889
+ if (!outcome.success)
3890
+ return outcome;
3891
+ next.revision = crypto.randomUUID();
3892
+ const entries = Object.values(next.sessions);
3893
+ const px = entries.reduce((ttl, entry) => Math.max(ttl, retainedUntil(entry) - Date.now()), 1);
3894
+ const written = await cache.kv.compareAndSwap(stateKey(scope), current.data, entries.length ? next : void 0, { px });
3895
+ if (!written.success)
3896
+ return repositoryFailure(written.error);
3897
+ if (written.data)
3898
+ return outcome;
3899
+ }
3900
+ return err(HaiIamError.CONFLICT, iamM("iam_sessionStateConflict"));
3901
+ } catch (cause) {
3902
+ return repositoryFailure(cause);
3903
+ }
3904
+ }
3905
+ function storeSession(state, session, tokens) {
3906
+ const scope = getUserSessionScope(session.userId);
3907
+ if (getTokenSessionScope(session.accessToken) !== scope || tokens.accessToken !== session.accessToken || getTokenSessionScope(tokens.refreshToken) !== scope)
3908
+ return err(HaiIamError.TOKEN_INVALID, iamM("iam_sessionStateFailed"));
3909
+ state.sessions[session.accessToken] = { session, refreshToken: tokens.refreshToken, refreshExpiresAt: session.createdAt.getTime() + refreshTokenMaxAge * 1e3 };
3910
+ return ok(void 0);
3911
+ }
3912
+ return {
3913
+ save: (session, tokens, singleDevice = false) => mutate(getUserSessionScope(session.userId), (state) => {
3914
+ if (singleDevice)
3915
+ state.sessions = {};
3916
+ return storeSession(state, session, tokens);
3917
+ }),
3918
+ rotate: (previous, refreshToken, session, tokens) => mutate(getUserSessionScope(previous.userId), (state) => {
3919
+ const old = state.sessions[previous.accessToken];
3920
+ if (!old || old.refreshToken !== refreshToken || old.refreshExpiresAt <= Date.now())
3921
+ return err(HaiIamError.TOKEN_EXPIRED, iamM("iam_refreshTokenExpired"));
3922
+ if (old.session.version !== previous.version || session.userId !== previous.userId)
3923
+ return err(HaiIamError.CONFLICT, iamM("iam_sessionStateConflict"));
3924
+ delete state.sessions[previous.accessToken];
3925
+ return storeSession(state, session, tokens);
3926
+ }),
3927
+ async getByAccessToken(token) {
3928
+ const scope = getTokenSessionScope(token);
3929
+ if (!scope)
3930
+ return ok(null);
3931
+ const result = await read(scope);
3932
+ if (!result.success)
3933
+ return result;
3934
+ const record = result.data?.sessions[token];
3935
+ if (!record)
3936
+ return ok(null);
3937
+ const session = restoreSession(record.session);
3938
+ return ok(session.expiresAt.getTime() <= Date.now() ? null : session);
4128
3939
  },
4129
- async getByRefreshToken(refreshToken) {
4130
- const mappingResult = await cache.kv.get(buildRefreshKey(refreshToken));
4131
- if (!mappingResult.success) {
4132
- return err(
4133
- HaiIamError.REPOSITORY_ERROR,
4134
- iamM("iam_querySessionMappingCacheFailed", { params: { message: mappingResult.error.message } }),
4135
- mappingResult.error
4136
- );
4137
- }
4138
- if (!mappingResult.data) {
3940
+ async getByRefreshToken(token) {
3941
+ const scope = getTokenSessionScope(token);
3942
+ if (!scope)
4139
3943
  return ok(null);
4140
- }
4141
- return repo.getByAccessToken(mappingResult.data.accessToken);
3944
+ const result = await read(scope);
3945
+ if (!result.success)
3946
+ return result;
3947
+ const record = Object.values(result.data?.sessions ?? {}).find((entry) => entry.refreshToken === token && entry.refreshExpiresAt > Date.now());
3948
+ if (!record)
3949
+ return ok(null);
3950
+ return ok(restoreSession(record.session));
4142
3951
  },
4143
- async removeRefreshToken(refreshToken) {
4144
- const result = await cache.kv.del(buildRefreshKey(refreshToken));
4145
- if (!result.success) {
4146
- return err(
4147
- HaiIamError.REPOSITORY_ERROR,
4148
- iamM("iam_deleteSessionMappingCacheFailed", { params: { message: result.error.message } }),
4149
- result.error
4150
- );
4151
- }
4152
- return ok(void 0);
3952
+ async updateByAccessToken(token, patch, expectedVersion) {
3953
+ const scope = getTokenSessionScope(token);
3954
+ if (!scope)
3955
+ return err(HaiIamError.SESSION_NOT_FOUND, iamM("iam_sessionNotExist"));
3956
+ return mutate(scope, (state) => {
3957
+ const entry = state.sessions[token];
3958
+ if (!entry || new Date(entry.session.expiresAt).getTime() <= Date.now())
3959
+ return err(HaiIamError.SESSION_NOT_FOUND, iamM("iam_sessionNotExist"));
3960
+ if (!expectedVersion || entry.session.version !== expectedVersion)
3961
+ return err(HaiIamError.CONFLICT, iamM("iam_sessionStateConflict"));
3962
+ const updated = applySessionPatch(restoreSession(entry.session), patch);
3963
+ if (![updated.expiresAt, updated.lastActiveAt].every((date) => Number.isFinite(date.getTime())))
3964
+ return err(HaiIamError.INVALID_ARGUMENT, iamM("iam_sessionMaxAgeInvalid"));
3965
+ entry.session = { ...updated, version: crypto.randomUUID() };
3966
+ return ok(void 0);
3967
+ });
4153
3968
  },
4154
- async patchUserSessions(userId, updates) {
4155
- const tokensResult = await cache.set_.smembers(buildUserTokensKey(userId));
4156
- if (!tokensResult.success) {
4157
- return err(
4158
- HaiIamError.REPOSITORY_ERROR,
4159
- iamM("iam_queryUserSessionCacheFailed", { params: { message: tokensResult.error.message } }),
4160
- tokensResult.error
4161
- );
4162
- }
4163
- const tokens = tokensResult.data;
4164
- const readResults = await Promise.allSettled(
4165
- tokens.map(async (token) => {
4166
- const sessionKey = buildTokenKey(token);
4167
- const [sessionResult, ttlResult] = await Promise.all([
4168
- cache.kv.get(sessionKey),
4169
- cache.kv.ttl(sessionKey)
4170
- ]);
4171
- return { token, sessionKey, sessionResult, ttlResult };
4172
- })
4173
- );
4174
- const staleTokens = [];
4175
- const writeOps = [];
4176
- for (const entry of readResults) {
4177
- if (entry.status === "rejected") {
4178
- return buildRepositoryError(
4179
- "iam_queryUserSessionCacheFailed",
4180
- String(entry.reason),
4181
- entry.reason
4182
- );
4183
- }
4184
- const { token, sessionKey, sessionResult, ttlResult } = entry.value;
4185
- if (!sessionResult.success || !sessionResult.data || !ttlResult.success || ttlResult.data <= 0) {
4186
- staleTokens.push(token);
4187
- continue;
4188
- }
4189
- const updated = { ...sessionResult.data, ...updates };
4190
- writeOps.push(cache.kv.set(sessionKey, updated, { ex: ttlResult.data }));
4191
- }
4192
- const writeResults = await Promise.allSettled(writeOps);
4193
- for (const writeResult of writeResults) {
4194
- if (writeResult.status === "rejected") {
4195
- return buildRepositoryError(
4196
- "iam_saveUserSessionCacheFailed",
4197
- String(writeResult.reason),
4198
- writeResult.reason
4199
- );
4200
- }
4201
- if (!writeResult.value.success) {
4202
- return buildRepositoryError(
4203
- "iam_saveUserSessionCacheFailed",
4204
- writeResult.value.error.message,
4205
- writeResult.value.error
4206
- );
3969
+ async touchByAccessToken(token, expectedVersion, now) {
3970
+ const scope = getTokenSessionScope(token);
3971
+ if (!scope)
3972
+ return err(HaiIamError.SESSION_NOT_FOUND, iamM("iam_sessionNotExist"));
3973
+ return mutate(scope, (state) => {
3974
+ const entry = state.sessions[token];
3975
+ if (!entry || new Date(entry.session.expiresAt).getTime() <= Date.now())
3976
+ return err(HaiIamError.SESSION_NOT_FOUND, iamM("iam_sessionNotExist"));
3977
+ if (entry.session.version !== expectedVersion)
3978
+ return err(HaiIamError.CONFLICT, iamM("iam_sessionStateConflict"));
3979
+ const current = restoreSession(entry.session);
3980
+ const next = {
3981
+ ...current,
3982
+ lastActiveAt: new Date(Math.max(current.lastActiveAt.getTime(), now.getTime())),
3983
+ expiresAt: new Date(Math.max(current.expiresAt.getTime(), now.getTime() + current.maxAge * 1e3))
3984
+ };
3985
+ entry.session = next;
3986
+ return ok(next);
3987
+ });
3988
+ },
3989
+ async removeByAccessToken(token) {
3990
+ const scope = getTokenSessionScope(token);
3991
+ if (!scope)
3992
+ return ok(void 0);
3993
+ return mutate(scope, (state) => {
3994
+ delete state.sessions[token];
3995
+ return ok(void 0);
3996
+ });
3997
+ },
3998
+ removeByUserId: (userId) => mutate(getUserSessionScope(userId), (state) => {
3999
+ const count = Object.keys(state.sessions).length;
4000
+ state.sessions = {};
4001
+ return ok(count);
4002
+ }),
4003
+ async removeRefreshToken(token) {
4004
+ const scope = getTokenSessionScope(token);
4005
+ if (!scope)
4006
+ return ok(void 0);
4007
+ return mutate(scope, (state) => {
4008
+ for (const entry of Object.values(state.sessions)) {
4009
+ if (entry.refreshToken === token)
4010
+ delete entry.refreshToken;
4207
4011
  }
4208
- }
4209
- if (staleTokens.length > 0) {
4210
- const staleCleanupResult = await cache.set_.srem(buildUserTokensKey(userId), ...staleTokens);
4211
- if (!staleCleanupResult.success) {
4212
- return buildRepositoryError(
4213
- "iam_deleteUserSessionCacheFailed",
4214
- staleCleanupResult.error.message,
4215
- staleCleanupResult.error
4216
- );
4012
+ return ok(void 0);
4013
+ });
4014
+ },
4015
+ patchUserSessions(userId, patch) {
4016
+ let targets;
4017
+ return mutate(getUserSessionScope(userId), (state) => {
4018
+ targets ??= new Map(Object.entries(state.sessions).map(([token, entry]) => [token, entry.session.version]));
4019
+ for (const [token, version] of targets) {
4020
+ const entry = state.sessions[token];
4021
+ if (!entry)
4022
+ continue;
4023
+ if (entry.session.version !== version)
4024
+ return err(HaiIamError.CONFLICT, iamM("iam_sessionStateConflict"));
4025
+ entry.session = { ...applySessionPatch(restoreSession(entry.session), patch), version: crypto.randomUUID() };
4217
4026
  }
4218
- }
4219
- return ok(void 0);
4027
+ return ok(void 0);
4028
+ });
4220
4029
  }
4221
4030
  };
4222
- return repo;
4223
4031
  }
4224
4032
 
4225
4033
  // src/session/iam-session-functions.ts
@@ -4236,10 +4044,10 @@ async function createSessionOperations(deps) {
4236
4044
  const { config } = deps;
4237
4045
  const sessionConfig = SessionConfigSchema.parse(config.session ?? {});
4238
4046
  const sessionRepository = createCacheSessionRepository(
4239
- sessionConfig.maxAge ?? 86400,
4240
4047
  sessionConfig.refreshTokenMaxAge ?? 604800
4241
4048
  );
4242
4049
  const functions = buildSessionFunctions({
4050
+ readCredential: deps.readCredential,
4243
4051
  maxAge: sessionConfig.maxAge,
4244
4052
  sliding: sessionConfig.sliding,
4245
4053
  singleDevice: sessionConfig.singleDevice,
@@ -4262,6 +4070,31 @@ function buildSessionFunctions(config) {
4262
4070
  const sliding = config.sliding ?? true;
4263
4071
  const singleDevice = config.singleDevice ?? false;
4264
4072
  const repo = config.sessionRepository;
4073
+ function candidate(options, credentialVersion) {
4074
+ const now = /* @__PURE__ */ new Date();
4075
+ const ttl = options.maxAge ?? maxAge;
4076
+ if (!Number.isSafeInteger(ttl) || ttl <= 0 || !Number.isFinite(new Date(now.getTime() + ttl * 1e3).getTime()))
4077
+ return err(HaiIamError.INVALID_ARGUMENT, iamM("iam_sessionMaxAgeInvalid"));
4078
+ const accessToken = generateSessionToken(options.userId);
4079
+ const tokens = { accessToken, refreshToken: generateSessionToken(options.userId), expiresIn: ttl, tokenType: "Bearer" };
4080
+ const session = buildSession(options, now, ttl, accessToken, credentialVersion);
4081
+ session.data = { ...session.data, _tokenPair: tokens };
4082
+ return ok({ session, tokens });
4083
+ }
4084
+ async function credential(userId, expected) {
4085
+ try {
4086
+ const current = await config.readCredential(userId);
4087
+ if (!current.success)
4088
+ return current;
4089
+ if (!current.data || current.data.enabled !== true)
4090
+ return err(HaiIamError.USER_DISABLED, iamM("iam_accountDisabled"));
4091
+ if (typeof current.data.version !== "string" || expected !== void 0 && current.data.version !== expected)
4092
+ return err(HaiIamError.SESSION_INVALID, iamM("iam_sessionExpired"));
4093
+ return ok(current.data.version);
4094
+ } catch {
4095
+ return err(HaiIamError.REPOSITORY_ERROR, iamM("iam_sessionExpired"));
4096
+ }
4097
+ }
4265
4098
  async function validateApiKey(session) {
4266
4099
  if (!session.apiKeyId)
4267
4100
  return ok(session);
@@ -4275,31 +4108,21 @@ function buildSessionFunctions(config) {
4275
4108
  return err(HaiIamError.APIKEY_INVALID, iamM("iam_apikeyInvalid"));
4276
4109
  if (!key.enabled)
4277
4110
  return err(HaiIamError.APIKEY_DISABLED, iamM("iam_apikeyDisabled"));
4278
- if (key.expiresAt && /* @__PURE__ */ new Date() > key.expiresAt)
4111
+ if (key.expiresAt && /* @__PURE__ */ new Date() >= key.expiresAt)
4279
4112
  return err(HaiIamError.APIKEY_EXPIRED, iamM("iam_apikeyExpired"));
4280
4113
  return ok(applyApiKeyScopes({ ...session, apiKeyScopes: key.scopes }, key.scopes));
4281
4114
  }
4282
4115
  return {
4283
4116
  async create(options) {
4284
4117
  try {
4285
- if (singleDevice) {
4286
- const clearResult = await repo.removeByUserId(options.userId);
4287
- if (!clearResult.success) {
4288
- return clearResult;
4289
- }
4290
- }
4291
- const accessToken = generateToken();
4292
- const tokenPair = {
4293
- accessToken,
4294
- refreshToken: generateToken(),
4295
- expiresIn: maxAge,
4296
- tokenType: "Bearer"
4297
- };
4298
- const now = /* @__PURE__ */ new Date();
4299
- const sessionTtl = options.maxAge ?? maxAge;
4300
- const session = buildSession(options, now, sessionTtl, accessToken);
4301
- session.data = { ...session.data, _tokenPair: tokenPair };
4302
- const saveResult = await repo.save(session, tokenPair);
4118
+ const bound = await credential(options.userId, options.expectedCredentialVersion);
4119
+ if (!bound.success)
4120
+ return bound;
4121
+ const built = candidate(options, bound.data);
4122
+ if (!built.success)
4123
+ return built;
4124
+ const { session, tokens } = built.data;
4125
+ const saveResult = await repo.save(session, tokens, singleDevice);
4303
4126
  if (!saveResult.success) {
4304
4127
  return saveResult;
4305
4128
  }
@@ -4329,20 +4152,23 @@ function buildSessionFunctions(config) {
4329
4152
  }
4330
4153
  return ok(null);
4331
4154
  }
4155
+ if (typeof session.credentialVersion !== "string")
4156
+ return err(HaiIamError.SESSION_INVALID, iamM("iam_sessionExpired"));
4157
+ const bound = await credential(session.userId, session.credentialVersion);
4158
+ if (!bound.success)
4159
+ return bound;
4332
4160
  const validated = await validateApiKey(session);
4333
4161
  if (!validated.success)
4334
4162
  return validated;
4163
+ let output = validated.data;
4335
4164
  if (sliding) {
4336
- const now = /* @__PURE__ */ new Date();
4337
- const updateResult = await repo.updateByAccessToken(accessToken, {
4338
- lastActiveAt: now,
4339
- expiresAt: new Date(now.getTime() + maxAge * 1e3)
4340
- });
4165
+ const updateResult = await repo.touchByAccessToken(accessToken, session.version, /* @__PURE__ */ new Date());
4341
4166
  if (!updateResult.success) {
4342
4167
  return updateResult;
4343
4168
  }
4169
+ output = { ...output, lastActiveAt: updateResult.data.lastActiveAt, expiresAt: updateResult.data.expiresAt };
4344
4170
  }
4345
- return ok(stripInternalSessionFields(validated.data));
4171
+ return ok(stripInternalSessionFields(output));
4346
4172
  },
4347
4173
  async verifyToken(accessToken) {
4348
4174
  const sessionResult = await this.get(accessToken);
@@ -4357,8 +4183,8 @@ function buildSessionFunctions(config) {
4357
4183
  }
4358
4184
  return ok(sessionResult.data);
4359
4185
  },
4360
- async update(accessToken, data) {
4361
- return repo.updateByAccessToken(accessToken, data);
4186
+ async update(accessToken, data, expectedVersion) {
4187
+ return repo.updateByAccessToken(accessToken, data, expectedVersion);
4362
4188
  },
4363
4189
  async delete(accessToken) {
4364
4190
  logger8.debug("Session delete requested");
@@ -4384,15 +4210,16 @@ function buildSessionFunctions(config) {
4384
4210
  iamM("iam_refreshTokenExpired")
4385
4211
  );
4386
4212
  }
4213
+ if (typeof oldSession.credentialVersion !== "string")
4214
+ return err(HaiIamError.SESSION_INVALID, iamM("iam_sessionExpired"));
4215
+ const bound = await credential(oldSession.userId, oldSession.credentialVersion);
4216
+ if (!bound.success)
4217
+ return bound;
4387
4218
  const validated = await validateApiKey(oldSession);
4388
4219
  if (!validated.success)
4389
4220
  return validated;
4390
4221
  const effectiveSession = validated.data;
4391
- const removeOldSessionResult = await repo.removeByAccessToken(oldSession.accessToken);
4392
- if (!removeOldSessionResult.success) {
4393
- return removeOldSessionResult;
4394
- }
4395
- const newSessionResult = await this.create({
4222
+ const next = candidate({
4396
4223
  userId: oldSession.userId,
4397
4224
  username: oldSession.username,
4398
4225
  displayName: oldSession.displayName,
@@ -4402,20 +4229,19 @@ function buildSessionFunctions(config) {
4402
4229
  apiKeyId: effectiveSession.apiKeyId,
4403
4230
  apiKeyScopes: effectiveSession.apiKeyScopes,
4404
4231
  source: oldSession.source,
4232
+ maxAge: oldSession.maxAge,
4405
4233
  data: oldSession.data ? { ...oldSession.data, _tokenPair: void 0 } : void 0
4406
- });
4407
- if (!newSessionResult.success) {
4408
- return newSessionResult;
4409
- }
4410
- const tokenPair = newSessionResult.data.data?._tokenPair;
4411
- if (!tokenPair) {
4412
- return err(
4413
- HaiIamError.TOKEN_REFRESH_FAILED,
4414
- iamM("iam_refreshTokenFailed")
4415
- );
4416
- }
4234
+ }, oldSession.credentialVersion);
4235
+ if (!next.success)
4236
+ return next;
4237
+ const rotated = await repo.rotate(oldSession, refreshToken, next.data.session, next.data.tokens);
4238
+ if (!rotated.success)
4239
+ return rotated;
4240
+ const current = await credential(oldSession.userId, oldSession.credentialVersion);
4241
+ if (!current.success)
4242
+ return current;
4417
4243
  logger8.debug("Token refreshed", { userId: oldSession.userId });
4418
- return ok(tokenPair);
4244
+ return ok(next.data.tokens);
4419
4245
  } catch (error) {
4420
4246
  return err(
4421
4247
  HaiIamError.TOKEN_REFRESH_FAILED,
@@ -4451,7 +4277,7 @@ function createTicketOperations() {
4451
4277
  expiresAt
4452
4278
  };
4453
4279
  const saved = await cache.kv.set(buildTicketKey(ticket), record, { px: ttlMs, nx: true });
4454
- if (!saved.success) {
4280
+ if (!saved.success || !saved.data) {
4455
4281
  logger9.warn("Failed to issue ticket", { purpose: options.purpose });
4456
4282
  return err(HaiIamError.TICKET_ISSUE_FAILED, iamM("iam_ticketIssueFailed"));
4457
4283
  }
@@ -4488,213 +4314,6 @@ function createTicketOperations() {
4488
4314
  }
4489
4315
  };
4490
4316
  }
4491
- function hashResetToken(token) {
4492
- const result = crypto$1.hash.hash(token);
4493
- if (!result.success) {
4494
- return err(
4495
- HaiIamError.REPOSITORY_ERROR,
4496
- iamM("iam_hashResetTokenFailed", { params: { message: result.error.message } }),
4497
- result.error
4498
- );
4499
- }
4500
- return ok(result.data);
4501
- }
4502
- var RESET_TOKEN_KEY_PREFIX = "hai:iam:reset:";
4503
- var RESET_ATTEMPTS_KEY_PREFIX = "hai:iam:reset:attempts:";
4504
- var RESET_USER_TOKEN_KEY_PREFIX = "hai:iam:reset:user:";
4505
- function buildResetTokenKey(hashedToken) {
4506
- return `${RESET_TOKEN_KEY_PREFIX}${hashedToken}`;
4507
- }
4508
- function buildAttemptsKey(userId) {
4509
- return `${RESET_ATTEMPTS_KEY_PREFIX}${userId}`;
4510
- }
4511
- function buildUserTokenKey(userId) {
4512
- return `${RESET_USER_TOKEN_KEY_PREFIX}${userId}`;
4513
- }
4514
- function buildResetTokenRepositoryError(messageKey, message, cause) {
4515
- return err(
4516
- HaiIamError.REPOSITORY_ERROR,
4517
- iamM(messageKey, { params: { message } }),
4518
- cause
4519
- );
4520
- }
4521
- var resetTokenRepoInstance = null;
4522
- function resetResetTokenRepoSingleton() {
4523
- resetTokenRepoInstance = null;
4524
- }
4525
- function createCacheResetTokenRepository() {
4526
- if (resetTokenRepoInstance)
4527
- return resetTokenRepoInstance;
4528
- async function clearTokenState(hashedToken, userId) {
4529
- const cleanupResults = await Promise.all([
4530
- cache.kv.del(buildResetTokenKey(hashedToken)),
4531
- cache.kv.del(buildAttemptsKey(userId)),
4532
- cache.kv.del(buildUserTokenKey(userId))
4533
- ]);
4534
- const failedCleanup = cleanupResults.find((result) => !result.success);
4535
- if (failedCleanup && !failedCleanup.success) {
4536
- return buildResetTokenRepositoryError(
4537
- "iam_saveResetTokenFailed",
4538
- failedCleanup.error.message,
4539
- failedCleanup.error
4540
- );
4541
- }
4542
- return ok(void 0);
4543
- }
4544
- const repo = {
4545
- async saveToken(token, userId, expiresAt) {
4546
- return withIamLock(`reset:${userId}`, async () => {
4547
- const hashResult = hashResetToken(token);
4548
- if (!hashResult.success) {
4549
- return hashResult;
4550
- }
4551
- const hashedToken = hashResult.data;
4552
- const ttlSeconds = Math.max(1, Math.ceil((expiresAt.getTime() - Date.now()) / 1e3));
4553
- const userTokenKey = buildUserTokenKey(userId);
4554
- const existingTokenResult = await cache.kv.get(userTokenKey);
4555
- if (!existingTokenResult.success) {
4556
- return buildResetTokenRepositoryError(
4557
- "iam_queryResetTokenFailed",
4558
- existingTokenResult.error.message,
4559
- existingTokenResult.error
4560
- );
4561
- }
4562
- if (existingTokenResult.data && existingTokenResult.data !== hashedToken) {
4563
- const removeExistingTokenResult = await cache.kv.del(buildResetTokenKey(existingTokenResult.data));
4564
- if (!removeExistingTokenResult.success) {
4565
- return buildResetTokenRepositoryError(
4566
- "iam_saveResetTokenFailed",
4567
- removeExistingTokenResult.error.message,
4568
- removeExistingTokenResult.error
4569
- );
4570
- }
4571
- }
4572
- const setResult = await cache.kv.set(buildResetTokenKey(hashedToken), userId, { ex: ttlSeconds });
4573
- if (!setResult.success) {
4574
- return err(
4575
- HaiIamError.REPOSITORY_ERROR,
4576
- iamM("iam_saveResetTokenFailed", { params: { message: setResult.error.message } }),
4577
- setResult.error
4578
- );
4579
- }
4580
- const saveUserTokenResult = await cache.kv.set(userTokenKey, hashedToken, { ex: ttlSeconds });
4581
- if (!saveUserTokenResult.success) {
4582
- await cache.kv.del(buildResetTokenKey(hashedToken));
4583
- return buildResetTokenRepositoryError(
4584
- "iam_saveResetTokenFailed",
4585
- saveUserTokenResult.error.message,
4586
- saveUserTokenResult.error
4587
- );
4588
- }
4589
- const attemptsResult = await cache.kv.set(buildAttemptsKey(userId), 0, { ex: ttlSeconds });
4590
- if (!attemptsResult.success) {
4591
- await Promise.all([
4592
- cache.kv.del(buildResetTokenKey(hashedToken)),
4593
- cache.kv.del(userTokenKey)
4594
- ]);
4595
- return buildResetTokenRepositoryError(
4596
- "iam_saveResetTokenFailed",
4597
- attemptsResult.error.message,
4598
- attemptsResult.error
4599
- );
4600
- }
4601
- return ok(void 0);
4602
- });
4603
- },
4604
- async useToken(token, maxAttempts, operation) {
4605
- const hash = hashResetToken(token);
4606
- if (!hash.success)
4607
- return hash;
4608
- const user = await cache.kv.get(buildResetTokenKey(hash.data));
4609
- if (!user.success)
4610
- return buildResetTokenRepositoryError("iam_queryResetTokenFailed", user.error.message, user.error);
4611
- if (!user.data)
4612
- return err(HaiIamError.RESET_TOKEN_INVALID, iamM("iam_resetTokenInvalid"));
4613
- return withIamLock(`reset:${user.data}`, async () => {
4614
- const verified = await repo.tryGetUserByToken(token, maxAttempts);
4615
- if (!verified.success)
4616
- return verified;
4617
- const result = await operation(verified.data);
4618
- if (!result.success)
4619
- return result;
4620
- return repo.removeToken(token);
4621
- });
4622
- },
4623
- async tryGetUserByToken(token, maxAttempts) {
4624
- const hashResult = hashResetToken(token);
4625
- if (!hashResult.success) {
4626
- return hashResult;
4627
- }
4628
- const hashedToken = hashResult.data;
4629
- const tokenKey = buildResetTokenKey(hashedToken);
4630
- const result = await cache.kv.get(tokenKey);
4631
- if (!result.success) {
4632
- return err(
4633
- HaiIamError.REPOSITORY_ERROR,
4634
- iamM("iam_queryResetTokenFailed", { params: { message: result.error.message } }),
4635
- result.error
4636
- );
4637
- }
4638
- if (!result.data) {
4639
- return err(
4640
- HaiIamError.RESET_TOKEN_INVALID,
4641
- iamM("iam_resetTokenInvalid")
4642
- );
4643
- }
4644
- const userId = result.data;
4645
- const current = await cache.kv.get(buildUserTokenKey(userId));
4646
- if (!current.success)
4647
- return buildResetTokenRepositoryError("iam_queryResetTokenFailed", current.error.message, current.error);
4648
- if (current.data !== hashedToken)
4649
- return err(HaiIamError.RESET_TOKEN_INVALID, iamM("iam_resetTokenInvalid"));
4650
- const attemptsKey = buildAttemptsKey(userId);
4651
- const incrResult = await cache.kv.incr(attemptsKey);
4652
- if (!incrResult.success) {
4653
- return buildResetTokenRepositoryError(
4654
- "iam_saveResetTokenFailed",
4655
- incrResult.error.message,
4656
- incrResult.error
4657
- );
4658
- }
4659
- const nextAttempts = incrResult.data;
4660
- if (nextAttempts > maxAttempts) {
4661
- const clearResult = await clearTokenState(hashedToken, userId);
4662
- if (!clearResult.success) {
4663
- return clearResult;
4664
- }
4665
- return err(
4666
- HaiIamError.RESET_TOKEN_MAX_ATTEMPTS,
4667
- iamM("iam_resetTokenMaxAttempts")
4668
- );
4669
- }
4670
- return ok(userId);
4671
- },
4672
- async removeToken(token) {
4673
- const hashResult = hashResetToken(token);
4674
- if (!hashResult.success) {
4675
- return hashResult;
4676
- }
4677
- const hashedToken = hashResult.data;
4678
- const tokenKey = buildResetTokenKey(hashedToken);
4679
- const result = await cache.kv.get(tokenKey);
4680
- if (!result.success) {
4681
- return buildResetTokenRepositoryError(
4682
- "iam_queryResetTokenFailed",
4683
- result.error.message,
4684
- result.error
4685
- );
4686
- }
4687
- if (!result.data) {
4688
- return ok(void 0);
4689
- }
4690
- return clearTokenState(hashedToken, result.data);
4691
- }
4692
- };
4693
- resetTokenRepoInstance = repo;
4694
- return repo;
4695
- }
4696
-
4697
- // src/user/iam-user-functions.ts
4698
4317
  var logger10 = core.logger.child({ module: "iam", scope: "user" });
4699
4318
  var USER_SORT_COLUMNS = {
4700
4319
  /** 用户名列。 */
@@ -4712,10 +4331,8 @@ async function createUserOperations(deps) {
4712
4331
  try {
4713
4332
  const { config, passwordStrategyResult, sessionFunctions, authzFunctions, onPasswordResetRequest } = deps;
4714
4333
  const userRepository = await createDbUserRepository();
4715
- const resetTokenRepository = createCacheResetTokenRepository();
4716
4334
  const functions = buildUserFunctions({
4717
4335
  userRepository,
4718
- resetTokenRepository,
4719
4336
  passwordStrategyResult,
4720
4337
  sessionFunctions,
4721
4338
  authzFunctions,
@@ -5172,14 +4789,14 @@ function buildPasswordChangeOps(ctx) {
5172
4789
  const hashResult = hashPassword(newPassword);
5173
4790
  if (!hashResult.success)
5174
4791
  return hashResult;
5175
- const updateResult = await userRepository.updateById(userId, {
5176
- passwordHash: hashResult.data,
5177
- passwordUpdatedAt: /* @__PURE__ */ new Date()
5178
- });
5179
- if (!updateResult.success) {
5180
- return mapRepositoryError("iam_updateUserFailed", updateResult.error.message);
5181
- }
5182
- await sessionFunctions.deleteByUserId(userId);
4792
+ const updateResult = await userRepository.setPassword({ userId, expectedVersion: userResult.data.credentialVersion ?? "", passwordHash: hashResult.data });
4793
+ if (!updateResult.success)
4794
+ return updateResult;
4795
+ if (!updateResult.data)
4796
+ return err(HaiIamError.CONFLICT, iamM("iam_securityStateFailed"));
4797
+ const revoked = await sessionFunctions.deleteByUserId(userId);
4798
+ if (!revoked.success)
4799
+ return revoked;
5183
4800
  logger10.info("Admin reset password", { userId });
5184
4801
  return ok(void 0);
5185
4802
  },
@@ -5229,14 +4846,14 @@ function buildPasswordChangeOps(ctx) {
5229
4846
  const hashResult = hashPassword(newPassword);
5230
4847
  if (!hashResult.success)
5231
4848
  return hashResult;
5232
- const updateResult = await userRepository.updateById(userId, {
5233
- passwordHash: hashResult.data,
5234
- passwordUpdatedAt: /* @__PURE__ */ new Date()
5235
- });
5236
- if (!updateResult.success) {
5237
- return mapRepositoryError("iam_updateUserFailed", updateResult.error.message);
5238
- }
5239
- await sessionFunctions.deleteByUserId(userId);
4849
+ const updateResult = await userRepository.setPassword({ userId, expectedVersion: user.credentialVersion ?? "", passwordHash: hashResult.data });
4850
+ if (!updateResult.success)
4851
+ return updateResult;
4852
+ if (!updateResult.data)
4853
+ return err(HaiIamError.CONFLICT, iamM("iam_securityStateFailed"));
4854
+ const revoked = await sessionFunctions.deleteByUserId(userId);
4855
+ if (!revoked.success)
4856
+ return revoked;
5240
4857
  logger10.info("Password changed", { userId });
5241
4858
  return ok(void 0);
5242
4859
  },
@@ -5258,7 +4875,7 @@ function buildPasswordChangeOps(ctx) {
5258
4875
  };
5259
4876
  }
5260
4877
  function buildPasswordResetOps(ctx) {
5261
- const { userRepository, resetTokenRepository, sessionFunctions, onPasswordResetRequest } = ctx;
4878
+ const { userRepository, sessionFunctions, onPasswordResetRequest } = ctx;
5262
4879
  const { validatePassword, hashPassword } = ctx;
5263
4880
  return {
5264
4881
  async requestPasswordReset(identifier) {
@@ -5280,15 +4897,19 @@ function buildPasswordResetOps(ctx) {
5280
4897
  return ok(void 0);
5281
4898
  const token = generateToken();
5282
4899
  const expiresAt = new Date(Date.now() + resetConfig.tokenExpiresIn * 1e3);
5283
- const saveResult = await resetTokenRepository.saveToken(token, user.id, expiresAt);
5284
- if (!saveResult.success) {
4900
+ const tokenHash = crypto$1.hash.hash(token);
4901
+ if (!tokenHash.success)
4902
+ return tokenHash;
4903
+ const saveResult = await userRepository.publishPasswordReset({ user: userResult.data, tokenHash: tokenHash.data, expiresAt });
4904
+ if (!saveResult.success)
5285
4905
  return saveResult;
5286
- }
4906
+ if (!saveResult.data)
4907
+ return err(HaiIamError.CONFLICT, iamM("iam_securityStateFailed"));
5287
4908
  try {
5288
- await onPasswordResetRequest(user, token, expiresAt);
4909
+ await onPasswordResetRequest(user, token, { expiresAt, idempotencyKey: `iam-reset:${tokenHash.data}` });
5289
4910
  } catch {
5290
4911
  logger10.error("Password reset callback failed", { userId: user.id });
5291
- return err(HaiIamError.INTERNAL_ERROR, iamM("iam_passwordResetDeliveryFailed"));
4912
+ return err(HaiIamError.INTERNAL_ERROR, iamM("iam_notificationDeliveryUnknown"));
5292
4913
  }
5293
4914
  logger10.info("Password reset token generated", { userId: user.id });
5294
4915
  return ok(void 0);
@@ -5298,41 +4919,28 @@ function buildPasswordResetOps(ctx) {
5298
4919
  const resetConfig = ctx.resetConfig;
5299
4920
  if (!resetConfig.enabled)
5300
4921
  return err(HaiIamError.PASSWORD_RESET_DISABLED, iamM("iam_passwordResetDisabled"));
5301
- return resetTokenRepository.useToken(token, resetConfig.maxAttempts, async (userId) => {
5302
- const validateResult = validatePassword(newPassword);
5303
- if (!validateResult.success)
5304
- return validateResult;
5305
- const userResult = await userRepository.findById(userId);
5306
- if (!userResult.success) {
5307
- return mapRepositoryError("iam_queryUserFailed", userResult.error.message);
5308
- }
5309
- if (!userResult.data) {
5310
- return err(
5311
- HaiIamError.USER_NOT_FOUND,
5312
- iamM("iam_userNotExist")
5313
- );
5314
- }
5315
- if (!userResult.data.enabled)
5316
- return err(HaiIamError.USER_DISABLED, iamM("iam_accountDisabled"));
5317
- const hashResult = hashPassword(newPassword);
5318
- if (!hashResult.success)
5319
- return hashResult;
5320
- const updateResult = await userRepository.updateById(userId, {
5321
- passwordHash: hashResult.data,
5322
- passwordUpdatedAt: /* @__PURE__ */ new Date(),
5323
- loginFailedCount: 0,
5324
- lockedUntil: null,
5325
- lastLoginFailedAt: null
5326
- });
5327
- if (!updateResult.success) {
5328
- return mapRepositoryError("iam_updateUserFailed", updateResult.error.message);
5329
- }
5330
- const revoked = await sessionFunctions.deleteByUserId(userId);
5331
- if (!revoked.success)
5332
- return revoked;
5333
- logger10.info("Password reset confirmed", { userId });
5334
- return ok(void 0);
5335
- });
4922
+ const tokenHash = crypto$1.hash.hash(token);
4923
+ if (!tokenHash.success)
4924
+ return tokenHash;
4925
+ const claimed = await userRepository.claimPasswordReset(tokenHash.data, resetConfig.maxAttempts);
4926
+ if (!claimed.success)
4927
+ return claimed;
4928
+ const validateResult = validatePassword(newPassword);
4929
+ if (!validateResult.success)
4930
+ return validateResult;
4931
+ const hashResult = hashPassword(newPassword);
4932
+ if (!hashResult.success)
4933
+ return hashResult;
4934
+ const committed = await userRepository.commitPasswordReset({ user: claimed.data, tokenHash: tokenHash.data, passwordHash: hashResult.data });
4935
+ if (!committed.success)
4936
+ return committed;
4937
+ if (!committed.data)
4938
+ return err(HaiIamError.RESET_TOKEN_INVALID, iamM("iam_resetTokenInvalid"));
4939
+ const revoked = await sessionFunctions.deleteByUserId(claimed.data.id);
4940
+ if (!revoked.success)
4941
+ return revoked;
4942
+ logger10.info("Password reset confirmed", { userId: claimed.data.id });
4943
+ return ok(void 0);
5336
4944
  }
5337
4945
  };
5338
4946
  }
@@ -5434,7 +5042,16 @@ var iam = {
5434
5042
  const parsed = parseResult.data;
5435
5043
  const { onPasswordResetRequest, onOtpSendEmail, onOtpSendSms } = await resolveIamNotifications(config);
5436
5044
  logger11.info("IAM notification channels resolved", { email: Boolean(onOtpSendEmail), sms: Boolean(onOtpSendSms), passwordReset: Boolean(onPasswordResetRequest) });
5437
- const sessionResult = await createSessionOperations({ config: parsed });
5045
+ const credentialUsers = await createDbUserRepository();
5046
+ const sessionResult = await createSessionOperations({
5047
+ config: parsed,
5048
+ readCredential: async (userId) => {
5049
+ const found = await credentialUsers.findById(userId);
5050
+ if (!found.success)
5051
+ return found;
5052
+ return ok(found.data ? { version: found.data.credentialVersion ?? "", enabled: found.data.enabled } : null);
5053
+ }
5054
+ });
5438
5055
  if (!sessionResult.success) {
5439
5056
  return sessionResult;
5440
5057
  }
@@ -5548,13 +5165,12 @@ var iam = {
5548
5165
  resetApiKeyRepoSingleton();
5549
5166
  resetOtpRepoSingleton();
5550
5167
  resetUserRepoSingleton();
5551
- resetResetTokenRepoSingleton();
5552
5168
  resetRoleRepoSingleton();
5553
5169
  resetPermissionRepoSingleton();
5554
5170
  logger11.info("IAM module closed");
5555
5171
  }
5556
5172
  };
5557
5173
 
5558
- export { AgreementConfigSchema, ApiKeyConfigSchema, AuthStrategyTypeSchema, AuthUiConfigSchema, HaiIamError, IamConfigSchema, LdapConfigSchema, LoginConfigSchema, OtpConfigSchema, PasswordConfigSchema, PasswordResetConfigSchema, RbacConfigSchema, RegisterConfigSchema, SecurityConfigSchema, SessionConfigSchema, iam };
5174
+ export { iam };
5559
5175
  //# sourceMappingURL=index.js.map
5560
5176
  //# sourceMappingURL=index.js.map