@vunexa/lixa 0.1.6-alpha.7 → 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/README.md +1 -2
- package/README.template.md +1 -2
- package/dist/dao/session-cache.d.ts +2 -1
- package/dist/dao/session-cache.d.ts.map +1 -1
- package/dist/dao/state-cache.d.ts +4 -0
- package/dist/dao/state-cache.d.ts.map +1 -1
- package/dist/dao/types.d.ts +21 -75
- package/dist/dao/types.d.ts.map +1 -1
- package/dist/errors.d.ts +90 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/export-types/index.d.ts +439 -95
- package/dist/express.d.ts +103 -0
- package/dist/express.d.ts.map +1 -0
- package/dist/index.cjs +509 -129
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +408 -96
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +483 -128
- package/dist/index.js.map +1 -1
- package/dist/lixa.d.ts +25 -21
- package/dist/lixa.d.ts.map +1 -1
- package/dist/types.d.ts +28 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/cookies.d.ts +142 -0
- package/dist/utils/cookies.d.ts.map +1 -0
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -31,11 +31,36 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
31
31
|
var src_exports = {};
|
|
32
32
|
__export(src_exports, {
|
|
33
33
|
AccountLinkingStrategy: () => AccountLinkingStrategy,
|
|
34
|
+
AccountUnlinkError: () => AccountUnlinkError,
|
|
35
|
+
DEFAULT_SESSION_COOKIE_NAME: () => DEFAULT_SESSION_COOKIE_NAME,
|
|
36
|
+
DEFAULT_SESSION_MAX_AGE_SECONDS: () => DEFAULT_SESSION_MAX_AGE_SECONDS,
|
|
37
|
+
DEFAULT_STATE_COOKIE_NAME: () => DEFAULT_STATE_COOKIE_NAME,
|
|
38
|
+
DEFAULT_STATE_MAX_AGE_SECONDS: () => DEFAULT_STATE_MAX_AGE_SECONDS,
|
|
39
|
+
EmailNotVerifiedError: () => EmailNotVerifiedError,
|
|
40
|
+
InvalidOAuthCallbackError: () => InvalidOAuthCallbackError,
|
|
41
|
+
InvalidProviderConfigError: () => InvalidProviderConfigError,
|
|
42
|
+
InvalidStateError: () => InvalidStateError,
|
|
34
43
|
Lixa: () => Lixa,
|
|
44
|
+
LixaError: () => LixaError,
|
|
45
|
+
ProviderNotConfiguredError: () => ProviderNotConfiguredError,
|
|
46
|
+
RefreshTokenError: () => RefreshTokenError,
|
|
47
|
+
SessionNotFoundError: () => SessionNotFoundError,
|
|
48
|
+
TokenExchangeError: () => TokenExchangeError,
|
|
49
|
+
clearSessionCookie: () => clearSessionCookie,
|
|
50
|
+
clearSessionCookieOnResponse: () => clearSessionCookieOnResponse,
|
|
51
|
+
clearStateCookie: () => clearStateCookie,
|
|
52
|
+
clearStateCookieOnResponse: () => clearStateCookieOnResponse,
|
|
53
|
+
createRequireAuthMiddleware: () => createRequireAuthMiddleware,
|
|
54
|
+
createSessionCookie: () => createSessionCookie,
|
|
55
|
+
createStateCookie: () => createStateCookie,
|
|
35
56
|
decodeIdToken: () => decodeIdToken,
|
|
36
57
|
determineProviderFromIssuer: () => determineProviderFromIssuer,
|
|
37
58
|
extractUserInfo: () => extractUserInfo,
|
|
38
|
-
fetchUserInfo: () => fetchUserInfo
|
|
59
|
+
fetchUserInfo: () => fetchUserInfo,
|
|
60
|
+
isProductionEnvironment: () => isProductionEnvironment,
|
|
61
|
+
serializeCookie: () => serializeCookie,
|
|
62
|
+
setSessionCookieOnResponse: () => setSessionCookieOnResponse,
|
|
63
|
+
setStateCookieOnResponse: () => setStateCookieOnResponse
|
|
39
64
|
});
|
|
40
65
|
module.exports = __toCommonJS(src_exports);
|
|
41
66
|
|
|
@@ -69,7 +94,7 @@ var LocalStateHandler = class {
|
|
|
69
94
|
}
|
|
70
95
|
};
|
|
71
96
|
}
|
|
72
|
-
// Default
|
|
97
|
+
// Default generateState implementation
|
|
73
98
|
async generateState(provider) {
|
|
74
99
|
const state = (0, import_crypto.randomBytes)(16).toString("hex");
|
|
75
100
|
const codeVerifier = (0, import_crypto.randomBytes)(32).toString("hex");
|
|
@@ -82,6 +107,10 @@ var LocalStateHandler = class {
|
|
|
82
107
|
}
|
|
83
108
|
};
|
|
84
109
|
}
|
|
110
|
+
// PascalCase alias for backward compatibility
|
|
111
|
+
async GenerateState(provider) {
|
|
112
|
+
return this.generateState(provider);
|
|
113
|
+
}
|
|
85
114
|
};
|
|
86
115
|
|
|
87
116
|
// src/lixa.ts
|
|
@@ -94,7 +123,20 @@ var LocalSessionHandler = class {
|
|
|
94
123
|
emailToSessionMap = /* @__PURE__ */ new Map();
|
|
95
124
|
sessionStorage;
|
|
96
125
|
constructor(defaultTtlSeconds = 600) {
|
|
97
|
-
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
|
+
});
|
|
98
140
|
this.sessionStorage = {
|
|
99
141
|
saveSession: async (sessionId, session, expiresInSeconds) => {
|
|
100
142
|
this.cache.set(sessionId, session, expiresInSeconds);
|
|
@@ -125,8 +167,8 @@ var LocalSessionHandler = class {
|
|
|
125
167
|
}
|
|
126
168
|
};
|
|
127
169
|
}
|
|
128
|
-
// Default
|
|
129
|
-
async
|
|
170
|
+
// Default generateSession implementation
|
|
171
|
+
async generateSession(tokenData, _providerMetadata) {
|
|
130
172
|
if (!tokenData.access_token || typeof tokenData.access_token !== "string") {
|
|
131
173
|
throw new Error("No valid access token found in OAuth response");
|
|
132
174
|
}
|
|
@@ -136,6 +178,10 @@ var LocalSessionHandler = class {
|
|
|
136
178
|
};
|
|
137
179
|
return session;
|
|
138
180
|
}
|
|
181
|
+
// PascalCase alias for backward compatibility
|
|
182
|
+
async GenerateSession(tokenData, providerMetadata) {
|
|
183
|
+
return this.generateSession(tokenData, providerMetadata);
|
|
184
|
+
}
|
|
139
185
|
};
|
|
140
186
|
|
|
141
187
|
// src/utils/user-info.ts
|
|
@@ -207,13 +253,91 @@ async function extractUserInfo(tokenData, providerMetadata) {
|
|
|
207
253
|
return { userInfo };
|
|
208
254
|
}
|
|
209
255
|
|
|
256
|
+
// src/errors.ts
|
|
257
|
+
var LixaError = class extends Error {
|
|
258
|
+
/**
|
|
259
|
+
* Standard error code string.
|
|
260
|
+
*/
|
|
261
|
+
code;
|
|
262
|
+
/**
|
|
263
|
+
* Additional error context data.
|
|
264
|
+
*/
|
|
265
|
+
details;
|
|
266
|
+
constructor(message, code = "LIXA_ERROR", details) {
|
|
267
|
+
super(message);
|
|
268
|
+
this.name = this.constructor.name;
|
|
269
|
+
this.code = code;
|
|
270
|
+
this.details = details;
|
|
271
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
var InvalidStateError = class extends LixaError {
|
|
275
|
+
constructor(message = "Invalid or expired state", details) {
|
|
276
|
+
super(message, "INVALID_STATE", details);
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
var ProviderNotConfiguredError = class extends LixaError {
|
|
280
|
+
constructor(provider, details) {
|
|
281
|
+
const message = details?.message && typeof details.message === "string" ? details.message : `Provider '${provider}' is not available. Either import it from '@vunexa/lixa-providers' and include it in the configuration, or provide a custom implementation using the 'provider' field: { provider: new CustomProvider(), clientId: '...', ... }`;
|
|
282
|
+
super(message, "PROVIDER_NOT_CONFIGURED", {
|
|
283
|
+
provider,
|
|
284
|
+
...details
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
var InvalidProviderConfigError = class extends LixaError {
|
|
289
|
+
constructor(message, details) {
|
|
290
|
+
super(message, "INVALID_PROVIDER_CONFIG", details);
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
var InvalidOAuthCallbackError = class extends LixaError {
|
|
294
|
+
constructor(message, details) {
|
|
295
|
+
super(message, "INVALID_OAUTH_CALLBACK", details);
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
var TokenExchangeError = class extends LixaError {
|
|
299
|
+
status;
|
|
300
|
+
constructor(message, status, details) {
|
|
301
|
+
super(message, "TOKEN_EXCHANGE_FAILED", { status, ...details });
|
|
302
|
+
this.status = status;
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
var SessionNotFoundError = class extends LixaError {
|
|
306
|
+
constructor(message = "Active session not found or has expired", details) {
|
|
307
|
+
super(message, "SESSION_NOT_FOUND", details);
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
var EmailNotVerifiedError = class extends LixaError {
|
|
311
|
+
constructor(email, details) {
|
|
312
|
+
super(
|
|
313
|
+
`Cannot link account: email '${email || "unknown"}' is not verified by the identity provider`,
|
|
314
|
+
"EMAIL_NOT_VERIFIED",
|
|
315
|
+
{ email, ...details }
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
var AccountUnlinkError = class extends LixaError {
|
|
320
|
+
constructor(message, details) {
|
|
321
|
+
super(message, "ACCOUNT_UNLINK_ERROR", details);
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
var RefreshTokenError = class extends LixaError {
|
|
325
|
+
constructor(message, details) {
|
|
326
|
+
super(message, "REFRESH_TOKEN_ERROR", details);
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
|
|
210
330
|
// src/lixa.ts
|
|
211
331
|
var Lixa = class _Lixa {
|
|
212
332
|
static DEFAULT_PROVIDERS = /* @__PURE__ */ new Map();
|
|
213
333
|
static CONFIGURED_PROVIDERS = /* @__PURE__ */ new Map();
|
|
214
334
|
// Legacy registry for backward compatibility
|
|
215
|
-
|
|
216
|
-
|
|
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();
|
|
217
341
|
config;
|
|
218
342
|
stateHandler;
|
|
219
343
|
sessionHandler;
|
|
@@ -234,10 +358,44 @@ var Lixa = class _Lixa {
|
|
|
234
358
|
*/
|
|
235
359
|
constructor(config) {
|
|
236
360
|
this.config = config;
|
|
237
|
-
this.stateHandler = config.stateHandler || _Lixa.LOCAL_STATE_HANDLER;
|
|
238
|
-
this.sessionHandler = config.sessionHandler || _Lixa.LOCAL_SESSION_HANDLER;
|
|
239
|
-
this.resourceHandler = config.resourceHandler || _Lixa.LOCAL_RESOURCE_HANDLER;
|
|
240
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;
|
|
241
399
|
this.log("INFO", "Init", "Initializing Lixa instance", {
|
|
242
400
|
providers: Object.keys(config.providers),
|
|
243
401
|
debug: this.debug
|
|
@@ -252,8 +410,9 @@ var Lixa = class _Lixa {
|
|
|
252
410
|
} else {
|
|
253
411
|
if (!_Lixa.DEFAULT_PROVIDERS.has(name) && !_Lixa.CONFIGURED_PROVIDERS.has(name)) {
|
|
254
412
|
this.log("ERROR", "Init", `Provider '${providerName}' not available`);
|
|
255
|
-
throw new
|
|
256
|
-
|
|
413
|
+
throw new ProviderNotConfiguredError(
|
|
414
|
+
providerName,
|
|
415
|
+
{ hint: `Ensure the provider is registered via '@vunexa/lixa-providers' or provided inline.` }
|
|
257
416
|
);
|
|
258
417
|
}
|
|
259
418
|
this.log("INFO", "Init", `Using registered provider: ${providerName}`);
|
|
@@ -266,7 +425,7 @@ var Lixa = class _Lixa {
|
|
|
266
425
|
*
|
|
267
426
|
* @param name - The provider name
|
|
268
427
|
* @param config - The provider configuration
|
|
269
|
-
* @throws
|
|
428
|
+
* @throws InvalidProviderConfigError when required fields are missing or invalid
|
|
270
429
|
*/
|
|
271
430
|
validateProviderConfig(name, config) {
|
|
272
431
|
const requiredFields = ["clientId", "clientSecret", "redirectUri", "scopes"];
|
|
@@ -275,18 +434,21 @@ var Lixa = class _Lixa {
|
|
|
275
434
|
return value === void 0 || value === null || typeof value === "string" && value.trim() === "";
|
|
276
435
|
});
|
|
277
436
|
if (missingFields.length > 0) {
|
|
278
|
-
throw new
|
|
279
|
-
`Provider '${name}' configuration is missing required fields: ${missingFields.join(", ")}
|
|
437
|
+
throw new InvalidProviderConfigError(
|
|
438
|
+
`Provider '${name}' configuration is missing required fields: ${missingFields.join(", ")}`,
|
|
439
|
+
{ provider: name, missingFields }
|
|
280
440
|
);
|
|
281
441
|
}
|
|
282
442
|
if (!Array.isArray(config.scopes)) {
|
|
283
|
-
throw new
|
|
284
|
-
`Provider '${name}' configuration error: 'scopes' must be an array of strings
|
|
443
|
+
throw new InvalidProviderConfigError(
|
|
444
|
+
`Provider '${name}' configuration error: 'scopes' must be an array of strings`,
|
|
445
|
+
{ provider: name }
|
|
285
446
|
);
|
|
286
447
|
}
|
|
287
448
|
if (config.scopes.length === 0) {
|
|
288
|
-
throw new
|
|
289
|
-
`Provider '${name}' configuration error: 'scopes' array cannot be empty
|
|
449
|
+
throw new InvalidProviderConfigError(
|
|
450
|
+
`Provider '${name}' configuration error: 'scopes' array cannot be empty`,
|
|
451
|
+
{ provider: name }
|
|
290
452
|
);
|
|
291
453
|
}
|
|
292
454
|
}
|
|
@@ -295,7 +457,7 @@ var Lixa = class _Lixa {
|
|
|
295
457
|
*
|
|
296
458
|
* @param name - The provider name
|
|
297
459
|
* @param provider - The provider implementation
|
|
298
|
-
* @throws
|
|
460
|
+
* @throws InvalidProviderConfigError when required properties are missing
|
|
299
461
|
*/
|
|
300
462
|
validateProviderImplementation(name, provider) {
|
|
301
463
|
const requiredProps = ["authorizationEndpoint", "tokenEndpoint", "userInfoEndpoint"];
|
|
@@ -304,24 +466,27 @@ var Lixa = class _Lixa {
|
|
|
304
466
|
return !value || typeof value !== "string" || value.trim() === "";
|
|
305
467
|
});
|
|
306
468
|
if (missingProps.length > 0) {
|
|
307
|
-
throw new
|
|
308
|
-
`Provider '${name}' implementation is missing required properties: ${missingProps.join(", ")}. All IProvider implementations must define: authorizationEndpoint, tokenEndpoint, and userInfoEndpoint
|
|
469
|
+
throw new InvalidProviderConfigError(
|
|
470
|
+
`Provider '${name}' implementation is missing required properties: ${missingProps.join(", ")}. All IProvider implementations must define: authorizationEndpoint, tokenEndpoint, and userInfoEndpoint.`,
|
|
471
|
+
{ provider: name, missingProps }
|
|
309
472
|
);
|
|
310
473
|
}
|
|
311
474
|
}
|
|
312
475
|
/**
|
|
313
|
-
* Structured
|
|
476
|
+
* Structured logging with standardized format and custom logger support.
|
|
314
477
|
*
|
|
315
|
-
* @param level - Log level (INFO, WARN, ERROR)
|
|
316
|
-
* @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)
|
|
317
480
|
* @param message - Log message
|
|
318
481
|
* @param data - Optional data to log
|
|
319
|
-
*
|
|
320
|
-
* @remarks
|
|
321
|
-
* Format: [Lixa] [timestamp] [level] [context] message
|
|
322
|
-
* Only logs when debug mode is enabled.
|
|
323
482
|
*/
|
|
324
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
|
+
}
|
|
325
490
|
if (!this.debug) return;
|
|
326
491
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
327
492
|
const prefix = `[Lixa] [${timestamp}] [${level}] [${context}]`;
|
|
@@ -372,8 +537,9 @@ var Lixa = class _Lixa {
|
|
|
372
537
|
if (legacyProvider) {
|
|
373
538
|
return legacyProvider;
|
|
374
539
|
}
|
|
375
|
-
throw new
|
|
376
|
-
|
|
540
|
+
throw new ProviderNotConfiguredError(
|
|
541
|
+
name,
|
|
542
|
+
{ hint: `Ensure the provider is included in the configuration with a 'provider' field, or registered using Lixa.registerProvider().` }
|
|
377
543
|
);
|
|
378
544
|
}
|
|
379
545
|
/**
|
|
@@ -563,23 +729,26 @@ var Lixa = class _Lixa {
|
|
|
563
729
|
const providerConfig = this.findProviderByType(providerType);
|
|
564
730
|
if (!providerConfig) {
|
|
565
731
|
this.log("ERROR", "Auth", `Provider '${String(provider)}' is not configured`);
|
|
566
|
-
throw new
|
|
732
|
+
throw new ProviderNotConfiguredError(String(provider), {
|
|
733
|
+
message: `Provider '${String(provider)}' is not configured in this Lixa instance`
|
|
734
|
+
});
|
|
567
735
|
}
|
|
568
736
|
const providerImpl = this.getProvider(providerType, providerConfig);
|
|
569
737
|
let stateValue;
|
|
570
738
|
let codeVerifier;
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
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);
|
|
574
743
|
stateValue = state || generated.state;
|
|
575
744
|
codeVerifier = generated.data.codeVerifier;
|
|
576
|
-
const storage = this.stateHandler.stateStorage ||
|
|
745
|
+
const storage = this.stateHandler.stateStorage || this.localStateHandler.stateStorage;
|
|
577
746
|
await storage.saveState(stateValue, generated.data, 300);
|
|
578
747
|
} else {
|
|
579
748
|
this.log("INFO", "State", "Using default state generation");
|
|
580
749
|
stateValue = state || (0, import_crypto2.randomBytes)(16).toString("hex");
|
|
581
750
|
codeVerifier = (0, import_crypto2.randomBytes)(32).toString("hex");
|
|
582
|
-
const storage = this.stateHandler.stateStorage ||
|
|
751
|
+
const storage = this.stateHandler.stateStorage || this.localStateHandler.stateStorage;
|
|
583
752
|
await storage.saveState(
|
|
584
753
|
stateValue,
|
|
585
754
|
{
|
|
@@ -588,12 +757,17 @@ var Lixa = class _Lixa {
|
|
|
588
757
|
codeVerifier
|
|
589
758
|
},
|
|
590
759
|
300
|
|
591
|
-
// 5 minutes in seconds
|
|
760
|
+
// 5 minutes in seconds (synchronized with cookie TTL)
|
|
592
761
|
);
|
|
593
762
|
}
|
|
594
763
|
const codeChallenge = _Lixa.buildCodeChallenge(codeVerifier);
|
|
595
764
|
this.log("INFO", "State", `Saved state for provider: ${providerType}`, { state: stateValue });
|
|
596
|
-
const authNScopes = this.resolveAuthNScopes(
|
|
765
|
+
const authNScopes = this.resolveAuthNScopes(
|
|
766
|
+
providerType,
|
|
767
|
+
providerConfig.scopes,
|
|
768
|
+
providerImpl,
|
|
769
|
+
providerConfig.allowNonAuthScopes
|
|
770
|
+
);
|
|
597
771
|
const params = new URLSearchParams({
|
|
598
772
|
client_id: providerConfig.clientId,
|
|
599
773
|
redirect_uri: providerConfig.redirectUri,
|
|
@@ -612,9 +786,12 @@ var Lixa = class _Lixa {
|
|
|
612
786
|
return authUrl;
|
|
613
787
|
}
|
|
614
788
|
/**
|
|
615
|
-
* Restricts primary authentication scopes strictly to AuthN identity scopes.
|
|
789
|
+
* Restricts primary authentication scopes strictly to AuthN identity scopes unless allowNonAuthScopes is true.
|
|
616
790
|
*/
|
|
617
|
-
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
|
+
}
|
|
618
795
|
const defaultAuthScopes = {
|
|
619
796
|
google: ["openid", "email", "profile"],
|
|
620
797
|
github: ["read:user", "user:email"],
|
|
@@ -643,7 +820,7 @@ var Lixa = class _Lixa {
|
|
|
643
820
|
"Auth",
|
|
644
821
|
`Primary authentication is strictly limited to AuthN scopes. Excluded non-AuthN resource scopes: [${nonAuthNScopes.join(
|
|
645
822
|
", "
|
|
646
|
-
)}].
|
|
823
|
+
)}]. Set 'allowNonAuthScopes: true' on provider config to include them, or use lixa.getResourceAuthUrl() post-login to connect resource providers.`
|
|
647
824
|
);
|
|
648
825
|
}
|
|
649
826
|
if (validAuthNScopes.length > 0) {
|
|
@@ -679,18 +856,18 @@ var Lixa = class _Lixa {
|
|
|
679
856
|
this.log("INFO", "Auth", `Handling OAuth callback for provider: ${providerType}`);
|
|
680
857
|
if (!code || code.trim() === "") {
|
|
681
858
|
this.log("ERROR", "Auth", "Invalid or missing authorization code in callback");
|
|
682
|
-
throw new
|
|
859
|
+
throw new InvalidOAuthCallbackError("Invalid or missing code in callback");
|
|
683
860
|
}
|
|
684
861
|
if (!state || state.trim() === "") {
|
|
685
862
|
this.log("ERROR", "Auth", "Invalid or missing state in callback");
|
|
686
|
-
throw new
|
|
863
|
+
throw new InvalidOAuthCallbackError("Invalid or missing state in callback");
|
|
687
864
|
}
|
|
688
865
|
this.log("INFO", "State", "Validating state parameter", { state });
|
|
689
|
-
const stateStorage = this.stateHandler.stateStorage ||
|
|
866
|
+
const stateStorage = this.stateHandler.stateStorage || this.localStateHandler.stateStorage;
|
|
690
867
|
const cachedState = await stateStorage.getState(state);
|
|
691
868
|
if (!cachedState) {
|
|
692
869
|
this.log("ERROR", "State", "State validation failed: state not found or expired", { state });
|
|
693
|
-
throw new
|
|
870
|
+
throw new InvalidStateError("Invalid or expired state", { state });
|
|
694
871
|
}
|
|
695
872
|
this.log("INFO", "State", "State validated successfully, removing from cache");
|
|
696
873
|
await stateStorage.deleteState(state);
|
|
@@ -698,7 +875,9 @@ var Lixa = class _Lixa {
|
|
|
698
875
|
const providerConfig = this.findProviderByType(providerType);
|
|
699
876
|
if (!providerConfig) {
|
|
700
877
|
this.log("ERROR", "Auth", `Provider '${String(provider)}' is not configured`);
|
|
701
|
-
throw new
|
|
878
|
+
throw new ProviderNotConfiguredError(String(provider), {
|
|
879
|
+
message: `Provider '${String(provider)}' is not configured in this Lixa instance`
|
|
880
|
+
});
|
|
702
881
|
}
|
|
703
882
|
const providerImpl = this.getProvider(providerType, providerConfig);
|
|
704
883
|
this.log("INFO", "Token", `Exchanging authorization code for tokens`, { provider: providerType });
|
|
@@ -724,9 +903,12 @@ var Lixa = class _Lixa {
|
|
|
724
903
|
extractedUserInfo = userInfo;
|
|
725
904
|
} catch {
|
|
726
905
|
}
|
|
727
|
-
const generateSession = this.sessionHandler.generateSession ||
|
|
728
|
-
if (
|
|
729
|
-
|
|
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");
|
|
730
912
|
} else {
|
|
731
913
|
this.log("INFO", "Session", "Using default session generation");
|
|
732
914
|
}
|
|
@@ -735,7 +917,7 @@ var Lixa = class _Lixa {
|
|
|
735
917
|
if (extractedUserInfo?.email && !session.email) {
|
|
736
918
|
session.email = extractedUserInfo.email;
|
|
737
919
|
}
|
|
738
|
-
const sessionStorage = this.sessionHandler.sessionStorage ||
|
|
920
|
+
const sessionStorage = this.sessionHandler.sessionStorage || this.localSessionHandler.sessionStorage;
|
|
739
921
|
const linkingConfig = this.config.accountLinking;
|
|
740
922
|
const mode = String(linkingConfig?.mode || "");
|
|
741
923
|
const isLinkByEmail = mode === "AUTO_LINK_BY_VERIFIED_EMAIL" /* AUTO_LINK_BY_VERIFIED_EMAIL */ || mode === "AUTO_LINK_BY_VERIFIED_EMAIL" || mode === "linkByEmail";
|
|
@@ -788,27 +970,31 @@ var Lixa = class _Lixa {
|
|
|
788
970
|
*/
|
|
789
971
|
async linkAccount(params) {
|
|
790
972
|
const { sessionId, provider, code, state } = params;
|
|
791
|
-
const sessionStorage = this.sessionHandler.sessionStorage ||
|
|
973
|
+
const sessionStorage = this.sessionHandler.sessionStorage || this.localSessionHandler.sessionStorage;
|
|
792
974
|
const existingSession = await sessionStorage.getSession(sessionId);
|
|
793
975
|
if (!existingSession) {
|
|
794
|
-
throw new
|
|
976
|
+
throw new SessionNotFoundError("Invalid session ID. User must be authenticated to link an account.", { sessionId });
|
|
977
|
+
}
|
|
978
|
+
if (!code || code.trim() === "") {
|
|
979
|
+
throw new InvalidOAuthCallbackError("Invalid or missing code in linkAccount");
|
|
795
980
|
}
|
|
796
981
|
const providerType = String(provider).toLowerCase();
|
|
797
982
|
const providerConfig = this.findProviderByType(providerType);
|
|
798
983
|
if (!providerConfig) {
|
|
799
|
-
throw new
|
|
984
|
+
throw new ProviderNotConfiguredError(String(provider));
|
|
800
985
|
}
|
|
801
986
|
const providerImpl = this.getProvider(providerType, providerConfig);
|
|
802
|
-
let codeVerifier
|
|
987
|
+
let codeVerifier;
|
|
803
988
|
if (state) {
|
|
804
|
-
const stateStorage = this.stateHandler.stateStorage ||
|
|
989
|
+
const stateStorage = this.stateHandler.stateStorage || this.localStateHandler.stateStorage;
|
|
805
990
|
const cachedState = await stateStorage.getState(state);
|
|
806
|
-
if (cachedState) {
|
|
807
|
-
|
|
808
|
-
await stateStorage.deleteState(state);
|
|
991
|
+
if (!cachedState) {
|
|
992
|
+
throw new InvalidStateError("Invalid or expired state during account linking", { state });
|
|
809
993
|
}
|
|
994
|
+
codeVerifier = cachedState.codeVerifier;
|
|
995
|
+
await stateStorage.deleteState(state);
|
|
810
996
|
}
|
|
811
|
-
const tokens = await this.exchangeCodeForToken(
|
|
997
|
+
const tokens = await this.exchangeCodeForToken(code, providerConfig, providerImpl, codeVerifier || "");
|
|
812
998
|
const providerMetadata = {
|
|
813
999
|
name: providerType,
|
|
814
1000
|
endpoints: {
|
|
@@ -846,14 +1032,17 @@ var Lixa = class _Lixa {
|
|
|
846
1032
|
* @returns Promise resolving to true on successful unlink
|
|
847
1033
|
*/
|
|
848
1034
|
async unlinkAccount(sessionId, providerToUnlink) {
|
|
849
|
-
const sessionStorage = this.sessionHandler.sessionStorage ||
|
|
1035
|
+
const sessionStorage = this.sessionHandler.sessionStorage || this.localSessionHandler.sessionStorage;
|
|
850
1036
|
const session = await sessionStorage.getSession(sessionId);
|
|
851
1037
|
if (!session || !session.accounts) {
|
|
852
|
-
throw new
|
|
1038
|
+
throw new AccountUnlinkError("Session not found or has no linked accounts.", { sessionId });
|
|
853
1039
|
}
|
|
854
1040
|
const linkedProviders = Object.keys(session.accounts);
|
|
855
1041
|
if (linkedProviders.length <= 1) {
|
|
856
|
-
throw new
|
|
1042
|
+
throw new AccountUnlinkError("Cannot unlink the only authentication provider for this account.", {
|
|
1043
|
+
sessionId,
|
|
1044
|
+
provider: providerToUnlink
|
|
1045
|
+
});
|
|
857
1046
|
}
|
|
858
1047
|
delete session.accounts[providerToUnlink.toLowerCase()];
|
|
859
1048
|
await sessionStorage.saveSession(sessionId, session, 86400);
|
|
@@ -872,20 +1061,20 @@ var Lixa = class _Lixa {
|
|
|
872
1061
|
*/
|
|
873
1062
|
async getResourceAuthUrl(params) {
|
|
874
1063
|
const { sessionId, provider, scopes, state, prompt, extraConfig } = params;
|
|
875
|
-
const sessionStorage = this.sessionHandler.sessionStorage ||
|
|
1064
|
+
const sessionStorage = this.sessionHandler.sessionStorage || this.localSessionHandler.sessionStorage;
|
|
876
1065
|
const activeSession = await sessionStorage.getSession(sessionId);
|
|
877
1066
|
if (!activeSession) {
|
|
878
|
-
throw new
|
|
1067
|
+
throw new SessionNotFoundError("Authentication required. Active session must exist to connect resource providers.", { sessionId });
|
|
879
1068
|
}
|
|
880
1069
|
const providerType = String(provider).toLowerCase();
|
|
881
1070
|
const providerConfig = this.findProviderByType(providerType);
|
|
882
1071
|
if (!providerConfig) {
|
|
883
|
-
throw new
|
|
1072
|
+
throw new ProviderNotConfiguredError(String(provider));
|
|
884
1073
|
}
|
|
885
1074
|
const providerImpl = this.getProvider(providerType, providerConfig);
|
|
886
1075
|
const stateValue = state || (0, import_crypto2.randomBytes)(16).toString("hex");
|
|
887
1076
|
const codeVerifier = (0, import_crypto2.randomBytes)(32).toString("hex");
|
|
888
|
-
const storage = this.stateHandler.stateStorage ||
|
|
1077
|
+
const storage = this.stateHandler.stateStorage || this.localStateHandler.stateStorage;
|
|
889
1078
|
await storage.saveState(
|
|
890
1079
|
stateValue,
|
|
891
1080
|
{
|
|
@@ -910,45 +1099,6 @@ var Lixa = class _Lixa {
|
|
|
910
1099
|
});
|
|
911
1100
|
return `${providerImpl.authorizationEndpoint}?${searchParams.toString()}`;
|
|
912
1101
|
}
|
|
913
|
-
/**
|
|
914
|
-
* Handles the OAuth callback for a connected resource provider and stores resource tokens on the session.
|
|
915
|
-
*
|
|
916
|
-
* @param params - Object containing sessionId, provider, code, state, and requested scopes
|
|
917
|
-
* @returns Updated Session containing stored resource tokens under session.resources[provider]
|
|
918
|
-
*/
|
|
919
|
-
static userResourceStore = /* @__PURE__ */ new Map();
|
|
920
|
-
static LOCAL_RESOURCE_HANDLER = {
|
|
921
|
-
resourceStorage: {
|
|
922
|
-
saveResource: async (userId, provider, resource) => {
|
|
923
|
-
let userMap = _Lixa.userResourceStore.get(userId);
|
|
924
|
-
if (!userMap) {
|
|
925
|
-
userMap = /* @__PURE__ */ new Map();
|
|
926
|
-
_Lixa.userResourceStore.set(userId, userMap);
|
|
927
|
-
}
|
|
928
|
-
userMap.set(provider.toLowerCase(), resource);
|
|
929
|
-
},
|
|
930
|
-
getResource: async (userId, provider) => {
|
|
931
|
-
const userMap = _Lixa.userResourceStore.get(userId);
|
|
932
|
-
return userMap?.get(provider.toLowerCase()) || null;
|
|
933
|
-
},
|
|
934
|
-
getUserResources: async (userId) => {
|
|
935
|
-
const userMap = _Lixa.userResourceStore.get(userId);
|
|
936
|
-
const result = {};
|
|
937
|
-
if (userMap) {
|
|
938
|
-
for (const [p, r] of userMap.entries()) {
|
|
939
|
-
result[p] = r;
|
|
940
|
-
}
|
|
941
|
-
}
|
|
942
|
-
return result;
|
|
943
|
-
},
|
|
944
|
-
deleteResource: async (userId, provider) => {
|
|
945
|
-
const userMap = _Lixa.userResourceStore.get(userId);
|
|
946
|
-
if (userMap) {
|
|
947
|
-
userMap.delete(provider.toLowerCase());
|
|
948
|
-
}
|
|
949
|
-
}
|
|
950
|
-
}
|
|
951
|
-
};
|
|
952
1102
|
getUserKeyFromSession(session) {
|
|
953
1103
|
return session.userId || session.email || session.id || "anonymous";
|
|
954
1104
|
}
|
|
@@ -957,25 +1107,26 @@ var Lixa = class _Lixa {
|
|
|
957
1107
|
*/
|
|
958
1108
|
async handleResourceCallback(params) {
|
|
959
1109
|
const { sessionId, provider, code, state, scopes } = params;
|
|
960
|
-
const sessionStorage = this.sessionHandler.sessionStorage ||
|
|
1110
|
+
const sessionStorage = this.sessionHandler.sessionStorage || this.localSessionHandler.sessionStorage;
|
|
961
1111
|
const activeSession = await sessionStorage.getSession(sessionId);
|
|
962
1112
|
if (!activeSession) {
|
|
963
|
-
throw new
|
|
1113
|
+
throw new SessionNotFoundError("Authentication required. Active session not found for resource connection.", { sessionId });
|
|
964
1114
|
}
|
|
965
1115
|
const providerType = String(provider).toLowerCase();
|
|
966
1116
|
const providerConfig = this.findProviderByType(providerType);
|
|
967
1117
|
if (!providerConfig) {
|
|
968
|
-
throw new
|
|
1118
|
+
throw new ProviderNotConfiguredError(String(provider));
|
|
969
1119
|
}
|
|
970
1120
|
const providerImpl = this.getProvider(providerType, providerConfig);
|
|
971
1121
|
let codeVerifier = (0, import_crypto2.randomBytes)(32).toString("hex");
|
|
972
1122
|
if (state) {
|
|
973
|
-
const stateStorage = this.stateHandler.stateStorage ||
|
|
1123
|
+
const stateStorage = this.stateHandler.stateStorage || this.localStateHandler.stateStorage;
|
|
974
1124
|
const cachedState = await stateStorage.getState(state);
|
|
975
|
-
if (cachedState) {
|
|
976
|
-
|
|
977
|
-
await stateStorage.deleteState(state);
|
|
1125
|
+
if (!cachedState) {
|
|
1126
|
+
throw new InvalidStateError("Invalid or expired state during resource connection callback", { state });
|
|
978
1127
|
}
|
|
1128
|
+
codeVerifier = cachedState.codeVerifier;
|
|
1129
|
+
await stateStorage.deleteState(state);
|
|
979
1130
|
}
|
|
980
1131
|
const tokens = await this.exchangeCodeForToken(code, providerConfig, providerImpl, codeVerifier);
|
|
981
1132
|
const expiresAt = tokens.expires_in ? Date.now() + tokens.expires_in * 1e3 : void 0;
|
|
@@ -989,7 +1140,7 @@ var Lixa = class _Lixa {
|
|
|
989
1140
|
connectedAt: Date.now()
|
|
990
1141
|
};
|
|
991
1142
|
const userKey = this.getUserKeyFromSession(activeSession);
|
|
992
|
-
const resourceStorage = this.resourceHandler.resourceStorage ||
|
|
1143
|
+
const resourceStorage = this.resourceHandler.resourceStorage || this.localResourceHandler.resourceStorage;
|
|
993
1144
|
await resourceStorage.saveResource(userKey, providerType, connectedResource);
|
|
994
1145
|
activeSession.resources = activeSession.resources || {};
|
|
995
1146
|
activeSession.resources[providerType] = connectedResource;
|
|
@@ -1004,7 +1155,7 @@ var Lixa = class _Lixa {
|
|
|
1004
1155
|
* @param provider - Resource provider identifier (e.g. 'github', 'google')
|
|
1005
1156
|
*/
|
|
1006
1157
|
async getUserResource(userIdOrEmail, provider) {
|
|
1007
|
-
const resourceStorage = this.resourceHandler.resourceStorage ||
|
|
1158
|
+
const resourceStorage = this.resourceHandler.resourceStorage || this.localResourceHandler.resourceStorage;
|
|
1008
1159
|
const providerType = provider.toLowerCase();
|
|
1009
1160
|
const resource = await resourceStorage.getResource(userIdOrEmail, providerType);
|
|
1010
1161
|
if (!resource) return null;
|
|
@@ -1022,7 +1173,7 @@ var Lixa = class _Lixa {
|
|
|
1022
1173
|
* Retrieves all connected resources for a specific User ID / Email.
|
|
1023
1174
|
*/
|
|
1024
1175
|
async getUserResources(userIdOrEmail) {
|
|
1025
|
-
const resourceStorage = this.resourceHandler.resourceStorage ||
|
|
1176
|
+
const resourceStorage = this.resourceHandler.resourceStorage || this.localResourceHandler.resourceStorage;
|
|
1026
1177
|
return await resourceStorage.getUserResources(userIdOrEmail);
|
|
1027
1178
|
}
|
|
1028
1179
|
/**
|
|
@@ -1032,7 +1183,7 @@ var Lixa = class _Lixa {
|
|
|
1032
1183
|
* @param provider - Provider identifier (e.g. 'github', 'google')
|
|
1033
1184
|
*/
|
|
1034
1185
|
async getConnectedResource(sessionId, provider) {
|
|
1035
|
-
const sessionStorage = this.sessionHandler.sessionStorage ||
|
|
1186
|
+
const sessionStorage = this.sessionHandler.sessionStorage || this.localSessionHandler.sessionStorage;
|
|
1036
1187
|
const activeSession = await sessionStorage.getSession(sessionId);
|
|
1037
1188
|
if (!activeSession) return null;
|
|
1038
1189
|
const providerType = provider.toLowerCase();
|
|
@@ -1057,20 +1208,40 @@ var Lixa = class _Lixa {
|
|
|
1057
1208
|
}
|
|
1058
1209
|
/**
|
|
1059
1210
|
* Refreshes a user's resource access token using its refresh token.
|
|
1211
|
+
* Deduplicates concurrent refresh requests via an in-flight promise mutex.
|
|
1060
1212
|
*
|
|
1061
1213
|
* @param userIdOrEmail - User identifier or email
|
|
1062
1214
|
* @param provider - Provider identifier (e.g. 'google', 'github')
|
|
1063
1215
|
*/
|
|
1064
1216
|
async refreshUserResourceToken(userIdOrEmail, provider, existingResource) {
|
|
1065
|
-
const
|
|
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;
|
|
1066
1234
|
const providerType = provider.toLowerCase();
|
|
1067
1235
|
let resource = existingResource || await resourceStorage.getResource(userIdOrEmail, providerType);
|
|
1068
1236
|
if (!resource || !resource.refreshToken) {
|
|
1069
|
-
throw new
|
|
1237
|
+
throw new RefreshTokenError(
|
|
1238
|
+
`No refresh token available for user '${userIdOrEmail}' on connected resource '${provider}'`,
|
|
1239
|
+
{ userId: userIdOrEmail, provider }
|
|
1240
|
+
);
|
|
1070
1241
|
}
|
|
1071
1242
|
const providerConfig = this.findProviderByType(providerType);
|
|
1072
1243
|
if (!providerConfig) {
|
|
1073
|
-
throw new
|
|
1244
|
+
throw new ProviderNotConfiguredError(provider);
|
|
1074
1245
|
}
|
|
1075
1246
|
const providerImpl = this.getProvider(providerType, providerConfig);
|
|
1076
1247
|
const body = {
|
|
@@ -1089,7 +1260,10 @@ var Lixa = class _Lixa {
|
|
|
1089
1260
|
});
|
|
1090
1261
|
if (!response.ok) {
|
|
1091
1262
|
const errorText = await response.text();
|
|
1092
|
-
throw new
|
|
1263
|
+
throw new RefreshTokenError(
|
|
1264
|
+
`Failed to refresh resource token for user '${userIdOrEmail}' on '${provider}': ${response.status} - ${errorText}`,
|
|
1265
|
+
{ userId: userIdOrEmail, provider, status: response.status }
|
|
1266
|
+
);
|
|
1093
1267
|
}
|
|
1094
1268
|
const tokens = await response.json();
|
|
1095
1269
|
const expiresAt = tokens.expires_in ? Date.now() + tokens.expires_in * 1e3 : void 0;
|
|
@@ -1109,10 +1283,10 @@ var Lixa = class _Lixa {
|
|
|
1109
1283
|
* Refreshes a connected resource access token for an active session.
|
|
1110
1284
|
*/
|
|
1111
1285
|
async refreshResourceToken(sessionId, provider) {
|
|
1112
|
-
const sessionStorage = this.sessionHandler.sessionStorage ||
|
|
1286
|
+
const sessionStorage = this.sessionHandler.sessionStorage || this.localSessionHandler.sessionStorage;
|
|
1113
1287
|
const activeSession = await sessionStorage.getSession(sessionId);
|
|
1114
1288
|
if (!activeSession) {
|
|
1115
|
-
throw new
|
|
1289
|
+
throw new SessionNotFoundError("Active session not found.", { sessionId });
|
|
1116
1290
|
}
|
|
1117
1291
|
const providerType = provider.toLowerCase();
|
|
1118
1292
|
const userKey = this.getUserKeyFromSession(activeSession);
|
|
@@ -1127,7 +1301,7 @@ var Lixa = class _Lixa {
|
|
|
1127
1301
|
* Disconnects a resource provider for a specific User ID / Email.
|
|
1128
1302
|
*/
|
|
1129
1303
|
async disconnectUserResource(userIdOrEmail, provider) {
|
|
1130
|
-
const resourceStorage = this.resourceHandler.resourceStorage ||
|
|
1304
|
+
const resourceStorage = this.resourceHandler.resourceStorage || this.localResourceHandler.resourceStorage;
|
|
1131
1305
|
await resourceStorage.deleteResource(userIdOrEmail, provider.toLowerCase());
|
|
1132
1306
|
return true;
|
|
1133
1307
|
}
|
|
@@ -1135,7 +1309,7 @@ var Lixa = class _Lixa {
|
|
|
1135
1309
|
* Disconnects a resource provider from an active session and user account.
|
|
1136
1310
|
*/
|
|
1137
1311
|
async disconnectResource(sessionId, provider) {
|
|
1138
|
-
const sessionStorage = this.sessionHandler.sessionStorage ||
|
|
1312
|
+
const sessionStorage = this.sessionHandler.sessionStorage || this.localSessionHandler.sessionStorage;
|
|
1139
1313
|
const activeSession = await sessionStorage.getSession(sessionId);
|
|
1140
1314
|
if (activeSession) {
|
|
1141
1315
|
const userKey = this.getUserKeyFromSession(activeSession);
|
|
@@ -1148,10 +1322,22 @@ var Lixa = class _Lixa {
|
|
|
1148
1322
|
}
|
|
1149
1323
|
return false;
|
|
1150
1324
|
}
|
|
1325
|
+
/**
|
|
1326
|
+
* Retrieves active session details from session storage.
|
|
1327
|
+
*/
|
|
1151
1328
|
async fetchSessionInfo(sessionId) {
|
|
1152
|
-
const sessionStorage = this.sessionHandler.sessionStorage ||
|
|
1329
|
+
const sessionStorage = this.sessionHandler.sessionStorage || this.localSessionHandler.sessionStorage;
|
|
1153
1330
|
return await sessionStorage.getSession(sessionId);
|
|
1154
1331
|
}
|
|
1332
|
+
/**
|
|
1333
|
+
* Deletes a session from session storage (e.g. on logout).
|
|
1334
|
+
*
|
|
1335
|
+
* @param sessionId - Active session identifier
|
|
1336
|
+
*/
|
|
1337
|
+
async deleteSession(sessionId) {
|
|
1338
|
+
const sessionStorage = this.sessionHandler.sessionStorage || this.localSessionHandler.sessionStorage;
|
|
1339
|
+
await sessionStorage.deleteSession(sessionId);
|
|
1340
|
+
}
|
|
1155
1341
|
async exchangeCodeForToken(code, providerConfig, providerImpl, codeVerifier) {
|
|
1156
1342
|
const body = {
|
|
1157
1343
|
client_id: providerConfig.clientId,
|
|
@@ -1182,8 +1368,10 @@ var Lixa = class _Lixa {
|
|
|
1182
1368
|
statusText: response.statusText,
|
|
1183
1369
|
error: errorBody
|
|
1184
1370
|
});
|
|
1185
|
-
throw new
|
|
1186
|
-
`Token exchange failed: ${response.status} ${response.statusText} - ${errorBody}
|
|
1371
|
+
throw new TokenExchangeError(
|
|
1372
|
+
`Token exchange failed: ${response.status} ${response.statusText} - ${errorBody}`,
|
|
1373
|
+
response.status,
|
|
1374
|
+
{ statusText: response.statusText, errorBody }
|
|
1187
1375
|
);
|
|
1188
1376
|
}
|
|
1189
1377
|
this.log("INFO", "Token", "Token exchange response received successfully");
|
|
@@ -1198,13 +1386,205 @@ var Lixa = class _Lixa {
|
|
|
1198
1386
|
return void 0;
|
|
1199
1387
|
}
|
|
1200
1388
|
};
|
|
1389
|
+
|
|
1390
|
+
// src/utils/cookies.ts
|
|
1391
|
+
var DEFAULT_SESSION_COOKIE_NAME = "lixa_session";
|
|
1392
|
+
var DEFAULT_STATE_COOKIE_NAME = "lixa_oauth_state";
|
|
1393
|
+
var DEFAULT_SESSION_MAX_AGE_SECONDS = 24 * 60 * 60;
|
|
1394
|
+
var DEFAULT_STATE_MAX_AGE_SECONDS = 5 * 60;
|
|
1395
|
+
function isProductionEnvironment() {
|
|
1396
|
+
return typeof process !== "undefined" && process.env?.NODE_ENV === "production";
|
|
1397
|
+
}
|
|
1398
|
+
function serializeCookie(name, value, options) {
|
|
1399
|
+
const isProd = isProductionEnvironment();
|
|
1400
|
+
const path = options?.path ?? "/";
|
|
1401
|
+
const httpOnly = options?.httpOnly ?? true;
|
|
1402
|
+
const secure = options?.secure ?? isProd;
|
|
1403
|
+
const sameSite = options?.sameSite ?? "lax";
|
|
1404
|
+
const parts = [`${encodeURIComponent(name)}=${encodeURIComponent(value)}`];
|
|
1405
|
+
if (path) {
|
|
1406
|
+
parts.push(`Path=${path}`);
|
|
1407
|
+
}
|
|
1408
|
+
if (typeof options?.maxAge === "number") {
|
|
1409
|
+
parts.push(`Max-Age=${Math.floor(options.maxAge)}`);
|
|
1410
|
+
const expires = new Date(Date.now() + options.maxAge * 1e3).toUTCString();
|
|
1411
|
+
parts.push(`Expires=${expires}`);
|
|
1412
|
+
}
|
|
1413
|
+
if (options?.domain) {
|
|
1414
|
+
parts.push(`Domain=${options.domain}`);
|
|
1415
|
+
}
|
|
1416
|
+
if (httpOnly) {
|
|
1417
|
+
parts.push("HttpOnly");
|
|
1418
|
+
}
|
|
1419
|
+
if (secure) {
|
|
1420
|
+
parts.push("Secure");
|
|
1421
|
+
}
|
|
1422
|
+
if (sameSite) {
|
|
1423
|
+
const capitalized = sameSite.charAt(0).toUpperCase() + sameSite.slice(1).toLowerCase();
|
|
1424
|
+
parts.push(`SameSite=${capitalized}`);
|
|
1425
|
+
}
|
|
1426
|
+
return parts.join("; ");
|
|
1427
|
+
}
|
|
1428
|
+
function createSessionCookie(sessionId, options) {
|
|
1429
|
+
const isProd = isProductionEnvironment();
|
|
1430
|
+
const resolvedOptions = {
|
|
1431
|
+
name: options?.name || DEFAULT_SESSION_COOKIE_NAME,
|
|
1432
|
+
path: options?.path ?? "/",
|
|
1433
|
+
maxAge: options?.maxAge ?? DEFAULT_SESSION_MAX_AGE_SECONDS,
|
|
1434
|
+
httpOnly: options?.httpOnly ?? true,
|
|
1435
|
+
secure: options?.secure ?? isProd,
|
|
1436
|
+
sameSite: options?.sameSite ?? "lax",
|
|
1437
|
+
domain: options?.domain
|
|
1438
|
+
};
|
|
1439
|
+
const name = resolvedOptions.name;
|
|
1440
|
+
const header = serializeCookie(name, sessionId, resolvedOptions);
|
|
1441
|
+
return {
|
|
1442
|
+
name,
|
|
1443
|
+
value: sessionId,
|
|
1444
|
+
options: resolvedOptions,
|
|
1445
|
+
header
|
|
1446
|
+
};
|
|
1447
|
+
}
|
|
1448
|
+
function clearSessionCookie(options) {
|
|
1449
|
+
const isProd = isProductionEnvironment();
|
|
1450
|
+
const resolvedOptions = {
|
|
1451
|
+
name: options?.name || DEFAULT_SESSION_COOKIE_NAME,
|
|
1452
|
+
path: options?.path ?? "/",
|
|
1453
|
+
maxAge: 0,
|
|
1454
|
+
httpOnly: options?.httpOnly ?? true,
|
|
1455
|
+
secure: options?.secure ?? isProd,
|
|
1456
|
+
sameSite: options?.sameSite ?? "lax",
|
|
1457
|
+
domain: options?.domain
|
|
1458
|
+
};
|
|
1459
|
+
const name = resolvedOptions.name;
|
|
1460
|
+
const header = serializeCookie(name, "", resolvedOptions);
|
|
1461
|
+
return {
|
|
1462
|
+
name,
|
|
1463
|
+
value: "",
|
|
1464
|
+
options: resolvedOptions,
|
|
1465
|
+
header
|
|
1466
|
+
};
|
|
1467
|
+
}
|
|
1468
|
+
function createStateCookie(state, options) {
|
|
1469
|
+
const isProd = isProductionEnvironment();
|
|
1470
|
+
const resolvedOptions = {
|
|
1471
|
+
name: options?.name || DEFAULT_STATE_COOKIE_NAME,
|
|
1472
|
+
path: options?.path ?? "/",
|
|
1473
|
+
maxAge: options?.maxAge ?? DEFAULT_STATE_MAX_AGE_SECONDS,
|
|
1474
|
+
httpOnly: options?.httpOnly ?? true,
|
|
1475
|
+
secure: options?.secure ?? isProd,
|
|
1476
|
+
sameSite: options?.sameSite ?? "lax",
|
|
1477
|
+
domain: options?.domain
|
|
1478
|
+
};
|
|
1479
|
+
const name = resolvedOptions.name;
|
|
1480
|
+
const header = serializeCookie(name, state, resolvedOptions);
|
|
1481
|
+
return {
|
|
1482
|
+
name,
|
|
1483
|
+
value: state,
|
|
1484
|
+
options: resolvedOptions,
|
|
1485
|
+
header
|
|
1486
|
+
};
|
|
1487
|
+
}
|
|
1488
|
+
function clearStateCookie(options) {
|
|
1489
|
+
const isProd = isProductionEnvironment();
|
|
1490
|
+
const resolvedOptions = {
|
|
1491
|
+
name: options?.name || DEFAULT_STATE_COOKIE_NAME,
|
|
1492
|
+
path: options?.path ?? "/",
|
|
1493
|
+
maxAge: 0,
|
|
1494
|
+
httpOnly: options?.httpOnly ?? true,
|
|
1495
|
+
secure: options?.secure ?? isProd,
|
|
1496
|
+
sameSite: options?.sameSite ?? "lax",
|
|
1497
|
+
domain: options?.domain
|
|
1498
|
+
};
|
|
1499
|
+
const name = resolvedOptions.name;
|
|
1500
|
+
const header = serializeCookie(name, "", resolvedOptions);
|
|
1501
|
+
return {
|
|
1502
|
+
name,
|
|
1503
|
+
value: "",
|
|
1504
|
+
options: resolvedOptions,
|
|
1505
|
+
header
|
|
1506
|
+
};
|
|
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
|
+
}
|
|
1201
1556
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1202
1557
|
0 && (module.exports = {
|
|
1203
1558
|
AccountLinkingStrategy,
|
|
1559
|
+
AccountUnlinkError,
|
|
1560
|
+
DEFAULT_SESSION_COOKIE_NAME,
|
|
1561
|
+
DEFAULT_SESSION_MAX_AGE_SECONDS,
|
|
1562
|
+
DEFAULT_STATE_COOKIE_NAME,
|
|
1563
|
+
DEFAULT_STATE_MAX_AGE_SECONDS,
|
|
1564
|
+
EmailNotVerifiedError,
|
|
1565
|
+
InvalidOAuthCallbackError,
|
|
1566
|
+
InvalidProviderConfigError,
|
|
1567
|
+
InvalidStateError,
|
|
1204
1568
|
Lixa,
|
|
1569
|
+
LixaError,
|
|
1570
|
+
ProviderNotConfiguredError,
|
|
1571
|
+
RefreshTokenError,
|
|
1572
|
+
SessionNotFoundError,
|
|
1573
|
+
TokenExchangeError,
|
|
1574
|
+
clearSessionCookie,
|
|
1575
|
+
clearSessionCookieOnResponse,
|
|
1576
|
+
clearStateCookie,
|
|
1577
|
+
clearStateCookieOnResponse,
|
|
1578
|
+
createRequireAuthMiddleware,
|
|
1579
|
+
createSessionCookie,
|
|
1580
|
+
createStateCookie,
|
|
1205
1581
|
decodeIdToken,
|
|
1206
1582
|
determineProviderFromIssuer,
|
|
1207
1583
|
extractUserInfo,
|
|
1208
|
-
fetchUserInfo
|
|
1584
|
+
fetchUserInfo,
|
|
1585
|
+
isProductionEnvironment,
|
|
1586
|
+
serializeCookie,
|
|
1587
|
+
setSessionCookieOnResponse,
|
|
1588
|
+
setStateCookieOnResponse
|
|
1209
1589
|
});
|
|
1210
1590
|
//# sourceMappingURL=index.cjs.map
|