@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.cjs CHANGED
@@ -47,7 +47,10 @@ __export(src_exports, {
47
47
  SessionNotFoundError: () => SessionNotFoundError,
48
48
  TokenExchangeError: () => TokenExchangeError,
49
49
  clearSessionCookie: () => clearSessionCookie,
50
+ clearSessionCookieOnResponse: () => clearSessionCookieOnResponse,
50
51
  clearStateCookie: () => clearStateCookie,
52
+ clearStateCookieOnResponse: () => clearStateCookieOnResponse,
53
+ createRequireAuthMiddleware: () => createRequireAuthMiddleware,
51
54
  createSessionCookie: () => createSessionCookie,
52
55
  createStateCookie: () => createStateCookie,
53
56
  decodeIdToken: () => decodeIdToken,
@@ -55,7 +58,9 @@ __export(src_exports, {
55
58
  extractUserInfo: () => extractUserInfo,
56
59
  fetchUserInfo: () => fetchUserInfo,
57
60
  isProductionEnvironment: () => isProductionEnvironment,
58
- serializeCookie: () => serializeCookie
61
+ serializeCookie: () => serializeCookie,
62
+ setSessionCookieOnResponse: () => setSessionCookieOnResponse,
63
+ setStateCookieOnResponse: () => setStateCookieOnResponse
59
64
  });
60
65
  module.exports = __toCommonJS(src_exports);
61
66
 
@@ -89,7 +94,7 @@ var LocalStateHandler = class {
89
94
  }
90
95
  };
91
96
  }
92
- // Default GenerateState implementation
97
+ // Default generateState implementation
93
98
  async generateState(provider) {
94
99
  const state = (0, import_crypto.randomBytes)(16).toString("hex");
95
100
  const codeVerifier = (0, import_crypto.randomBytes)(32).toString("hex");
@@ -102,6 +107,10 @@ var LocalStateHandler = class {
102
107
  }
103
108
  };
104
109
  }
110
+ // PascalCase alias for backward compatibility
111
+ async GenerateState(provider) {
112
+ return this.generateState(provider);
113
+ }
105
114
  };
106
115
 
107
116
  // src/lixa.ts
