@vunexa/lixa 0.1.6-alpha.8 → 0.1.6-alpha.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -28,7 +28,7 @@ var LocalStateHandler = class {
28
28
  }
29
29
  };
30
30
  }
31
- // Default GenerateState implementation
31
+ // Default generateState implementation
32
32
  async generateState(provider) {
33
33
  const state = randomBytes(16).toString("hex");
34
34
  const codeVerifier = randomBytes(32).toString("hex");
@@ -41,6 +41,10 @@ var LocalStateHandler = class {
41
41
  }
42
42
  };
43
43
  }
44
+ // PascalCase alias for backward compatibility
45
+ async GenerateState(provider) {
46
+ return this.generateState(provider);
47
+ }
44
48
  };
45
49
 
46
50
  // src/lixa.ts
@@ -53,7 +57,20 @@ var LocalSessionHandler = class {
53
57
  emailToSessionMap = /* @__PURE__ */ new Map();
54
58
  sessionStorage;
55
59
  constructor(defaultTtlSeconds = 600) {
56
- this.cache = new NodeCache2({ stdTTL: defaultTtlSeconds });
60
+ this.cache = new NodeCache2({ stdTTL: defaultTtlSeconds, checkperiod: 60 });
61
+ this.cache.on("expired", (_key, value) => {
62
+ if (value && typeof value === "object" && value.email) {
63
+ this.emailToSessionMap.delete(String(value.email).toLowerCase());
64
+ }
65
+ });
66
+ this.cache.on("del", (_key, value) => {
67
+ if (value && typeof value === "object" && value.email) {
68
+ this.emailToSessionMap.delete(String(value.email).toLowerCase());
69
+ }
70
+ });
71
+ this.cache.on("flush", () => {
72
+ this.emailToSessionMap.clear();
73
+ });
57
74
  this.sessionStorage = {
58
75
  saveSession: async (sessionId, session, expiresInSeconds) => {
59
76
  this.cache.set(sessionId, session, expiresInSeconds);
@@ -84,8 +101,8 @@ var LocalSessionHandler = class {
84
101
  }
85
102
  };
86
103
  }
87
- // Default GenerateSession implementation
88
- async GenerateSession(tokenData, providerMetadata) {
104
+ // Default generateSession implementation
105
+ async generateSession(tokenData, _providerMetadata) {
89
106
  if (!tokenData.access_token || typeof tokenData.access_token !== "string") {
90
107
  throw new Error("No valid access token found in OAuth response");
91
108
  }
@@ -95,6 +112,10 @@ var LocalSessionHandler = class {
95
112
  };
96
113
  return session;
97
114
  }
115
+ // PascalCase alias for backward compatibility
116
+ async GenerateSession(tokenData, providerMetadata) {
117
+ return this.generateSession(tokenData, providerMetadata);
118
+ }
98
119
  };
99
120
 
100
121
  // src/utils/user-info.ts
@@ -245,8 +266,12 @@ var Lixa = class _Lixa {
245
266
  static DEFAULT_PROVIDERS = /* @__PURE__ */ new Map();
246
267
  static CONFIGURED_PROVIDERS = /* @__PURE__ */ new Map();
247
268
  // Legacy registry for backward compatibility
248
- static LOCAL_STATE_HANDLER = new LocalStateHandler();
249
- static LOCAL_SESSION_HANDLER = new LocalSessionHandler();
269
+ // Instance-scoped fallback handlers to ensure no cross-instance state pollution
270
+ localStateHandler;
271
+ localSessionHandler;
272
+ localResourceHandler;
273
+ userResourceStore = /* @__PURE__ */ new Map();
274
+ refreshMutexes = /* @__PURE__ */ new Map();
250
275
  config;
251
276
  stateHandler;
252
277
  sessionHandler;
@@ -267,10 +292,44 @@ var Lixa = class _Lixa {
267
292
  */
268
293
  constructor(config) {
269
294
  this.config = config;
270
- this.stateHandler = config.stateHandler || _Lixa.LOCAL_STATE_HANDLER;
271
- this.sessionHandler = config.sessionHandler || _Lixa.LOCAL_SESSION_HANDLER;
272
- this.resourceHandler = config.resourceHandler || _Lixa.LOCAL_RESOURCE_HANDLER;
273
295
  this.debug = config.debug || false;
296
+ this.localStateHandler = new LocalStateHandler();
297
+ this.localSessionHandler = new LocalSessionHandler();
298
+ this.localResourceHandler = {
299
+ resourceStorage: {
300
+ saveResource: async (userId, provider, resource) => {
301
+ let userMap = this.userResourceStore.get(userId);
302
+ if (!userMap) {
303
+ userMap = /* @__PURE__ */ new Map();
304
+ this.userResourceStore.set(userId, userMap);
305
+ }
306
+ userMap.set(provider.toLowerCase(), resource);
307
+ },
308
+ getResource: async (userId, provider) => {
309
+ const userMap = this.userResourceStore.get(userId);
310
+ return userMap?.get(provider.toLowerCase()) || null;
311
+ },
312
+ getUserResources: async (userId) => {
313
+ const userMap = this.userResourceStore.get(userId);
314
+ const result = {};
315
+ if (userMap) {
316
+ for (const [p, r] of userMap.entries()) {
317
+ result[p] = r;
318
+ }
319
+ }
320
+ return result;
321
+ },
322
+ deleteResource: async (userId, provider) => {
323
+ const userMap = this.userResourceStore.get(userId);
324
+ if (userMap) {
325
+ userMap.delete(provider.toLowerCase());
326
+ }
327
+ }
328
+ }
329
+ };
330
+ this.stateHandler = config.stateHandler || this.localStateHandler;
331
+ this.sessionHandler = config.sessionHandler || this.localSessionHandler;
332
+ this.resourceHandler = config.resourceHandler || this.localResourceHandler;
274
333
  this.log("INFO", "Init", "Initializing Lixa instance", {
275
334
  providers: Object.keys(config.providers),
276
335
  debug: this.debug
@@ -348,18 +407,20 @@ var Lixa = class _Lixa {
348
407
  }
349
408
  }
350
409
  /**
351
- * Structured debug logging with standardized format.
410
+ * Structured logging with standardized format and custom logger support.
352
411
  *
353
- * @param level - Log level (INFO, WARN, ERROR)
354
- * @param context - Context of the log (Init, Auth, Token, Session, State)
412
+ * @param level - Log level (INFO, WARN, ERROR, DEBUG)
413
+ * @param context - Context of the log (Init, Auth, Token, Session, State, AccountLinking, Resource)
355
414
  * @param message - Log message
356
415
  * @param data - Optional data to log
357
- *
358
- * @remarks
359
- * Format: [Lixa] [timestamp] [level] [context] message
360
- * Only logs when debug mode is enabled.
361
416
  */
362
417
  log(level, context, message, data) {
418
+ if (this.config.logger) {
419
+ try {
420
+ this.config.logger.log(level, context, message, data);
421
+ } catch {
422
+ }
423
+ }
363
424
  if (!this.debug) return;
364
425
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
365
426
  const prefix = `[Lixa] [${timestamp}] [${level}] [${context}]`;
@@ -609,18 +670,19 @@ var Lixa = class _Lixa {
609
670
  const providerImpl = this.getProvider(providerType, providerConfig);
610
671
  let stateValue;
611
672
  let codeVerifier;
612
- if (this.stateHandler.generateState) {
613
- this.log("INFO", "State", "Calling custom GenerateState");
614
- const generated = await this.stateHandler.generateState(providerType);
673
+ const generateStateFn = this.stateHandler.generateState || this.stateHandler.GenerateState;
674
+ if (generateStateFn) {
675
+ this.log("INFO", "State", "Calling custom generateState");
676
+ const generated = await generateStateFn(providerType);
615
677
  stateValue = state || generated.state;
616
678
  codeVerifier = generated.data.codeVerifier;
617
- const storage = this.stateHandler.stateStorage || _Lixa.LOCAL_STATE_HANDLER.stateStorage;
679
+ const storage = this.stateHandler.stateStorage || this.localStateHandler.stateStorage;
618
680
  await storage.saveState(stateValue, generated.data, 300);
619
681
  } else {
620
682
  this.log("INFO", "State", "Using default state generation");
621
683
  stateValue = state || randomBytes2(16).toString("hex");
622
684
  codeVerifier = randomBytes2(32).toString("hex");
623
- const storage = this.stateHandler.stateStorage || _Lixa.LOCAL_STATE_HANDLER.stateStorage;
685
+ const storage = this.stateHandler.stateStorage || this.localStateHandler.stateStorage;
624
686
  await storage.saveState(
625
687
  stateValue,
626
688
  {
@@ -629,12 +691,17 @@ var Lixa = class _Lixa {
629
691
  codeVerifier
630
692
  },
631
693
  300
632
- // 5 minutes in seconds
694
+ // 5 minutes in seconds (synchronized with cookie TTL)
633
695
  );
634
696
  }
635
697
  const codeChallenge = _Lixa.buildCodeChallenge(codeVerifier);
636
698
  this.log("INFO", "State", `Saved state for provider: ${providerType}`, { state: stateValue });
637
- const authNScopes = this.resolveAuthNScopes(providerType, providerConfig.scopes, providerImpl);
699
+ const authNScopes = this.resolveAuthNScopes(
700
+ providerType,
701
+ providerConfig.scopes,
702
+ providerImpl,
703
+ providerConfig.allowNonAuthScopes
704
+ );
638
705
  const params = new URLSearchParams({
639
706
  client_id: providerConfig.clientId,
640
707
  redirect_uri: providerConfig.redirectUri,
@@ -653,9 +720,12 @@ var Lixa = class _Lixa {
653
720
  return authUrl;
654
721
  }
655
722
  /**
656
- * Restricts primary authentication scopes strictly to AuthN identity scopes.
723
+ * Restricts primary authentication scopes strictly to AuthN identity scopes unless allowNonAuthScopes is true.
657
724
  */
658
- resolveAuthNScopes(providerType, configuredScopes, providerImpl) {
725
+ resolveAuthNScopes(providerType, configuredScopes, providerImpl, allowNonAuthScopes) {
726
+ if (allowNonAuthScopes) {
727
+ return configuredScopes && configuredScopes.length > 0 ? configuredScopes : providerImpl.authScopes || ["openid", "email", "profile"];
728
+ }
659
729
  const defaultAuthScopes = {
660
730
  google: ["openid", "email", "profile"],
661
731
  github: ["read:user", "user:email"],
@@ -684,7 +754,7 @@ var Lixa = class _Lixa {
684
754
  "Auth",
685
755
  `Primary authentication is strictly limited to AuthN scopes. Excluded non-AuthN resource scopes: [${nonAuthNScopes.join(
686
756
  ", "
687
- )}]. Use lixa.getResourceAuthUrl() post-login to connect resource providers.`
757
+ )}]. Set 'allowNonAuthScopes: true' on provider config to include them, or use lixa.getResourceAuthUrl() post-login to connect resource providers.`
688
758
  );
689
759
  }
690
760
  if (validAuthNScopes.length > 0) {
@@ -727,7 +797,7 @@ var Lixa = class _Lixa {
727
797
  throw new InvalidOAuthCallbackError("Invalid or missing state in callback");
728
798
  }
729
799
  this.log("INFO", "State", "Validating state parameter", { state });
730
- const stateStorage = this.stateHandler.stateStorage || _Lixa.LOCAL_STATE_HANDLER.stateStorage;
800
+ const stateStorage = this.stateHandler.stateStorage || this.localStateHandler.stateStorage;
731
801
  const cachedState = await stateStorage.getState(state);
732
802
  if (!cachedState) {
733
803
  this.log("ERROR", "State", "State validation failed: state not found or expired", { state });
@@ -767,9 +837,12 @@ var Lixa = class _Lixa {
767
837
  extractedUserInfo = userInfo;
768
838
  } catch {
769
839
  }
770
- const generateSession = this.sessionHandler.generateSession || _Lixa.LOCAL_SESSION_HANDLER.GenerateSession.bind(_Lixa.LOCAL_SESSION_HANDLER);
771
- if (this.sessionHandler.generateSession) {
772
- this.log("INFO", "Session", "Calling custom GenerateSession");
840
+ const generateSession = this.sessionHandler.generateSession || this.sessionHandler.GenerateSession || (this.localSessionHandler.generateSession ? this.localSessionHandler.generateSession.bind(this.localSessionHandler) : void 0);
841
+ if (!generateSession) {
842
+ throw new Error("No session generation handler available");
843
+ }
844
+ if (this.sessionHandler.generateSession || this.sessionHandler.GenerateSession) {
845
+ this.log("INFO", "Session", "Calling custom generateSession");
773
846
  } else {
774
847
  this.log("INFO", "Session", "Using default session generation");
775
848
  }
@@ -778,7 +851,7 @@ var Lixa = class _Lixa {
778
851
  if (extractedUserInfo?.email && !session.email) {
779
852
  session.email = extractedUserInfo.email;
780
853
  }
781
- const sessionStorage = this.sessionHandler.sessionStorage || _Lixa.LOCAL_SESSION_HANDLER.sessionStorage;
854
+ const sessionStorage = this.sessionHandler.sessionStorage || this.localSessionHandler.sessionStorage;
782
855
  const linkingConfig = this.config.accountLinking;
783
856
  const mode = String(linkingConfig?.mode || "");
784
857
  const isLinkByEmail = mode === "AUTO_LINK_BY_VERIFIED_EMAIL" /* AUTO_LINK_BY_VERIFIED_EMAIL */ || mode === "AUTO_LINK_BY_VERIFIED_EMAIL" || mode === "linkByEmail";
@@ -831,27 +904,31 @@ var Lixa = class _Lixa {
831
904
  */
832
905
  async linkAccount(params) {
833
906
  const { sessionId, provider, code, state } = params;
834
- const sessionStorage = this.sessionHandler.sessionStorage || _Lixa.LOCAL_SESSION_HANDLER.sessionStorage;
907
+ const sessionStorage = this.sessionHandler.sessionStorage || this.localSessionHandler.sessionStorage;
835
908
  const existingSession = await sessionStorage.getSession(sessionId);
836
909
  if (!existingSession) {
837
910
  throw new SessionNotFoundError("Invalid session ID. User must be authenticated to link an account.", { sessionId });
838
911
  }
912
+ if (!code || code.trim() === "") {
913
+ throw new InvalidOAuthCallbackError("Invalid or missing code in linkAccount");
914
+ }
839
915
  const providerType = String(provider).toLowerCase();
840
916
  const providerConfig = this.findProviderByType(providerType);
841
917
  if (!providerConfig) {
842
918
  throw new ProviderNotConfiguredError(String(provider));
843
919
  }
844
920
  const providerImpl = this.getProvider(providerType, providerConfig);
845
- let codeVerifier = randomBytes2(32).toString("hex");
921
+ let codeVerifier;
846
922
  if (state) {
847
- const stateStorage = this.stateHandler.stateStorage || _Lixa.LOCAL_STATE_HANDLER.stateStorage;
923
+ const stateStorage = this.stateHandler.stateStorage || this.localStateHandler.stateStorage;
848
924
  const cachedState = await stateStorage.getState(state);
849
- if (cachedState) {
850
- codeVerifier = cachedState.codeVerifier;
851
- await stateStorage.deleteState(state);
925
+ if (!cachedState) {
926
+ throw new InvalidStateError("Invalid or expired state during account linking", { state });
852
927
  }
928
+ codeVerifier = cachedState.codeVerifier;
929
+ await stateStorage.deleteState(state);
853
930
  }
854
- const tokens = await this.exchangeCodeForToken(providerType, providerConfig, providerImpl, codeVerifier);
931
+ const tokens = await this.exchangeCodeForToken(code, providerConfig, providerImpl, codeVerifier || "");
855
932
  const providerMetadata = {
856
933
  name: providerType,
857
934
  endpoints: {
@@ -889,7 +966,7 @@ var Lixa = class _Lixa {
889
966
  * @returns Promise resolving to true on successful unlink
890
967
  */
891
968
  async unlinkAccount(sessionId, providerToUnlink) {
892
- const sessionStorage = this.sessionHandler.sessionStorage || _Lixa.LOCAL_SESSION_HANDLER.sessionStorage;
969
+ const sessionStorage = this.sessionHandler.sessionStorage || this.localSessionHandler.sessionStorage;
893
970
  const session = await sessionStorage.getSession(sessionId);
894
971
  if (!session || !session.accounts) {
895
972
  throw new AccountUnlinkError("Session not found or has no linked accounts.", { sessionId });
@@ -918,7 +995,7 @@ var Lixa = class _Lixa {
918
995
  */
919
996
  async getResourceAuthUrl(params) {
920
997
  const { sessionId, provider, scopes, state, prompt, extraConfig } = params;
921
- const sessionStorage = this.sessionHandler.sessionStorage || _Lixa.LOCAL_SESSION_HANDLER.sessionStorage;
998
+ const sessionStorage = this.sessionHandler.sessionStorage || this.localSessionHandler.sessionStorage;
922
999
  const activeSession = await sessionStorage.getSession(sessionId);
923
1000
  if (!activeSession) {
924
1001
  throw new SessionNotFoundError("Authentication required. Active session must exist to connect resource providers.", { sessionId });
@@ -931,7 +1008,7 @@ var Lixa = class _Lixa {
931
1008
  const providerImpl = this.getProvider(providerType, providerConfig);
932
1009
  const stateValue = state || randomBytes2(16).toString("hex");
933
1010
  const codeVerifier = randomBytes2(32).toString("hex");
934
- const storage = this.stateHandler.stateStorage || _Lixa.LOCAL_STATE_HANDLER.stateStorage;
1011
+ const storage = this.stateHandler.stateStorage || this.localStateHandler.stateStorage;
935
1012
  await storage.saveState(
936
1013
  stateValue,
937
1014
  {
@@ -956,45 +1033,6 @@ var Lixa = class _Lixa {
956
1033
  });
957
1034
  return `${providerImpl.authorizationEndpoint}?${searchParams.toString()}`;
958
1035
  }
959
- /**
960
- * Handles the OAuth callback for a connected resource provider and stores resource tokens on the session.
961
- *
962
- * @param params - Object containing sessionId, provider, code, state, and requested scopes
963
- * @returns Updated Session containing stored resource tokens under session.resources[provider]
964
- */
965
- static userResourceStore = /* @__PURE__ */ new Map();
966
- static LOCAL_RESOURCE_HANDLER = {
967
- resourceStorage: {
968
- saveResource: async (userId, provider, resource) => {
969
- let userMap = _Lixa.userResourceStore.get(userId);
970
- if (!userMap) {
971
- userMap = /* @__PURE__ */ new Map();
972
- _Lixa.userResourceStore.set(userId, userMap);
973
- }
974
- userMap.set(provider.toLowerCase(), resource);
975
- },
976
- getResource: async (userId, provider) => {
977
- const userMap = _Lixa.userResourceStore.get(userId);
978
- return userMap?.get(provider.toLowerCase()) || null;
979
- },
980
- getUserResources: async (userId) => {
981
- const userMap = _Lixa.userResourceStore.get(userId);
982
- const result = {};
983
- if (userMap) {
984
- for (const [p, r] of userMap.entries()) {
985
- result[p] = r;
986
- }
987
- }
988
- return result;
989
- },
990
- deleteResource: async (userId, provider) => {
991
- const userMap = _Lixa.userResourceStore.get(userId);
992
- if (userMap) {
993
- userMap.delete(provider.toLowerCase());
994
- }
995
- }
996
- }
997
- };
998
1036
  getUserKeyFromSession(session) {
999
1037
  return session.userId || session.email || session.id || "anonymous";
1000
1038
  }
@@ -1003,7 +1041,7 @@ var Lixa = class _Lixa {
1003
1041
  */
1004
1042
  async handleResourceCallback(params) {
1005
1043
  const { sessionId, provider, code, state, scopes } = params;
1006
- const sessionStorage = this.sessionHandler.sessionStorage || _Lixa.LOCAL_SESSION_HANDLER.sessionStorage;
1044
+ const sessionStorage = this.sessionHandler.sessionStorage || this.localSessionHandler.sessionStorage;
1007
1045
  const activeSession = await sessionStorage.getSession(sessionId);
1008
1046
  if (!activeSession) {
1009
1047
  throw new SessionNotFoundError("Authentication required. Active session not found for resource connection.", { sessionId });
@@ -1016,12 +1054,13 @@ var Lixa = class _Lixa {
1016
1054
  const providerImpl = this.getProvider(providerType, providerConfig);
1017
1055
  let codeVerifier = randomBytes2(32).toString("hex");
1018
1056
  if (state) {
1019
- const stateStorage = this.stateHandler.stateStorage || _Lixa.LOCAL_STATE_HANDLER.stateStorage;
1057
+ const stateStorage = this.stateHandler.stateStorage || this.localStateHandler.stateStorage;
1020
1058
  const cachedState = await stateStorage.getState(state);
1021
- if (cachedState) {
1022
- codeVerifier = cachedState.codeVerifier;
1023
- await stateStorage.deleteState(state);
1059
+ if (!cachedState) {
1060
+ throw new InvalidStateError("Invalid or expired state during resource connection callback", { state });
1024
1061
  }
1062
+ codeVerifier = cachedState.codeVerifier;
1063
+ await stateStorage.deleteState(state);
1025
1064
  }
1026
1065
  const tokens = await this.exchangeCodeForToken(code, providerConfig, providerImpl, codeVerifier);
1027
1066
  const expiresAt = tokens.expires_in ? Date.now() + tokens.expires_in * 1e3 : void 0;
@@ -1035,7 +1074,7 @@ var Lixa = class _Lixa {
1035
1074
  connectedAt: Date.now()
1036
1075
  };
1037
1076
  const userKey = this.getUserKeyFromSession(activeSession);
1038
- const resourceStorage = this.resourceHandler.resourceStorage || _Lixa.LOCAL_RESOURCE_HANDLER.resourceStorage;
1077
+ const resourceStorage = this.resourceHandler.resourceStorage || this.localResourceHandler.resourceStorage;
1039
1078
  await resourceStorage.saveResource(userKey, providerType, connectedResource);
1040
1079
  activeSession.resources = activeSession.resources || {};
1041
1080
  activeSession.resources[providerType] = connectedResource;
@@ -1050,7 +1089,7 @@ var Lixa = class _Lixa {
1050
1089
  * @param provider - Resource provider identifier (e.g. 'github', 'google')
1051
1090
  */
1052
1091
  async getUserResource(userIdOrEmail, provider) {
1053
- const resourceStorage = this.resourceHandler.resourceStorage || _Lixa.LOCAL_RESOURCE_HANDLER.resourceStorage;
1092
+ const resourceStorage = this.resourceHandler.resourceStorage || this.localResourceHandler.resourceStorage;
1054
1093
  const providerType = provider.toLowerCase();
1055
1094
  const resource = await resourceStorage.getResource(userIdOrEmail, providerType);
1056
1095
  if (!resource) return null;
@@ -1068,7 +1107,7 @@ var Lixa = class _Lixa {
1068
1107
  * Retrieves all connected resources for a specific User ID / Email.
1069
1108
  */
1070
1109
  async getUserResources(userIdOrEmail) {
1071
- const resourceStorage = this.resourceHandler.resourceStorage || _Lixa.LOCAL_RESOURCE_HANDLER.resourceStorage;
1110
+ const resourceStorage = this.resourceHandler.resourceStorage || this.localResourceHandler.resourceStorage;
1072
1111
  return await resourceStorage.getUserResources(userIdOrEmail);
1073
1112
  }
1074
1113
  /**
@@ -1078,7 +1117,7 @@ var Lixa = class _Lixa {
1078
1117
  * @param provider - Provider identifier (e.g. 'github', 'google')
1079
1118
  */
1080
1119
  async getConnectedResource(sessionId, provider) {
1081
- const sessionStorage = this.sessionHandler.sessionStorage || _Lixa.LOCAL_SESSION_HANDLER.sessionStorage;
1120
+ const sessionStorage = this.sessionHandler.sessionStorage || this.localSessionHandler.sessionStorage;
1082
1121
  const activeSession = await sessionStorage.getSession(sessionId);
1083
1122
  if (!activeSession) return null;
1084
1123
  const providerType = provider.toLowerCase();
@@ -1103,12 +1142,29 @@ var Lixa = class _Lixa {
1103
1142
  }
1104
1143
  /**
1105
1144
  * Refreshes a user's resource access token using its refresh token.
1145
+ * Deduplicates concurrent refresh requests via an in-flight promise mutex.
1106
1146
  *
1107
1147
  * @param userIdOrEmail - User identifier or email
1108
1148
  * @param provider - Provider identifier (e.g. 'google', 'github')
1109
1149
  */
1110
1150
  async refreshUserResourceToken(userIdOrEmail, provider, existingResource) {
1111
- const resourceStorage = this.resourceHandler.resourceStorage || _Lixa.LOCAL_RESOURCE_HANDLER.resourceStorage;
1151
+ const mutexKey = `${userIdOrEmail.toLowerCase()}:${provider.toLowerCase()}`;
1152
+ const existingPromise = this.refreshMutexes.get(mutexKey);
1153
+ if (existingPromise) {
1154
+ this.log("INFO", "Token", `Concurrent refresh request detected for '${mutexKey}'. Reusing in-flight promise.`);
1155
+ return existingPromise;
1156
+ }
1157
+ const refreshPromise = this.executeRefreshUserResourceToken(userIdOrEmail, provider, existingResource).finally(() => {
1158
+ this.refreshMutexes.delete(mutexKey);
1159
+ });
1160
+ this.refreshMutexes.set(mutexKey, refreshPromise);
1161
+ return refreshPromise;
1162
+ }
1163
+ /**
1164
+ * Internal execution of refresh token exchange.
1165
+ */
1166
+ async executeRefreshUserResourceToken(userIdOrEmail, provider, existingResource) {
1167
+ const resourceStorage = this.resourceHandler.resourceStorage || this.localResourceHandler.resourceStorage;
1112
1168
  const providerType = provider.toLowerCase();
1113
1169
  let resource = existingResource || await resourceStorage.getResource(userIdOrEmail, providerType);
1114
1170
  if (!resource || !resource.refreshToken) {
@@ -1161,7 +1217,7 @@ var Lixa = class _Lixa {
1161
1217
  * Refreshes a connected resource access token for an active session.
1162
1218
  */
1163
1219
  async refreshResourceToken(sessionId, provider) {
1164
- const sessionStorage = this.sessionHandler.sessionStorage || _Lixa.LOCAL_SESSION_HANDLER.sessionStorage;
1220
+ const sessionStorage = this.sessionHandler.sessionStorage || this.localSessionHandler.sessionStorage;
1165
1221
  const activeSession = await sessionStorage.getSession(sessionId);
1166
1222
  if (!activeSession) {
1167
1223
  throw new SessionNotFoundError("Active session not found.", { sessionId });
@@ -1179,7 +1235,7 @@ var Lixa = class _Lixa {
1179
1235
  * Disconnects a resource provider for a specific User ID / Email.
1180
1236
  */
1181
1237
  async disconnectUserResource(userIdOrEmail, provider) {
1182
- const resourceStorage = this.resourceHandler.resourceStorage || _Lixa.LOCAL_RESOURCE_HANDLER.resourceStorage;
1238
+ const resourceStorage = this.resourceHandler.resourceStorage || this.localResourceHandler.resourceStorage;
1183
1239
  await resourceStorage.deleteResource(userIdOrEmail, provider.toLowerCase());
1184
1240
  return true;
1185
1241
  }
@@ -1187,7 +1243,7 @@ var Lixa = class _Lixa {
1187
1243
  * Disconnects a resource provider from an active session and user account.
1188
1244
  */
1189
1245
  async disconnectResource(sessionId, provider) {
1190
- const sessionStorage = this.sessionHandler.sessionStorage || _Lixa.LOCAL_SESSION_HANDLER.sessionStorage;
1246
+ const sessionStorage = this.sessionHandler.sessionStorage || this.localSessionHandler.sessionStorage;
1191
1247
  const activeSession = await sessionStorage.getSession(sessionId);
1192
1248
  if (activeSession) {
1193
1249
  const userKey = this.getUserKeyFromSession(activeSession);
@@ -1204,7 +1260,7 @@ var Lixa = class _Lixa {
1204
1260
  * Retrieves active session details from session storage.
1205
1261
  */
1206
1262
  async fetchSessionInfo(sessionId) {
1207
- const sessionStorage = this.sessionHandler.sessionStorage || _Lixa.LOCAL_SESSION_HANDLER.sessionStorage;
1263
+ const sessionStorage = this.sessionHandler.sessionStorage || this.localSessionHandler.sessionStorage;
1208
1264
  return await sessionStorage.getSession(sessionId);
1209
1265
  }
1210
1266
  /**
@@ -1213,7 +1269,7 @@ var Lixa = class _Lixa {
1213
1269
  * @param sessionId - Active session identifier
1214
1270
  */
1215
1271
  async deleteSession(sessionId) {
1216
- const sessionStorage = this.sessionHandler.sessionStorage || _Lixa.LOCAL_SESSION_HANDLER.sessionStorage;
1272
+ const sessionStorage = this.sessionHandler.sessionStorage || this.localSessionHandler.sessionStorage;
1217
1273
  await sessionStorage.deleteSession(sessionId);
1218
1274
  }
1219
1275
  async exchangeCodeForToken(code, providerConfig, providerImpl, codeVerifier) {
@@ -1269,7 +1325,7 @@ var Lixa = class _Lixa {
1269
1325
  var DEFAULT_SESSION_COOKIE_NAME = "lixa_session";
1270
1326
  var DEFAULT_STATE_COOKIE_NAME = "lixa_oauth_state";
1271
1327
  var DEFAULT_SESSION_MAX_AGE_SECONDS = 24 * 60 * 60;
1272
- var DEFAULT_STATE_MAX_AGE_SECONDS = 10 * 60;
1328
+ var DEFAULT_STATE_MAX_AGE_SECONDS = 5 * 60;
1273
1329
  function isProductionEnvironment() {
1274
1330
  return typeof process !== "undefined" && process.env?.NODE_ENV === "production";
1275
1331
  }
@@ -1383,6 +1439,54 @@ function clearStateCookie(options) {
1383
1439
  header
1384
1440
  };
1385
1441
  }
1442
+
1443
+ // src/express.ts
1444
+ function setSessionCookieOnResponse(res, sessionId, options) {
1445
+ const cookie = createSessionCookie(sessionId, options);
1446
+ res.cookie(cookie.name, cookie.value, cookie.options);
1447
+ }
1448
+ function clearSessionCookieOnResponse(res, options) {
1449
+ const cookie = clearSessionCookie(options);
1450
+ res.cookie(cookie.name, cookie.value, cookie.options);
1451
+ }
1452
+ function setStateCookieOnResponse(res, state, options) {
1453
+ const cookie = createStateCookie(state, options);
1454
+ res.cookie(cookie.name, cookie.value, cookie.options);
1455
+ }
1456
+ function clearStateCookieOnResponse(res, options) {
1457
+ const cookie = clearStateCookie(options);
1458
+ res.cookie(cookie.name, cookie.value, cookie.options);
1459
+ }
1460
+ function createRequireAuthMiddleware(lixa, options) {
1461
+ const cookieName = options?.sessionCookieName || DEFAULT_SESSION_COOKIE_NAME;
1462
+ return async (req, res, next) => {
1463
+ try {
1464
+ const sessionId = req.cookies?.[cookieName];
1465
+ if (!sessionId) {
1466
+ if (options?.unauthorizedHandler) {
1467
+ options.unauthorizedHandler(req, res);
1468
+ return;
1469
+ }
1470
+ res.status(401).json({ error: "Unauthorized: Missing session cookie" });
1471
+ return;
1472
+ }
1473
+ const session = await lixa.fetchSessionInfo(sessionId);
1474
+ if (!session) {
1475
+ if (options?.unauthorizedHandler) {
1476
+ options.unauthorizedHandler(req, res);
1477
+ return;
1478
+ }
1479
+ res.status(401).json({ error: "Unauthorized: Invalid or expired session" });
1480
+ return;
1481
+ }
1482
+ req.sessionInfo = session;
1483
+ req.sessionId = sessionId;
1484
+ next();
1485
+ } catch (error) {
1486
+ next(error);
1487
+ }
1488
+ };
1489
+ }
1386
1490
  export {
1387
1491
  AccountLinkingStrategy,
1388
1492
  AccountUnlinkError,
@@ -1401,7 +1505,10 @@ export {
1401
1505
  SessionNotFoundError,
1402
1506
  TokenExchangeError,
1403
1507
  clearSessionCookie,
1508
+ clearSessionCookieOnResponse,
1404
1509
  clearStateCookie,
1510
+ clearStateCookieOnResponse,
1511
+ createRequireAuthMiddleware,
1405
1512
  createSessionCookie,
1406
1513
  createStateCookie,
1407
1514
  decodeIdToken,
@@ -1409,6 +1516,8 @@ export {
1409
1516
  extractUserInfo,
1410
1517
  fetchUserInfo,
1411
1518
  isProductionEnvironment,
1412
- serializeCookie
1519
+ serializeCookie,
1520
+ setSessionCookieOnResponse,
1521
+ setStateCookieOnResponse
1413
1522
  };
1414
1523
  //# sourceMappingURL=index.js.map