@vunexa/lixa 0.1.6-alpha.13 → 0.1.6-alpha.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -30,8 +30,11 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var src_exports = {};
32
32
  __export(src_exports, {
33
+ AccountLinkingChallengeRequiredError: () => AccountLinkingChallengeRequiredError,
33
34
  AccountLinkingStrategy: () => AccountLinkingStrategy,
35
+ AccountNotVerifiedError: () => AccountNotVerifiedError,
34
36
  AccountUnlinkError: () => AccountUnlinkError,
37
+ CompositeIdentityProvider: () => CompositeIdentityProvider,
35
38
  CredentialsManager: () => CredentialsManager,
36
39
  CredentialsNotConfiguredError: () => CredentialsNotConfiguredError,
37
40
  DEFAULT_SESSION_COOKIE_NAME: () => DEFAULT_SESSION_COOKIE_NAME,
@@ -39,6 +42,8 @@ __export(src_exports, {
39
42
  DEFAULT_STATE_COOKIE_NAME: () => DEFAULT_STATE_COOKIE_NAME,
40
43
  DEFAULT_STATE_MAX_AGE_SECONDS: () => DEFAULT_STATE_MAX_AGE_SECONDS,
41
44
  EmailNotVerifiedError: () => EmailNotVerifiedError,
45
+ IdentityNotConfiguredError: () => IdentityNotConfiguredError,
46
+ IdentityProviderError: () => IdentityProviderError,
42
47
  InvalidCredentialsError: () => InvalidCredentialsError,
43
48
  InvalidOAuthCallbackError: () => InvalidOAuthCallbackError,
44
49
  InvalidProviderConfigError: () => InvalidProviderConfigError,
@@ -49,7 +54,9 @@ __export(src_exports, {
49
54
  Pbkdf2PasswordHasher: () => Pbkdf2PasswordHasher,
50
55
  ProviderNotConfiguredError: () => ProviderNotConfiguredError,
51
56
  RefreshTokenError: () => RefreshTokenError,
57
+ SSOAccountAlreadyExistsError: () => SSOAccountAlreadyExistsError,
52
58
  ScryptPasswordHasher: () => ScryptPasswordHasher,
59
+ SelfManagedIdentityProvider: () => SelfManagedIdentityProvider,
53
60
  SessionNotFoundError: () => SessionNotFoundError,
54
61
  TokenExchangeError: () => TokenExchangeError,
55
62
  UserAlreadyExistsError: () => UserAlreadyExistsError,
@@ -64,6 +71,7 @@ __export(src_exports, {
64
71
  determineProviderFromIssuer: () => determineProviderFromIssuer,
65
72
  extractUserInfo: () => extractUserInfo,
66
73
  fetchUserInfo: () => fetchUserInfo,
74
+ identity: () => identity_exports,
67
75
  isProductionEnvironment: () => isProductionEnvironment,
68
76
  serializeCookie: () => serializeCookie,
69
77
  validatePasswordPolicy: () => validatePasswordPolicy
@@ -71,7 +79,7 @@ __export(src_exports, {
71
79
  module.exports = __toCommonJS(src_exports);
72
80
 
73
81
  // src/lixa.ts
74
- var import_crypto4 = require("crypto");
82
+ var import_crypto5 = require("crypto");
75
83
 
76
84
  // src/credentials/hasher.ts
77
85
  var import_crypto = __toESM(require("crypto"), 1);
@@ -453,10 +461,65 @@ var WeakPasswordError = class extends LixaError {
453
461
  }
454
462
  };
455
463
  var CredentialsNotConfiguredError = class extends LixaError {
456
- constructor(message = "Credentials authentication is not configured in this Lixa instance. Pass credentials: {} in LixaConfig to enable it.", details) {
464
+ constructor(message = "Credentials authentication is not configured in this Lixa instance. Pass credentials: {} or identity: ... in LixaConfig to enable it.", details) {
457
465
  super(message, "CREDENTIALS_NOT_CONFIGURED", details);
458
466
  }
459
467
  };
468
+ var IdentityProviderError = class extends LixaError {
469
+ provider;
470
+ statusCode;
471
+ constructor(message, provider, statusCode, details) {
472
+ super(message, "IDENTITY_PROVIDER_ERROR", { provider, statusCode, ...details });
473
+ this.provider = provider;
474
+ this.statusCode = statusCode;
475
+ }
476
+ };
477
+ var IdentityNotConfiguredError = class extends CredentialsNotConfiguredError {
478
+ constructor(message = "Identity provider is not configured in this Lixa instance. Pass identity: ... or credentials: {} in LixaConfig to enable it.", details) {
479
+ super(message, details);
480
+ }
481
+ };
482
+ var AccountLinkingChallengeRequiredError = class extends LixaError {
483
+ challengeToken;
484
+ email;
485
+ incomingProvider;
486
+ incomingProviderUserId;
487
+ existingUserId;
488
+ existingProviders;
489
+ hasPassword;
490
+ constructor(message, details) {
491
+ super(message, "ACCOUNT_LINKING_CHALLENGE_REQUIRED", details);
492
+ this.challengeToken = details.challengeToken;
493
+ this.email = details.email;
494
+ this.incomingProvider = details.incomingProvider;
495
+ this.incomingProviderUserId = details.incomingProviderUserId;
496
+ this.existingUserId = details.existingUserId;
497
+ this.existingProviders = details.existingProviders;
498
+ this.hasPassword = details.hasPassword;
499
+ }
500
+ };
501
+ var SSOAccountAlreadyExistsError = class extends LixaError {
502
+ existingProviders;
503
+ email;
504
+ constructor(email, existingProviders, details) {
505
+ super(
506
+ `An account with email '${email}' already exists via ${existingProviders.join(", ")}. Please sign in with your original provider and set a password in Settings.`,
507
+ "SSO_ACCOUNT_EXISTS",
508
+ { email, existingProviders, ...details }
509
+ );
510
+ this.email = email;
511
+ this.existingProviders = existingProviders;
512
+ }
513
+ };
514
+ var AccountNotVerifiedError = class extends LixaError {
515
+ constructor(identifier, details) {
516
+ super(
517
+ `Account${identifier ? ` '${identifier}'` : ""} has not been verified. Please confirm your account with the verification code sent to your email.`,
518
+ "ACCOUNT_NOT_VERIFIED",
519
+ { identifier, ...details }
520
+ );
521
+ }
522
+ };
460
523
 
461
524
  // src/credentials/credentials-manager.ts
462
525
  var CredentialsManager = class {
@@ -620,6 +683,41 @@ var CredentialsManager = class {
620
683
  await this.storage.updatePassword(user.id, newHash);
621
684
  return true;
622
685
  }
686
+ /**
687
+ * Sets a password for a user who doesn't have one yet (e.g. SSO-first users),
688
+ * or updates an existing password without requiring the old one.
689
+ * This should only be called from an authenticated context (user already verified their identity via SSO).
690
+ *
691
+ * @param params - userId and newPassword
692
+ * @returns True if password was successfully set
693
+ */
694
+ async setPassword(params) {
695
+ const policyResult = await validatePasswordPolicy(params.newPassword, this.policy);
696
+ if (!policyResult.valid) {
697
+ throw new WeakPasswordError(policyResult.errors);
698
+ }
699
+ const newHash = await this.hasher.hash(params.newPassword);
700
+ const existingUser = await this.storage.findUserById(params.userId);
701
+ if (existingUser) {
702
+ await this.storage.updatePassword(params.userId, newHash);
703
+ return true;
704
+ }
705
+ const now = Date.now();
706
+ const identifier = params.identifier || params.email || params.userId;
707
+ const normalized = this.normalizeIdentifier(identifier);
708
+ const isEmail = normalized.includes("@");
709
+ const user = {
710
+ id: params.userId,
711
+ identifier: normalized,
712
+ email: isEmail ? normalized : params.email?.trim().toLowerCase(),
713
+ username: !isEmail ? normalized : void 0,
714
+ passwordHash: newHash,
715
+ createdAt: now,
716
+ updatedAt: now
717
+ };
718
+ await this.storage.saveUser(user);
719
+ return true;
720
+ }
623
721
  /**
624
722
  * Finds a user by ID and returns safe user data.
625
723
  */
@@ -652,16 +750,394 @@ var CredentialsManager = class {
652
750
  }
653
751
  };
654
752
 
753
+ // src/identity/self-managed.ts
754
+ var import_crypto3 = __toESM(require("crypto"), 1);
755
+ var SelfManagedIdentityProvider = class {
756
+ name = "self-managed";
757
+ manager;
758
+ pendingSignups = /* @__PURE__ */ new Map();
759
+ requireVerification;
760
+ config;
761
+ constructor(configOrManager) {
762
+ if (configOrManager instanceof CredentialsManager) {
763
+ this.manager = configOrManager;
764
+ this.config = {};
765
+ this.requireVerification = false;
766
+ } else {
767
+ this.config = configOrManager || {};
768
+ this.manager = new CredentialsManager(this.config);
769
+ this.requireVerification = this.config.requireVerification ?? false;
770
+ }
771
+ }
772
+ mapUser(user) {
773
+ return {
774
+ id: user.id,
775
+ identifier: user.identifier,
776
+ username: user.username,
777
+ email: user.email,
778
+ emailVerified: true,
779
+ createdAt: user.createdAt,
780
+ updatedAt: user.updatedAt,
781
+ metadata: user.metadata,
782
+ provider: this.name
783
+ };
784
+ }
785
+ generateConfirmationCode() {
786
+ return String(import_crypto3.default.randomInt(1e5, 999999));
787
+ }
788
+ /**
789
+ * Registers a new user with optional pending verification.
790
+ * When requireVerification is true, credentials are NOT persisted to the database until confirmSignUp() is called.
791
+ */
792
+ async signUp(params) {
793
+ if (!this.requireVerification) {
794
+ const user = await this.manager.signUp(params);
795
+ return this.mapUser(user);
796
+ }
797
+ const rawIdentifier = params.username || params.identifier || params.email || "";
798
+ const trimmed = (rawIdentifier || "").trim();
799
+ if (!trimmed) {
800
+ throw new InvalidCredentialsError("Identifier cannot be empty");
801
+ }
802
+ const identifierType = this.config.identifierType || "both";
803
+ if (identifierType === "email") {
804
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
805
+ if (!emailRegex.test(trimmed)) {
806
+ throw new InvalidCredentialsError("Identifier must be a valid email address");
807
+ }
808
+ }
809
+ const normalized = trimmed.toLowerCase();
810
+ const isEmail = normalized.includes("@");
811
+ const email = params.email ? params.email.trim().toLowerCase() : isEmail ? normalized : void 0;
812
+ const username = params.username ? params.username.trim() : !isEmail ? normalized : void 0;
813
+ if (this.config.requireUsername && (!username || username.trim() === "")) {
814
+ throw new InvalidCredentialsError("Username is required");
815
+ }
816
+ if (this.config.requireEmail && (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email))) {
817
+ throw new InvalidCredentialsError("A valid email address is required");
818
+ }
819
+ const existing = await this.manager.getUserByIdentifier(normalized);
820
+ if (existing) {
821
+ throw new UserAlreadyExistsError(normalized);
822
+ }
823
+ const existingPending = this.pendingSignups.get(normalized);
824
+ if (existingPending && existingPending.expiresAt > Date.now()) {
825
+ throw new UserAlreadyExistsError(normalized);
826
+ }
827
+ const policy = { minLength: 8, maxLength: 128, ...this.config.policy };
828
+ const policyResult = await validatePasswordPolicy(params.password, policy);
829
+ if (!policyResult.valid) {
830
+ throw new WeakPasswordError(policyResult.errors);
831
+ }
832
+ const passwordHash = await this.manager.getHasher().hash(params.password);
833
+ const userId = typeof import_crypto3.default.randomUUID === "function" ? import_crypto3.default.randomUUID() : import_crypto3.default.randomBytes(16).toString("hex");
834
+ const confirmationCode = this.generateConfirmationCode();
835
+ const now = Date.now();
836
+ const pending = {
837
+ identifier: normalized,
838
+ email,
839
+ username,
840
+ passwordHash,
841
+ confirmationCode,
842
+ userId,
843
+ metadata: params.metadata,
844
+ createdAt: now,
845
+ expiresAt: now + 15 * 60 * 1e3
846
+ // 15 minutes TTL
847
+ };
848
+ this.pendingSignups.set(normalized, pending);
849
+ console.log(`[Lixa Self-Managed] Verification code for '${normalized}': ${confirmationCode}`);
850
+ return {
851
+ id: userId,
852
+ identifier: normalized,
853
+ username,
854
+ email,
855
+ emailVerified: false,
856
+ createdAt: now,
857
+ updatedAt: now,
858
+ metadata: {
859
+ ...params.metadata,
860
+ requiresVerification: true,
861
+ devConfirmationCode: confirmationCode
862
+ },
863
+ provider: this.name
864
+ };
865
+ }
866
+ /**
867
+ * Confirms a pending signup registration using the verification code.
868
+ * Only after successful confirmation is the user persisted to the database.
869
+ */
870
+ async confirmSignUp(params) {
871
+ const normalized = (params.identifier || "").trim().toLowerCase();
872
+ const pending = this.pendingSignups.get(normalized);
873
+ if (!pending) {
874
+ throw new InvalidCredentialsError("No pending registration found for this identifier. Please sign up first.");
875
+ }
876
+ if (pending.expiresAt < Date.now()) {
877
+ this.pendingSignups.delete(normalized);
878
+ throw new InvalidCredentialsError("Verification code has expired. Please sign up again.");
879
+ }
880
+ const code = (params.confirmationCode || "").trim();
881
+ if (code !== pending.confirmationCode) {
882
+ throw new InvalidCredentialsError("Invalid verification code.");
883
+ }
884
+ const storage = this.manager.getStorage();
885
+ const user = {
886
+ id: pending.userId,
887
+ identifier: pending.identifier,
888
+ email: pending.email,
889
+ username: pending.username,
890
+ passwordHash: pending.passwordHash,
891
+ createdAt: pending.createdAt,
892
+ updatedAt: Date.now(),
893
+ metadata: pending.metadata
894
+ };
895
+ await storage.saveUser(user);
896
+ this.pendingSignups.delete(normalized);
897
+ console.log(`[Lixa Self-Managed] User '${normalized}' verified and persisted to database.`);
898
+ return true;
899
+ }
900
+ /**
901
+ * Resends the verification code for a pending signup.
902
+ */
903
+ async resendConfirmationCode(params) {
904
+ const normalized = (params.identifier || "").trim().toLowerCase();
905
+ const pending = this.pendingSignups.get(normalized);
906
+ if (!pending) {
907
+ throw new InvalidCredentialsError("No pending registration found for this identifier.");
908
+ }
909
+ const newCode = this.generateConfirmationCode();
910
+ pending.confirmationCode = newCode;
911
+ pending.expiresAt = Date.now() + 15 * 60 * 1e3;
912
+ this.pendingSignups.set(normalized, pending);
913
+ console.log(`[Lixa Self-Managed] New verification code for '${normalized}': ${newCode}`);
914
+ return {
915
+ success: true,
916
+ deliveryMedium: "CONSOLE",
917
+ destination: pending.email || normalized
918
+ };
919
+ }
920
+ /**
921
+ * Authenticates a user with password.
922
+ */
923
+ async signIn(params) {
924
+ const user = await this.manager.verifyCredentials(params);
925
+ if (!user) {
926
+ throw new InvalidCredentialsError();
927
+ }
928
+ return {
929
+ user: this.mapUser(user)
930
+ };
931
+ }
932
+ /**
933
+ * Verifies credentials without throwing an error if invalid.
934
+ */
935
+ async verifyCredentials(params) {
936
+ const user = await this.manager.verifyCredentials(params);
937
+ return user ? this.mapUser(user) : null;
938
+ }
939
+ /**
940
+ * Changes a user's password.
941
+ */
942
+ async changePassword(params) {
943
+ return await this.manager.changePassword(params);
944
+ }
945
+ /**
946
+ * Retrieves a user by their unique ID.
947
+ */
948
+ async getUserById(id) {
949
+ const user = await this.manager.getUserById(id);
950
+ return user ? this.mapUser(user) : null;
951
+ }
952
+ /**
953
+ * Retrieves a user by their identifier (email or username).
954
+ */
955
+ async getUserByIdentifier(identifier) {
956
+ const user = await this.manager.getUserByIdentifier(identifier);
957
+ return user ? this.mapUser(user) : null;
958
+ }
959
+ /**
960
+ * Deletes a user from storage if supported by the storage adapter.
961
+ */
962
+ async deleteUser(id) {
963
+ const storage = this.manager.getStorage();
964
+ if (typeof storage.deleteUser === "function") {
965
+ await storage.deleteUser(id);
966
+ }
967
+ }
968
+ /**
969
+ * Returns the underlying CredentialsManager instance.
970
+ */
971
+ getCredentialsManager() {
972
+ return this.manager;
973
+ }
974
+ };
975
+
976
+ // src/identity/composite.ts
977
+ var CompositeIdentityProvider = class {
978
+ name = "composite";
979
+ providers;
980
+ defaultProvider;
981
+ oauthProviders;
982
+ constructor(options) {
983
+ if (!options.providers || Object.keys(options.providers).length === 0) {
984
+ throw new IdentityProviderError(
985
+ "CompositeIdentityProvider requires at least one child provider in 'providers'",
986
+ this.name
987
+ );
988
+ }
989
+ this.providers = options.providers;
990
+ this.defaultProvider = options.defaultProvider;
991
+ this.oauthProviders = options.oauthProviders;
992
+ if (this.defaultProvider && !this.providers[this.defaultProvider]) {
993
+ throw new IdentityProviderError(
994
+ `Configured defaultProvider '${String(this.defaultProvider)}' does not exist in 'providers'. Available: ${Object.keys(this.providers).join(", ")}`,
995
+ this.name
996
+ );
997
+ }
998
+ }
999
+ /**
1000
+ * Resolves the target child provider based on explicit parameter or defaultProvider.
1001
+ * Throws an error immediately if the provider cannot be resolved or does not exist.
1002
+ */
1003
+ selectProvider(providerName) {
1004
+ const targetKey = providerName || this.defaultProvider;
1005
+ if (!targetKey) {
1006
+ throw new IdentityProviderError(
1007
+ `No provider specified and no defaultProvider configured in CompositeIdentityProvider. Available providers: ${Object.keys(this.providers).join(", ")}`,
1008
+ this.name
1009
+ );
1010
+ }
1011
+ const provider = this.providers[targetKey];
1012
+ if (!provider) {
1013
+ throw new IdentityProviderError(
1014
+ `Provider '${targetKey}' is not configured in CompositeIdentityProvider. Available providers: ${Object.keys(this.providers).join(", ")}`,
1015
+ this.name
1016
+ );
1017
+ }
1018
+ return provider;
1019
+ }
1020
+ /**
1021
+ * Returns all OAuth / OIDC provider configurations aggregated across this composite provider
1022
+ * and any child providers exposing `getOAuthProviders()` or `getProviders()`.
1023
+ */
1024
+ getProviders() {
1025
+ const result = {};
1026
+ if (this.oauthProviders) {
1027
+ Object.assign(result, this.oauthProviders);
1028
+ }
1029
+ for (const child of Object.values(this.providers)) {
1030
+ if (typeof child.getOAuthProviders === "function") {
1031
+ Object.assign(result, child.getOAuthProviders());
1032
+ } else if (typeof child.getProviders === "function") {
1033
+ Object.assign(result, child.getProviders());
1034
+ }
1035
+ }
1036
+ return result;
1037
+ }
1038
+ async signUp(params) {
1039
+ const provider = this.selectProvider(params.provider);
1040
+ return provider.signUp(params);
1041
+ }
1042
+ async signIn(params) {
1043
+ const provider = this.selectProvider(params.provider);
1044
+ return provider.signIn(params);
1045
+ }
1046
+ async verifyCredentials(params) {
1047
+ const provider = this.selectProvider(params.provider);
1048
+ if (typeof provider.verifyCredentials === "function") {
1049
+ return provider.verifyCredentials(params);
1050
+ }
1051
+ const result = await provider.signIn(params);
1052
+ return result.user;
1053
+ }
1054
+ async changePassword(params) {
1055
+ const provider = this.selectProvider(params.provider);
1056
+ if (typeof provider.changePassword !== "function") {
1057
+ throw new IdentityProviderError(
1058
+ `Password change is not supported by provider '${provider.name}'`,
1059
+ this.name
1060
+ );
1061
+ }
1062
+ return provider.changePassword(params);
1063
+ }
1064
+ async forgotPassword(params) {
1065
+ const provider = this.selectProvider(params.provider);
1066
+ if (typeof provider.forgotPassword !== "function") {
1067
+ throw new IdentityProviderError(
1068
+ `Password reset is not supported by provider '${provider.name}'`,
1069
+ this.name
1070
+ );
1071
+ }
1072
+ return provider.forgotPassword(params);
1073
+ }
1074
+ async confirmPasswordReset(params) {
1075
+ const provider = this.selectProvider(params.provider);
1076
+ if (typeof provider.confirmPasswordReset !== "function") {
1077
+ throw new IdentityProviderError(
1078
+ `Confirm password reset is not supported by provider '${provider.name}'`,
1079
+ this.name
1080
+ );
1081
+ }
1082
+ return provider.confirmPasswordReset(params);
1083
+ }
1084
+ async confirmSignUp(params) {
1085
+ const provider = this.selectProvider(params.provider);
1086
+ if (typeof provider.confirmSignUp !== "function") {
1087
+ throw new IdentityProviderError(
1088
+ `Account confirmation is not supported by provider '${provider.name}'`,
1089
+ this.name
1090
+ );
1091
+ }
1092
+ return provider.confirmSignUp(params);
1093
+ }
1094
+ async resendConfirmationCode(params) {
1095
+ const provider = this.selectProvider(params.provider);
1096
+ if (typeof provider.resendConfirmationCode !== "function") {
1097
+ throw new IdentityProviderError(
1098
+ `Resending confirmation code is not supported by provider '${provider.name}'`,
1099
+ this.name
1100
+ );
1101
+ }
1102
+ return provider.resendConfirmationCode(params);
1103
+ }
1104
+ async getUserById(id, providerName) {
1105
+ const provider = this.selectProvider(providerName);
1106
+ if (typeof provider.getUserById !== "function") {
1107
+ return null;
1108
+ }
1109
+ return provider.getUserById(id);
1110
+ }
1111
+ async getUserByIdentifier(identifier, providerName) {
1112
+ const provider = this.selectProvider(providerName);
1113
+ if (typeof provider.getUserByIdentifier !== "function") {
1114
+ return null;
1115
+ }
1116
+ return provider.getUserByIdentifier(identifier);
1117
+ }
1118
+ async deleteUser(id, providerName) {
1119
+ const provider = this.selectProvider(providerName);
1120
+ if (typeof provider.deleteUser !== "function") {
1121
+ throw new IdentityProviderError(
1122
+ `User deletion is not supported by provider '${provider.name}'`,
1123
+ this.name
1124
+ );
1125
+ }
1126
+ return provider.deleteUser(id);
1127
+ }
1128
+ };
1129
+
655
1130
  // src/types.ts
656
1131
  var AccountLinkingStrategy = /* @__PURE__ */ ((AccountLinkingStrategy2) => {
657
1132
  AccountLinkingStrategy2["AUTO_LINK_BY_VERIFIED_EMAIL"] = "AUTO_LINK_BY_VERIFIED_EMAIL";
658
1133
  AccountLinkingStrategy2["ISOLATED"] = "ISOLATED";
1134
+ AccountLinkingStrategy2["STEP_UP_VERIFICATION"] = "STEP_UP_VERIFICATION";
659
1135
  return AccountLinkingStrategy2;
660
1136
  })(AccountLinkingStrategy || {});
661
1137
 
662
1138
  // src/dao/state-cache.ts
663
1139
  var import_node_cache = __toESM(require("node-cache"), 1);
664
- var import_crypto3 = require("crypto");
1140
+ var import_crypto4 = require("crypto");
665
1141
  var LocalStateHandler = class {
666
1142
  cache;
667
1143
  stateStorage;
@@ -681,8 +1157,8 @@ var LocalStateHandler = class {
681
1157
  }
682
1158
  // Default generateState implementation
683
1159
  async generateState(provider) {
684
- const state = (0, import_crypto3.randomBytes)(16).toString("hex");
685
- const codeVerifier = (0, import_crypto3.randomBytes)(32).toString("hex");
1160
+ const state = (0, import_crypto4.randomBytes)(16).toString("hex");
1161
+ const codeVerifier = (0, import_crypto4.randomBytes)(32).toString("hex");
686
1162
  return {
687
1163
  state,
688
1164
  data: {
@@ -699,7 +1175,7 @@ var LocalStateHandler = class {
699
1175
  };
700
1176
 
701
1177
  // src/lixa.ts
702
- var import_crypto5 = __toESM(require("crypto"), 1);
1178
+ var import_crypto6 = __toESM(require("crypto"), 1);
703
1179
 
704
1180
  // src/dao/session-cache.ts
705
1181
  var import_node_cache2 = __toESM(require("node-cache"), 1);
@@ -757,10 +1233,7 @@ var LocalSessionHandler = class {
757
1233
  if (!tokenData.access_token || typeof tokenData.access_token !== "string") {
758
1234
  throw new Error("No valid access token found in OAuth response");
759
1235
  }
760
- const session = {
761
- token: tokenData.access_token,
762
- raw: tokenData
763
- };
1236
+ const session = {};
764
1237
  return session;
765
1238
  }
766
1239
  // PascalCase alias for backward compatibility
@@ -821,7 +1294,7 @@ async function fetchUserInfo(accessToken, userInfoEndpoint) {
821
1294
  family_name: "family_name" in data ? String(data.family_name) : void 0,
822
1295
  name: "name" in data ? String(data.name) : void 0,
823
1296
  picture: "picture" in data ? String(data.picture) : void 0,
824
- email_verified: "email_verified" in data ? Boolean(data.email_verified) : void 0,
1297
+ email_verified: "email_verified" in data ? data.email_verified === true || data.email_verified === "true" : void 0,
825
1298
  iss: "iss" in data ? String(data.iss) : void 0
826
1299
  };
827
1300
  }
@@ -829,68 +1302,213 @@ async function extractUserInfo(tokenData, providerMetadata) {
829
1302
  let userInfo;
830
1303
  const userInfoEndpoint = providerMetadata.endpoints.userInfo;
831
1304
  if (tokenData.id_token) {
832
- userInfo = decodeIdToken(tokenData.id_token);
833
- } else if (tokenData.access_token) {
1305
+ try {
1306
+ userInfo = decodeIdToken(tokenData.id_token);
1307
+ if (userInfo && userInfo.email_verified !== void 0) {
1308
+ userInfo.email_verified = userInfo.email_verified === true || userInfo.email_verified === "true";
1309
+ }
1310
+ } catch {
1311
+ }
1312
+ }
1313
+ if ((!userInfo || !userInfo.email) && tokenData.access_token && userInfoEndpoint) {
1314
+ try {
1315
+ const fetched = await fetchUserInfo(tokenData.access_token, userInfoEndpoint);
1316
+ userInfo = { ...fetched, ...userInfo || {}, email: fetched.email || userInfo?.email || "" };
1317
+ if (fetched.email_verified !== void 0 && (userInfo.email_verified === void 0 || userInfo.email_verified === false)) {
1318
+ userInfo.email_verified = fetched.email_verified === true || fetched.email_verified === "true";
1319
+ }
1320
+ } catch {
1321
+ }
1322
+ } else if (!userInfo && tokenData.access_token && userInfoEndpoint) {
834
1323
  userInfo = await fetchUserInfo(tokenData.access_token, userInfoEndpoint);
835
- } else {
1324
+ }
1325
+ if (!userInfo) {
836
1326
  throw new Error("No ID token or access token available to fetch user info");
837
1327
  }
838
1328
  return { userInfo };
839
1329
  }
840
1330
 
841
- // src/lixa.ts
842
- var Lixa = class _Lixa {
843
- static DEFAULT_PROVIDERS = /* @__PURE__ */ new Map();
844
- static CONFIGURED_PROVIDERS = /* @__PURE__ */ new Map();
845
- // Legacy registry for backward compatibility
846
- // Instance-scoped fallback handlers to ensure no cross-instance state pollution
847
- localStateHandler;
848
- localSessionHandler;
849
- localResourceHandler;
850
- userResourceStore = /* @__PURE__ */ new Map();
851
- refreshMutexes = /* @__PURE__ */ new Map();
852
- credentialsManager;
853
- config;
854
- stateHandler;
855
- sessionHandler;
856
- resourceHandler;
857
- debug;
858
- /**
859
- * Creates a new Lixa instance with the provided configuration.
860
- *
861
- * @remarks
862
- * Providers can be passed inline in the configuration using the `provider` field.
863
- * Provider resolution priority: inline custom provider \> default providers \> legacy registry.
864
- *
865
- * @param config - The configuration object containing provider settings and optional session strategy
866
- *
867
- * @throws Error when provider configuration is missing required fields
868
- * @throws Error when provider implementation is missing required properties
869
- * @throws Error when provider is not available and no inline implementation is provided
870
- */
871
- constructor(config) {
872
- this.config = config;
873
- this.debug = config.debug || false;
874
- this.localStateHandler = new LocalStateHandler();
875
- this.localSessionHandler = new LocalSessionHandler();
876
- this.localResourceHandler = {
877
- resourceStorage: {
878
- saveResource: async (userId, provider, resource) => {
879
- let userMap = this.userResourceStore.get(userId);
880
- if (!userMap) {
881
- userMap = /* @__PURE__ */ new Map();
882
- this.userResourceStore.set(userId, userMap);
883
- }
884
- userMap.set(provider.toLowerCase(), resource);
885
- },
886
- getResource: async (userId, provider) => {
887
- const userMap = this.userResourceStore.get(userId);
888
- return userMap?.get(provider.toLowerCase()) || null;
889
- },
890
- getUserResources: async (userId) => {
891
- const userMap = this.userResourceStore.get(userId);
892
- const result = {};
893
- if (userMap) {
1331
+ // src/utils/cookies.ts
1332
+ var DEFAULT_SESSION_COOKIE_NAME = "lixa_session";
1333
+ var DEFAULT_STATE_COOKIE_NAME = "lixa_oauth_state";
1334
+ var DEFAULT_SESSION_MAX_AGE_SECONDS = 24 * 60 * 60;
1335
+ var DEFAULT_STATE_MAX_AGE_SECONDS = 5 * 60;
1336
+ function isProductionEnvironment() {
1337
+ return typeof process !== "undefined" && process.env?.NODE_ENV === "production";
1338
+ }
1339
+ function serializeCookie(name, value, options) {
1340
+ const isProd = isProductionEnvironment();
1341
+ const path = options?.path ?? "/";
1342
+ const httpOnly = options?.httpOnly ?? true;
1343
+ const secure = options?.secure ?? isProd;
1344
+ const sameSite = options?.sameSite ?? "lax";
1345
+ const parts = [`${encodeURIComponent(name)}=${encodeURIComponent(value)}`];
1346
+ if (path) {
1347
+ parts.push(`Path=${path}`);
1348
+ }
1349
+ if (typeof options?.maxAge === "number") {
1350
+ parts.push(`Max-Age=${Math.floor(options.maxAge)}`);
1351
+ const expires = new Date(Date.now() + options.maxAge * 1e3).toUTCString();
1352
+ parts.push(`Expires=${expires}`);
1353
+ }
1354
+ if (options?.domain) {
1355
+ parts.push(`Domain=${options.domain}`);
1356
+ }
1357
+ if (httpOnly) {
1358
+ parts.push("HttpOnly");
1359
+ }
1360
+ if (secure) {
1361
+ parts.push("Secure");
1362
+ }
1363
+ if (sameSite) {
1364
+ const capitalized = sameSite.charAt(0).toUpperCase() + sameSite.slice(1).toLowerCase();
1365
+ parts.push(`SameSite=${capitalized}`);
1366
+ }
1367
+ return parts.join("; ");
1368
+ }
1369
+ function createSessionCookie(sessionId, options) {
1370
+ const isProd = isProductionEnvironment();
1371
+ const resolvedOptions = {
1372
+ name: options?.name || DEFAULT_SESSION_COOKIE_NAME,
1373
+ path: options?.path ?? "/",
1374
+ maxAge: options?.maxAge ?? DEFAULT_SESSION_MAX_AGE_SECONDS,
1375
+ httpOnly: options?.httpOnly ?? true,
1376
+ secure: options?.secure ?? isProd,
1377
+ sameSite: options?.sameSite ?? "lax",
1378
+ domain: options?.domain
1379
+ };
1380
+ const name = resolvedOptions.name;
1381
+ const header = serializeCookie(name, sessionId, resolvedOptions);
1382
+ return {
1383
+ name,
1384
+ value: sessionId,
1385
+ options: resolvedOptions,
1386
+ header
1387
+ };
1388
+ }
1389
+ function clearSessionCookie(options) {
1390
+ const isProd = isProductionEnvironment();
1391
+ const resolvedOptions = {
1392
+ name: options?.name || DEFAULT_SESSION_COOKIE_NAME,
1393
+ path: options?.path ?? "/",
1394
+ maxAge: 0,
1395
+ httpOnly: options?.httpOnly ?? true,
1396
+ secure: options?.secure ?? isProd,
1397
+ sameSite: options?.sameSite ?? "lax",
1398
+ domain: options?.domain
1399
+ };
1400
+ const name = resolvedOptions.name;
1401
+ const header = serializeCookie(name, "", resolvedOptions);
1402
+ return {
1403
+ name,
1404
+ value: "",
1405
+ options: resolvedOptions,
1406
+ header
1407
+ };
1408
+ }
1409
+ function createStateCookie(state, options) {
1410
+ const isProd = isProductionEnvironment();
1411
+ const resolvedOptions = {
1412
+ name: options?.name || DEFAULT_STATE_COOKIE_NAME,
1413
+ path: options?.path ?? "/",
1414
+ maxAge: options?.maxAge ?? DEFAULT_STATE_MAX_AGE_SECONDS,
1415
+ httpOnly: options?.httpOnly ?? true,
1416
+ secure: options?.secure ?? isProd,
1417
+ sameSite: options?.sameSite ?? "lax",
1418
+ domain: options?.domain
1419
+ };
1420
+ const name = resolvedOptions.name;
1421
+ const header = serializeCookie(name, state, resolvedOptions);
1422
+ return {
1423
+ name,
1424
+ value: state,
1425
+ options: resolvedOptions,
1426
+ header
1427
+ };
1428
+ }
1429
+ function clearStateCookie(options) {
1430
+ const isProd = isProductionEnvironment();
1431
+ const resolvedOptions = {
1432
+ name: options?.name || DEFAULT_STATE_COOKIE_NAME,
1433
+ path: options?.path ?? "/",
1434
+ maxAge: 0,
1435
+ httpOnly: options?.httpOnly ?? true,
1436
+ secure: options?.secure ?? isProd,
1437
+ sameSite: options?.sameSite ?? "lax",
1438
+ domain: options?.domain
1439
+ };
1440
+ const name = resolvedOptions.name;
1441
+ const header = serializeCookie(name, "", resolvedOptions);
1442
+ return {
1443
+ name,
1444
+ value: "",
1445
+ options: resolvedOptions,
1446
+ header
1447
+ };
1448
+ }
1449
+
1450
+ // src/lixa.ts
1451
+ var Lixa = class _Lixa {
1452
+ static DEFAULT_PROVIDERS = /* @__PURE__ */ new Map();
1453
+ static CONFIGURED_PROVIDERS = /* @__PURE__ */ new Map();
1454
+ // Legacy registry for backward compatibility
1455
+ // Instance-scoped fallback handlers to ensure no cross-instance state pollution
1456
+ localStateHandler;
1457
+ localSessionHandler;
1458
+ localResourceHandler;
1459
+ localAccountHandler;
1460
+ userResourceStore = /* @__PURE__ */ new Map();
1461
+ userAccountStore = /* @__PURE__ */ new Map();
1462
+ refreshMutexes = /* @__PURE__ */ new Map();
1463
+ identityProvider;
1464
+ credentialsManager;
1465
+ config;
1466
+ stateHandler;
1467
+ sessionHandler;
1468
+ resourceHandler;
1469
+ accountHandler;
1470
+ debug;
1471
+ sessionCookieResolver;
1472
+ /**
1473
+ * Creates a new Lixa instance with the provided configuration.
1474
+ *
1475
+ * @remarks
1476
+ * Providers can be passed inline in the configuration using the `provider` field.
1477
+ * Provider resolution priority: inline custom provider \> default providers \> legacy registry.
1478
+ *
1479
+ * @param config - The configuration object containing provider settings and optional session strategy
1480
+ *
1481
+ * @throws Error when provider configuration is missing required fields
1482
+ * @throws Error when provider implementation is missing required properties
1483
+ * @throws Error when provider is not available and no inline implementation is provided
1484
+ */
1485
+ constructor(config) {
1486
+ this.sessionCookieResolver = config.sessionCookieName;
1487
+ this.config = {
1488
+ ...config,
1489
+ ...config.providers ? { providers: config.providers } : {}
1490
+ };
1491
+ this.debug = config.debug || false;
1492
+ this.localStateHandler = new LocalStateHandler();
1493
+ this.localSessionHandler = new LocalSessionHandler();
1494
+ this.localResourceHandler = {
1495
+ resourceStorage: {
1496
+ saveResource: async (userId, provider, resource) => {
1497
+ let userMap = this.userResourceStore.get(userId);
1498
+ if (!userMap) {
1499
+ userMap = /* @__PURE__ */ new Map();
1500
+ this.userResourceStore.set(userId, userMap);
1501
+ }
1502
+ userMap.set(provider.toLowerCase(), resource);
1503
+ },
1504
+ getResource: async (userId, provider) => {
1505
+ const userMap = this.userResourceStore.get(userId);
1506
+ return userMap?.get(provider.toLowerCase()) || null;
1507
+ },
1508
+ getUserResources: async (userId) => {
1509
+ const userMap = this.userResourceStore.get(userId);
1510
+ const result = {};
1511
+ if (userMap) {
894
1512
  for (const [p, r] of userMap.entries()) {
895
1513
  result[p] = r;
896
1514
  }
@@ -905,19 +1523,84 @@ var Lixa = class _Lixa {
905
1523
  }
906
1524
  }
907
1525
  };
908
- this.stateHandler = config.stateHandler || this.localStateHandler;
909
- this.sessionHandler = config.sessionHandler || this.localSessionHandler;
910
- this.resourceHandler = config.resourceHandler || this.localResourceHandler;
911
- if (config.credentials !== void 0 && config.credentials.enabled !== false) {
912
- this.credentialsManager = new CredentialsManager(config.credentials);
913
- }
1526
+ this.localAccountHandler = {
1527
+ accountStorage: {
1528
+ saveAccount: async (userId, provider, account) => {
1529
+ let userMap = this.userAccountStore.get(userId);
1530
+ if (!userMap) {
1531
+ userMap = /* @__PURE__ */ new Map();
1532
+ this.userAccountStore.set(userId, userMap);
1533
+ }
1534
+ userMap.set(provider.toLowerCase(), account);
1535
+ },
1536
+ getAccount: async (userId, provider) => {
1537
+ const userMap = this.userAccountStore.get(userId);
1538
+ return userMap?.get(provider.toLowerCase()) || null;
1539
+ },
1540
+ getUserAccounts: async (userId) => {
1541
+ const userMap = this.userAccountStore.get(userId);
1542
+ const result = {};
1543
+ if (userMap) {
1544
+ for (const [p, a] of userMap.entries()) {
1545
+ result[p] = a;
1546
+ }
1547
+ }
1548
+ return result;
1549
+ },
1550
+ deleteAccount: async (userId, provider) => {
1551
+ const userMap = this.userAccountStore.get(userId);
1552
+ if (userMap) {
1553
+ userMap.delete(provider.toLowerCase());
1554
+ }
1555
+ }
1556
+ }
1557
+ };
1558
+ const storage = config.storage;
1559
+ this.stateHandler = config.stateHandler || (storage?.stateStorage ? { stateStorage: storage.stateStorage } : this.localStateHandler);
1560
+ this.sessionHandler = config.sessionHandler || (storage?.sessionStorage ? { sessionStorage: storage.sessionStorage } : this.localSessionHandler);
1561
+ this.resourceHandler = config.resourceHandler || (storage?.resourceStorage ? { resourceStorage: storage.resourceStorage } : this.localResourceHandler);
1562
+ this.accountHandler = storage?.accountStorage ? { accountStorage: storage.accountStorage } : this.localAccountHandler;
1563
+ const identityConfig = config.identityProvider || config.identity;
1564
+ if (identityConfig) {
1565
+ if (typeof identityConfig.signUp === "function") {
1566
+ this.identityProvider = identityConfig;
1567
+ if (this.identityProvider instanceof SelfManagedIdentityProvider) {
1568
+ this.credentialsManager = this.identityProvider.getCredentialsManager();
1569
+ } else if (this.identityProvider instanceof CompositeIdentityProvider) {
1570
+ for (const child of Object.values(this.identityProvider.providers)) {
1571
+ if (child instanceof SelfManagedIdentityProvider) {
1572
+ this.credentialsManager = child.getCredentialsManager();
1573
+ break;
1574
+ }
1575
+ }
1576
+ }
1577
+ } else {
1578
+ const selfManaged = new SelfManagedIdentityProvider(identityConfig);
1579
+ this.identityProvider = selfManaged;
1580
+ this.credentialsManager = selfManaged.getCredentialsManager();
1581
+ }
1582
+ } else if (config.credentials !== void 0 && config.credentials.enabled !== false) {
1583
+ const selfManaged = new SelfManagedIdentityProvider(config.credentials);
1584
+ this.identityProvider = selfManaged;
1585
+ this.credentialsManager = selfManaged.getCredentialsManager();
1586
+ }
1587
+ if (this.identityProvider && typeof this.identityProvider.getProviders === "function") {
1588
+ const idpOAuthProviders = this.identityProvider.getProviders();
1589
+ this.config.providers = {
1590
+ ...idpOAuthProviders,
1591
+ ...this.config.providers || {}
1592
+ };
1593
+ }
1594
+ const oauthProviders = this.config.providers;
914
1595
  this.log("INFO", "Init", "Initializing Lixa instance", {
915
- providers: config.providers ? Object.keys(config.providers) : [],
916
- credentialsEnabled: this.credentialsManager !== void 0,
1596
+ providers: oauthProviders ? Object.keys(oauthProviders) : [],
1597
+ identityProvider: this.identityProvider ? this.identityProvider.name : void 0,
1598
+ identityEnabled: this.identityProvider !== void 0,
1599
+ credentialsEnabled: this.identityProvider !== void 0,
917
1600
  debug: this.debug
918
1601
  });
919
- if (config.providers) {
920
- for (const [providerName, providerConfig] of Object.entries(config.providers)) {
1602
+ if (oauthProviders) {
1603
+ for (const [providerName, providerConfig] of Object.entries(oauthProviders)) {
921
1604
  const name = providerName.toLowerCase();
922
1605
  const typedConfig = providerConfig;
923
1606
  this.validateProviderConfig(providerName, typedConfig);
@@ -946,7 +1629,22 @@ var Lixa = class _Lixa {
946
1629
  * @throws InvalidProviderConfigError when required fields are missing or invalid
947
1630
  */
948
1631
  validateProviderConfig(name, config) {
949
- const requiredFields = ["clientId", "clientSecret", "redirectUri", "scopes"];
1632
+ const hasScopes = Array.isArray(config.scopes) && config.scopes.length > 0 || config.scopes && typeof config.scopes === "object" && !Array.isArray(config.scopes) && (config.scopes.auth && config.scopes.auth.length > 0 || config.scopes.resource && config.scopes.resource.length > 0) || config.authScopes && config.authScopes.length > 0 || config.resourceScopes && config.resourceScopes.length > 0;
1633
+ if (!hasScopes) {
1634
+ const p = config.provider;
1635
+ if (p?.defaultScopes && Array.isArray(p.defaultScopes) && p.defaultScopes.length > 0) {
1636
+ config.scopes = { auth: [...p.defaultScopes] };
1637
+ } else if (p?.authScopes && Array.isArray(p.authScopes) && p.authScopes.length > 0) {
1638
+ config.scopes = { auth: [...p.authScopes] };
1639
+ } else if (name === "google" || name.startsWith("google")) {
1640
+ config.scopes = { auth: ["openid", "profile", "email"] };
1641
+ } else if (name === "github" || name.startsWith("github")) {
1642
+ config.scopes = { auth: ["read:user", "user:email"] };
1643
+ } else if (name.startsWith("cognito")) {
1644
+ config.scopes = { auth: ["openid", "email", "profile"] };
1645
+ }
1646
+ }
1647
+ const requiredFields = ["clientId", "clientSecret", "redirectUri"];
950
1648
  const missingFields = requiredFields.filter((field) => {
951
1649
  const value = config[field];
952
1650
  return value === void 0 || value === null || typeof value === "string" && value.trim() === "";
@@ -957,16 +1655,33 @@ var Lixa = class _Lixa {
957
1655
  { provider: name, missingFields }
958
1656
  );
959
1657
  }
960
- if (!Array.isArray(config.scopes)) {
961
- throw new InvalidProviderConfigError(
962
- `Provider '${name}' configuration error: 'scopes' must be an array of strings`,
963
- { provider: name }
964
- );
965
- }
966
- if (config.scopes.length === 0) {
1658
+ if (config.scopes) {
1659
+ if (Array.isArray(config.scopes)) {
1660
+ if (config.scopes.length === 0) {
1661
+ throw new InvalidProviderConfigError(
1662
+ `Provider '${name}' configuration error: 'scopes' array cannot be empty`,
1663
+ { provider: name }
1664
+ );
1665
+ }
1666
+ } else if (typeof config.scopes === "object") {
1667
+ const hasAuth = Array.isArray(config.scopes.auth) && config.scopes.auth.length > 0;
1668
+ const hasResource = Array.isArray(config.scopes.resource) && config.scopes.resource.length > 0;
1669
+ if (!hasAuth && !hasResource) {
1670
+ throw new InvalidProviderConfigError(
1671
+ `Provider '${name}' configuration error: 'scopes' object must specify non-empty 'auth' or 'resource' array`,
1672
+ { provider: name }
1673
+ );
1674
+ }
1675
+ } else {
1676
+ throw new InvalidProviderConfigError(
1677
+ `Provider '${name}' configuration error: 'scopes' must be an array of strings or { auth?: string[]; resource?: string[] }`,
1678
+ { provider: name }
1679
+ );
1680
+ }
1681
+ } else if (!config.authScopes && !config.resourceScopes) {
967
1682
  throw new InvalidProviderConfigError(
968
- `Provider '${name}' configuration error: 'scopes' array cannot be empty`,
969
- { provider: name }
1683
+ `Provider '${name}' configuration is missing required fields: scopes`,
1684
+ { provider: name, missingFields: ["scopes"] }
970
1685
  );
971
1686
  }
972
1687
  }
@@ -1139,7 +1854,7 @@ var Lixa = class _Lixa {
1139
1854
  * The state parameter is used to prevent CSRF attacks in OAuth flows.
1140
1855
  */
1141
1856
  static generateRandomState() {
1142
- return (0, import_crypto4.randomBytes)(16).toString("hex");
1857
+ return (0, import_crypto5.randomBytes)(16).toString("hex");
1143
1858
  }
1144
1859
  /**
1145
1860
  * Generates a cryptographically secure code verifier for PKCE flows.
@@ -1176,7 +1891,7 @@ var Lixa = class _Lixa {
1176
1891
  * @internal
1177
1892
  */
1178
1893
  static generateCodeVerifier() {
1179
- return (0, import_crypto4.randomBytes)(32).toString("hex");
1894
+ return (0, import_crypto5.randomBytes)(32).toString("hex");
1180
1895
  }
1181
1896
  /**
1182
1897
  * Generates a code challenge from a code verifier for PKCE flows.
@@ -1222,7 +1937,7 @@ var Lixa = class _Lixa {
1222
1937
  * @internal
1223
1938
  */
1224
1939
  static buildCodeChallenge(codeVerifier) {
1225
- const hash = import_crypto5.default.createHash("sha256").update(codeVerifier).digest("base64");
1940
+ const hash = import_crypto6.default.createHash("sha256").update(codeVerifier).digest("base64");
1226
1941
  return hash.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1227
1942
  }
1228
1943
  /**
@@ -1264,8 +1979,8 @@ var Lixa = class _Lixa {
1264
1979
  await storage.saveState(stateValue, generated.data, 300);
1265
1980
  } else {
1266
1981
  this.log("INFO", "State", "Using default state generation");
1267
- stateValue = state || (0, import_crypto4.randomBytes)(16).toString("hex");
1268
- codeVerifier = (0, import_crypto4.randomBytes)(32).toString("hex");
1982
+ stateValue = state || (0, import_crypto5.randomBytes)(16).toString("hex");
1983
+ codeVerifier = (0, import_crypto5.randomBytes)(32).toString("hex");
1269
1984
  const storage = this.stateHandler.stateStorage || this.localStateHandler.stateStorage;
1270
1985
  await storage.saveState(
1271
1986
  stateValue,
@@ -1280,12 +1995,17 @@ var Lixa = class _Lixa {
1280
1995
  }
1281
1996
  const codeChallenge = _Lixa.buildCodeChallenge(codeVerifier);
1282
1997
  this.log("INFO", "State", `Saved state for provider: ${providerType}`, { state: stateValue });
1998
+ const rawAuthScopes = providerConfig.authScopes || (Array.isArray(providerConfig.scopes) ? providerConfig.scopes : providerConfig.scopes?.auth) || [];
1283
1999
  const authNScopes = this.resolveAuthNScopes(
1284
2000
  providerType,
1285
- providerConfig.scopes,
2001
+ rawAuthScopes,
1286
2002
  providerImpl,
1287
2003
  providerConfig.allowNonAuthScopes
1288
2004
  );
2005
+ const authorizationParams = {
2006
+ ...providerConfig.extraConfig || {},
2007
+ ...providerConfig.authorizationParams || {}
2008
+ };
1289
2009
  const params = new URLSearchParams({
1290
2010
  client_id: providerConfig.clientId,
1291
2011
  redirect_uri: providerConfig.redirectUri,
@@ -1294,7 +2014,7 @@ var Lixa = class _Lixa {
1294
2014
  response_type: "code",
1295
2015
  code_challenge: codeChallenge,
1296
2016
  code_challenge_method: "S256",
1297
- ...providerConfig.extraConfig
2017
+ ...authorizationParams
1298
2018
  });
1299
2019
  const authUrl = `${providerImpl.authorizationEndpoint}?${params.toString()}`;
1300
2020
  this.log("INFO", "Auth", `Authorization URL generated successfully`, {
@@ -1368,7 +2088,8 @@ var Lixa = class _Lixa {
1368
2088
  async handleCallback({
1369
2089
  provider,
1370
2090
  code,
1371
- state
2091
+ state,
2092
+ sessionId
1372
2093
  }) {
1373
2094
  const providerType = String(provider).toLowerCase();
1374
2095
  this.log("INFO", "Auth", `Handling OAuth callback for provider: ${providerType}`);
@@ -1432,53 +2153,156 @@ var Lixa = class _Lixa {
1432
2153
  }
1433
2154
  const session = await generateSession(tokens, providerMetadata);
1434
2155
  session.provider = session.provider || providerType;
2156
+ session.token = session.token || tokens.access_token;
2157
+ session.raw = session.raw || tokens;
2158
+ if (extractedUserInfo?.sub && !session.userId) {
2159
+ session.userId = extractedUserInfo.sub;
2160
+ } else if (extractedUserInfo?.id && !session.userId) {
2161
+ session.userId = String(extractedUserInfo.id);
2162
+ }
1435
2163
  if (extractedUserInfo?.email && !session.email) {
1436
2164
  session.email = extractedUserInfo.email;
1437
2165
  }
1438
2166
  const sessionStorage = this.sessionHandler.sessionStorage || this.localSessionHandler.sessionStorage;
1439
2167
  const linkingConfig = this.config.accountLinking;
1440
- const mode = String(linkingConfig?.mode || "");
1441
- const isLinkByEmail = mode === "AUTO_LINK_BY_VERIFIED_EMAIL" /* AUTO_LINK_BY_VERIFIED_EMAIL */ || mode === "AUTO_LINK_BY_VERIFIED_EMAIL" || mode === "linkByEmail";
2168
+ const mode = typeof linkingConfig === "object" ? String(linkingConfig?.mode || "") : "";
2169
+ const isLinkByEmail = linkingConfig === true || linkingConfig === "linkByEmail" || linkingConfig === "AUTO_LINK_BY_VERIFIED_EMAIL" /* AUTO_LINK_BY_VERIFIED_EMAIL */ || mode === "AUTO_LINK_BY_VERIFIED_EMAIL" /* AUTO_LINK_BY_VERIFIED_EMAIL */ || mode === "AUTO_LINK_BY_VERIFIED_EMAIL" || mode === "linkByEmail";
2170
+ const isStepUpMode = linkingConfig === "STEP_UP_VERIFICATION" || linkingConfig === "STEP_UP_VERIFICATION" /* STEP_UP_VERIFICATION */ || mode === "STEP_UP_VERIFICATION" || mode === "STEP_UP_VERIFICATION" /* STEP_UP_VERIFICATION */ || mode === "stepUp";
1442
2171
  const email = extractedUserInfo?.email;
1443
- const isVerified = extractedUserInfo?.email_verified !== false;
1444
- const requireVerified = linkingConfig?.requireVerifiedEmail ?? true;
1445
- const canLink = isLinkByEmail && email && (!requireVerified || isVerified);
2172
+ const isVerified = extractedUserInfo?.email_verified === true;
2173
+ const requireVerified = typeof linkingConfig === "object" ? linkingConfig?.requireVerifiedEmail ?? true : true;
2174
+ const canLink = (isLinkByEmail || isStepUpMode) && email && (!requireVerified || isVerified);
2175
+ if ((isLinkByEmail || isStepUpMode) && email && !isVerified && requireVerified) {
2176
+ this.log("WARN", "AccountLinking", `Rejected account linking for provider '${providerType}': email '${email}' is not cryptographically verified by the provider.`);
2177
+ }
2178
+ if (sessionId) {
2179
+ const activeSession = await sessionStorage.getSession(sessionId);
2180
+ if (activeSession) {
2181
+ this.log("INFO", "AccountLinking", `Linking provider '${providerType}' directly to active session '${sessionId}'`);
2182
+ const linkedAcc2 = {
2183
+ provider: providerType,
2184
+ providerUserId: extractedUserInfo?.sub || extractedUserInfo?.id,
2185
+ email: extractedUserInfo?.email || activeSession.email,
2186
+ linkedAt: Date.now()
2187
+ };
2188
+ if (this.accountHandler?.accountStorage) {
2189
+ const userKey = this.getUserKeyFromSession(activeSession);
2190
+ await this.accountHandler.accountStorage.saveAccount(userKey, providerType, this.sanitizeIdentityForStorage(linkedAcc2));
2191
+ }
2192
+ return sessionId;
2193
+ }
2194
+ }
2195
+ if (canLink) {
2196
+ const existingAccounts = this.accountHandler?.accountStorage?.findAccountsByEmail ? await this.accountHandler.accountStorage.findAccountsByEmail(email) : [];
2197
+ const existingSessionRecord = sessionStorage.getSessionByEmail ? await sessionStorage.getSessionByEmail(email) : null;
2198
+ const existingProviders = /* @__PURE__ */ new Set();
2199
+ let existingUserId;
2200
+ if (existingSessionRecord) {
2201
+ existingUserId = existingSessionRecord.session.userId || existingSessionRecord.session.email;
2202
+ if (existingSessionRecord.session.provider) {
2203
+ existingProviders.add(existingSessionRecord.session.provider.toLowerCase());
2204
+ }
2205
+ if (existingSessionRecord.session.accounts) {
2206
+ Object.keys(existingSessionRecord.session.accounts).forEach(
2207
+ (p) => existingProviders.add(p.toLowerCase())
2208
+ );
2209
+ }
2210
+ }
2211
+ for (const acc of existingAccounts) {
2212
+ if (!existingUserId) existingUserId = acc.userId;
2213
+ existingProviders.add(acc.provider.toLowerCase());
2214
+ }
2215
+ if (existingProviders.size > 0 && !existingProviders.has(providerType.toLowerCase())) {
2216
+ if (isStepUpMode) {
2217
+ let hasPassword = existingProviders.has("credentials") || existingProviders.has("self-managed") || existingProviders.has("cognito");
2218
+ if (!hasPassword && this.credentialsManager) {
2219
+ try {
2220
+ const credUser = await this.credentialsManager.getUserByIdentifier(email);
2221
+ if (credUser) {
2222
+ hasPassword = true;
2223
+ }
2224
+ } catch {
2225
+ }
2226
+ }
2227
+ const challengeToken = (0, import_crypto5.randomBytes)(24).toString("hex");
2228
+ const stateStorage2 = this.stateHandler.stateStorage || this.localStateHandler.stateStorage;
2229
+ await stateStorage2.saveState(
2230
+ `linking_challenge:${challengeToken}`,
2231
+ {
2232
+ provider: providerType,
2233
+ codeVerifier: "",
2234
+ createdAt: Date.now(),
2235
+ challengeToken,
2236
+ email,
2237
+ incomingProvider: providerType,
2238
+ incomingProviderUserId: extractedUserInfo?.sub || extractedUserInfo?.id,
2239
+ existingUserId: existingUserId || email,
2240
+ existingProviders: Array.from(existingProviders),
2241
+ hasPassword,
2242
+ tokens
2243
+ },
2244
+ 600
2245
+ );
2246
+ this.log(
2247
+ "WARN",
2248
+ "AccountLinking",
2249
+ `Step-up challenge required for user '${email}'. Existing: [${Array.from(existingProviders).join(
2250
+ ", "
2251
+ )}], Incoming: '${providerType}'`
2252
+ );
2253
+ throw new AccountLinkingChallengeRequiredError(
2254
+ `An account with email '${email}' already exists. Verification required before linking.`,
2255
+ {
2256
+ challengeToken,
2257
+ email,
2258
+ incomingProvider: providerType,
2259
+ incomingProviderUserId: extractedUserInfo?.sub || extractedUserInfo?.id,
2260
+ existingUserId: existingUserId || email,
2261
+ existingProviders: Array.from(existingProviders),
2262
+ hasPassword
2263
+ }
2264
+ );
2265
+ }
2266
+ }
2267
+ }
1446
2268
  if (canLink && sessionStorage.getSessionByEmail) {
1447
2269
  const existingRecord = await sessionStorage.getSessionByEmail(email);
1448
2270
  if (existingRecord) {
1449
2271
  this.log("INFO", "AccountLinking", `Linking provider '${providerType}' to existing session for email '${email}'`);
1450
2272
  const { sessionId: existingSessionId, session: existingSession } = existingRecord;
1451
- existingSession.accounts = existingSession.accounts || {};
1452
- existingSession.accounts[providerType] = {
2273
+ const linkedAcc2 = {
1453
2274
  provider: providerType,
1454
2275
  providerUserId: extractedUserInfo?.sub || extractedUserInfo?.id,
1455
2276
  email,
1456
- accessToken: tokens.access_token,
1457
- raw: tokens,
1458
2277
  linkedAt: Date.now()
1459
2278
  };
1460
2279
  existingSession.provider = providerType;
1461
2280
  existingSession.token = tokens.access_token;
1462
2281
  existingSession.raw = tokens;
1463
2282
  await sessionStorage.saveSession(existingSessionId, existingSession, 86400);
2283
+ if (this.accountHandler?.accountStorage) {
2284
+ const userKey = this.getUserKeyFromSession(existingSession);
2285
+ await this.accountHandler.accountStorage.saveAccount(userKey, providerType, this.sanitizeIdentityForStorage(linkedAcc2));
2286
+ }
1464
2287
  return existingSessionId;
1465
2288
  }
1466
2289
  }
1467
- session.accounts = session.accounts || {};
1468
- session.accounts[providerType] = {
2290
+ const linkedAcc = {
1469
2291
  provider: providerType,
1470
2292
  providerUserId: extractedUserInfo?.sub || extractedUserInfo?.id,
1471
2293
  email: extractedUserInfo?.email,
1472
- accessToken: tokens.access_token,
1473
- raw: tokens,
1474
2294
  linkedAt: Date.now()
1475
2295
  };
1476
- const sessionId = (0, import_crypto4.randomBytes)(32).toString("hex");
1477
- session.id = sessionId;
1478
- this.log("INFO", "Session", "Storing session", { sessionId });
1479
- await sessionStorage.saveSession(sessionId, session, 86400);
1480
- this.log("INFO", "Session", "Session created successfully", { sessionId });
1481
- return sessionId;
2296
+ if (this.accountHandler?.accountStorage) {
2297
+ const userKey = this.getUserKeyFromSession(session);
2298
+ await this.accountHandler.accountStorage.saveAccount(userKey, providerType, this.sanitizeIdentityForStorage(linkedAcc));
2299
+ }
2300
+ const newSessionId = (0, import_crypto5.randomBytes)(32).toString("hex");
2301
+ session.sessionId = newSessionId;
2302
+ this.log("INFO", "Session", "Storing session", { sessionId: newSessionId });
2303
+ await sessionStorage.saveSession(newSessionId, session, 86400);
2304
+ this.log("INFO", "Session", "Session created successfully", { sessionId: newSessionId });
2305
+ return newSessionId;
1482
2306
  }
1483
2307
  /**
1484
2308
  * Explicitly link a new OAuth provider account to an active session.
@@ -1527,19 +2351,28 @@ var Lixa = class _Lixa {
1527
2351
  extractedUserInfo = userInfo;
1528
2352
  } catch {
1529
2353
  }
1530
- existingSession.accounts = existingSession.accounts || {};
1531
- existingSession.accounts[providerType] = {
2354
+ const linkedAcc = {
1532
2355
  provider: providerType,
1533
2356
  providerUserId: extractedUserInfo?.sub || extractedUserInfo?.id,
1534
2357
  email: extractedUserInfo?.email,
1535
- accessToken: tokens.access_token,
1536
- raw: tokens,
1537
2358
  linkedAt: Date.now()
1538
2359
  };
1539
2360
  if (extractedUserInfo?.email && !existingSession.email) {
1540
2361
  existingSession.email = extractedUserInfo.email;
2362
+ await sessionStorage.saveSession(sessionId, existingSession, 86400);
2363
+ }
2364
+ if (this.accountHandler?.accountStorage) {
2365
+ const userKey = this.getUserKeyFromSession(existingSession);
2366
+ await this.accountHandler.accountStorage.saveAccount(
2367
+ userKey,
2368
+ providerType,
2369
+ this.sanitizeIdentityForStorage(linkedAcc)
2370
+ );
1541
2371
  }
1542
- await sessionStorage.saveSession(sessionId, existingSession, 86400);
2372
+ this.log("INFO", "AccountLinking", `Linked provider '${providerType}' directly to session '${sessionId}'`, {
2373
+ provider: providerType,
2374
+ sessionId
2375
+ });
1543
2376
  return sessionId;
1544
2377
  }
1545
2378
  /**
@@ -1552,46 +2385,203 @@ var Lixa = class _Lixa {
1552
2385
  async unlinkAccount(sessionId, providerToUnlink) {
1553
2386
  const sessionStorage = this.sessionHandler.sessionStorage || this.localSessionHandler.sessionStorage;
1554
2387
  const session = await sessionStorage.getSession(sessionId);
1555
- if (!session || !session.accounts) {
1556
- throw new AccountUnlinkError("Session not found or has no linked accounts.", { sessionId });
2388
+ if (!session) {
2389
+ throw new AccountUnlinkError("Session not found.", { sessionId });
2390
+ }
2391
+ const providerKey = providerToUnlink.toLowerCase();
2392
+ const userKey = this.getUserKeyFromSession(session);
2393
+ let allAccounts = {};
2394
+ if (this.accountHandler?.accountStorage) {
2395
+ allAccounts = await this.accountHandler.accountStorage.getUserAccounts(userKey) || {};
2396
+ }
2397
+ if (!allAccounts[providerKey]) {
2398
+ throw new AccountUnlinkError(`Provider '${providerToUnlink}' is not linked to this account.`, {
2399
+ sessionId,
2400
+ provider: providerToUnlink
2401
+ });
1557
2402
  }
1558
- const linkedProviders = Object.keys(session.accounts);
2403
+ const linkedProviders = Object.keys(allAccounts);
1559
2404
  if (linkedProviders.length <= 1) {
1560
2405
  throw new AccountUnlinkError("Cannot unlink the only authentication provider for this account.", {
1561
2406
  sessionId,
1562
2407
  provider: providerToUnlink
1563
2408
  });
1564
2409
  }
1565
- delete session.accounts[providerToUnlink.toLowerCase()];
1566
- await sessionStorage.saveSession(sessionId, session, 86400);
2410
+ if (this.accountHandler?.accountStorage) {
2411
+ await this.accountHandler.accountStorage.deleteAccount(userKey, providerKey);
2412
+ }
2413
+ if (session.provider?.toLowerCase() === providerKey) {
2414
+ const remaining = linkedProviders.filter((p) => p !== providerKey);
2415
+ session.provider = remaining.length > 0 ? remaining[0] : void 0;
2416
+ await sessionStorage.saveSession(sessionId, session, 86400);
2417
+ }
2418
+ this.log("INFO", "AccountLinking", `Unlinked provider '${providerKey}' for user '${session.userId || session.email}'`, {
2419
+ sessionId,
2420
+ provider: providerKey
2421
+ });
1567
2422
  return true;
1568
2423
  }
1569
2424
  /**
1570
- * Generates an authorization URL for connecting a resource provider (AuthZ) post-login.
1571
- *
1572
- * @remarks
1573
- * Resource authorization is kept strictly separate from primary authentication (AuthN).
1574
- * Call this method after a user is authenticated to request permissions for external API access
1575
- * (e.g. GitHub repositories, Google Drive, Slack, etc.).
2425
+ * Confirms a pending account linking step-up challenge using the user's password.
2426
+ * On successful verification, links the pending provider to the existing account and creates a session.
1576
2427
  *
1577
- * @param params - Object containing sessionId, provider, requested resource scopes, and optional state
1578
- * @returns The authorization URL for resource consent
2428
+ * @param params - challengeToken and password
2429
+ * @returns Session ID, Session, and User identity
2430
+ * @throws InvalidStateError if challenge is invalid or expired
2431
+ * @throws InvalidCredentialsError if password verification fails
1579
2432
  */
1580
- async getResourceAuthUrl(params) {
1581
- const { sessionId, provider, scopes, state, prompt, extraConfig } = params;
1582
- const sessionStorage = this.sessionHandler.sessionStorage || this.localSessionHandler.sessionStorage;
1583
- const activeSession = await sessionStorage.getSession(sessionId);
1584
- if (!activeSession) {
1585
- throw new SessionNotFoundError("Authentication required. Active session must exist to connect resource providers.", { sessionId });
1586
- }
2433
+ async confirmLinkingWithPassword(params) {
2434
+ const { challengeToken, password } = params;
2435
+ const stateStorage = this.stateHandler.stateStorage || this.localStateHandler.stateStorage;
2436
+ const stateKey = `linking_challenge:${challengeToken}`;
2437
+ const cached = await stateStorage.getState(stateKey);
2438
+ if (!cached || !cached.challengeToken) {
2439
+ throw new InvalidStateError("Invalid or expired account linking challenge.");
2440
+ }
2441
+ let verifiedUser = null;
2442
+ let passwordTokens = void 0;
2443
+ if (this.identityProvider) {
2444
+ try {
2445
+ const signInResult = await this.identityProvider.signIn({
2446
+ identifier: cached.email,
2447
+ password
2448
+ });
2449
+ verifiedUser = signInResult.user;
2450
+ passwordTokens = signInResult.tokens;
2451
+ } catch {
2452
+ verifiedUser = await this.verifyCredentials({
2453
+ identifier: cached.email,
2454
+ password
2455
+ });
2456
+ }
2457
+ } else {
2458
+ verifiedUser = await this.verifyCredentials({
2459
+ identifier: cached.email,
2460
+ password
2461
+ });
2462
+ }
2463
+ if (!verifiedUser) {
2464
+ throw new InvalidCredentialsError("Invalid password for account linking verification.");
2465
+ }
2466
+ await stateStorage.deleteState(stateKey);
2467
+ const providerType = String(cached.incomingProvider).toLowerCase();
2468
+ const incomingTokens = cached.tokens;
2469
+ const linkedAcc = {
2470
+ provider: providerType,
2471
+ providerUserId: cached.incomingProviderUserId,
2472
+ email: cached.email,
2473
+ linkedAt: Date.now()
2474
+ };
2475
+ if (this.accountHandler?.accountStorage) {
2476
+ await this.accountHandler.accountStorage.saveAccount(
2477
+ cached.existingUserId,
2478
+ providerType,
2479
+ this.sanitizeIdentityForStorage(linkedAcc)
2480
+ );
2481
+ }
2482
+ const passwordProviderName = verifiedUser && verifiedUser.provider || (this.identityProvider instanceof CompositeIdentityProvider ? this.identityProvider.defaultProvider : this.identityProvider?.name) || "credentials";
2483
+ const passwordAccountKey = passwordProviderName === "self-managed" ? "credentials" : passwordProviderName;
2484
+ if (this.accountHandler?.accountStorage) {
2485
+ await this.accountHandler.accountStorage.saveAccount(
2486
+ cached.existingUserId,
2487
+ passwordAccountKey,
2488
+ this.sanitizeIdentityForStorage({
2489
+ provider: passwordAccountKey,
2490
+ providerUserId: verifiedUser.id,
2491
+ email: verifiedUser.email,
2492
+ linkedAt: Date.now()
2493
+ })
2494
+ );
2495
+ }
2496
+ const sessionId = (0, import_crypto5.randomBytes)(32).toString("hex");
2497
+ const session = {
2498
+ sessionId,
2499
+ userId: cached.existingUserId,
2500
+ email: cached.email,
2501
+ provider: providerType,
2502
+ token: incomingTokens?.access_token,
2503
+ raw: incomingTokens
2504
+ };
2505
+ const sessionStorage = this.sessionHandler.sessionStorage || this.localSessionHandler.sessionStorage;
2506
+ await sessionStorage.saveSession(sessionId, session, 86400);
2507
+ this.log(
2508
+ "INFO",
2509
+ "AccountLinking",
2510
+ `Successfully linked '${providerType}' to user '${cached.existingUserId}' via password step-up.`
2511
+ );
2512
+ return { sessionId, session, user: verifiedUser };
2513
+ }
2514
+ /**
2515
+ * Sets a password for an SSO-authenticated user who doesn't have one yet.
2516
+ * Must be called from an authenticated context (user has active session proving identity).
2517
+ *
2518
+ * @param params - userId, newPassword, optional identifier and email
2519
+ * @returns True if password was set successfully
2520
+ * @throws IdentityNotConfiguredError if no identity provider configured
2521
+ * @throws WeakPasswordError if password doesn't meet policy
2522
+ */
2523
+ async setPassword(params) {
2524
+ this.log("INFO", "Identity", "Processing setPassword request", { userId: params.userId });
2525
+ if (this.credentialsManager) {
2526
+ const result = await this.credentialsManager.setPassword(params);
2527
+ if (result && this.accountHandler?.accountStorage) {
2528
+ const providerName = this.identityProvider?.name === "self-managed" ? "credentials" : this.identityProvider?.name || "credentials";
2529
+ const credentialsAccount = {
2530
+ provider: providerName,
2531
+ providerUserId: params.userId,
2532
+ email: params.email,
2533
+ linkedAt: Date.now()
2534
+ };
2535
+ await this.accountHandler.accountStorage.saveAccount(
2536
+ params.userId,
2537
+ providerName,
2538
+ this.sanitizeIdentityForStorage(credentialsAccount)
2539
+ );
2540
+ }
2541
+ this.log("INFO", "Identity", "Password set successfully", { userId: params.userId });
2542
+ return result;
2543
+ }
2544
+ if (this.identityProvider && typeof this.identityProvider.changePassword === "function") {
2545
+ throw new IdentityProviderError(
2546
+ "Setting a password for SSO users is only supported with self-managed credentials. Use the provider's admin console instead.",
2547
+ this.identityProvider.name
2548
+ );
2549
+ }
2550
+ throw new CredentialsNotConfiguredError(
2551
+ "Cannot set password: No credentials manager is configured on this Lixa instance."
2552
+ );
2553
+ }
2554
+ /**
2555
+ * Generates an authorization URL for connecting a resource provider (AuthZ) post-login.
2556
+ *
2557
+ * @remarks
2558
+ * Resource authorization is kept strictly separate from primary authentication (AuthN).
2559
+ * Call this method after a user is authenticated to request permissions for external API access
2560
+ * (e.g. GitHub repositories, Google Drive, Slack, etc.).
2561
+ *
2562
+ * @param params - Object containing sessionId, provider, requested resource scopes, and optional state
2563
+ * @returns The authorization URL for resource consent
2564
+ */
2565
+ async getResourceAuthUrl(params) {
2566
+ const { sessionId, provider, state, prompt, extraConfig, authorizationParams } = params;
2567
+ const sessionStorage = this.sessionHandler.sessionStorage || this.localSessionHandler.sessionStorage;
2568
+ const activeSession = await sessionStorage.getSession(sessionId);
2569
+ if (!activeSession) {
2570
+ throw new SessionNotFoundError("Authentication required. Active session must exist to connect resource providers.", { sessionId });
2571
+ }
1587
2572
  const providerType = String(provider).toLowerCase();
1588
2573
  const providerConfig = this.findProviderByType(providerType);
1589
2574
  if (!providerConfig) {
1590
2575
  throw new ProviderNotConfiguredError(String(provider));
1591
2576
  }
2577
+ const defaultResourceScopes = providerConfig.resourceScopes || (Array.isArray(providerConfig.scopes) ? void 0 : providerConfig.scopes?.resource) || [];
2578
+ const requestedScopes = params.scopes && params.scopes.length > 0 ? params.scopes : defaultResourceScopes;
2579
+ if (requestedScopes.length === 0) {
2580
+ throw new Error(`No scopes specified for resource '${provider}'. Provide scopes in getResourceAuthUrl() or configure 'scopes.resource' / 'resourceScopes' on the provider.`);
2581
+ }
1592
2582
  const providerImpl = this.getProvider(providerType, providerConfig);
1593
- const stateValue = state || (0, import_crypto4.randomBytes)(16).toString("hex");
1594
- const codeVerifier = (0, import_crypto4.randomBytes)(32).toString("hex");
2583
+ const stateValue = state || (0, import_crypto5.randomBytes)(16).toString("hex");
2584
+ const codeVerifier = (0, import_crypto5.randomBytes)(32).toString("hex");
1595
2585
  const storage = this.stateHandler.stateStorage || this.localStateHandler.stateStorage;
1596
2586
  await storage.saveState(
1597
2587
  stateValue,
@@ -1603,22 +2593,35 @@ var Lixa = class _Lixa {
1603
2593
  300
1604
2594
  );
1605
2595
  const codeChallenge = _Lixa.buildCodeChallenge(codeVerifier);
2596
+ const mergedAuthParams = {
2597
+ ...providerConfig.extraConfig || {},
2598
+ ...providerConfig.authorizationParams || {},
2599
+ ...extraConfig || {},
2600
+ ...authorizationParams || {}
2601
+ };
1606
2602
  const searchParams = new URLSearchParams({
1607
2603
  client_id: providerConfig.clientId,
1608
2604
  redirect_uri: providerConfig.redirectUri,
1609
- scope: scopes.join(" "),
2605
+ scope: requestedScopes.join(" "),
1610
2606
  state: stateValue,
1611
2607
  response_type: "code",
1612
2608
  code_challenge: codeChallenge,
1613
2609
  code_challenge_method: "S256",
1614
2610
  ...prompt ? { prompt } : {},
1615
- ...providerConfig.extraConfig,
1616
- ...extraConfig
2611
+ ...mergedAuthParams
1617
2612
  });
1618
2613
  return `${providerImpl.authorizationEndpoint}?${searchParams.toString()}`;
1619
2614
  }
1620
2615
  getUserKeyFromSession(session) {
1621
- return session.userId || session.email || session.id || "anonymous";
2616
+ return session.userId || session.email || session.sessionId || "anonymous";
2617
+ }
2618
+ sanitizeIdentityForStorage(account) {
2619
+ return {
2620
+ provider: account.provider,
2621
+ providerUserId: account.providerUserId,
2622
+ email: account.email,
2623
+ linkedAt: account.linkedAt || Date.now()
2624
+ };
1622
2625
  }
1623
2626
  /**
1624
2627
  * Handles the OAuth callback for a connected resource provider and stores resource tokens bound to user account.
@@ -1636,7 +2639,7 @@ var Lixa = class _Lixa {
1636
2639
  throw new ProviderNotConfiguredError(String(provider));
1637
2640
  }
1638
2641
  const providerImpl = this.getProvider(providerType, providerConfig);
1639
- let codeVerifier = (0, import_crypto4.randomBytes)(32).toString("hex");
2642
+ let codeVerifier = (0, import_crypto5.randomBytes)(32).toString("hex");
1640
2643
  if (state) {
1641
2644
  const stateStorage = this.stateHandler.stateStorage || this.localStateHandler.stateStorage;
1642
2645
  const cachedState = await stateStorage.getState(state);
@@ -1648,12 +2651,14 @@ var Lixa = class _Lixa {
1648
2651
  }
1649
2652
  const tokens = await this.exchangeCodeForToken(code, providerConfig, providerImpl, codeVerifier);
1650
2653
  const expiresAt = tokens.expires_in ? Date.now() + tokens.expires_in * 1e3 : void 0;
2654
+ const defaultResourceScopes = providerConfig.resourceScopes || (Array.isArray(providerConfig.scopes) ? void 0 : providerConfig.scopes?.resource) || [];
2655
+ const effectiveScopes = scopes || (tokens.scope ? tokens.scope.split(" ") : defaultResourceScopes);
1651
2656
  const connectedResource = {
1652
2657
  provider: providerType,
1653
2658
  accessToken: tokens.access_token,
1654
2659
  refreshToken: tokens.refresh_token,
1655
2660
  expiresAt,
1656
- scopes: scopes || (tokens.scope ? tokens.scope.split(" ") : []),
2661
+ scopes: effectiveScopes,
1657
2662
  raw: tokens,
1658
2663
  connectedAt: Date.now()
1659
2664
  };
@@ -1665,6 +2670,45 @@ var Lixa = class _Lixa {
1665
2670
  await sessionStorage.saveSession(sessionId, activeSession, 86400);
1666
2671
  return activeSession;
1667
2672
  }
2673
+ /**
2674
+ * Retrieves configured default resource scopes for a provider.
2675
+ *
2676
+ * @param provider - Provider name (e.g. 'github', 'google')
2677
+ * @returns Array of configured resource scopes, or undefined if not configured
2678
+ */
2679
+ getResourceScopes(provider) {
2680
+ const providerConfig = this.findProviderByType(String(provider).toLowerCase());
2681
+ if (!providerConfig) return void 0;
2682
+ if (providerConfig.resourceScopes) return providerConfig.resourceScopes;
2683
+ if (providerConfig.scopes && !Array.isArray(providerConfig.scopes)) {
2684
+ return providerConfig.scopes.resource;
2685
+ }
2686
+ return void 0;
2687
+ }
2688
+ /**
2689
+ * Returns the resolved session cookie name.
2690
+ * Dynamically reflects any changes to the active identity provider.
2691
+ *
2692
+ * @param req - Optional request object for multi-tenant request-scoped cookie resolution
2693
+ * @returns Resolved session cookie name
2694
+ */
2695
+ getSessionCookieName(req) {
2696
+ if (typeof this.sessionCookieResolver === "function") {
2697
+ return this.sessionCookieResolver(this, req);
2698
+ }
2699
+ if (typeof this.sessionCookieResolver === "string" && this.sessionCookieResolver.length > 0) {
2700
+ return this.sessionCookieResolver;
2701
+ }
2702
+ return DEFAULT_SESSION_COOKIE_NAME;
2703
+ }
2704
+ /**
2705
+ * Sets or updates the session cookie name or dynamic resolver on this instance.
2706
+ *
2707
+ * @param resolver - Static cookie name or dynamic resolver function
2708
+ */
2709
+ setSessionCookieName(resolver) {
2710
+ this.sessionCookieResolver = resolver;
2711
+ }
1668
2712
  /**
1669
2713
  * Retrieves a connected resource for a specific User ID / Email directly (independent of session IDs).
1670
2714
  * Automatically refreshes expired access tokens if a refresh token is present.
@@ -1845,7 +2889,32 @@ var Lixa = class _Lixa {
1845
2889
  */
1846
2890
  async fetchSessionInfo(sessionId) {
1847
2891
  const sessionStorage = this.sessionHandler.sessionStorage || this.localSessionHandler.sessionStorage;
1848
- return await sessionStorage.getSession(sessionId);
2892
+ const session = await sessionStorage.getSession(sessionId);
2893
+ if (!session) return null;
2894
+ if (session.accounts) {
2895
+ delete session.accounts;
2896
+ }
2897
+ return session;
2898
+ }
2899
+ /**
2900
+ * Retrieves all linked accounts for a user from account storage.
2901
+ *
2902
+ * @param userIdOrSessionId - User ID, email, or active session ID
2903
+ * @returns Record mapping provider name to linked account metadata
2904
+ */
2905
+ async getUserAccounts(userIdOrSessionId) {
2906
+ let userKey = userIdOrSessionId;
2907
+ if (this.sessionHandler.sessionStorage || this.localSessionHandler.sessionStorage) {
2908
+ const sessionStorage = this.sessionHandler.sessionStorage || this.localSessionHandler.sessionStorage;
2909
+ const session = await sessionStorage.getSession(userIdOrSessionId);
2910
+ if (session) {
2911
+ userKey = this.getUserKeyFromSession(session);
2912
+ }
2913
+ }
2914
+ if (this.accountHandler?.accountStorage) {
2915
+ return await this.accountHandler.accountStorage.getUserAccounts(userKey) || {};
2916
+ }
2917
+ return {};
1849
2918
  }
1850
2919
  /**
1851
2920
  * Deletes a session from session storage (e.g. on logout).
@@ -1896,117 +2965,178 @@ var Lixa = class _Lixa {
1896
2965
  return response.json();
1897
2966
  }
1898
2967
  /**
1899
- * Checks if credentials (username and password) authentication is configured and enabled.
2968
+ * Checks if an identity provider (Self-Managed Database, AWS Cognito, Auth0, Okta, etc.) is configured.
2969
+ *
2970
+ * @returns True if identity provider is configured and available
2971
+ */
2972
+ isIdentityEnabled() {
2973
+ return this.identityProvider !== void 0;
2974
+ }
2975
+ /**
2976
+ * Checks if credentials / identity authentication is configured and enabled.
1900
2977
  *
1901
- * @returns True if credentials authentication is available
2978
+ * @returns True if credentials / identity authentication is available
1902
2979
  */
1903
2980
  isCredentialsEnabled() {
1904
- return this.credentialsManager !== void 0;
2981
+ return this.identityProvider !== void 0;
1905
2982
  }
1906
2983
  /**
1907
- * Returns the underlying CredentialsManager instance if configured.
2984
+ * Returns the configured IdentityProvider instance.
2985
+ */
2986
+ getIdentityProvider() {
2987
+ return this.identityProvider;
2988
+ }
2989
+ /**
2990
+ * Dynamically sets or updates the active identity provider (e.g. SelfManaged, Cognito, Auth0, Okta).
2991
+ *
2992
+ * @param provider - Identity provider instance or undefined to disable
2993
+ */
2994
+ setIdentityProvider(provider) {
2995
+ this.identityProvider = provider;
2996
+ if (provider instanceof SelfManagedIdentityProvider) {
2997
+ this.credentialsManager = provider.getCredentialsManager();
2998
+ } else {
2999
+ this.credentialsManager = void 0;
3000
+ }
3001
+ }
3002
+ /**
3003
+ * Returns the underlying CredentialsManager instance if using Self-Managed identity.
1908
3004
  */
1909
3005
  getCredentialsManager() {
1910
3006
  return this.credentialsManager;
1911
3007
  }
1912
3008
  /**
1913
- * Registers a new user with username/email and password.
1914
- * Automatically enforces password policy, hashes password with Scrypt/configured hasher,
1915
- * stores credentials, and creates a session (unless autoCreateSessionOnSignUp is false).
3009
+ * Registers a new user with the configured identity provider (Self-Managed DB, Cognito, Auth0, Okta).
3010
+ * Automatically creates an active session unless autoCreateSessionOnSignUp is explicitly set to false.
1916
3011
  *
1917
3012
  * @param params - Registration parameters (identifier, password, email, username, metadata)
1918
- * @returns Created user (without password hash) and optional active session
3013
+ * @returns Created user identity and optional active session
1919
3014
  * @throws WeakPasswordError if password does not meet policy requirements
1920
3015
  * @throws UserAlreadyExistsError if identifier is already registered
1921
- * @throws CredentialsNotConfiguredError if credentials auth is not configured
3016
+ * @throws IdentityNotConfiguredError if identity authentication is not configured
1922
3017
  */
1923
3018
  async signUp(params) {
1924
- if (!this.credentialsManager) {
1925
- throw new CredentialsNotConfiguredError();
3019
+ if (!this.identityProvider) {
3020
+ throw new IdentityNotConfiguredError();
3021
+ }
3022
+ const rawProviderName = params.provider || this.identityProvider.name;
3023
+ const providerName = rawProviderName === "self-managed" ? "credentials" : rawProviderName;
3024
+ this.log("INFO", "Identity", `Processing signUp request via ${providerName}`, { identifier: params.identifier });
3025
+ const signUpEmail = params.email || (params.identifier?.includes("@") ? params.identifier : void 0);
3026
+ if (signUpEmail && this.accountHandler?.accountStorage?.findAccountsByEmail) {
3027
+ const normalizedEmail = signUpEmail.trim().toLowerCase();
3028
+ const existingAccounts = await this.accountHandler.accountStorage.findAccountsByEmail(normalizedEmail);
3029
+ if (existingAccounts.length > 0) {
3030
+ const existingProviderNames = existingAccounts.map((a) => a.provider);
3031
+ const hasCredentials = existingProviderNames.some(
3032
+ (p) => p === "credentials" || p === "self-managed" || p === "cognito"
3033
+ );
3034
+ if (!hasCredentials) {
3035
+ this.log("WARN", "Identity", `SignUp blocked: SSO account already exists for '${normalizedEmail}' via [${existingProviderNames.join(", ")}]`);
3036
+ throw new SSOAccountAlreadyExistsError(normalizedEmail, existingProviderNames);
3037
+ }
3038
+ }
3039
+ }
3040
+ const user = await this.identityProvider.signUp(params);
3041
+ const isVerificationRequired = user.metadata?.requiresVerification === true || user.emailVerified === false && (user.metadata?.devConfirmationCode !== void 0 || this.identityProvider.name === "self-managed" || this.identityProvider.name === "cognito");
3042
+ if (isVerificationRequired) {
3043
+ this.log("INFO", "Identity", "SignUp requires verification \u2014 no session created", {
3044
+ identifier: user.identifier,
3045
+ email: user.email
3046
+ });
3047
+ return {
3048
+ user,
3049
+ requiresVerification: true,
3050
+ devConfirmationCode: user.metadata?.devConfirmationCode
3051
+ };
1926
3052
  }
1927
- this.log("INFO", "Credentials", "Processing signUp request", { identifier: params.identifier });
1928
- const user = await this.credentialsManager.signUp(params);
1929
3053
  const autoSession = this.config.credentials?.autoCreateSessionOnSignUp ?? true;
1930
3054
  if (!autoSession) {
1931
3055
  return { user };
1932
3056
  }
1933
- const sessionId = (0, import_crypto4.randomBytes)(16).toString("hex");
1934
- const token = (0, import_crypto4.randomBytes)(32).toString("hex");
3057
+ const sessionId = (0, import_crypto5.randomBytes)(16).toString("hex");
3058
+ const token = (0, import_crypto5.randomBytes)(32).toString("hex");
1935
3059
  const ttl = this.config.credentials?.sessionTtlSeconds ?? 86400;
3060
+ const activeProviderName = params.provider || user && user.provider || (this.identityProvider instanceof CompositeIdentityProvider ? this.identityProvider.defaultProvider : this.identityProvider.name);
3061
+ const accountKey = activeProviderName === "self-managed" ? "credentials" : activeProviderName || "credentials";
1936
3062
  const credentialsAccount = {
1937
- provider: "credentials",
3063
+ provider: accountKey,
1938
3064
  providerUserId: user.id,
1939
3065
  email: user.email,
1940
- accessToken: token,
1941
- raw: {
1942
- access_token: token,
1943
- token_type: "Bearer",
1944
- expires_in: ttl
1945
- },
1946
3066
  linkedAt: Date.now()
1947
3067
  };
1948
3068
  const session = {
1949
- id: sessionId,
3069
+ sessionId,
1950
3070
  userId: user.id,
1951
3071
  email: user.email,
1952
- token,
1953
- provider: "credentials",
1954
- accounts: {
1955
- credentials: credentialsAccount
1956
- }
3072
+ provider: accountKey
1957
3073
  };
3074
+ if (this.accountHandler?.accountStorage) {
3075
+ const userKey = this.getUserKeyFromSession(session);
3076
+ await this.accountHandler.accountStorage.saveAccount(userKey, accountKey, this.sanitizeIdentityForStorage(credentialsAccount));
3077
+ }
1958
3078
  const sessionStorage = this.sessionHandler.sessionStorage || this.localSessionHandler.sessionStorage;
1959
3079
  await sessionStorage.saveSession(sessionId, session, ttl);
1960
- this.log("INFO", "Credentials", "Created session on signUp", { userId: user.id, sessionId });
3080
+ this.log("INFO", "Identity", "Created session on signUp", { userId: user.id, sessionId, provider: accountKey });
1961
3081
  return { user, sessionId, session };
1962
3082
  }
1963
3083
  /**
1964
- * Authenticates a user with username/email and password.
1965
- * Performs constant-time verification with timing attack mitigation, and creates an active session.
1966
- * If account linking is configured with AUTO_LINK_BY_VERIFIED_EMAIL, merges with existing session.
3084
+ * Authenticates a user with credentials via the configured identity provider (Self-Managed DB, Cognito, Auth0, Okta, or Composite).
3085
+ * Creates an active session and automatically links with existing sessions when AUTO_LINK_BY_VERIFIED_EMAIL is configured.
1967
3086
  *
1968
- * @param params - Sign-in parameters (identifier, password)
3087
+ * @param params - Sign-in parameters (identifier, password, optional provider)
1969
3088
  * @returns Authenticated user, session ID, and session object
1970
3089
  * @throws InvalidCredentialsError if authentication fails
1971
- * @throws CredentialsNotConfiguredError if credentials auth is not configured
3090
+ * @throws IdentityNotConfiguredError if identity authentication is not configured
1972
3091
  */
1973
3092
  async signIn(params) {
1974
- if (!this.credentialsManager) {
1975
- throw new CredentialsNotConfiguredError();
3093
+ if (!this.identityProvider) {
3094
+ throw new IdentityNotConfiguredError();
1976
3095
  }
1977
3096
  const lookupKey = params.username || params.identifier;
1978
- this.log("INFO", "Credentials", "Processing signIn request", { identifier: lookupKey });
1979
- const user = await this.credentialsManager.verifyCredentials(params);
1980
- if (!user) {
1981
- this.log("WARN", "Credentials", "Invalid credentials provided", { identifier: lookupKey });
1982
- throw new InvalidCredentialsError();
1983
- }
3097
+ const rawProviderName = params.provider || this.identityProvider.name;
3098
+ const providerName = rawProviderName === "self-managed" ? "credentials" : rawProviderName;
3099
+ this.log("INFO", "Identity", `Processing signIn request via ${providerName}`, { identifier: lookupKey });
3100
+ const authResult = await this.identityProvider.signIn(params);
3101
+ const user = authResult.user;
3102
+ const tokens = authResult.tokens;
1984
3103
  const ttl = this.config.credentials?.sessionTtlSeconds ?? 86400;
1985
- const token = (0, import_crypto4.randomBytes)(32).toString("hex");
3104
+ const token = tokens?.accessToken || (0, import_crypto5.randomBytes)(32).toString("hex");
3105
+ const rawTokens = {
3106
+ access_token: token,
3107
+ token_type: tokens?.tokenType || "Bearer",
3108
+ expires_in: tokens?.expiresIn || ttl,
3109
+ id_token: tokens?.idToken,
3110
+ refresh_token: tokens?.refreshToken,
3111
+ ...tokens?.raw
3112
+ };
3113
+ const activeProviderName = params.provider || user && user.provider || (this.identityProvider instanceof CompositeIdentityProvider ? this.identityProvider.defaultProvider : this.identityProvider.name);
3114
+ const accountKey = activeProviderName === "self-managed" ? "credentials" : activeProviderName || "credentials";
1986
3115
  const credentialsAccount = {
1987
- provider: "credentials",
3116
+ provider: accountKey,
1988
3117
  providerUserId: user.id,
1989
3118
  email: user.email,
1990
- accessToken: token,
1991
- raw: {
1992
- access_token: token,
1993
- token_type: "Bearer",
1994
- expires_in: ttl
1995
- },
1996
3119
  linkedAt: Date.now()
1997
3120
  };
1998
3121
  const sessionStorage = this.sessionHandler.sessionStorage || this.localSessionHandler.sessionStorage;
1999
- const linkingMode = this.config.accountLinking?.mode;
2000
- const isAutoLink = linkingMode === "AUTO_LINK_BY_VERIFIED_EMAIL" /* AUTO_LINK_BY_VERIFIED_EMAIL */ || linkingMode === "AUTO_LINK_BY_VERIFIED_EMAIL" || linkingMode === "linkByEmail";
3122
+ const linkingConfig = this.config.accountLinking;
3123
+ const linkingMode = typeof linkingConfig === "object" ? linkingConfig?.mode : void 0;
3124
+ const isAutoLink = linkingConfig === true || linkingConfig === "linkByEmail" || linkingConfig === "AUTO_LINK_BY_VERIFIED_EMAIL" /* AUTO_LINK_BY_VERIFIED_EMAIL */ || linkingMode === "AUTO_LINK_BY_VERIFIED_EMAIL" /* AUTO_LINK_BY_VERIFIED_EMAIL */ || linkingMode === "AUTO_LINK_BY_VERIFIED_EMAIL" || linkingMode === "linkByEmail";
2001
3125
  if (isAutoLink && user.email && sessionStorage.getSessionByEmail) {
2002
3126
  const existing = await sessionStorage.getSessionByEmail(user.email);
2003
3127
  if (existing) {
2004
3128
  const mergedSession = { ...existing.session };
2005
- mergedSession.accounts = { ...mergedSession.accounts || {}, credentials: credentialsAccount };
2006
3129
  if (!mergedSession.userId) mergedSession.userId = user.id;
2007
3130
  if (!mergedSession.email) mergedSession.email = user.email;
3131
+ mergedSession.provider = accountKey;
3132
+ mergedSession.token = token;
3133
+ mergedSession.raw = rawTokens;
3134
+ if (this.accountHandler?.accountStorage) {
3135
+ const userKey = this.getUserKeyFromSession(mergedSession);
3136
+ await this.accountHandler.accountStorage.saveAccount(userKey, accountKey, this.sanitizeIdentityForStorage(credentialsAccount));
3137
+ }
2008
3138
  await sessionStorage.saveSession(existing.sessionId, mergedSession, ttl);
2009
- this.log("INFO", "AccountLinking", "Linked credentials to existing session by email", {
3139
+ this.log("INFO", "AccountLinking", `Linked ${providerName} to existing session by email`, {
2010
3140
  sessionId: existing.sessionId,
2011
3141
  email: user.email
2012
3142
  });
@@ -2017,53 +3147,229 @@ var Lixa = class _Lixa {
2017
3147
  };
2018
3148
  }
2019
3149
  }
2020
- const sessionId = (0, import_crypto4.randomBytes)(16).toString("hex");
3150
+ const sessionId = (0, import_crypto5.randomBytes)(16).toString("hex");
3151
+ let targetUserId = user.id;
3152
+ const allAccounts = {
3153
+ [accountKey]: credentialsAccount
3154
+ };
3155
+ if (this.accountHandler?.accountStorage) {
3156
+ if (user.email && this.accountHandler.accountStorage.findAccountsByEmail) {
3157
+ const matches = await this.accountHandler.accountStorage.findAccountsByEmail(user.email);
3158
+ if (matches.length > 0) {
3159
+ targetUserId = matches[0].userId;
3160
+ for (const match of matches) {
3161
+ const userAccs = await this.accountHandler.accountStorage.getUserAccounts(match.userId);
3162
+ if (userAccs) {
3163
+ for (const [k, v] of Object.entries(userAccs)) {
3164
+ if (k !== accountKey && !allAccounts[k]) {
3165
+ allAccounts[k] = v;
3166
+ }
3167
+ }
3168
+ }
3169
+ }
3170
+ }
3171
+ }
3172
+ const directAccounts = await this.accountHandler.accountStorage.getUserAccounts(user.id);
3173
+ if (directAccounts) {
3174
+ for (const [k, v] of Object.entries(directAccounts)) {
3175
+ if (k !== accountKey && !allAccounts[k]) {
3176
+ allAccounts[k] = v;
3177
+ }
3178
+ }
3179
+ }
3180
+ await this.accountHandler.accountStorage.saveAccount(
3181
+ targetUserId,
3182
+ accountKey,
3183
+ this.sanitizeIdentityForStorage(credentialsAccount)
3184
+ );
3185
+ for (const [pKey, pAcc] of Object.entries(allAccounts)) {
3186
+ if (pKey !== accountKey) {
3187
+ await this.accountHandler.accountStorage.saveAccount(
3188
+ targetUserId,
3189
+ pKey,
3190
+ this.sanitizeIdentityForStorage(pAcc)
3191
+ );
3192
+ }
3193
+ }
3194
+ }
2021
3195
  const session = {
2022
- id: sessionId,
2023
- userId: user.id,
3196
+ sessionId,
3197
+ userId: targetUserId,
2024
3198
  email: user.email,
3199
+ provider: accountKey,
2025
3200
  token,
2026
- provider: "credentials",
2027
- accounts: {
2028
- credentials: credentialsAccount
2029
- }
3201
+ raw: rawTokens
2030
3202
  };
2031
3203
  await sessionStorage.saveSession(sessionId, session, ttl);
2032
- this.log("INFO", "Credentials", "User signed in successfully", { userId: user.id, sessionId });
3204
+ this.log("INFO", "Identity", `User signed in successfully via ${accountKey}`, { userId: targetUserId, sessionId });
2033
3205
  return { user, sessionId, session };
2034
3206
  }
2035
3207
  /**
2036
3208
  * Verifies username/email and password credentials without generating a session.
2037
3209
  *
2038
- * @param params - Verification parameters (identifier, password)
2039
- * @returns User credentials (without password hash) or null if invalid
2040
- * @throws CredentialsNotConfiguredError if credentials auth is not configured
3210
+ * @param params - Verification parameters (identifier, password, optional provider)
3211
+ * @returns User credentials/identity or null if invalid
3212
+ * @throws IdentityNotConfiguredError if identity auth is not configured
2041
3213
  */
2042
3214
  async verifyCredentials(params) {
2043
- if (!this.credentialsManager) {
2044
- throw new CredentialsNotConfiguredError();
3215
+ if (!this.identityProvider) {
3216
+ throw new IdentityNotConfiguredError();
3217
+ }
3218
+ if (typeof this.identityProvider.verifyCredentials === "function") {
3219
+ return this.identityProvider.verifyCredentials(params);
3220
+ }
3221
+ try {
3222
+ const result = await this.identityProvider.signIn(params);
3223
+ return result.user;
3224
+ } catch {
3225
+ return null;
2045
3226
  }
2046
- return this.credentialsManager.verifyCredentials(params);
2047
3227
  }
2048
3228
  /**
2049
3229
  * Updates a user's password after verifying the current password and enforcing policy on the new password.
2050
3230
  *
2051
- * @param params - Change password parameters (userId/identifier, oldPassword, newPassword)
3231
+ * @param params - Change password parameters (userId/identifier, oldPassword, newPassword, optional provider)
2052
3232
  * @returns True if password was updated successfully
2053
3233
  * @throws UserNotFoundError if user is not found
2054
3234
  * @throws InvalidCredentialsError if current password is incorrect
2055
3235
  * @throws WeakPasswordError if new password does not meet policy requirements
2056
- * @throws CredentialsNotConfiguredError if credentials auth is not configured
3236
+ * @throws IdentityNotConfiguredError if identity auth is not configured
2057
3237
  */
2058
3238
  async changePassword(params) {
2059
- if (!this.credentialsManager) {
2060
- throw new CredentialsNotConfiguredError();
3239
+ if (!this.identityProvider) {
3240
+ throw new IdentityNotConfiguredError();
2061
3241
  }
2062
- this.log("INFO", "Credentials", "Processing changePassword request", {
3242
+ if (typeof this.identityProvider.changePassword !== "function") {
3243
+ throw new IdentityProviderError(
3244
+ `Password change is not supported by the '${this.identityProvider.name}' identity provider`,
3245
+ this.identityProvider.name
3246
+ );
3247
+ }
3248
+ this.log("INFO", "Identity", "Processing changePassword request", {
2063
3249
  userId: params.userId,
2064
3250
  identifier: params.identifier
2065
3251
  });
2066
- return this.credentialsManager.changePassword(params);
3252
+ return this.identityProvider.changePassword(params);
3253
+ }
3254
+ /**
3255
+ * Initiates a password reset (forgot password flow) through the configured identity provider.
3256
+ *
3257
+ * @param params - Forgot password parameters (identifier, optional metadata, optional provider)
3258
+ * @returns ForgotPasswordResult indicating status and delivery medium (email/SMS)
3259
+ * @throws IdentityNotConfiguredError if identity auth is not configured
3260
+ */
3261
+ async forgotPassword(params) {
3262
+ if (!this.identityProvider) {
3263
+ throw new IdentityNotConfiguredError();
3264
+ }
3265
+ if (typeof this.identityProvider.forgotPassword !== "function") {
3266
+ throw new IdentityProviderError(
3267
+ `Password reset request is not supported by the '${this.identityProvider.name}' identity provider`,
3268
+ this.identityProvider.name
3269
+ );
3270
+ }
3271
+ this.log("INFO", "Identity", "Processing forgotPassword request", { identifier: params.identifier });
3272
+ return this.identityProvider.forgotPassword(params);
3273
+ }
3274
+ /**
3275
+ * Confirms a password reset using a verification code / token and sets the new password.
3276
+ *
3277
+ * @param params - Verification code, identifier, new password, and optional provider
3278
+ * @returns True if password reset was successfully confirmed
3279
+ * @throws IdentityNotConfiguredError if identity auth is not configured
3280
+ */
3281
+ async confirmPasswordReset(params) {
3282
+ if (!this.identityProvider) {
3283
+ throw new IdentityNotConfiguredError();
3284
+ }
3285
+ if (typeof this.identityProvider.confirmPasswordReset !== "function") {
3286
+ throw new IdentityProviderError(
3287
+ `Confirm password reset is not supported by the '${this.identityProvider.name}' identity provider`,
3288
+ this.identityProvider.name
3289
+ );
3290
+ }
3291
+ this.log("INFO", "Identity", "Processing confirmPasswordReset request", { identifier: params.identifier });
3292
+ return this.identityProvider.confirmPasswordReset(params);
3293
+ }
3294
+ /**
3295
+ * Confirms user account registration using a verification code.
3296
+ *
3297
+ * @param params - Verification parameters including identifier, confirmationCode, and optional provider
3298
+ * @returns True if confirmation was successful
3299
+ * @throws IdentityNotConfiguredError if identity auth is not configured
3300
+ * @throws IdentityProviderError if identity provider does not support confirmation
3301
+ */
3302
+ async confirmSignUp(params) {
3303
+ if (!this.identityProvider) {
3304
+ throw new IdentityNotConfiguredError();
3305
+ }
3306
+ if (typeof this.identityProvider.confirmSignUp !== "function") {
3307
+ throw new IdentityProviderError(
3308
+ `Account confirmation is not supported by the '${this.identityProvider.name}' identity provider`,
3309
+ this.identityProvider.name
3310
+ );
3311
+ }
3312
+ this.log("INFO", "Identity", "Processing confirmSignUp request", { identifier: params.identifier });
3313
+ return this.identityProvider.confirmSignUp(params);
3314
+ }
3315
+ /**
3316
+ * Resends the sign up confirmation code to the user.
3317
+ *
3318
+ * @param params - Resend parameters including user identifier and optional provider
3319
+ * @returns Details about the code delivery (medium, destination)
3320
+ * @throws IdentityNotConfiguredError if identity auth is not configured
3321
+ * @throws IdentityProviderError if identity provider does not support resending codes
3322
+ */
3323
+ async resendConfirmationCode(params) {
3324
+ if (!this.identityProvider) {
3325
+ throw new IdentityNotConfiguredError();
3326
+ }
3327
+ if (typeof this.identityProvider.resendConfirmationCode !== "function") {
3328
+ throw new IdentityProviderError(
3329
+ `Resending confirmation code is not supported by the '${this.identityProvider.name}' identity provider`,
3330
+ this.identityProvider.name
3331
+ );
3332
+ }
3333
+ this.log("INFO", "Identity", "Processing resendConfirmationCode request", { identifier: params.identifier });
3334
+ return this.identityProvider.resendConfirmationCode(params);
3335
+ }
3336
+ /**
3337
+ * Retrieves a user by their unique ID from the configured identity provider.
3338
+ *
3339
+ * @param id - Unique user ID
3340
+ * @param provider - Optional provider key when using CompositeIdentityProvider
3341
+ * @returns UserIdentity or null if not found
3342
+ */
3343
+ async getUserById(id, provider) {
3344
+ if (!this.identityProvider || typeof this.identityProvider.getUserById !== "function") {
3345
+ return null;
3346
+ }
3347
+ return this.identityProvider.getUserById(id, provider);
3348
+ }
3349
+ /**
3350
+ * Retrieves a user by their identifier (username or email) from the configured identity provider.
3351
+ *
3352
+ * @param identifier - Username or email string
3353
+ * @param provider - Optional provider key when using CompositeIdentityProvider
3354
+ * @returns UserIdentity or null if not found
3355
+ */
3356
+ async getUserByIdentifier(identifier, provider) {
3357
+ if (!this.identityProvider || typeof this.identityProvider.getUserByIdentifier !== "function") {
3358
+ return null;
3359
+ }
3360
+ return this.identityProvider.getUserByIdentifier(identifier, provider);
3361
+ }
3362
+ /**
3363
+ * Deletes a user account from the configured identity provider.
3364
+ *
3365
+ * @param id - Unique user ID
3366
+ * @param provider - Optional provider key when using CompositeIdentityProvider
3367
+ */
3368
+ async deleteUser(id, provider) {
3369
+ if (!this.identityProvider || typeof this.identityProvider.deleteUser !== "function") {
3370
+ return;
3371
+ }
3372
+ await this.identityProvider.deleteUser(id, provider);
2067
3373
  }
2068
3374
  findProviderByType(providerType) {
2069
3375
  for (const [key, value] of Object.entries(this.config.providers || {})) {
@@ -2075,125 +3381,6 @@ var Lixa = class _Lixa {
2075
3381
  }
2076
3382
  };
2077
3383
 
2078
- // src/utils/cookies.ts
2079
- var DEFAULT_SESSION_COOKIE_NAME = "lixa_session";
2080
- var DEFAULT_STATE_COOKIE_NAME = "lixa_oauth_state";
2081
- var DEFAULT_SESSION_MAX_AGE_SECONDS = 24 * 60 * 60;
2082
- var DEFAULT_STATE_MAX_AGE_SECONDS = 5 * 60;
2083
- function isProductionEnvironment() {
2084
- return typeof process !== "undefined" && process.env?.NODE_ENV === "production";
2085
- }
2086
- function serializeCookie(name, value, options) {
2087
- const isProd = isProductionEnvironment();
2088
- const path = options?.path ?? "/";
2089
- const httpOnly = options?.httpOnly ?? true;
2090
- const secure = options?.secure ?? isProd;
2091
- const sameSite = options?.sameSite ?? "lax";
2092
- const parts = [`${encodeURIComponent(name)}=${encodeURIComponent(value)}`];
2093
- if (path) {
2094
- parts.push(`Path=${path}`);
2095
- }
2096
- if (typeof options?.maxAge === "number") {
2097
- parts.push(`Max-Age=${Math.floor(options.maxAge)}`);
2098
- const expires = new Date(Date.now() + options.maxAge * 1e3).toUTCString();
2099
- parts.push(`Expires=${expires}`);
2100
- }
2101
- if (options?.domain) {
2102
- parts.push(`Domain=${options.domain}`);
2103
- }
2104
- if (httpOnly) {
2105
- parts.push("HttpOnly");
2106
- }
2107
- if (secure) {
2108
- parts.push("Secure");
2109
- }
2110
- if (sameSite) {
2111
- const capitalized = sameSite.charAt(0).toUpperCase() + sameSite.slice(1).toLowerCase();
2112
- parts.push(`SameSite=${capitalized}`);
2113
- }
2114
- return parts.join("; ");
2115
- }
2116
- function createSessionCookie(sessionId, options) {
2117
- const isProd = isProductionEnvironment();
2118
- const resolvedOptions = {
2119
- name: options?.name || DEFAULT_SESSION_COOKIE_NAME,
2120
- path: options?.path ?? "/",
2121
- maxAge: options?.maxAge ?? DEFAULT_SESSION_MAX_AGE_SECONDS,
2122
- httpOnly: options?.httpOnly ?? true,
2123
- secure: options?.secure ?? isProd,
2124
- sameSite: options?.sameSite ?? "lax",
2125
- domain: options?.domain
2126
- };
2127
- const name = resolvedOptions.name;
2128
- const header = serializeCookie(name, sessionId, resolvedOptions);
2129
- return {
2130
- name,
2131
- value: sessionId,
2132
- options: resolvedOptions,
2133
- header
2134
- };
2135
- }
2136
- function clearSessionCookie(options) {
2137
- const isProd = isProductionEnvironment();
2138
- const resolvedOptions = {
2139
- name: options?.name || DEFAULT_SESSION_COOKIE_NAME,
2140
- path: options?.path ?? "/",
2141
- maxAge: 0,
2142
- httpOnly: options?.httpOnly ?? true,
2143
- secure: options?.secure ?? isProd,
2144
- sameSite: options?.sameSite ?? "lax",
2145
- domain: options?.domain
2146
- };
2147
- const name = resolvedOptions.name;
2148
- const header = serializeCookie(name, "", resolvedOptions);
2149
- return {
2150
- name,
2151
- value: "",
2152
- options: resolvedOptions,
2153
- header
2154
- };
2155
- }
2156
- function createStateCookie(state, options) {
2157
- const isProd = isProductionEnvironment();
2158
- const resolvedOptions = {
2159
- name: options?.name || DEFAULT_STATE_COOKIE_NAME,
2160
- path: options?.path ?? "/",
2161
- maxAge: options?.maxAge ?? DEFAULT_STATE_MAX_AGE_SECONDS,
2162
- httpOnly: options?.httpOnly ?? true,
2163
- secure: options?.secure ?? isProd,
2164
- sameSite: options?.sameSite ?? "lax",
2165
- domain: options?.domain
2166
- };
2167
- const name = resolvedOptions.name;
2168
- const header = serializeCookie(name, state, resolvedOptions);
2169
- return {
2170
- name,
2171
- value: state,
2172
- options: resolvedOptions,
2173
- header
2174
- };
2175
- }
2176
- function clearStateCookie(options) {
2177
- const isProd = isProductionEnvironment();
2178
- const resolvedOptions = {
2179
- name: options?.name || DEFAULT_STATE_COOKIE_NAME,
2180
- path: options?.path ?? "/",
2181
- maxAge: 0,
2182
- httpOnly: options?.httpOnly ?? true,
2183
- secure: options?.secure ?? isProd,
2184
- sameSite: options?.sameSite ?? "lax",
2185
- domain: options?.domain
2186
- };
2187
- const name = resolvedOptions.name;
2188
- const header = serializeCookie(name, "", resolvedOptions);
2189
- return {
2190
- name,
2191
- value: "",
2192
- options: resolvedOptions,
2193
- header
2194
- };
2195
- }
2196
-
2197
3384
  // src/credentials/index.ts
2198
3385
  var credentials_exports = {};
2199
3386
  __export(credentials_exports, {
@@ -2203,10 +3390,20 @@ __export(credentials_exports, {
2203
3390
  ScryptPasswordHasher: () => ScryptPasswordHasher,
2204
3391
  validatePasswordPolicy: () => validatePasswordPolicy
2205
3392
  });
3393
+
3394
+ // src/identity/index.ts
3395
+ var identity_exports = {};
3396
+ __export(identity_exports, {
3397
+ CompositeIdentityProvider: () => CompositeIdentityProvider,
3398
+ SelfManagedIdentityProvider: () => SelfManagedIdentityProvider
3399
+ });
2206
3400
  // Annotate the CommonJS export names for ESM import in node:
2207
3401
  0 && (module.exports = {
3402
+ AccountLinkingChallengeRequiredError,
2208
3403
  AccountLinkingStrategy,
3404
+ AccountNotVerifiedError,
2209
3405
  AccountUnlinkError,
3406
+ CompositeIdentityProvider,
2210
3407
  CredentialsManager,
2211
3408
  CredentialsNotConfiguredError,
2212
3409
  DEFAULT_SESSION_COOKIE_NAME,
@@ -2214,6 +3411,8 @@ __export(credentials_exports, {
2214
3411
  DEFAULT_STATE_COOKIE_NAME,
2215
3412
  DEFAULT_STATE_MAX_AGE_SECONDS,
2216
3413
  EmailNotVerifiedError,
3414
+ IdentityNotConfiguredError,
3415
+ IdentityProviderError,
2217
3416
  InvalidCredentialsError,
2218
3417
  InvalidOAuthCallbackError,
2219
3418
  InvalidProviderConfigError,
@@ -2224,7 +3423,9 @@ __export(credentials_exports, {
2224
3423
  Pbkdf2PasswordHasher,
2225
3424
  ProviderNotConfiguredError,
2226
3425
  RefreshTokenError,
3426
+ SSOAccountAlreadyExistsError,
2227
3427
  ScryptPasswordHasher,
3428
+ SelfManagedIdentityProvider,
2228
3429
  SessionNotFoundError,
2229
3430
  TokenExchangeError,
2230
3431
  UserAlreadyExistsError,
@@ -2239,6 +3440,7 @@ __export(credentials_exports, {
2239
3440
  determineProviderFromIssuer,
2240
3441
  extractUserInfo,
2241
3442
  fetchUserInfo,
3443
+ identity,
2242
3444
  isProductionEnvironment,
2243
3445
  serializeCookie,
2244
3446
  validatePasswordPolicy