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