@@ -114,7 +123,20 @@ var LocalSessionHandler = class {
114
123
  emailToSessionMap = /* @__PURE__ */ new Map();
115
124
  sessionStorage;
116
125
  constructor(defaultTtlSeconds = 600) {
117
- this.cache = new import_node_cache2.default({ stdTTL: defaultTtlSeconds });
126
+ this.cache = new import_node_cache2.default({ stdTTL: defaultTtlSeconds, checkperiod: 60 });
127
+ this.cache.on("expired", (_key, value) => {
128
+ if (value && typeof value === "object" && value.email) {
129
+ this.emailToSessionMap.delete(String(value.email).toLowerCase());
130
+ }
131
+ });
132
+ this.cache.on("del", (_key, value) => {
133
+ if (value && typeof value === "object" && value.email) {
134
+ this.emailToSessionMap.delete(String(value.email).toLowerCase());
135
+ }
136
+ });
137
+ this.cache.on("flush", () => {
138
+ this.emailToSessionMap.clear();
139
+ });
118
140
  this.sessionStorage = {
119
141
  saveSession: async (sessionId, session, expiresInSeconds) => {
120
142
  this.cache.set(sessionId, session, expiresInSeconds);
@@ -145,8 +167,8 @@ var LocalSessionHandler = class {
145
167
  }
146
168
  };
147
169
  }
148
- // Default GenerateSession implementation
149
- async GenerateSession(tokenData, providerMetadata) {
170
+ // Default generateSession implementation
171
+ async generateSession(tokenData, _providerMetadata) {
150
172
  if (!tokenData.access_token || typeof tokenData.access_token !== "string") {
151
173
  throw new Error("No valid access token found in OAuth response");
152
174
  }
@@ -156,6 +178,10 @@ var LocalSessionHandler = class {
156
178
  };
157
179
  return session;
158
180
  }
181
+ // PascalCase alias for backward compatibility
182
+ async GenerateSession(tokenData, providerMetadata) {
183
+ return this.generateSession(tokenData, providerMetadata);
184
+ }
159
185
  };
160
186
 
161
187
  // src/utils/user-info.ts
@@ -306,8 +332,12 @@ var Lixa = class _Lixa {
306
332
  static DEFAULT_PROVIDERS = /* @__PURE__ */ new Map();
307
333
  static CONFIGURED_PROVIDERS = /* @__PURE__ */ new Map();
308
334
  // Legacy registry for backward compatibility
309
- static LOCAL_STATE_HANDLER = new LocalStateHandler();
310
- static LOCAL_SESSION_HANDLER = new LocalSessionHandler();
335
+ // Instance-scoped fallback handlers to ensure no cross-instance state pollution
336
+ localStateHandler;
337
+ localSessionHandler;
338
+ localResourceHandler;
339
+ userResourceStore = /* @__PURE__ */ new Map();
340
+ refreshMutexes = /* @__PURE__ */ new Map();
311
341
  config;
312
342
  stateHandler;
313
343
  sessionHandler;
@@ -328,10 +358,44 @@ var Lixa = class _Lixa {
328
358
  */
329
359
  constructor(config) {
330
360
  this.config = config;
331
- this.stateHandler = config.stateHandler || _Lixa.LOCAL_STATE_HANDLER;
332
- this.sessionHandler = config.sessionHandler || _Lixa.LOCAL_SESSION_HANDLER;
333
- this.resourceHandler = config.resourceHandler || _Lixa.LOCAL_RESOURCE_HANDLER;
334
361
  this.debug = config.debug || false;
362
+ this.localStateHandler = new LocalStateHandler();
363
+ this.localSessionHandler = new LocalSessionHandler();
364
+ this.localResourceHandler = {
365
+ resourceStorage: {
366
+ saveResource: async (userId, provider, resource) => {
367
+ let userMap = this.userResourceStore.get(userId);
368
+ if (!userMap) {
369
+ userMap = /* @__PURE__ */ new Map();
370
+ this.userResourceStore.set(userId, userMap);
371
+ }
372
+ userMap.set(provider.toLowerCase(), resource);
373
+ },
374
+ getResource: async (userId, provider) => {
375
+ const userMap = this.userResourceStore.get(userId);
376
+ return userMap?.get(provider.toLowerCase()) || null;
377
+ },
378
+ getUserResources: async (userId) => {
379
+ const userMap = this.userResourceStore.get(userId);
380
+ const result = {};
381
+ if (userMap) {
382
+ for (const [p, r] of userMap.entries()) {
383
+ result[p] = r;
384
+ }
385
+ }
386
+ return result;
387
+ },
388
+ deleteResource: async (userId, provider) => {
389
+ const userMap = this.userResourceStore.get(userId);
390
+ if (userMap) {
391
+ userMap.delete(provider.toLowerCase());
392
+ }
393
+ }
394
+ }
395
+ };
396
+ this.stateHandler = config.stateHandler || this.localStateHandler;
397
+ this.sessionHandler = config.sessionHandler || this.localSessionHandler;
398
+ this.resourceHandler = config.resourceHandler || this.localResourceHandler;
335
399
  this.log("INFO", "Init", "Initializing Lixa instance", {
336
400
  providers: Object.keys(config.providers),
337
401
  debug: this.debug
@@ -409,18 +473,20 @@ var Lixa = class _Lixa {
409
473
  }
410
474
  }
411
475
  /**
412
- * Structured debug logging with standardized format.
476
+ * Structured logging with standardized format and custom logger support.
413
477
  *
414
- * @param level - Log level (INFO, WARN, ERROR)
415
- * @param context - Context of the log (Init, Auth, Token, Session, State)
478
+ * @param level - Log level (INFO, WARN, ERROR, DEBUG)
479
+ * @param context - Context of the log (Init, Auth, Token, Session, State, AccountLinking, Resource)
416
480
  * @param message - Log message
417
481
  * @param data - Optional data to log
418
- *
419
- * @remarks
420
- * Format: [Lixa] [timestamp] [level] [context] message
421
- * Only logs when debug mode is enabled.
422
482
  */
423
483
  log(level, context, message, data) {
484
+ if (this.config.logger) {
485
+ try {
486
+ this.config.logger.log(level, context, message, data);
487
+ } catch {
488
+ }
489
+ }
424
490
  if (!this.debug) return;
425
491
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
426
492
  const prefix = `[Lixa] [${timestamp}] [${level}] [${context}]`;
@@ -670,18 +736,19 @@ var Lixa = class _Lixa {
670
736
  const providerImpl = this.getProvider(providerType, providerConfig);
671
737
  let stateValue;
672
738
  let codeVerifier;
673
- if (this.stateHandler.generateState) {
674
- this.log("INFO", "State", "Calling custom GenerateState");
675
- const generated = await this.stateHandler.generateState(providerType);
739
+ const generateStateFn = this.stateHandler.generateState || this.stateHandler.GenerateState;
740
+ if (generateStateFn) {
741
+ this.log("INFO", "State", "Calling custom generateState");
742
+ const generated = await generateStateFn(providerType);
676
743
  stateValue = state || generated.state;
677
744
  codeVerifier = generated.data.codeVerifier;
678
- const storage = this.stateHandler.stateStorage || _Lixa.LOCAL_STATE_HANDLER.stateStorage;
745
+ const storage = this.stateHandler.stateStorage || this.localStateHandler.stateStorage;
679
746
  await storage.saveState(stateValue, generated.data, 300);
680
747
  } else {
681
748
  this.log("INFO", "State", "Using default state generation");
682
749
  stateValue = state || (0, import_crypto2.randomBytes)(16).toString("hex");
683
750
  codeVerifier = (0, import_crypto2.randomBytes)(32).toString("hex");
684
- const storage = this.stateHandler.stateStorage || _Lixa.LOCAL_STATE_HANDLER.stateStorage;
751
+ const storage = this.stateHandler.stateStorage || this.localStateHandler.stateStorage;
685
752
  await storage.saveState(
686
753
  stateValue,
687
754
  {
@@ -690,12 +757,17 @@ var Lixa = class _Lixa {
690
757
  codeVerifier
691
758
  },
692
759
  300
693
- // 5 minutes in seconds
760
+ // 5 minutes in seconds (synchronized with cookie TTL)
694
761
  );
695
762
  }
696
763
  const codeChallenge = _Lixa.buildCodeChallenge(codeVerifier);
697
764
  this.log("INFO", "State", `Saved state for provider: ${providerType}`, { state: stateValue });
698
- const authNScopes = this.resolveAuthNScopes(providerType, providerConfig.scopes, providerImpl);
765
+ const authNScopes = this.resolveAuthNScopes(
766
+ providerType,
767
+ providerConfig.scopes,
768
+ providerImpl,
769
+ providerConfig.allowNonAuthScopes
770
+ );
699
771
  const params = new URLSearchParams({
700
772
  client_id: providerConfig.clientId,
701
773
  redirect_uri: providerConfig.redirectUri,
@@ -714,9 +786,12 @@ var Lixa = class _Lixa {
714
786
  return authUrl;
715
787
  }
716
788
  /**
717
- * Restricts primary authentication scopes strictly to AuthN identity scopes.
789
+ * Restricts primary authentication scopes strictly to AuthN identity scopes unless allowNonAuthScopes is true.
718
790
  */
719
- resolveAuthNScopes(providerType, configuredScopes, providerImpl) {
791
+ resolveAuthNScopes(providerType, configuredScopes, providerImpl, allowNonAuthScopes) {
792
+ if (allowNonAuthScopes) {
793
+ return configuredScopes && configuredScopes.length > 0 ? configuredScopes : providerImpl.authScopes || ["openid", "email", "profile"];
794
+ }
720
795
  const defaultAuthScopes = {
721
796
  google: ["openid", "email", "profile"],
722
797
  github: ["read:user", "user:email"],
@@ -745,7 +820,7 @@ var Lixa = class _Lixa {
745
820
  "Auth",
746
821
  `Primary authentication is strictly limited to AuthN scopes. Excluded non-AuthN resource scopes: [${nonAuthNScopes.join(
747
822
  ", "
748
- )}]. Use lixa.getResourceAuthUrl() post-login to connect resource providers.`
823
+ )}]. Set 'allowNonAuthScopes: true' on provider config to include them, or use lixa.getResourceAuthUrl() post-login to connect resource providers.`
749
824
  );
750
825
  }
751
826
  if (validAuthNScopes.length > 0) {
@@ -788,7 +863,7 @@ var Lixa = class _Lixa {
788
863
  throw new InvalidOAuthCallbackError("Invalid or missing state in callback");
789
864
  }
790
865
  this.log("INFO", "State", "Validating state parameter", { state });
791
- const stateStorage = this.stateHandler.stateStorage || _Lixa.LOCAL_STATE_HANDLER.stateStorage;
866
+ const stateStorage = this.stateHandler.stateStorage || this.localStateHandler.stateStorage;
792
867
  const cachedState = await stateStorage.getState(state);
793
868
  if (!cachedState) {
794
869
  this.log("ERROR", "State", "State validation failed: state not found or expired", { state });
@@ -828,9 +903,12 @@ var Lixa = class _Lixa {
828
903
  extractedUserInfo = userInfo;
829
904
  } catch {
830
905
  }
831
- const generateSession = this.sessionHandler.generateSession || _Lixa.LOCAL_SESSION_HANDLER.GenerateSession.bind(_Lixa.LOCAL_SESSION_HANDLER);
832
- if (this.sessionHandler.generateSession) {
833
- this.log("INFO", "Session", "Calling custom GenerateSession");
906
+ const generateSession = this.sessionHandler.generateSession || this.sessionHandler.GenerateSession || (this.localSessionHandler.generateSession ? this.localSessionHandler.generateSession.bind(this.localSessionHandler) : void 0);
907
+ if (!generateSession) {
908
+ throw new Error("No session generation handler available");
909
+ }
910
+ if (this.sessionHandler.generateSession || this.sessionHandler.GenerateSession) {
911
+ this.log("INFO", "Session", "Calling custom generateSession");
834
912
  } else {
835
913
  this.log("INFO", "Session", "Using default session generation");
836
914
  }
@@ -839,7 +917,7 @@ var Lixa = class _Lixa {
839
917
  if (extractedUserInfo?.email && !session.email) {
840
918
  session.email = extractedUserInfo.email;
841
919
  }
842
- const sessionStorage = this.sessionHandler.sessionStorage || _Lixa.LOCAL_SESSION_HANDLER.sessionStorage;
920
+ const sessionStorage = this.sessionHandler.sessionStorage || this.localSessionHandler.sessionStorage;
843
921
  const linkingConfig = this.config.accountLinking;
844
922
  const mode = String(linkingConfig?.mode || "");
845
923
  const isLinkByEmail = mode === "AUTO_LINK_BY_VERIFIED_EMAIL" /* AUTO_LINK_BY_VERIFIED_EMAIL */ || mode === "AUTO_LINK_BY_VERIFIED_EMAIL" || mode === "linkByEmail";
@@ -892,27 +970,31 @@ var Lixa = class _Lixa {
892
970
  */
893
971
  async linkAccount(params) {
894
972
  const { sessionId, provider, code, state } = params;
895
- const sessionStorage = this.sessionHandler.sessionStorage || _Lixa.LOCAL_SESSION_HANDLER.sessionStorage;
973
+ const sessionStorage = this.sessionHandler.sessionStorage || this.localSessionHandler.sessionStorage;
896
974
  const existingSession = await sessionStorage.getSession(sessionId);
897
975
  if (!existingSession) {
898
976
  throw new SessionNotFoundError("Invalid session ID. User must be authenticated to link an account.", { sessionId });
899
977
  }
978
+ if (!code || code.trim() === "") {
979
+ throw new InvalidOAuthCallbackError("Invalid or missing code in linkAccount");
980
+ }
900
981
  const providerType = String(provider).toLowerCase();
901
982
  const providerConfig = this.findProviderByType(providerType);
902
983
  if (!providerConfig) {
903
984
  throw new ProviderNotConfiguredError(String(provider));
904
985
  }
905
986
  const providerImpl = this.getProvider(providerType, providerConfig);
906
- let codeVerifier = (0, import_crypto2.randomBytes)(32).toString("hex");
987
+ let codeVerifier;
907
988
  if (state) {
908
- const stateStorage = this.stateHandler.stateStorage || _Lixa.LOCAL_STATE_HANDLER.stateStorage;
989
+ const stateStorage = this.stateHandler.stateStorage || this.localStateHandler.stateStorage;
909
990
  const cachedState = await stateStorage.getState(state);
910
- if (cachedState) {
911
- codeVerifier = cachedState.codeVerifier;
912
- await stateStorage.deleteState(state);
991
+ if (!cachedState) {
992
+ throw new InvalidStateError("Invalid or expired state during account linking", { state });
913
993
  }
994
+ codeVerifier = cachedState.codeVerifier;
995
+ await stateStorage.deleteState(state);
914
996
  }
915
- const tokens = await this.exchangeCodeForToken(providerType, providerConfig, providerImpl, codeVerifier);
997
+ const tokens = await this.exchangeCodeForToken(code, providerConfig, providerImpl, codeVerifier || "");
916
998
  const providerMetadata = {
917
999
  name: providerType,
918
1000
  endpoints: {
@@ -950,7 +1032,7 @@ var Lixa = class _Lixa {
950
1032
  * @returns Promise resolving to true on successful unlink
951
1033
  */
952
1034
  async unlinkAccount(sessionId, providerToUnlink) {
953
- const sessionStorage = this.sessionHandler.sessionStorage || _Lixa.LOCAL_SESSION_HANDLER.sessionStorage;
1035
+ const sessionStorage = this.sessionHandler.sessionStorage || this.localSessionHandler.sessionStorage;
954
1036
  const session = await sessionStorage.getSession(sessionId);
955
1037
  if (!session || !session.accounts) {
956
1038
  throw new AccountUnlinkError("Session not found or has no linked accounts.", { sessionId });
@@ -979,7 +1061,7 @@ var Lixa = class _Lixa {
979
1061
  */
980
1062
  async getResourceAuthUrl(params) {
981
1063
  const { sessionId, provider, scopes, state, prompt, extraConfig } = params;
982
- const sessionStorage = this.sessionHandler.sessionStorage || _Lixa.LOCAL_SESSION_HANDLER.sessionStorage;
1064
+ const sessionStorage = this.sessionHandler.sessionStorage || this.localSessionHandler.sessionStorage;
983
1065
  const activeSession = await sessionStorage.getSession(sessionId);
984
1066
  if (!activeSession) {
985
1067
  throw new SessionNotFoundError("Authentication required. Active session must exist to connect resource providers.", { sessionId });
@@ -992,7 +1074,7 @@ var Lixa = class _Lixa {
992
1074
  const providerImpl = this.getProvider(providerType, providerConfig);
993
1075
  const stateValue = state || (0, import_crypto2.randomBytes)(16).toString("hex");
994
1076
  const codeVerifier = (0, import_crypto2.randomBytes)(32).toString("hex");
995
- const storage = this.stateHandler.stateStorage || _Lixa.LOCAL_STATE_HANDLER.stateStorage;
1077
+ const storage = this.stateHandler.stateStorage || this.localStateHandler.stateStorage;
996
1078
  await storage.saveState(
997
1079
  stateValue,
998
1080
  {
@@ -1017,45 +1099,6 @@ var Lixa = class _Lixa {
1017
1099
  });
1018
1100
  return `${providerImpl.authorizationEndpoint}?${searchParams.toString()}`;
1019
1101
  }
1020
- /**
1021
- * Handles the OAuth callback for a connected resource provider and stores resource tokens on the session.
1022
- *
1023
- * @param params - Object containing sessionId, provider, code, state, and requested scopes
1024
- * @returns Updated Session containing stored resource tokens under session.resources[provider]
1025
- */
1026
- static userResourceStore = /* @__PURE__ */ new Map();
1027
- static LOCAL_RESOURCE_HANDLER = {
1028
- resourceStorage: {
1029
- saveResource: async (userId, provider, resource) => {
1030
- let userMap = _Lixa.userResourceStore.get(userId);
1031
- if (!userMap) {
1032
- userMap = /* @__PURE__ */ new Map();
1033
- _Lixa.userResourceStore.set(userId, userMap);
1034
- }
1035
- userMap.set(provider.toLowerCase(), resource);
1036
- },
1037
- getResource: async (userId, provider) => {
1038
- const userMap = _Lixa.userResourceStore.get(userId);
1039
- return userMap?.get(provider.toLowerCase()) || null;
1040
- },
1041
- getUserResources: async (userId) => {
1042
- const userMap = _Lixa.userResourceStore.get(userId);
1043
- const result = {};
1044
- if (userMap) {
1045
- for (const [p, r] of userMap.entries()) {
1046
- result[p] = r;
1047
- }
1048
- }
1049
- return result;
1050
- },
1051
- deleteResource: async (userId, provider) => {
1052
- const userMap = _Lixa.userResourceStore.get(userId);
1053
- if (userMap) {
1054
- userMap.delete(provider.toLowerCase());
1055
- }
1056
- }
1057
- }
1058
- };
1059
1102
  getUserKeyFromSession(session) {
1060
1103
  return session.userId || session.email || session.id || "anonymous";
1061
1104
  }
@@ -1064,7 +1107,7 @@ var Lixa = class _Lixa {
1064
1107
  */
1065
1108
  async handleResourceCallback(params) {
1066
1109
  const { sessionId, provider, code, state, scopes } = params;
1067
- const sessionStorage = this.sessionHandler.sessionStorage || _Lixa.LOCAL_SESSION_HANDLER.sessionStorage;
1110
+ const sessionStorage = this.sessionHandler.sessionStorage || this.localSessionHandler.sessionStorage;
1068
1111
  const activeSession = await sessionStorage.getSession(sessionId);
1069
1112
  if (!activeSession) {
1070
1113
  throw new SessionNotFoundError("Authentication required. Active session not found for resource connection.", { sessionId });
@@ -1077,12 +1120,13 @@ var Lixa = class _Lixa {
1077
1120
  const providerImpl = this.getProvider(providerType, providerConfig);
1078
1121
  let codeVerifier = (0, import_crypto2.randomBytes)(32).toString("hex");
1079
1122
  if (state) {
1080
- const stateStorage = this.stateHandler.stateStorage || _Lixa.LOCAL_STATE_HANDLER.stateStorage;
1123
+ const stateStorage = this.stateHandler.stateStorage || this.localStateHandler.stateStorage;
1081
1124
  const cachedState = await stateStorage.getState(state);
1082
- if (cachedState) {
1083
- codeVerifier = cachedState.codeVerifier;
1084
- await stateStorage.deleteState(state);
1125
+ if (!cachedState) {
1126
+ throw new InvalidStateError("Invalid or expired state during resource connection callback", { state });
1085
1127
  }
1128
+ codeVerifier = cachedState.codeVerifier;
1129
+ await stateStorage.deleteState(state);
1086
1130
  }
1087
1131
  const tokens = await this.exchangeCodeForToken(code, providerConfig, providerImpl, codeVerifier);
1088
1132
  const expiresAt = tokens.expires_in ? Date.now() + tokens.expires_in * 1e3 : void 0;
@@ -1096,7 +1140,7 @@ var Lixa = class _Lixa {
1096
1140
  connectedAt: Date.now()
1097
1141
  };
1098
1142
  const userKey = this.getUserKeyFromSession(activeSession);
1099
- const resourceStorage = this.resourceHandler.resourceStorage || _Lixa.LOCAL_RESOURCE_HANDLER.resourceStorage;
1143
+ const resourceStorage = this.resourceHandler.resourceStorage || this.localResourceHandler.resourceStorage;
1100
1144
  await resourceStorage.saveResource(userKey, providerType, connectedResource);
1101
1145
  activeSession.resources = activeSession.resources || {};
1102
1146
  activeSession.resources[providerType] = connectedResource;
@@ -1111,7 +1155,7 @@ var Lixa = class _Lixa {
1111
1155
  * @param provider - Resource provider identifier (e.g. 'github', 'google')
1112
1156
  */
1113
1157
  async getUserResource(userIdOrEmail, provider) {
1114
- const resourceStorage = this.resourceHandler.resourceStorage || _Lixa.LOCAL_RESOURCE_HANDLER.resourceStorage;
1158
+ const resourceStorage = this.resourceHandler.resourceStorage || this.localResourceHandler.resourceStorage;
1115
1159
  const providerType = provider.toLowerCase();
1116
1160
  const resource = await resourceStorage.getResource(userIdOrEmail, providerType);
1117
1161
  if (!resource) return null;
@@ -1129,7 +1173,7 @@ var Lixa = class _Lixa {
1129
1173
  * Retrieves all connected resources for a specific User ID / Email.
1130
1174
  */
1131
1175
  async getUserResources(userIdOrEmail) {
1132
- const resourceStorage = this.resourceHandler.resourceStorage || _Lixa.LOCAL_RESOURCE_HANDLER.resourceStorage;
1176
+ const resourceStorage = this.resourceHandler.resourceStorage || this.localResourceHandler.resourceStorage;
1133
1177
  return await resourceStorage.getUserResources(userIdOrEmail);
1134
1178
  }
1135
1179
  /**
@@ -1139,7 +1183,7 @@ var Lixa = class _Lixa {
1139
1183
  * @param provider - Provider identifier (e.g. 'github', 'google')
1140
1184
  */
1141
1185
  async getConnectedResource(sessionId, provider) {
1142
- const sessionStorage = this.sessionHandler.sessionStorage || _Lixa.LOCAL_SESSION_HANDLER.sessionStorage;
1186
+ const sessionStorage = this.sessionHandler.sessionStorage || this.localSessionHandler.sessionStorage;
1143
1187
  const activeSession = await sessionStorage.getSession(sessionId);
1144
1188
  if (!activeSession) return null;
1145
1189
  const providerType = provider.toLowerCase();
@@ -1164,12 +1208,29 @@ var Lixa = class _Lixa {
1164
1208
  }
1165
1209
  /**
1166
1210
  * Refreshes a user's resource access token using its refresh token.
1211
+ * Deduplicates concurrent refresh requests via an in-flight promise mutex.
1167
1212
  *
1168
1213
  * @param userIdOrEmail - User identifier or email
1169
1214
  * @param provider - Provider identifier (e.g. 'google', 'github')
1170
1215
  */
1171
1216
  async refreshUserResourceToken(userIdOrEmail, provider, existingResource) {
1172
- const resourceStorage = this.resourceHandler.resourceStorage || _Lixa.LOCAL_RESOURCE_HANDLER.resourceStorage;
1217
+ const mutexKey = `${userIdOrEmail.toLowerCase()}:${provider.toLowerCase()}`;
1218
+ const existingPromise = this.refreshMutexes.get(mutexKey);
1219
+ if (existingPromise) {
1220
+ this.log("INFO", "Token", `Concurrent refresh request detected for '${mutexKey}'. Reusing in-flight promise.`);
1221
+ return existingPromise;
1222
+ }
1223
+ const refreshPromise = this.executeRefreshUserResourceToken(userIdOrEmail, provider, existingResource).finally(() => {
1224
+ this.refreshMutexes.delete(mutexKey);
1225
+ });
1226
+ this.refreshMutexes.set(mutexKey, refreshPromise);
1227
+ return refreshPromise;
1228
+ }
1229
+ /**
1230
+ * Internal execution of refresh token exchange.
1231
+ */
1232
+ async executeRefreshUserResourceToken(userIdOrEmail, provider, existingResource) {
1233
+ const resourceStorage = this.resourceHandler.resourceStorage || this.localResourceHandler.resourceStorage;
1173
1234
  const providerType = provider.toLowerCase();
1174
1235
  let resource = existingResource || await resourceStorage.getResource(userIdOrEmail, providerType);
1175
1236
  if (!resource || !resource.refreshToken) {
@@ -1222,7 +1283,7 @@ var Lixa = class _Lixa {
1222
1283
  * Refreshes a connected resource access token for an active session.
1223
1284
  */
1224
1285
  async refreshResourceToken(sessionId, provider) {
1225
- const sessionStorage = this.sessionHandler.sessionStorage || _Lixa.LOCAL_SESSION_HANDLER.sessionStorage;
1286
+ const sessionStorage = this.sessionHandler.sessionStorage || this.localSessionHandler.sessionStorage;
1226
1287
  const activeSession = await sessionStorage.getSession(sessionId);
1227
1288
  if (!activeSession) {
1228
1289
  throw new SessionNotFoundError("Active session not found.", { sessionId });
@@ -1240,7 +1301,7 @@ var Lixa = class _Lixa {
1240
1301
  * Disconnects a resource provider for a specific User ID / Email.
1241
1302
  */
1242
1303
  async disconnectUserResource(userIdOrEmail, provider) {
1243
- const resourceStorage = this.resourceHandler.resourceStorage || _Lixa.LOCAL_RESOURCE_HANDLER.resourceStorage;
1304
+ const resourceStorage = this.resourceHandler.resourceStorage || this.localResourceHandler.resourceStorage;
1244
1305
  await resourceStorage.deleteResource(userIdOrEmail, provider.toLowerCase());
1245
1306
  return true;
1246
1307
  }
@@ -1248,7 +1309,7 @@ var Lixa = class _Lixa {
1248
1309
  * Disconnects a resource provider from an active session and user account.
1249
1310
  */
1250
1311
  async disconnectResource(sessionId, provider) {
1251
- const sessionStorage = this.sessionHandler.sessionStorage || _Lixa.LOCAL_SESSION_HANDLER.sessionStorage;
1312
+ const sessionStorage = this.sessionHandler.sessionStorage || this.localSessionHandler.sessionStorage;
1252
1313
  const activeSession = await sessionStorage.getSession(sessionId);
1253
1314
  if (activeSession) {
1254
1315
  const userKey = this.getUserKeyFromSession(activeSession);
@@ -1265,7 +1326,7 @@ var Lixa = class _Lixa {
1265
1326
  * Retrieves active session details from session storage.
1266
1327
  */
1267
1328
  async fetchSessionInfo(sessionId) {
1268
- const sessionStorage = this.sessionHandler.sessionStorage || _Lixa.LOCAL_SESSION_HANDLER.sessionStorage;
1329
+ const sessionStorage = this.sessionHandler.sessionStorage || this.localSessionHandler.sessionStorage;
1269
1330
  return await sessionStorage.getSession(sessionId);
1270
1331
  }
1271
1332
  /**
@@ -1274,7 +1335,7 @@ var Lixa = class _Lixa {
1274
1335
  * @param sessionId - Active session identifier
1275
1336
  */
1276
1337
  async deleteSession(sessionId) {
1277
- const sessionStorage = this.sessionHandler.sessionStorage || _Lixa.LOCAL_SESSION_HANDLER.sessionStorage;
1338
+ const sessionStorage = this.sessionHandler.sessionStorage || this.localSessionHandler.sessionStorage;
1278
1339
  await sessionStorage.deleteSession(sessionId);
1279
1340
  }
1280
1341
  async exchangeCodeForToken(code, providerConfig, providerImpl, codeVerifier) {
@@ -1330,7 +1391,7 @@ var Lixa = class _Lixa {
1330
1391
  var DEFAULT_SESSION_COOKIE_NAME = "lixa_session";
1331
1392
  var DEFAULT_STATE_COOKIE_NAME = "lixa_oauth_state";
1332
1393
  var DEFAULT_SESSION_MAX_AGE_SECONDS = 24 * 60 * 60;
1333
- var DEFAULT_STATE_MAX_AGE_SECONDS = 10 * 60;
1394
+ var DEFAULT_STATE_MAX_AGE_SECONDS = 5 * 60;
1334
1395
  function isProductionEnvironment() {
1335
1396
  return typeof process !== "undefined" && process.env?.NODE_ENV === "production";
1336
1397
  }
@@ -1444,6 +1505,54 @@ function clearStateCookie(options) {
1444
1505
  header
1445
1506
  };
1446
1507
  }
1508
+
1509
+ // src/express.ts
1510
+ function setSessionCookieOnResponse(res, sessionId, options) {
1511
+ const cookie = createSessionCookie(sessionId, options);
1512
+ res.cookie(cookie.name, cookie.value, cookie.options);
1513
+ }
1514
+ function clearSessionCookieOnResponse(res, options) {
1515
+ const cookie = clearSessionCookie(options);
1516
+ res.cookie(cookie.name, cookie.value, cookie.options);
1517
+ }
1518
+ function setStateCookieOnResponse(res, state, options) {
1519
+ const cookie = createStateCookie(state, options);
1520
+ res.cookie(cookie.name, cookie.value, cookie.options);
1521
+ }
1522
+ function clearStateCookieOnResponse(res, options) {
1523
+ const cookie = clearStateCookie(options);
1524
+ res.cookie(cookie.name, cookie.value, cookie.options);
1525
+ }
1526
+ function createRequireAuthMiddleware(lixa, options) {
1527
+ const cookieName = options?.sessionCookieName || DEFAULT_SESSION_COOKIE_NAME;
1528
+ return async (req, res, next) => {
1529
+ try {
1530
+ const sessionId = req.cookies?.[cookieName];
1531
+ if (!sessionId) {
1532
+ if (options?.unauthorizedHandler) {
1533
+ options.unauthorizedHandler(req, res);
1534
+ return;
1535
+ }
1536
+ res.status(401).json({ error: "Unauthorized: Missing session cookie" });
1537
+ return;
1538
+ }
1539
+ const session = await lixa.fetchSessionInfo(sessionId);
1540
+ if (!session) {
1541
+ if (options?.unauthorizedHandler) {
1542
+ options.unauthorizedHandler(req, res);
1543
+ return;
1544
+ }
1545
+ res.status(401).json({ error: "Unauthorized: Invalid or expired session" });
1546
+ return;
1547
+ }
1548
+ req.sessionInfo = session;
1549
+ req.sessionId = sessionId;
1550
+ next();
1551
+ } catch (error) {
1552
+ next(error);
1553
+ }
1554
+ };
1555
+ }
1447
1556
  // Annotate the CommonJS export names for ESM import in node:
1448
1557
  0 && (module.exports = {
1449
1558
  AccountLinkingStrategy,
@@ -1463,7 +1572,10 @@ function clearStateCookie(options) {
1463
1572
  SessionNotFoundError,
1464
1573
  TokenExchangeError,
1465
1574
  clearSessionCookie,
1575
+ clearSessionCookieOnResponse,
1466
1576
  clearStateCookie,
1577
+ clearStateCookieOnResponse,
1578
+ createRequireAuthMiddleware,
1467
1579
  createSessionCookie,
1468
1580
  createStateCookie,
1469
1581
  decodeIdToken,
@@ -1471,6 +1583,8 @@ function clearStateCookie(options) {
1471
1583
  extractUserInfo,
1472
1584
  fetchUserInfo,
1473
1585
  isProductionEnvironment,
1474
- serializeCookie
1586
+ serializeCookie,
1587
+ setSessionCookieOnResponse,
1588
+ setStateCookieOnResponse
1475
1589
  });
1476
1590
  //# sourceMappingURL=index.cjs.map