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