@vunexa/lixa 0.1.4 → 0.1.6-alpha.10
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 +530 -15
- package/README.template.md +288 -44
- 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 +68 -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 +435 -107
- package/dist/index.cjs +590 -95
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +411 -108
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +569 -94
- package/dist/index.js.map +1 -1
- package/dist/lixa.d.ts +57 -21
- package/dist/lixa.d.ts.map +1 -1
- package/dist/models/session.d.ts +17 -11
- package/dist/models/session.d.ts.map +1 -1
- package/dist/types.d.ts +39 -1
- 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,31 @@ 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
|
+
clearStateCookie: () => clearStateCookie,
|
|
51
|
+
createSessionCookie: () => createSessionCookie,
|
|
52
|
+
createStateCookie: () => createStateCookie,
|
|
35
53
|
decodeIdToken: () => decodeIdToken,
|
|
36
54
|
determineProviderFromIssuer: () => determineProviderFromIssuer,
|
|
37
55
|
extractUserInfo: () => extractUserInfo,
|
|
38
|
-
fetchUserInfo: () => fetchUserInfo
|
|
56
|
+
fetchUserInfo: () => fetchUserInfo,
|
|
57
|
+
isProductionEnvironment: () => isProductionEnvironment,
|
|
58
|
+
serializeCookie: () => serializeCookie
|
|
39
59
|
});
|
|
40
60
|
module.exports = __toCommonJS(src_exports);
|
|
41
61
|
|
|
@@ -69,7 +89,7 @@ var LocalStateHandler = class {
|
|
|
69
89
|
}
|
|
70
90
|
};
|
|
71
91
|
}
|
|
72
|
-
// Default
|
|
92
|
+
// Default generateState implementation
|
|
73
93
|
async generateState(provider) {
|
|
74
94
|
const state = (0, import_crypto.randomBytes)(16).toString("hex");
|
|
75
95
|
const codeVerifier = (0, import_crypto.randomBytes)(32).toString("hex");
|
|
@@ -82,6 +102,10 @@ var LocalStateHandler = class {
|
|
|
82
102
|
}
|
|
83
103
|
};
|
|
84
104
|
}
|
|
105
|
+
// PascalCase alias for backward compatibility
|
|
106
|
+
async GenerateState(provider) {
|
|
107
|
+
return this.generateState(provider);
|
|
108
|
+
}
|
|
85
109
|
};
|
|
86
110
|
|
|
87
111
|
// src/lixa.ts
|
|
@@ -94,7 +118,20 @@ var LocalSessionHandler = class {
|
|
|
94
118
|
emailToSessionMap = /* @__PURE__ */ new Map();
|
|
95
119
|
sessionStorage;
|
|
96
120
|
constructor(defaultTtlSeconds = 600) {
|
|
97
|
-
this.cache = new import_node_cache2.default({ stdTTL: defaultTtlSeconds });
|
|
121
|
+
this.cache = new import_node_cache2.default({ stdTTL: defaultTtlSeconds, checkperiod: 60 });
|
|
122
|
+
this.cache.on("expired", (_key, value) => {
|
|
123
|
+
if (value && typeof value === "object" && value.email) {
|
|
124
|
+
this.emailToSessionMap.delete(String(value.email).toLowerCase());
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
this.cache.on("del", (_key, value) => {
|
|
128
|
+
if (value && typeof value === "object" && value.email) {
|
|
129
|
+
this.emailToSessionMap.delete(String(value.email).toLowerCase());
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
this.cache.on("flush", () => {
|
|
133
|
+
this.emailToSessionMap.clear();
|
|
134
|
+
});
|
|
98
135
|
this.sessionStorage = {
|
|
99
136
|
saveSession: async (sessionId, session, expiresInSeconds) => {
|
|
100
137
|
this.cache.set(sessionId, session, expiresInSeconds);
|
|
@@ -125,8 +162,8 @@ var LocalSessionHandler = class {
|
|
|
125
162
|
}
|
|
126
163
|
};
|
|
127
164
|
}
|
|
128
|
-
// Default
|
|
129
|
-
async
|
|
165
|
+
// Default generateSession implementation
|
|
166
|
+
async generateSession(tokenData, _providerMetadata) {
|
|
130
167
|
if (!tokenData.access_token || typeof tokenData.access_token !== "string") {
|
|
131
168
|
throw new Error("No valid access token found in OAuth response");
|
|
132
169
|
}
|
|
@@ -136,6 +173,10 @@ var LocalSessionHandler = class {
|
|
|
136
173
|
};
|
|
137
174
|
return session;
|
|
138
175
|
}
|
|
176
|
+
// PascalCase alias for backward compatibility
|
|
177
|
+
async GenerateSession(tokenData, providerMetadata) {
|
|
178
|
+
return this.generateSession(tokenData, providerMetadata);
|
|
179
|
+
}
|
|
139
180
|
};
|
|
140
181
|
|
|
141
182
|
// src/utils/user-info.ts
|
|
@@ -207,16 +248,95 @@ async function extractUserInfo(tokenData, providerMetadata) {
|
|
|
207
248
|
return { userInfo };
|
|
208
249
|
}
|
|
209
250
|
|
|
251
|
+
// src/errors.ts
|
|
252
|
+
var LixaError = class extends Error {
|
|
253
|
+
/**
|
|
254
|
+
* Standard error code string.
|
|
255
|
+
*/
|
|
256
|
+
code;
|
|
257
|
+
/**
|
|
258
|
+
* Additional error context data.
|
|
259
|
+
*/
|
|
260
|
+
details;
|
|
261
|
+
constructor(message, code = "LIXA_ERROR", details) {
|
|
262
|
+
super(message);
|
|
263
|
+
this.name = this.constructor.name;
|
|
264
|
+
this.code = code;
|
|
265
|
+
this.details = details;
|
|
266
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
var InvalidStateError = class extends LixaError {
|
|
270
|
+
constructor(message = "Invalid or expired state", details) {
|
|
271
|
+
super(message, "INVALID_STATE", details);
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
var ProviderNotConfiguredError = class extends LixaError {
|
|
275
|
+
constructor(provider, details) {
|
|
276
|
+
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: '...', ... }`;
|
|
277
|
+
super(message, "PROVIDER_NOT_CONFIGURED", {
|
|
278
|
+
provider,
|
|
279
|
+
...details
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
var InvalidProviderConfigError = class extends LixaError {
|
|
284
|
+
constructor(message, details) {
|
|
285
|
+
super(message, "INVALID_PROVIDER_CONFIG", details);
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
var InvalidOAuthCallbackError = class extends LixaError {
|
|
289
|
+
constructor(message, details) {
|
|
290
|
+
super(message, "INVALID_OAUTH_CALLBACK", details);
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
var TokenExchangeError = class extends LixaError {
|
|
294
|
+
status;
|
|
295
|
+
constructor(message, status, details) {
|
|
296
|
+
super(message, "TOKEN_EXCHANGE_FAILED", { status, ...details });
|
|
297
|
+
this.status = status;
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
var SessionNotFoundError = class extends LixaError {
|
|
301
|
+
constructor(message = "Active session not found or has expired", details) {
|
|
302
|
+
super(message, "SESSION_NOT_FOUND", details);
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
var EmailNotVerifiedError = class extends LixaError {
|
|
306
|
+
constructor(email, details) {
|
|
307
|
+
super(
|
|
308
|
+
`Cannot link account: email '${email || "unknown"}' is not verified by the identity provider`,
|
|
309
|
+
"EMAIL_NOT_VERIFIED",
|
|
310
|
+
{ email, ...details }
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
var AccountUnlinkError = class extends LixaError {
|
|
315
|
+
constructor(message, details) {
|
|
316
|
+
super(message, "ACCOUNT_UNLINK_ERROR", details);
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
var RefreshTokenError = class extends LixaError {
|
|
320
|
+
constructor(message, details) {
|
|
321
|
+
super(message, "REFRESH_TOKEN_ERROR", details);
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
|
|
210
325
|
// src/lixa.ts
|
|
211
326
|
var Lixa = class _Lixa {
|
|
212
327
|
static DEFAULT_PROVIDERS = /* @__PURE__ */ new Map();
|
|
213
328
|
static CONFIGURED_PROVIDERS = /* @__PURE__ */ new Map();
|
|
214
329
|
// Legacy registry for backward compatibility
|
|
215
|
-
|
|
216
|
-
|
|
330
|
+
// Instance-scoped fallback handlers to ensure no cross-instance state pollution
|
|
331
|
+
localStateHandler;
|
|
332
|
+
localSessionHandler;
|
|
333
|
+
localResourceHandler;
|
|
334
|
+
userResourceStore = /* @__PURE__ */ new Map();
|
|
335
|
+
refreshMutexes = /* @__PURE__ */ new Map();
|
|
217
336
|
config;
|
|
218
337
|
stateHandler;
|
|
219
338
|
sessionHandler;
|
|
339
|
+
resourceHandler;
|
|
220
340
|
debug;
|
|
221
341
|
/**
|
|
222
342
|
* Creates a new Lixa instance with the provided configuration.
|
|
@@ -233,9 +353,44 @@ var Lixa = class _Lixa {
|
|
|
233
353
|
*/
|
|
234
354
|
constructor(config) {
|
|
235
355
|
this.config = config;
|
|
236
|
-
this.stateHandler = config.stateHandler || _Lixa.LOCAL_STATE_HANDLER;
|
|
237
|
-
this.sessionHandler = config.sessionHandler || _Lixa.LOCAL_SESSION_HANDLER;
|
|
238
356
|
this.debug = config.debug || false;
|
|
357
|
+
this.localStateHandler = new LocalStateHandler();
|
|
358
|
+
this.localSessionHandler = new LocalSessionHandler();
|
|
359
|
+
this.localResourceHandler = {
|
|
360
|
+
resourceStorage: {
|
|
361
|
+
saveResource: async (userId, provider, resource) => {
|
|
362
|
+
let userMap = this.userResourceStore.get(userId);
|
|
363
|
+
if (!userMap) {
|
|
364
|
+
userMap = /* @__PURE__ */ new Map();
|
|
365
|
+
this.userResourceStore.set(userId, userMap);
|
|
366
|
+
}
|
|
367
|
+
userMap.set(provider.toLowerCase(), resource);
|
|
368
|
+
},
|
|
369
|
+
getResource: async (userId, provider) => {
|
|
370
|
+
const userMap = this.userResourceStore.get(userId);
|
|
371
|
+
return userMap?.get(provider.toLowerCase()) || null;
|
|
372
|
+
},
|
|
373
|
+
getUserResources: async (userId) => {
|
|
374
|
+
const userMap = this.userResourceStore.get(userId);
|
|
375
|
+
const result = {};
|
|
376
|
+
if (userMap) {
|
|
377
|
+
for (const [p, r] of userMap.entries()) {
|
|
378
|
+
result[p] = r;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return result;
|
|
382
|
+
},
|
|
383
|
+
deleteResource: async (userId, provider) => {
|
|
384
|
+
const userMap = this.userResourceStore.get(userId);
|
|
385
|
+
if (userMap) {
|
|
386
|
+
userMap.delete(provider.toLowerCase());
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
this.stateHandler = config.stateHandler || this.localStateHandler;
|
|
392
|
+
this.sessionHandler = config.sessionHandler || this.localSessionHandler;
|
|
393
|
+
this.resourceHandler = config.resourceHandler || this.localResourceHandler;
|
|
239
394
|
this.log("INFO", "Init", "Initializing Lixa instance", {
|
|
240
395
|
providers: Object.keys(config.providers),
|
|
241
396
|
debug: this.debug
|
|
@@ -250,8 +405,9 @@ var Lixa = class _Lixa {
|
|
|
250
405
|
} else {
|
|
251
406
|
if (!_Lixa.DEFAULT_PROVIDERS.has(name) && !_Lixa.CONFIGURED_PROVIDERS.has(name)) {
|
|
252
407
|
this.log("ERROR", "Init", `Provider '${providerName}' not available`);
|
|
253
|
-
throw new
|
|
254
|
-
|
|
408
|
+
throw new ProviderNotConfiguredError(
|
|
409
|
+
providerName,
|
|
410
|
+
{ hint: `Ensure the provider is registered via '@vunexa/lixa-providers' or provided inline.` }
|
|
255
411
|
);
|
|
256
412
|
}
|
|
257
413
|
this.log("INFO", "Init", `Using registered provider: ${providerName}`);
|
|
@@ -264,7 +420,7 @@ var Lixa = class _Lixa {
|
|
|
264
420
|
*
|
|
265
421
|
* @param name - The provider name
|
|
266
422
|
* @param config - The provider configuration
|
|
267
|
-
* @throws
|
|
423
|
+
* @throws InvalidProviderConfigError when required fields are missing or invalid
|
|
268
424
|
*/
|
|
269
425
|
validateProviderConfig(name, config) {
|
|
270
426
|
const requiredFields = ["clientId", "clientSecret", "redirectUri", "scopes"];
|
|
@@ -273,18 +429,21 @@ var Lixa = class _Lixa {
|
|
|
273
429
|
return value === void 0 || value === null || typeof value === "string" && value.trim() === "";
|
|
274
430
|
});
|
|
275
431
|
if (missingFields.length > 0) {
|
|
276
|
-
throw new
|
|
277
|
-
`Provider '${name}' configuration is missing required fields: ${missingFields.join(", ")}
|
|
432
|
+
throw new InvalidProviderConfigError(
|
|
433
|
+
`Provider '${name}' configuration is missing required fields: ${missingFields.join(", ")}`,
|
|
434
|
+
{ provider: name, missingFields }
|
|
278
435
|
);
|
|
279
436
|
}
|
|
280
437
|
if (!Array.isArray(config.scopes)) {
|
|
281
|
-
throw new
|
|
282
|
-
`Provider '${name}' configuration error: 'scopes' must be an array of strings
|
|
438
|
+
throw new InvalidProviderConfigError(
|
|
439
|
+
`Provider '${name}' configuration error: 'scopes' must be an array of strings`,
|
|
440
|
+
{ provider: name }
|
|
283
441
|
);
|
|
284
442
|
}
|
|
285
443
|
if (config.scopes.length === 0) {
|
|
286
|
-
throw new
|
|
287
|
-
`Provider '${name}' configuration error: 'scopes' array cannot be empty
|
|
444
|
+
throw new InvalidProviderConfigError(
|
|
445
|
+
`Provider '${name}' configuration error: 'scopes' array cannot be empty`,
|
|
446
|
+
{ provider: name }
|
|
288
447
|
);
|
|
289
448
|
}
|
|
290
449
|
}
|
|
@@ -293,7 +452,7 @@ var Lixa = class _Lixa {
|
|
|
293
452
|
*
|
|
294
453
|
* @param name - The provider name
|
|
295
454
|
* @param provider - The provider implementation
|
|
296
|
-
* @throws
|
|
455
|
+
* @throws InvalidProviderConfigError when required properties are missing
|
|
297
456
|
*/
|
|
298
457
|
validateProviderImplementation(name, provider) {
|
|
299
458
|
const requiredProps = ["authorizationEndpoint", "tokenEndpoint", "userInfoEndpoint"];
|
|
@@ -302,24 +461,27 @@ var Lixa = class _Lixa {
|
|
|
302
461
|
return !value || typeof value !== "string" || value.trim() === "";
|
|
303
462
|
});
|
|
304
463
|
if (missingProps.length > 0) {
|
|
305
|
-
throw new
|
|
306
|
-
`Provider '${name}' implementation is missing required properties: ${missingProps.join(", ")}. All IProvider implementations must define: authorizationEndpoint, tokenEndpoint, and userInfoEndpoint
|
|
464
|
+
throw new InvalidProviderConfigError(
|
|
465
|
+
`Provider '${name}' implementation is missing required properties: ${missingProps.join(", ")}. All IProvider implementations must define: authorizationEndpoint, tokenEndpoint, and userInfoEndpoint.`,
|
|
466
|
+
{ provider: name, missingProps }
|
|
307
467
|
);
|
|
308
468
|
}
|
|
309
469
|
}
|
|
310
470
|
/**
|
|
311
|
-
* Structured
|
|
471
|
+
* Structured logging with standardized format and custom logger support.
|
|
312
472
|
*
|
|
313
|
-
* @param level - Log level (INFO, WARN, ERROR)
|
|
314
|
-
* @param context - Context of the log (Init, Auth, Token, Session, State)
|
|
473
|
+
* @param level - Log level (INFO, WARN, ERROR, DEBUG)
|
|
474
|
+
* @param context - Context of the log (Init, Auth, Token, Session, State, AccountLinking, Resource)
|
|
315
475
|
* @param message - Log message
|
|
316
476
|
* @param data - Optional data to log
|
|
317
|
-
*
|
|
318
|
-
* @remarks
|
|
319
|
-
* Format: [Lixa] [timestamp] [level] [context] message
|
|
320
|
-
* Only logs when debug mode is enabled.
|
|
321
477
|
*/
|
|
322
478
|
log(level, context, message, data) {
|
|
479
|
+
if (this.config.logger) {
|
|
480
|
+
try {
|
|
481
|
+
this.config.logger.log(level, context, message, data);
|
|
482
|
+
} catch {
|
|
483
|
+
}
|
|
484
|
+
}
|
|
323
485
|
if (!this.debug) return;
|
|
324
486
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
325
487
|
const prefix = `[Lixa] [${timestamp}] [${level}] [${context}]`;
|
|
@@ -370,8 +532,9 @@ var Lixa = class _Lixa {
|
|
|
370
532
|
if (legacyProvider) {
|
|
371
533
|
return legacyProvider;
|
|
372
534
|
}
|
|
373
|
-
throw new
|
|
374
|
-
|
|
535
|
+
throw new ProviderNotConfiguredError(
|
|
536
|
+
name,
|
|
537
|
+
{ hint: `Ensure the provider is included in the configuration with a 'provider' field, or registered using Lixa.registerProvider().` }
|
|
375
538
|
);
|
|
376
539
|
}
|
|
377
540
|
/**
|
|
@@ -561,23 +724,26 @@ var Lixa = class _Lixa {
|
|
|
561
724
|
const providerConfig = this.findProviderByType(providerType);
|
|
562
725
|
if (!providerConfig) {
|
|
563
726
|
this.log("ERROR", "Auth", `Provider '${String(provider)}' is not configured`);
|
|
564
|
-
throw new
|
|
727
|
+
throw new ProviderNotConfiguredError(String(provider), {
|
|
728
|
+
message: `Provider '${String(provider)}' is not configured in this Lixa instance`
|
|
729
|
+
});
|
|
565
730
|
}
|
|
566
731
|
const providerImpl = this.getProvider(providerType, providerConfig);
|
|
567
732
|
let stateValue;
|
|
568
733
|
let codeVerifier;
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
734
|
+
const generateStateFn = this.stateHandler.generateState || this.stateHandler.GenerateState;
|
|
735
|
+
if (generateStateFn) {
|
|
736
|
+
this.log("INFO", "State", "Calling custom generateState");
|
|
737
|
+
const generated = await generateStateFn(providerType);
|
|
572
738
|
stateValue = state || generated.state;
|
|
573
739
|
codeVerifier = generated.data.codeVerifier;
|
|
574
|
-
const storage = this.stateHandler.stateStorage ||
|
|
740
|
+
const storage = this.stateHandler.stateStorage || this.localStateHandler.stateStorage;
|
|
575
741
|
await storage.saveState(stateValue, generated.data, 300);
|
|
576
742
|
} else {
|
|
577
743
|
this.log("INFO", "State", "Using default state generation");
|
|
578
744
|
stateValue = state || (0, import_crypto2.randomBytes)(16).toString("hex");
|
|
579
745
|
codeVerifier = (0, import_crypto2.randomBytes)(32).toString("hex");
|
|
580
|
-
const storage = this.stateHandler.stateStorage ||
|
|
746
|
+
const storage = this.stateHandler.stateStorage || this.localStateHandler.stateStorage;
|
|
581
747
|
await storage.saveState(
|
|
582
748
|
stateValue,
|
|
583
749
|
{
|
|
@@ -586,12 +752,17 @@ var Lixa = class _Lixa {
|
|
|
586
752
|
codeVerifier
|
|
587
753
|
},
|
|
588
754
|
300
|
|
589
|
-
// 5 minutes in seconds
|
|
755
|
+
// 5 minutes in seconds (synchronized with cookie TTL)
|
|
590
756
|
);
|
|
591
757
|
}
|
|
592
758
|
const codeChallenge = _Lixa.buildCodeChallenge(codeVerifier);
|
|
593
759
|
this.log("INFO", "State", `Saved state for provider: ${providerType}`, { state: stateValue });
|
|
594
|
-
const authNScopes = this.resolveAuthNScopes(
|
|
760
|
+
const authNScopes = this.resolveAuthNScopes(
|
|
761
|
+
providerType,
|
|
762
|
+
providerConfig.scopes,
|
|
763
|
+
providerImpl,
|
|
764
|
+
providerConfig.allowNonAuthScopes
|
|
765
|
+
);
|
|
595
766
|
const params = new URLSearchParams({
|
|
596
767
|
client_id: providerConfig.clientId,
|
|
597
768
|
redirect_uri: providerConfig.redirectUri,
|
|
@@ -610,9 +781,12 @@ var Lixa = class _Lixa {
|
|
|
610
781
|
return authUrl;
|
|
611
782
|
}
|
|
612
783
|
/**
|
|
613
|
-
* Restricts primary authentication scopes strictly to AuthN identity scopes.
|
|
784
|
+
* Restricts primary authentication scopes strictly to AuthN identity scopes unless allowNonAuthScopes is true.
|
|
614
785
|
*/
|
|
615
|
-
resolveAuthNScopes(providerType, configuredScopes, providerImpl) {
|
|
786
|
+
resolveAuthNScopes(providerType, configuredScopes, providerImpl, allowNonAuthScopes) {
|
|
787
|
+
if (allowNonAuthScopes) {
|
|
788
|
+
return configuredScopes && configuredScopes.length > 0 ? configuredScopes : providerImpl.authScopes || ["openid", "email", "profile"];
|
|
789
|
+
}
|
|
616
790
|
const defaultAuthScopes = {
|
|
617
791
|
google: ["openid", "email", "profile"],
|
|
618
792
|
github: ["read:user", "user:email"],
|
|
@@ -641,7 +815,7 @@ var Lixa = class _Lixa {
|
|
|
641
815
|
"Auth",
|
|
642
816
|
`Primary authentication is strictly limited to AuthN scopes. Excluded non-AuthN resource scopes: [${nonAuthNScopes.join(
|
|
643
817
|
", "
|
|
644
|
-
)}].
|
|
818
|
+
)}]. Set 'allowNonAuthScopes: true' on provider config to include them, or use lixa.getResourceAuthUrl() post-login to connect resource providers.`
|
|
645
819
|
);
|
|
646
820
|
}
|
|
647
821
|
if (validAuthNScopes.length > 0) {
|
|
@@ -677,18 +851,18 @@ var Lixa = class _Lixa {
|
|
|
677
851
|
this.log("INFO", "Auth", `Handling OAuth callback for provider: ${providerType}`);
|
|
678
852
|
if (!code || code.trim() === "") {
|
|
679
853
|
this.log("ERROR", "Auth", "Invalid or missing authorization code in callback");
|
|
680
|
-
throw new
|
|
854
|
+
throw new InvalidOAuthCallbackError("Invalid or missing code in callback");
|
|
681
855
|
}
|
|
682
856
|
if (!state || state.trim() === "") {
|
|
683
857
|
this.log("ERROR", "Auth", "Invalid or missing state in callback");
|
|
684
|
-
throw new
|
|
858
|
+
throw new InvalidOAuthCallbackError("Invalid or missing state in callback");
|
|
685
859
|
}
|
|
686
860
|
this.log("INFO", "State", "Validating state parameter", { state });
|
|
687
|
-
const stateStorage = this.stateHandler.stateStorage ||
|
|
861
|
+
const stateStorage = this.stateHandler.stateStorage || this.localStateHandler.stateStorage;
|
|
688
862
|
const cachedState = await stateStorage.getState(state);
|
|
689
863
|
if (!cachedState) {
|
|
690
864
|
this.log("ERROR", "State", "State validation failed: state not found or expired", { state });
|
|
691
|
-
throw new
|
|
865
|
+
throw new InvalidStateError("Invalid or expired state", { state });
|
|
692
866
|
}
|
|
693
867
|
this.log("INFO", "State", "State validated successfully, removing from cache");
|
|
694
868
|
await stateStorage.deleteState(state);
|
|
@@ -696,7 +870,9 @@ var Lixa = class _Lixa {
|
|
|
696
870
|
const providerConfig = this.findProviderByType(providerType);
|
|
697
871
|
if (!providerConfig) {
|
|
698
872
|
this.log("ERROR", "Auth", `Provider '${String(provider)}' is not configured`);
|
|
699
|
-
throw new
|
|
873
|
+
throw new ProviderNotConfiguredError(String(provider), {
|
|
874
|
+
message: `Provider '${String(provider)}' is not configured in this Lixa instance`
|
|
875
|
+
});
|
|
700
876
|
}
|
|
701
877
|
const providerImpl = this.getProvider(providerType, providerConfig);
|
|
702
878
|
this.log("INFO", "Token", `Exchanging authorization code for tokens`, { provider: providerType });
|
|
@@ -722,9 +898,12 @@ var Lixa = class _Lixa {
|
|
|
722
898
|
extractedUserInfo = userInfo;
|
|
723
899
|
} catch {
|
|
724
900
|
}
|
|
725
|
-
const generateSession = this.sessionHandler.generateSession ||
|
|
726
|
-
if (
|
|
727
|
-
|
|
901
|
+
const generateSession = this.sessionHandler.generateSession || this.sessionHandler.GenerateSession || (this.localSessionHandler.generateSession ? this.localSessionHandler.generateSession.bind(this.localSessionHandler) : void 0);
|
|
902
|
+
if (!generateSession) {
|
|
903
|
+
throw new Error("No session generation handler available");
|
|
904
|
+
}
|
|
905
|
+
if (this.sessionHandler.generateSession || this.sessionHandler.GenerateSession) {
|
|
906
|
+
this.log("INFO", "Session", "Calling custom generateSession");
|
|
728
907
|
} else {
|
|
729
908
|
this.log("INFO", "Session", "Using default session generation");
|
|
730
909
|
}
|
|
@@ -733,7 +912,7 @@ var Lixa = class _Lixa {
|
|
|
733
912
|
if (extractedUserInfo?.email && !session.email) {
|
|
734
913
|
session.email = extractedUserInfo.email;
|
|
735
914
|
}
|
|
736
|
-
const sessionStorage = this.sessionHandler.sessionStorage ||
|
|
915
|
+
const sessionStorage = this.sessionHandler.sessionStorage || this.localSessionHandler.sessionStorage;
|
|
737
916
|
const linkingConfig = this.config.accountLinking;
|
|
738
917
|
const mode = String(linkingConfig?.mode || "");
|
|
739
918
|
const isLinkByEmail = mode === "AUTO_LINK_BY_VERIFIED_EMAIL" /* AUTO_LINK_BY_VERIFIED_EMAIL */ || mode === "AUTO_LINK_BY_VERIFIED_EMAIL" || mode === "linkByEmail";
|
|
@@ -786,27 +965,31 @@ var Lixa = class _Lixa {
|
|
|
786
965
|
*/
|
|
787
966
|
async linkAccount(params) {
|
|
788
967
|
const { sessionId, provider, code, state } = params;
|
|
789
|
-
const sessionStorage = this.sessionHandler.sessionStorage ||
|
|
968
|
+
const sessionStorage = this.sessionHandler.sessionStorage || this.localSessionHandler.sessionStorage;
|
|
790
969
|
const existingSession = await sessionStorage.getSession(sessionId);
|
|
791
970
|
if (!existingSession) {
|
|
792
|
-
throw new
|
|
971
|
+
throw new SessionNotFoundError("Invalid session ID. User must be authenticated to link an account.", { sessionId });
|
|
972
|
+
}
|
|
973
|
+
if (!code || code.trim() === "") {
|
|
974
|
+
throw new InvalidOAuthCallbackError("Invalid or missing code in linkAccount");
|
|
793
975
|
}
|
|
794
976
|
const providerType = String(provider).toLowerCase();
|
|
795
977
|
const providerConfig = this.findProviderByType(providerType);
|
|
796
978
|
if (!providerConfig) {
|
|
797
|
-
throw new
|
|
979
|
+
throw new ProviderNotConfiguredError(String(provider));
|
|
798
980
|
}
|
|
799
981
|
const providerImpl = this.getProvider(providerType, providerConfig);
|
|
800
|
-
let codeVerifier
|
|
982
|
+
let codeVerifier;
|
|
801
983
|
if (state) {
|
|
802
|
-
const stateStorage = this.stateHandler.stateStorage ||
|
|
984
|
+
const stateStorage = this.stateHandler.stateStorage || this.localStateHandler.stateStorage;
|
|
803
985
|
const cachedState = await stateStorage.getState(state);
|
|
804
|
-
if (cachedState) {
|
|
805
|
-
|
|
806
|
-
await stateStorage.deleteState(state);
|
|
986
|
+
if (!cachedState) {
|
|
987
|
+
throw new InvalidStateError("Invalid or expired state during account linking", { state });
|
|
807
988
|
}
|
|
989
|
+
codeVerifier = cachedState.codeVerifier;
|
|
990
|
+
await stateStorage.deleteState(state);
|
|
808
991
|
}
|
|
809
|
-
const tokens = await this.exchangeCodeForToken(
|
|
992
|
+
const tokens = await this.exchangeCodeForToken(code, providerConfig, providerImpl, codeVerifier || "");
|
|
810
993
|
const providerMetadata = {
|
|
811
994
|
name: providerType,
|
|
812
995
|
endpoints: {
|
|
@@ -844,14 +1027,17 @@ var Lixa = class _Lixa {
|
|
|
844
1027
|
* @returns Promise resolving to true on successful unlink
|
|
845
1028
|
*/
|
|
846
1029
|
async unlinkAccount(sessionId, providerToUnlink) {
|
|
847
|
-
const sessionStorage = this.sessionHandler.sessionStorage ||
|
|
1030
|
+
const sessionStorage = this.sessionHandler.sessionStorage || this.localSessionHandler.sessionStorage;
|
|
848
1031
|
const session = await sessionStorage.getSession(sessionId);
|
|
849
1032
|
if (!session || !session.accounts) {
|
|
850
|
-
throw new
|
|
1033
|
+
throw new AccountUnlinkError("Session not found or has no linked accounts.", { sessionId });
|
|
851
1034
|
}
|
|
852
1035
|
const linkedProviders = Object.keys(session.accounts);
|
|
853
1036
|
if (linkedProviders.length <= 1) {
|
|
854
|
-
throw new
|
|
1037
|
+
throw new AccountUnlinkError("Cannot unlink the only authentication provider for this account.", {
|
|
1038
|
+
sessionId,
|
|
1039
|
+
provider: providerToUnlink
|
|
1040
|
+
});
|
|
855
1041
|
}
|
|
856
1042
|
delete session.accounts[providerToUnlink.toLowerCase()];
|
|
857
1043
|
await sessionStorage.saveSession(sessionId, session, 86400);
|
|
@@ -870,20 +1056,20 @@ var Lixa = class _Lixa {
|
|
|
870
1056
|
*/
|
|
871
1057
|
async getResourceAuthUrl(params) {
|
|
872
1058
|
const { sessionId, provider, scopes, state, prompt, extraConfig } = params;
|
|
873
|
-
const sessionStorage = this.sessionHandler.sessionStorage ||
|
|
1059
|
+
const sessionStorage = this.sessionHandler.sessionStorage || this.localSessionHandler.sessionStorage;
|
|
874
1060
|
const activeSession = await sessionStorage.getSession(sessionId);
|
|
875
1061
|
if (!activeSession) {
|
|
876
|
-
throw new
|
|
1062
|
+
throw new SessionNotFoundError("Authentication required. Active session must exist to connect resource providers.", { sessionId });
|
|
877
1063
|
}
|
|
878
1064
|
const providerType = String(provider).toLowerCase();
|
|
879
1065
|
const providerConfig = this.findProviderByType(providerType);
|
|
880
1066
|
if (!providerConfig) {
|
|
881
|
-
throw new
|
|
1067
|
+
throw new ProviderNotConfiguredError(String(provider));
|
|
882
1068
|
}
|
|
883
1069
|
const providerImpl = this.getProvider(providerType, providerConfig);
|
|
884
1070
|
const stateValue = state || (0, import_crypto2.randomBytes)(16).toString("hex");
|
|
885
1071
|
const codeVerifier = (0, import_crypto2.randomBytes)(32).toString("hex");
|
|
886
|
-
const storage = this.stateHandler.stateStorage ||
|
|
1072
|
+
const storage = this.stateHandler.stateStorage || this.localStateHandler.stateStorage;
|
|
887
1073
|
await storage.saveState(
|
|
888
1074
|
stateValue,
|
|
889
1075
|
{
|
|
@@ -908,77 +1094,245 @@ var Lixa = class _Lixa {
|
|
|
908
1094
|
});
|
|
909
1095
|
return `${providerImpl.authorizationEndpoint}?${searchParams.toString()}`;
|
|
910
1096
|
}
|
|
1097
|
+
getUserKeyFromSession(session) {
|
|
1098
|
+
return session.userId || session.email || session.id || "anonymous";
|
|
1099
|
+
}
|
|
911
1100
|
/**
|
|
912
|
-
* Handles the OAuth callback for a connected resource provider and stores resource tokens
|
|
913
|
-
*
|
|
914
|
-
* @param params - Object containing sessionId, provider, code, state, and requested scopes
|
|
915
|
-
* @returns Updated Session containing stored resource tokens under session.resources[provider]
|
|
1101
|
+
* Handles the OAuth callback for a connected resource provider and stores resource tokens bound to user account.
|
|
916
1102
|
*/
|
|
917
1103
|
async handleResourceCallback(params) {
|
|
918
1104
|
const { sessionId, provider, code, state, scopes } = params;
|
|
919
|
-
const sessionStorage = this.sessionHandler.sessionStorage ||
|
|
1105
|
+
const sessionStorage = this.sessionHandler.sessionStorage || this.localSessionHandler.sessionStorage;
|
|
920
1106
|
const activeSession = await sessionStorage.getSession(sessionId);
|
|
921
1107
|
if (!activeSession) {
|
|
922
|
-
throw new
|
|
1108
|
+
throw new SessionNotFoundError("Authentication required. Active session not found for resource connection.", { sessionId });
|
|
923
1109
|
}
|
|
924
1110
|
const providerType = String(provider).toLowerCase();
|
|
925
1111
|
const providerConfig = this.findProviderByType(providerType);
|
|
926
1112
|
if (!providerConfig) {
|
|
927
|
-
throw new
|
|
1113
|
+
throw new ProviderNotConfiguredError(String(provider));
|
|
928
1114
|
}
|
|
929
1115
|
const providerImpl = this.getProvider(providerType, providerConfig);
|
|
930
1116
|
let codeVerifier = (0, import_crypto2.randomBytes)(32).toString("hex");
|
|
931
1117
|
if (state) {
|
|
932
|
-
const stateStorage = this.stateHandler.stateStorage ||
|
|
1118
|
+
const stateStorage = this.stateHandler.stateStorage || this.localStateHandler.stateStorage;
|
|
933
1119
|
const cachedState = await stateStorage.getState(state);
|
|
934
|
-
if (cachedState) {
|
|
935
|
-
|
|
936
|
-
await stateStorage.deleteState(state);
|
|
1120
|
+
if (!cachedState) {
|
|
1121
|
+
throw new InvalidStateError("Invalid or expired state during resource connection callback", { state });
|
|
937
1122
|
}
|
|
1123
|
+
codeVerifier = cachedState.codeVerifier;
|
|
1124
|
+
await stateStorage.deleteState(state);
|
|
938
1125
|
}
|
|
939
1126
|
const tokens = await this.exchangeCodeForToken(code, providerConfig, providerImpl, codeVerifier);
|
|
940
|
-
|
|
941
|
-
|
|
1127
|
+
const expiresAt = tokens.expires_in ? Date.now() + tokens.expires_in * 1e3 : void 0;
|
|
1128
|
+
const connectedResource = {
|
|
942
1129
|
provider: providerType,
|
|
943
1130
|
accessToken: tokens.access_token,
|
|
944
1131
|
refreshToken: tokens.refresh_token,
|
|
1132
|
+
expiresAt,
|
|
945
1133
|
scopes: scopes || (tokens.scope ? tokens.scope.split(" ") : []),
|
|
946
1134
|
raw: tokens,
|
|
947
1135
|
connectedAt: Date.now()
|
|
948
1136
|
};
|
|
1137
|
+
const userKey = this.getUserKeyFromSession(activeSession);
|
|
1138
|
+
const resourceStorage = this.resourceHandler.resourceStorage || this.localResourceHandler.resourceStorage;
|
|
1139
|
+
await resourceStorage.saveResource(userKey, providerType, connectedResource);
|
|
1140
|
+
activeSession.resources = activeSession.resources || {};
|
|
1141
|
+
activeSession.resources[providerType] = connectedResource;
|
|
949
1142
|
await sessionStorage.saveSession(sessionId, activeSession, 86400);
|
|
950
1143
|
return activeSession;
|
|
951
1144
|
}
|
|
952
1145
|
/**
|
|
953
|
-
* Retrieves a connected resource
|
|
1146
|
+
* Retrieves a connected resource for a specific User ID / Email directly (independent of session IDs).
|
|
1147
|
+
* Automatically refreshes expired access tokens if a refresh token is present.
|
|
1148
|
+
*
|
|
1149
|
+
* @param userIdOrEmail - User identifier or email
|
|
1150
|
+
* @param provider - Resource provider identifier (e.g. 'github', 'google')
|
|
1151
|
+
*/
|
|
1152
|
+
async getUserResource(userIdOrEmail, provider) {
|
|
1153
|
+
const resourceStorage = this.resourceHandler.resourceStorage || this.localResourceHandler.resourceStorage;
|
|
1154
|
+
const providerType = provider.toLowerCase();
|
|
1155
|
+
const resource = await resourceStorage.getResource(userIdOrEmail, providerType);
|
|
1156
|
+
if (!resource) return null;
|
|
1157
|
+
if (resource.refreshToken && resource.expiresAt && Date.now() >= resource.expiresAt - 6e4) {
|
|
1158
|
+
this.log("INFO", "Token", `Resource access token for user '${userIdOrEmail}' on '${providerType}' is expired. Auto-refreshing...`);
|
|
1159
|
+
try {
|
|
1160
|
+
return await this.refreshUserResourceToken(userIdOrEmail, providerType);
|
|
1161
|
+
} catch (error) {
|
|
1162
|
+
this.log("ERROR", "Token", `Failed to auto-refresh resource token for user '${userIdOrEmail}' on '${providerType}'`, { error: String(error) });
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
return resource;
|
|
1166
|
+
}
|
|
1167
|
+
/**
|
|
1168
|
+
* Retrieves all connected resources for a specific User ID / Email.
|
|
1169
|
+
*/
|
|
1170
|
+
async getUserResources(userIdOrEmail) {
|
|
1171
|
+
const resourceStorage = this.resourceHandler.resourceStorage || this.localResourceHandler.resourceStorage;
|
|
1172
|
+
return await resourceStorage.getUserResources(userIdOrEmail);
|
|
1173
|
+
}
|
|
1174
|
+
/**
|
|
1175
|
+
* Retrieves a connected resource provider token for an active session, auto-refreshing expired tokens if possible.
|
|
954
1176
|
*
|
|
955
1177
|
* @param sessionId - Active session ID
|
|
956
|
-
* @param provider - Provider identifier (e.g. 'github')
|
|
1178
|
+
* @param provider - Provider identifier (e.g. 'github', 'google')
|
|
957
1179
|
*/
|
|
958
1180
|
async getConnectedResource(sessionId, provider) {
|
|
959
|
-
const sessionStorage = this.sessionHandler.sessionStorage ||
|
|
1181
|
+
const sessionStorage = this.sessionHandler.sessionStorage || this.localSessionHandler.sessionStorage;
|
|
960
1182
|
const activeSession = await sessionStorage.getSession(sessionId);
|
|
961
|
-
if (!activeSession
|
|
962
|
-
|
|
1183
|
+
if (!activeSession) return null;
|
|
1184
|
+
const providerType = provider.toLowerCase();
|
|
1185
|
+
const userKey = this.getUserKeyFromSession(activeSession);
|
|
1186
|
+
let resource = await this.getUserResource(userKey, providerType);
|
|
1187
|
+
if (!resource && activeSession.resources) {
|
|
1188
|
+
resource = activeSession.resources[providerType] || null;
|
|
1189
|
+
}
|
|
1190
|
+
if (resource) {
|
|
1191
|
+
if (resource.refreshToken && resource.expiresAt && Date.now() >= resource.expiresAt - 6e4) {
|
|
1192
|
+
this.log("INFO", "Token", `Resource access token for user '${userKey}' on '${providerType}' is expired. Auto-refreshing...`);
|
|
1193
|
+
try {
|
|
1194
|
+
resource = await this.refreshResourceToken(sessionId, providerType);
|
|
1195
|
+
} catch (error) {
|
|
1196
|
+
this.log("ERROR", "Token", `Failed to auto-refresh resource token for user '${userKey}' on '${providerType}'`, { error: String(error) });
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
activeSession.resources = activeSession.resources || {};
|
|
1200
|
+
activeSession.resources[providerType] = resource;
|
|
1201
|
+
}
|
|
1202
|
+
return resource;
|
|
963
1203
|
}
|
|
964
1204
|
/**
|
|
965
|
-
*
|
|
1205
|
+
* Refreshes a user's resource access token using its refresh token.
|
|
1206
|
+
* Deduplicates concurrent refresh requests via an in-flight promise mutex.
|
|
966
1207
|
*
|
|
967
|
-
* @param
|
|
968
|
-
* @param provider - Provider identifier
|
|
1208
|
+
* @param userIdOrEmail - User identifier or email
|
|
1209
|
+
* @param provider - Provider identifier (e.g. 'google', 'github')
|
|
969
1210
|
*/
|
|
970
|
-
async
|
|
971
|
-
const
|
|
1211
|
+
async refreshUserResourceToken(userIdOrEmail, provider, existingResource) {
|
|
1212
|
+
const mutexKey = `${userIdOrEmail.toLowerCase()}:${provider.toLowerCase()}`;
|
|
1213
|
+
const existingPromise = this.refreshMutexes.get(mutexKey);
|
|
1214
|
+
if (existingPromise) {
|
|
1215
|
+
this.log("INFO", "Token", `Concurrent refresh request detected for '${mutexKey}'. Reusing in-flight promise.`);
|
|
1216
|
+
return existingPromise;
|
|
1217
|
+
}
|
|
1218
|
+
const refreshPromise = this.executeRefreshUserResourceToken(userIdOrEmail, provider, existingResource).finally(() => {
|
|
1219
|
+
this.refreshMutexes.delete(mutexKey);
|
|
1220
|
+
});
|
|
1221
|
+
this.refreshMutexes.set(mutexKey, refreshPromise);
|
|
1222
|
+
return refreshPromise;
|
|
1223
|
+
}
|
|
1224
|
+
/**
|
|
1225
|
+
* Internal execution of refresh token exchange.
|
|
1226
|
+
*/
|
|
1227
|
+
async executeRefreshUserResourceToken(userIdOrEmail, provider, existingResource) {
|
|
1228
|
+
const resourceStorage = this.resourceHandler.resourceStorage || this.localResourceHandler.resourceStorage;
|
|
1229
|
+
const providerType = provider.toLowerCase();
|
|
1230
|
+
let resource = existingResource || await resourceStorage.getResource(userIdOrEmail, providerType);
|
|
1231
|
+
if (!resource || !resource.refreshToken) {
|
|
1232
|
+
throw new RefreshTokenError(
|
|
1233
|
+
`No refresh token available for user '${userIdOrEmail}' on connected resource '${provider}'`,
|
|
1234
|
+
{ userId: userIdOrEmail, provider }
|
|
1235
|
+
);
|
|
1236
|
+
}
|
|
1237
|
+
const providerConfig = this.findProviderByType(providerType);
|
|
1238
|
+
if (!providerConfig) {
|
|
1239
|
+
throw new ProviderNotConfiguredError(provider);
|
|
1240
|
+
}
|
|
1241
|
+
const providerImpl = this.getProvider(providerType, providerConfig);
|
|
1242
|
+
const body = {
|
|
1243
|
+
client_id: providerConfig.clientId,
|
|
1244
|
+
client_secret: providerConfig.clientSecret,
|
|
1245
|
+
grant_type: "refresh_token",
|
|
1246
|
+
refresh_token: resource.refreshToken
|
|
1247
|
+
};
|
|
1248
|
+
const response = await fetch(providerImpl.tokenEndpoint, {
|
|
1249
|
+
method: "POST",
|
|
1250
|
+
headers: {
|
|
1251
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
1252
|
+
Accept: "application/json"
|
|
1253
|
+
},
|
|
1254
|
+
body: new URLSearchParams(body).toString()
|
|
1255
|
+
});
|
|
1256
|
+
if (!response.ok) {
|
|
1257
|
+
const errorText = await response.text();
|
|
1258
|
+
throw new RefreshTokenError(
|
|
1259
|
+
`Failed to refresh resource token for user '${userIdOrEmail}' on '${provider}': ${response.status} - ${errorText}`,
|
|
1260
|
+
{ userId: userIdOrEmail, provider, status: response.status }
|
|
1261
|
+
);
|
|
1262
|
+
}
|
|
1263
|
+
const tokens = await response.json();
|
|
1264
|
+
const expiresAt = tokens.expires_in ? Date.now() + tokens.expires_in * 1e3 : void 0;
|
|
1265
|
+
resource.accessToken = tokens.access_token;
|
|
1266
|
+
if (tokens.refresh_token) {
|
|
1267
|
+
resource.refreshToken = tokens.refresh_token;
|
|
1268
|
+
}
|
|
1269
|
+
if (expiresAt) {
|
|
1270
|
+
resource.expiresAt = expiresAt;
|
|
1271
|
+
}
|
|
1272
|
+
resource.raw = tokens;
|
|
1273
|
+
resource.connectedAt = Date.now();
|
|
1274
|
+
await resourceStorage.saveResource(userIdOrEmail, providerType, resource);
|
|
1275
|
+
return resource;
|
|
1276
|
+
}
|
|
1277
|
+
/**
|
|
1278
|
+
* Refreshes a connected resource access token for an active session.
|
|
1279
|
+
*/
|
|
1280
|
+
async refreshResourceToken(sessionId, provider) {
|
|
1281
|
+
const sessionStorage = this.sessionHandler.sessionStorage || this.localSessionHandler.sessionStorage;
|
|
972
1282
|
const activeSession = await sessionStorage.getSession(sessionId);
|
|
973
|
-
if (!activeSession
|
|
974
|
-
|
|
1283
|
+
if (!activeSession) {
|
|
1284
|
+
throw new SessionNotFoundError("Active session not found.", { sessionId });
|
|
1285
|
+
}
|
|
1286
|
+
const providerType = provider.toLowerCase();
|
|
1287
|
+
const userKey = this.getUserKeyFromSession(activeSession);
|
|
1288
|
+
const existingResource = activeSession.resources ? activeSession.resources[providerType] : void 0;
|
|
1289
|
+
const refreshed = await this.refreshUserResourceToken(userKey, providerType, existingResource);
|
|
1290
|
+
activeSession.resources = activeSession.resources || {};
|
|
1291
|
+
activeSession.resources[providerType] = refreshed;
|
|
975
1292
|
await sessionStorage.saveSession(sessionId, activeSession, 86400);
|
|
1293
|
+
return refreshed;
|
|
1294
|
+
}
|
|
1295
|
+
/**
|
|
1296
|
+
* Disconnects a resource provider for a specific User ID / Email.
|
|
1297
|
+
*/
|
|
1298
|
+
async disconnectUserResource(userIdOrEmail, provider) {
|
|
1299
|
+
const resourceStorage = this.resourceHandler.resourceStorage || this.localResourceHandler.resourceStorage;
|
|
1300
|
+
await resourceStorage.deleteResource(userIdOrEmail, provider.toLowerCase());
|
|
976
1301
|
return true;
|
|
977
1302
|
}
|
|
1303
|
+
/**
|
|
1304
|
+
* Disconnects a resource provider from an active session and user account.
|
|
1305
|
+
*/
|
|
1306
|
+
async disconnectResource(sessionId, provider) {
|
|
1307
|
+
const sessionStorage = this.sessionHandler.sessionStorage || this.localSessionHandler.sessionStorage;
|
|
1308
|
+
const activeSession = await sessionStorage.getSession(sessionId);
|
|
1309
|
+
if (activeSession) {
|
|
1310
|
+
const userKey = this.getUserKeyFromSession(activeSession);
|
|
1311
|
+
await this.disconnectUserResource(userKey, provider);
|
|
1312
|
+
if (activeSession.resources) {
|
|
1313
|
+
delete activeSession.resources[provider.toLowerCase()];
|
|
1314
|
+
await sessionStorage.saveSession(sessionId, activeSession, 86400);
|
|
1315
|
+
}
|
|
1316
|
+
return true;
|
|
1317
|
+
}
|
|
1318
|
+
return false;
|
|
1319
|
+
}
|
|
1320
|
+
/**
|
|
1321
|
+
* Retrieves active session details from session storage.
|
|
1322
|
+
*/
|
|
978
1323
|
async fetchSessionInfo(sessionId) {
|
|
979
|
-
const sessionStorage = this.sessionHandler.sessionStorage ||
|
|
1324
|
+
const sessionStorage = this.sessionHandler.sessionStorage || this.localSessionHandler.sessionStorage;
|
|
980
1325
|
return await sessionStorage.getSession(sessionId);
|
|
981
1326
|
}
|
|
1327
|
+
/**
|
|
1328
|
+
* Deletes a session from session storage (e.g. on logout).
|
|
1329
|
+
*
|
|
1330
|
+
* @param sessionId - Active session identifier
|
|
1331
|
+
*/
|
|
1332
|
+
async deleteSession(sessionId) {
|
|
1333
|
+
const sessionStorage = this.sessionHandler.sessionStorage || this.localSessionHandler.sessionStorage;
|
|
1334
|
+
await sessionStorage.deleteSession(sessionId);
|
|
1335
|
+
}
|
|
982
1336
|
async exchangeCodeForToken(code, providerConfig, providerImpl, codeVerifier) {
|
|
983
1337
|
const body = {
|
|
984
1338
|
client_id: providerConfig.clientId,
|
|
@@ -1009,8 +1363,10 @@ var Lixa = class _Lixa {
|
|
|
1009
1363
|
statusText: response.statusText,
|
|
1010
1364
|
error: errorBody
|
|
1011
1365
|
});
|
|
1012
|
-
throw new
|
|
1013
|
-
`Token exchange failed: ${response.status} ${response.statusText} - ${errorBody}
|
|
1366
|
+
throw new TokenExchangeError(
|
|
1367
|
+
`Token exchange failed: ${response.status} ${response.statusText} - ${errorBody}`,
|
|
1368
|
+
response.status,
|
|
1369
|
+
{ statusText: response.statusText, errorBody }
|
|
1014
1370
|
);
|
|
1015
1371
|
}
|
|
1016
1372
|
this.log("INFO", "Token", "Token exchange response received successfully");
|
|
@@ -1025,13 +1381,152 @@ var Lixa = class _Lixa {
|
|
|
1025
1381
|
return void 0;
|
|
1026
1382
|
}
|
|
1027
1383
|
};
|
|
1384
|
+
|
|
1385
|
+
// src/utils/cookies.ts
|
|
1386
|
+
var DEFAULT_SESSION_COOKIE_NAME = "lixa_session";
|
|
1387
|
+
var DEFAULT_STATE_COOKIE_NAME = "lixa_oauth_state";
|
|
1388
|
+
var DEFAULT_SESSION_MAX_AGE_SECONDS = 24 * 60 * 60;
|
|
1389
|
+
var DEFAULT_STATE_MAX_AGE_SECONDS = 5 * 60;
|
|
1390
|
+
function isProductionEnvironment() {
|
|
1391
|
+
return typeof process !== "undefined" && process.env?.NODE_ENV === "production";
|
|
1392
|
+
}
|
|
1393
|
+
function serializeCookie(name, value, options) {
|
|
1394
|
+
const isProd = isProductionEnvironment();
|
|
1395
|
+
const path = options?.path ?? "/";
|
|
1396
|
+
const httpOnly = options?.httpOnly ?? true;
|
|
1397
|
+
const secure = options?.secure ?? isProd;
|
|
1398
|
+
const sameSite = options?.sameSite ?? "lax";
|
|
1399
|
+
const parts = [`${encodeURIComponent(name)}=${encodeURIComponent(value)}`];
|
|
1400
|
+
if (path) {
|
|
1401
|
+
parts.push(`Path=${path}`);
|
|
1402
|
+
}
|
|
1403
|
+
if (typeof options?.maxAge === "number") {
|
|
1404
|
+
parts.push(`Max-Age=${Math.floor(options.maxAge)}`);
|
|
1405
|
+
const expires = new Date(Date.now() + options.maxAge * 1e3).toUTCString();
|
|
1406
|
+
parts.push(`Expires=${expires}`);
|
|
1407
|
+
}
|
|
1408
|
+
if (options?.domain) {
|
|
1409
|
+
parts.push(`Domain=${options.domain}`);
|
|
1410
|
+
}
|
|
1411
|
+
if (httpOnly) {
|
|
1412
|
+
parts.push("HttpOnly");
|
|
1413
|
+
}
|
|
1414
|
+
if (secure) {
|
|
1415
|
+
parts.push("Secure");
|
|
1416
|
+
}
|
|
1417
|
+
if (sameSite) {
|
|
1418
|
+
const capitalized = sameSite.charAt(0).toUpperCase() + sameSite.slice(1).toLowerCase();
|
|
1419
|
+
parts.push(`SameSite=${capitalized}`);
|
|
1420
|
+
}
|
|
1421
|
+
return parts.join("; ");
|
|
1422
|
+
}
|
|
1423
|
+
function createSessionCookie(sessionId, options) {
|
|
1424
|
+
const isProd = isProductionEnvironment();
|
|
1425
|
+
const resolvedOptions = {
|
|
1426
|
+
name: options?.name || DEFAULT_SESSION_COOKIE_NAME,
|
|
1427
|
+
path: options?.path ?? "/",
|
|
1428
|
+
maxAge: options?.maxAge ?? DEFAULT_SESSION_MAX_AGE_SECONDS,
|
|
1429
|
+
httpOnly: options?.httpOnly ?? true,
|
|
1430
|
+
secure: options?.secure ?? isProd,
|
|
1431
|
+
sameSite: options?.sameSite ?? "lax",
|
|
1432
|
+
domain: options?.domain
|
|
1433
|
+
};
|
|
1434
|
+
const name = resolvedOptions.name;
|
|
1435
|
+
const header = serializeCookie(name, sessionId, resolvedOptions);
|
|
1436
|
+
return {
|
|
1437
|
+
name,
|
|
1438
|
+
value: sessionId,
|
|
1439
|
+
options: resolvedOptions,
|
|
1440
|
+
header
|
|
1441
|
+
};
|
|
1442
|
+
}
|
|
1443
|
+
function clearSessionCookie(options) {
|
|
1444
|
+
const isProd = isProductionEnvironment();
|
|
1445
|
+
const resolvedOptions = {
|
|
1446
|
+
name: options?.name || DEFAULT_SESSION_COOKIE_NAME,
|
|
1447
|
+
path: options?.path ?? "/",
|
|
1448
|
+
maxAge: 0,
|
|
1449
|
+
httpOnly: options?.httpOnly ?? true,
|
|
1450
|
+
secure: options?.secure ?? isProd,
|
|
1451
|
+
sameSite: options?.sameSite ?? "lax",
|
|
1452
|
+
domain: options?.domain
|
|
1453
|
+
};
|
|
1454
|
+
const name = resolvedOptions.name;
|
|
1455
|
+
const header = serializeCookie(name, "", resolvedOptions);
|
|
1456
|
+
return {
|
|
1457
|
+
name,
|
|
1458
|
+
value: "",
|
|
1459
|
+
options: resolvedOptions,
|
|
1460
|
+
header
|
|
1461
|
+
};
|
|
1462
|
+
}
|
|
1463
|
+
function createStateCookie(state, options) {
|
|
1464
|
+
const isProd = isProductionEnvironment();
|
|
1465
|
+
const resolvedOptions = {
|
|
1466
|
+
name: options?.name || DEFAULT_STATE_COOKIE_NAME,
|
|
1467
|
+
path: options?.path ?? "/",
|
|
1468
|
+
maxAge: options?.maxAge ?? DEFAULT_STATE_MAX_AGE_SECONDS,
|
|
1469
|
+
httpOnly: options?.httpOnly ?? true,
|
|
1470
|
+
secure: options?.secure ?? isProd,
|
|
1471
|
+
sameSite: options?.sameSite ?? "lax",
|
|
1472
|
+
domain: options?.domain
|
|
1473
|
+
};
|
|
1474
|
+
const name = resolvedOptions.name;
|
|
1475
|
+
const header = serializeCookie(name, state, resolvedOptions);
|
|
1476
|
+
return {
|
|
1477
|
+
name,
|
|
1478
|
+
value: state,
|
|
1479
|
+
options: resolvedOptions,
|
|
1480
|
+
header
|
|
1481
|
+
};
|
|
1482
|
+
}
|
|
1483
|
+
function clearStateCookie(options) {
|
|
1484
|
+
const isProd = isProductionEnvironment();
|
|
1485
|
+
const resolvedOptions = {
|
|
1486
|
+
name: options?.name || DEFAULT_STATE_COOKIE_NAME,
|
|
1487
|
+
path: options?.path ?? "/",
|
|
1488
|
+
maxAge: 0,
|
|
1489
|
+
httpOnly: options?.httpOnly ?? true,
|
|
1490
|
+
secure: options?.secure ?? isProd,
|
|
1491
|
+
sameSite: options?.sameSite ?? "lax",
|
|
1492
|
+
domain: options?.domain
|
|
1493
|
+
};
|
|
1494
|
+
const name = resolvedOptions.name;
|
|
1495
|
+
const header = serializeCookie(name, "", resolvedOptions);
|
|
1496
|
+
return {
|
|
1497
|
+
name,
|
|
1498
|
+
value: "",
|
|
1499
|
+
options: resolvedOptions,
|
|
1500
|
+
header
|
|
1501
|
+
};
|
|
1502
|
+
}
|
|
1028
1503
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1029
1504
|
0 && (module.exports = {
|
|
1030
1505
|
AccountLinkingStrategy,
|
|
1506
|
+
AccountUnlinkError,
|
|
1507
|
+
DEFAULT_SESSION_COOKIE_NAME,
|
|
1508
|
+
DEFAULT_SESSION_MAX_AGE_SECONDS,
|
|
1509
|
+
DEFAULT_STATE_COOKIE_NAME,
|
|
1510
|
+
DEFAULT_STATE_MAX_AGE_SECONDS,
|
|
1511
|
+
EmailNotVerifiedError,
|
|
1512
|
+
InvalidOAuthCallbackError,
|
|
1513
|
+
InvalidProviderConfigError,
|
|
1514
|
+
InvalidStateError,
|
|
1031
1515
|
Lixa,
|
|
1516
|
+
LixaError,
|
|
1517
|
+
ProviderNotConfiguredError,
|
|
1518
|
+
RefreshTokenError,
|
|
1519
|
+
SessionNotFoundError,
|
|
1520
|
+
TokenExchangeError,
|
|
1521
|
+
clearSessionCookie,
|
|
1522
|
+
clearStateCookie,
|
|
1523
|
+
createSessionCookie,
|
|
1524
|
+
createStateCookie,
|
|
1032
1525
|
decodeIdToken,
|
|
1033
1526
|
determineProviderFromIssuer,
|
|
1034
1527
|
extractUserInfo,
|
|
1035
|
-
fetchUserInfo
|
|
1528
|
+
fetchUserInfo,
|
|
1529
|
+
isProductionEnvironment,
|
|
1530
|
+
serializeCookie
|
|
1036
1531
|
});
|
|
1037
1532
|
//# sourceMappingURL=index.cjs.map
|