@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.js
CHANGED
|
@@ -28,7 +28,7 @@ var LocalStateHandler = class {
|
|
|
28
28
|
}
|
|
29
29
|
};
|
|
30
30
|
}
|
|
31
|
-
// Default
|
|
31
|
+
// Default generateState implementation
|
|
32
32
|
async generateState(provider) {
|
|
33
33
|
const state = randomBytes(16).toString("hex");
|
|
34
34
|
const codeVerifier = randomBytes(32).toString("hex");
|
|
@@ -41,6 +41,10 @@ var LocalStateHandler = class {
|
|
|
41
41
|
}
|
|
42
42
|
};
|
|
43
43
|
}
|
|
44
|
+
// PascalCase alias for backward compatibility
|
|
45
|
+
async GenerateState(provider) {
|
|
46
|
+
return this.generateState(provider);
|
|
47
|
+
}
|
|
44
48
|
};
|
|
45
49
|
|
|
46
50
|
// src/lixa.ts
|
|
@@ -53,7 +57,20 @@ var LocalSessionHandler = class {
|
|
|
53
57
|
emailToSessionMap = /* @__PURE__ */ new Map();
|
|
54
58
|
sessionStorage;
|
|
55
59
|
constructor(defaultTtlSeconds = 600) {
|
|
56
|
-
this.cache = new NodeCache2({ stdTTL: defaultTtlSeconds });
|
|
60
|
+
this.cache = new NodeCache2({ stdTTL: defaultTtlSeconds, checkperiod: 60 });
|
|
61
|
+
this.cache.on("expired", (_key, value) => {
|
|
62
|
+
if (value && typeof value === "object" && value.email) {
|
|
63
|
+
this.emailToSessionMap.delete(String(value.email).toLowerCase());
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
this.cache.on("del", (_key, value) => {
|
|
67
|
+
if (value && typeof value === "object" && value.email) {
|
|
68
|
+
this.emailToSessionMap.delete(String(value.email).toLowerCase());
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
this.cache.on("flush", () => {
|
|
72
|
+
this.emailToSessionMap.clear();
|
|
73
|
+
});
|
|
57
74
|
this.sessionStorage = {
|
|
58
75
|
saveSession: async (sessionId, session, expiresInSeconds) => {
|
|
59
76
|
this.cache.set(sessionId, session, expiresInSeconds);
|
|
@@ -84,8 +101,8 @@ var LocalSessionHandler = class {
|
|
|
84
101
|
}
|
|
85
102
|
};
|
|
86
103
|
}
|
|
87
|
-
// Default
|
|
88
|
-
async
|
|
104
|
+
// Default generateSession implementation
|
|
105
|
+
async generateSession(tokenData, _providerMetadata) {
|
|
89
106
|
if (!tokenData.access_token || typeof tokenData.access_token !== "string") {
|
|
90
107
|
throw new Error("No valid access token found in OAuth response");
|
|
91
108
|
}
|
|
@@ -95,6 +112,10 @@ var LocalSessionHandler = class {
|
|
|
95
112
|
};
|
|
96
113
|
return session;
|
|
97
114
|
}
|
|
115
|
+
// PascalCase alias for backward compatibility
|
|
116
|
+
async GenerateSession(tokenData, providerMetadata) {
|
|
117
|
+
return this.generateSession(tokenData, providerMetadata);
|
|
118
|
+
}
|
|
98
119
|
};
|
|
99
120
|
|
|
100
121
|
// src/utils/user-info.ts
|
|
@@ -166,16 +187,95 @@ async function extractUserInfo(tokenData, providerMetadata) {
|
|
|
166
187
|
return { userInfo };
|
|
167
188
|
}
|
|
168
189
|
|
|
190
|
+
// src/errors.ts
|
|
191
|
+
var LixaError = class extends Error {
|
|
192
|
+
/**
|
|
193
|
+
* Standard error code string.
|
|
194
|
+
*/
|
|
195
|
+
code;
|
|
196
|
+
/**
|
|
197
|
+
* Additional error context data.
|
|
198
|
+
*/
|
|
199
|
+
details;
|
|
200
|
+
constructor(message, code = "LIXA_ERROR", details) {
|
|
201
|
+
super(message);
|
|
202
|
+
this.name = this.constructor.name;
|
|
203
|
+
this.code = code;
|
|
204
|
+
this.details = details;
|
|
205
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
var InvalidStateError = class extends LixaError {
|
|
209
|
+
constructor(message = "Invalid or expired state", details) {
|
|
210
|
+
super(message, "INVALID_STATE", details);
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
var ProviderNotConfiguredError = class extends LixaError {
|
|
214
|
+
constructor(provider, details) {
|
|
215
|
+
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: '...', ... }`;
|
|
216
|
+
super(message, "PROVIDER_NOT_CONFIGURED", {
|
|
217
|
+
provider,
|
|
218
|
+
...details
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
var InvalidProviderConfigError = class extends LixaError {
|
|
223
|
+
constructor(message, details) {
|
|
224
|
+
super(message, "INVALID_PROVIDER_CONFIG", details);
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
var InvalidOAuthCallbackError = class extends LixaError {
|
|
228
|
+
constructor(message, details) {
|
|
229
|
+
super(message, "INVALID_OAUTH_CALLBACK", details);
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
var TokenExchangeError = class extends LixaError {
|
|
233
|
+
status;
|
|
234
|
+
constructor(message, status, details) {
|
|
235
|
+
super(message, "TOKEN_EXCHANGE_FAILED", { status, ...details });
|
|
236
|
+
this.status = status;
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
var SessionNotFoundError = class extends LixaError {
|
|
240
|
+
constructor(message = "Active session not found or has expired", details) {
|
|
241
|
+
super(message, "SESSION_NOT_FOUND", details);
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
var EmailNotVerifiedError = class extends LixaError {
|
|
245
|
+
constructor(email, details) {
|
|
246
|
+
super(
|
|
247
|
+
`Cannot link account: email '${email || "unknown"}' is not verified by the identity provider`,
|
|
248
|
+
"EMAIL_NOT_VERIFIED",
|
|
249
|
+
{ email, ...details }
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
var AccountUnlinkError = class extends LixaError {
|
|
254
|
+
constructor(message, details) {
|
|
255
|
+
super(message, "ACCOUNT_UNLINK_ERROR", details);
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
var RefreshTokenError = class extends LixaError {
|
|
259
|
+
constructor(message, details) {
|
|
260
|
+
super(message, "REFRESH_TOKEN_ERROR", details);
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
|
|
169
264
|
// src/lixa.ts
|
|
170
265
|
var Lixa = class _Lixa {
|
|
171
266
|
static DEFAULT_PROVIDERS = /* @__PURE__ */ new Map();
|
|
172
267
|
static CONFIGURED_PROVIDERS = /* @__PURE__ */ new Map();
|
|
173
268
|
// Legacy registry for backward compatibility
|
|
174
|
-
|
|
175
|
-
|
|
269
|
+
// Instance-scoped fallback handlers to ensure no cross-instance state pollution
|
|
270
|
+
localStateHandler;
|
|
271
|
+
localSessionHandler;
|
|
272
|
+
localResourceHandler;
|
|
273
|
+
userResourceStore = /* @__PURE__ */ new Map();
|
|
274
|
+
refreshMutexes = /* @__PURE__ */ new Map();
|
|
176
275
|
config;
|
|
177
276
|
stateHandler;
|
|
178
277
|
sessionHandler;
|
|
278
|
+
resourceHandler;
|
|
179
279
|
debug;
|
|
180
280
|
/**
|
|
181
281
|
* Creates a new Lixa instance with the provided configuration.
|
|
@@ -192,9 +292,44 @@ var Lixa = class _Lixa {
|
|
|
192
292
|
*/
|
|
193
293
|
constructor(config) {
|
|
194
294
|
this.config = config;
|
|
195
|
-
this.stateHandler = config.stateHandler || _Lixa.LOCAL_STATE_HANDLER;
|
|
196
|
-
this.sessionHandler = config.sessionHandler || _Lixa.LOCAL_SESSION_HANDLER;
|
|
197
295
|
this.debug = config.debug || false;
|
|
296
|
+
this.localStateHandler = new LocalStateHandler();
|
|
297
|
+
this.localSessionHandler = new LocalSessionHandler();
|
|
298
|
+
this.localResourceHandler = {
|
|
299
|
+
resourceStorage: {
|
|
300
|
+
saveResource: async (userId, provider, resource) => {
|
|
301
|
+
let userMap = this.userResourceStore.get(userId);
|
|
302
|
+
if (!userMap) {
|
|
303
|
+
userMap = /* @__PURE__ */ new Map();
|
|
304
|
+
this.userResourceStore.set(userId, userMap);
|
|
305
|
+
}
|
|
306
|
+
userMap.set(provider.toLowerCase(), resource);
|
|
307
|
+
},
|
|
308
|
+
getResource: async (userId, provider) => {
|
|
309
|
+
const userMap = this.userResourceStore.get(userId);
|
|
310
|
+
return userMap?.get(provider.toLowerCase()) || null;
|
|
311
|
+
},
|
|
312
|
+
getUserResources: async (userId) => {
|
|
313
|
+
const userMap = this.userResourceStore.get(userId);
|
|
314
|
+
const result = {};
|
|
315
|
+
if (userMap) {
|
|
316
|
+
for (const [p, r] of userMap.entries()) {
|
|
317
|
+
result[p] = r;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return result;
|
|
321
|
+
},
|
|
322
|
+
deleteResource: async (userId, provider) => {
|
|
323
|
+
const userMap = this.userResourceStore.get(userId);
|
|
324
|
+
if (userMap) {
|
|
325
|
+
userMap.delete(provider.toLowerCase());
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
this.stateHandler = config.stateHandler || this.localStateHandler;
|
|
331
|
+
this.sessionHandler = config.sessionHandler || this.localSessionHandler;
|
|
332
|
+
this.resourceHandler = config.resourceHandler || this.localResourceHandler;
|
|
198
333
|
this.log("INFO", "Init", "Initializing Lixa instance", {
|
|
199
334
|
providers: Object.keys(config.providers),
|
|
200
335
|
debug: this.debug
|
|
@@ -209,8 +344,9 @@ var Lixa = class _Lixa {
|
|
|
209
344
|
} else {
|
|
210
345
|
if (!_Lixa.DEFAULT_PROVIDERS.has(name) && !_Lixa.CONFIGURED_PROVIDERS.has(name)) {
|
|
211
346
|
this.log("ERROR", "Init", `Provider '${providerName}' not available`);
|
|
212
|
-
throw new
|
|
213
|
-
|
|
347
|
+
throw new ProviderNotConfiguredError(
|
|
348
|
+
providerName,
|
|
349
|
+
{ hint: `Ensure the provider is registered via '@vunexa/lixa-providers' or provided inline.` }
|
|
214
350
|
);
|
|
215
351
|
}
|
|
216
352
|
this.log("INFO", "Init", `Using registered provider: ${providerName}`);
|
|
@@ -223,7 +359,7 @@ var Lixa = class _Lixa {
|
|
|
223
359
|
*
|
|
224
360
|
* @param name - The provider name
|
|
225
361
|
* @param config - The provider configuration
|
|
226
|
-
* @throws
|
|
362
|
+
* @throws InvalidProviderConfigError when required fields are missing or invalid
|
|
227
363
|
*/
|
|
228
364
|
validateProviderConfig(name, config) {
|
|
229
365
|
const requiredFields = ["clientId", "clientSecret", "redirectUri", "scopes"];
|
|
@@ -232,18 +368,21 @@ var Lixa = class _Lixa {
|
|
|
232
368
|
return value === void 0 || value === null || typeof value === "string" && value.trim() === "";
|
|
233
369
|
});
|
|
234
370
|
if (missingFields.length > 0) {
|
|
235
|
-
throw new
|
|
236
|
-
`Provider '${name}' configuration is missing required fields: ${missingFields.join(", ")}
|
|
371
|
+
throw new InvalidProviderConfigError(
|
|
372
|
+
`Provider '${name}' configuration is missing required fields: ${missingFields.join(", ")}`,
|
|
373
|
+
{ provider: name, missingFields }
|
|
237
374
|
);
|
|
238
375
|
}
|
|
239
376
|
if (!Array.isArray(config.scopes)) {
|
|
240
|
-
throw new
|
|
241
|
-
`Provider '${name}' configuration error: 'scopes' must be an array of strings
|
|
377
|
+
throw new InvalidProviderConfigError(
|
|
378
|
+
`Provider '${name}' configuration error: 'scopes' must be an array of strings`,
|
|
379
|
+
{ provider: name }
|
|
242
380
|
);
|
|
243
381
|
}
|
|
244
382
|
if (config.scopes.length === 0) {
|
|
245
|
-
throw new
|
|
246
|
-
`Provider '${name}' configuration error: 'scopes' array cannot be empty
|
|
383
|
+
throw new InvalidProviderConfigError(
|
|
384
|
+
`Provider '${name}' configuration error: 'scopes' array cannot be empty`,
|
|
385
|
+
{ provider: name }
|
|
247
386
|
);
|
|
248
387
|
}
|
|
249
388
|
}
|
|
@@ -252,7 +391,7 @@ var Lixa = class _Lixa {
|
|
|
252
391
|
*
|
|
253
392
|
* @param name - The provider name
|
|
254
393
|
* @param provider - The provider implementation
|
|
255
|
-
* @throws
|
|
394
|
+
* @throws InvalidProviderConfigError when required properties are missing
|
|
256
395
|
*/
|
|
257
396
|
validateProviderImplementation(name, provider) {
|
|
258
397
|
const requiredProps = ["authorizationEndpoint", "tokenEndpoint", "userInfoEndpoint"];
|
|
@@ -261,24 +400,27 @@ var Lixa = class _Lixa {
|
|
|
261
400
|
return !value || typeof value !== "string" || value.trim() === "";
|
|
262
401
|
});
|
|
263
402
|
if (missingProps.length > 0) {
|
|
264
|
-
throw new
|
|
265
|
-
`Provider '${name}' implementation is missing required properties: ${missingProps.join(", ")}. All IProvider implementations must define: authorizationEndpoint, tokenEndpoint, and userInfoEndpoint
|
|
403
|
+
throw new InvalidProviderConfigError(
|
|
404
|
+
`Provider '${name}' implementation is missing required properties: ${missingProps.join(", ")}. All IProvider implementations must define: authorizationEndpoint, tokenEndpoint, and userInfoEndpoint.`,
|
|
405
|
+
{ provider: name, missingProps }
|
|
266
406
|
);
|
|
267
407
|
}
|
|
268
408
|
}
|
|
269
409
|
/**
|
|
270
|
-
* Structured
|
|
410
|
+
* Structured logging with standardized format and custom logger support.
|
|
271
411
|
*
|
|
272
|
-
* @param level - Log level (INFO, WARN, ERROR)
|
|
273
|
-
* @param context - Context of the log (Init, Auth, Token, Session, State)
|
|
412
|
+
* @param level - Log level (INFO, WARN, ERROR, DEBUG)
|
|
413
|
+
* @param context - Context of the log (Init, Auth, Token, Session, State, AccountLinking, Resource)
|
|
274
414
|
* @param message - Log message
|
|
275
415
|
* @param data - Optional data to log
|
|
276
|
-
*
|
|
277
|
-
* @remarks
|
|
278
|
-
* Format: [Lixa] [timestamp] [level] [context] message
|
|
279
|
-
* Only logs when debug mode is enabled.
|
|
280
416
|
*/
|
|
281
417
|
log(level, context, message, data) {
|
|
418
|
+
if (this.config.logger) {
|
|
419
|
+
try {
|
|
420
|
+
this.config.logger.log(level, context, message, data);
|
|
421
|
+
} catch {
|
|
422
|
+
}
|
|
423
|
+
}
|
|
282
424
|
if (!this.debug) return;
|
|
283
425
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
284
426
|
const prefix = `[Lixa] [${timestamp}] [${level}] [${context}]`;
|
|
@@ -329,8 +471,9 @@ var Lixa = class _Lixa {
|
|
|
329
471
|
if (legacyProvider) {
|
|
330
472
|
return legacyProvider;
|
|
331
473
|
}
|
|
332
|
-
throw new
|
|
333
|
-
|
|
474
|
+
throw new ProviderNotConfiguredError(
|
|
475
|
+
name,
|
|
476
|
+
{ hint: `Ensure the provider is included in the configuration with a 'provider' field, or registered using Lixa.registerProvider().` }
|
|
334
477
|
);
|
|
335
478
|
}
|
|
336
479
|
/**
|
|
@@ -520,23 +663,26 @@ var Lixa = class _Lixa {
|
|
|
520
663
|
const providerConfig = this.findProviderByType(providerType);
|
|
521
664
|
if (!providerConfig) {
|
|
522
665
|
this.log("ERROR", "Auth", `Provider '${String(provider)}' is not configured`);
|
|
523
|
-
throw new
|
|
666
|
+
throw new ProviderNotConfiguredError(String(provider), {
|
|
667
|
+
message: `Provider '${String(provider)}' is not configured in this Lixa instance`
|
|
668
|
+
});
|
|
524
669
|
}
|
|
525
670
|
const providerImpl = this.getProvider(providerType, providerConfig);
|
|
526
671
|
let stateValue;
|
|
527
672
|
let codeVerifier;
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
673
|
+
const generateStateFn = this.stateHandler.generateState || this.stateHandler.GenerateState;
|
|
674
|
+
if (generateStateFn) {
|
|
675
|
+
this.log("INFO", "State", "Calling custom generateState");
|
|
676
|
+
const generated = await generateStateFn(providerType);
|
|
531
677
|
stateValue = state || generated.state;
|
|
532
678
|
codeVerifier = generated.data.codeVerifier;
|
|
533
|
-
const storage = this.stateHandler.stateStorage ||
|
|
679
|
+
const storage = this.stateHandler.stateStorage || this.localStateHandler.stateStorage;
|
|
534
680
|
await storage.saveState(stateValue, generated.data, 300);
|
|
535
681
|
} else {
|
|
536
682
|
this.log("INFO", "State", "Using default state generation");
|
|
537
683
|
stateValue = state || randomBytes2(16).toString("hex");
|
|
538
684
|
codeVerifier = randomBytes2(32).toString("hex");
|
|
539
|
-
const storage = this.stateHandler.stateStorage ||
|
|
685
|
+
const storage = this.stateHandler.stateStorage || this.localStateHandler.stateStorage;
|
|
540
686
|
await storage.saveState(
|
|
541
687
|
stateValue,
|
|
542
688
|
{
|
|
@@ -545,12 +691,17 @@ var Lixa = class _Lixa {
|
|
|
545
691
|
codeVerifier
|
|
546
692
|
},
|
|
547
693
|
300
|
|
548
|
-
// 5 minutes in seconds
|
|
694
|
+
// 5 minutes in seconds (synchronized with cookie TTL)
|
|
549
695
|
);
|
|
550
696
|
}
|
|
551
697
|
const codeChallenge = _Lixa.buildCodeChallenge(codeVerifier);
|
|
552
698
|
this.log("INFO", "State", `Saved state for provider: ${providerType}`, { state: stateValue });
|
|
553
|
-
const authNScopes = this.resolveAuthNScopes(
|
|
699
|
+
const authNScopes = this.resolveAuthNScopes(
|
|
700
|
+
providerType,
|
|
701
|
+
providerConfig.scopes,
|
|
702
|
+
providerImpl,
|
|
703
|
+
providerConfig.allowNonAuthScopes
|
|
704
|
+
);
|
|
554
705
|
const params = new URLSearchParams({
|
|
555
706
|
client_id: providerConfig.clientId,
|
|
556
707
|
redirect_uri: providerConfig.redirectUri,
|
|
@@ -569,9 +720,12 @@ var Lixa = class _Lixa {
|
|
|
569
720
|
return authUrl;
|
|
570
721
|
}
|
|
571
722
|
/**
|
|
572
|
-
* Restricts primary authentication scopes strictly to AuthN identity scopes.
|
|
723
|
+
* Restricts primary authentication scopes strictly to AuthN identity scopes unless allowNonAuthScopes is true.
|
|
573
724
|
*/
|
|
574
|
-
resolveAuthNScopes(providerType, configuredScopes, providerImpl) {
|
|
725
|
+
resolveAuthNScopes(providerType, configuredScopes, providerImpl, allowNonAuthScopes) {
|
|
726
|
+
if (allowNonAuthScopes) {
|
|
727
|
+
return configuredScopes && configuredScopes.length > 0 ? configuredScopes : providerImpl.authScopes || ["openid", "email", "profile"];
|
|
728
|
+
}
|
|
575
729
|
const defaultAuthScopes = {
|
|
576
730
|
google: ["openid", "email", "profile"],
|
|
577
731
|
github: ["read:user", "user:email"],
|
|
@@ -600,7 +754,7 @@ var Lixa = class _Lixa {
|
|
|
600
754
|
"Auth",
|
|
601
755
|
`Primary authentication is strictly limited to AuthN scopes. Excluded non-AuthN resource scopes: [${nonAuthNScopes.join(
|
|
602
756
|
", "
|
|
603
|
-
)}].
|
|
757
|
+
)}]. Set 'allowNonAuthScopes: true' on provider config to include them, or use lixa.getResourceAuthUrl() post-login to connect resource providers.`
|
|
604
758
|
);
|
|
605
759
|
}
|
|
606
760
|
if (validAuthNScopes.length > 0) {
|
|
@@ -636,18 +790,18 @@ var Lixa = class _Lixa {
|
|
|
636
790
|
this.log("INFO", "Auth", `Handling OAuth callback for provider: ${providerType}`);
|
|
637
791
|
if (!code || code.trim() === "") {
|
|
638
792
|
this.log("ERROR", "Auth", "Invalid or missing authorization code in callback");
|
|
639
|
-
throw new
|
|
793
|
+
throw new InvalidOAuthCallbackError("Invalid or missing code in callback");
|
|
640
794
|
}
|
|
641
795
|
if (!state || state.trim() === "") {
|
|
642
796
|
this.log("ERROR", "Auth", "Invalid or missing state in callback");
|
|
643
|
-
throw new
|
|
797
|
+
throw new InvalidOAuthCallbackError("Invalid or missing state in callback");
|
|
644
798
|
}
|
|
645
799
|
this.log("INFO", "State", "Validating state parameter", { state });
|
|
646
|
-
const stateStorage = this.stateHandler.stateStorage ||
|
|
800
|
+
const stateStorage = this.stateHandler.stateStorage || this.localStateHandler.stateStorage;
|
|
647
801
|
const cachedState = await stateStorage.getState(state);
|
|
648
802
|
if (!cachedState) {
|
|
649
803
|
this.log("ERROR", "State", "State validation failed: state not found or expired", { state });
|
|
650
|
-
throw new
|
|
804
|
+
throw new InvalidStateError("Invalid or expired state", { state });
|
|
651
805
|
}
|
|
652
806
|
this.log("INFO", "State", "State validated successfully, removing from cache");
|
|
653
807
|
await stateStorage.deleteState(state);
|
|
@@ -655,7 +809,9 @@ var Lixa = class _Lixa {
|
|
|
655
809
|
const providerConfig = this.findProviderByType(providerType);
|
|
656
810
|
if (!providerConfig) {
|
|
657
811
|
this.log("ERROR", "Auth", `Provider '${String(provider)}' is not configured`);
|
|
658
|
-
throw new
|
|
812
|
+
throw new ProviderNotConfiguredError(String(provider), {
|
|
813
|
+
message: `Provider '${String(provider)}' is not configured in this Lixa instance`
|
|
814
|
+
});
|
|
659
815
|
}
|
|
660
816
|
const providerImpl = this.getProvider(providerType, providerConfig);
|
|
661
817
|
this.log("INFO", "Token", `Exchanging authorization code for tokens`, { provider: providerType });
|
|
@@ -681,9 +837,12 @@ var Lixa = class _Lixa {
|
|
|
681
837
|
extractedUserInfo = userInfo;
|
|
682
838
|
} catch {
|
|
683
839
|
}
|
|
684
|
-
const generateSession = this.sessionHandler.generateSession ||
|
|
685
|
-
if (
|
|
686
|
-
|
|
840
|
+
const generateSession = this.sessionHandler.generateSession || this.sessionHandler.GenerateSession || (this.localSessionHandler.generateSession ? this.localSessionHandler.generateSession.bind(this.localSessionHandler) : void 0);
|
|
841
|
+
if (!generateSession) {
|
|
842
|
+
throw new Error("No session generation handler available");
|
|
843
|
+
}
|
|
844
|
+
if (this.sessionHandler.generateSession || this.sessionHandler.GenerateSession) {
|
|
845
|
+
this.log("INFO", "Session", "Calling custom generateSession");
|
|
687
846
|
} else {
|
|
688
847
|
this.log("INFO", "Session", "Using default session generation");
|
|
689
848
|
}
|
|
@@ -692,7 +851,7 @@ var Lixa = class _Lixa {
|
|
|
692
851
|
if (extractedUserInfo?.email && !session.email) {
|
|
693
852
|
session.email = extractedUserInfo.email;
|
|
694
853
|
}
|
|
695
|
-
const sessionStorage = this.sessionHandler.sessionStorage ||
|
|
854
|
+
const sessionStorage = this.sessionHandler.sessionStorage || this.localSessionHandler.sessionStorage;
|
|
696
855
|
const linkingConfig = this.config.accountLinking;
|
|
697
856
|
const mode = String(linkingConfig?.mode || "");
|
|
698
857
|
const isLinkByEmail = mode === "AUTO_LINK_BY_VERIFIED_EMAIL" /* AUTO_LINK_BY_VERIFIED_EMAIL */ || mode === "AUTO_LINK_BY_VERIFIED_EMAIL" || mode === "linkByEmail";
|
|
@@ -745,27 +904,31 @@ var Lixa = class _Lixa {
|
|
|
745
904
|
*/
|
|
746
905
|
async linkAccount(params) {
|
|
747
906
|
const { sessionId, provider, code, state } = params;
|
|
748
|
-
const sessionStorage = this.sessionHandler.sessionStorage ||
|
|
907
|
+
const sessionStorage = this.sessionHandler.sessionStorage || this.localSessionHandler.sessionStorage;
|
|
749
908
|
const existingSession = await sessionStorage.getSession(sessionId);
|
|
750
909
|
if (!existingSession) {
|
|
751
|
-
throw new
|
|
910
|
+
throw new SessionNotFoundError("Invalid session ID. User must be authenticated to link an account.", { sessionId });
|
|
911
|
+
}
|
|
912
|
+
if (!code || code.trim() === "") {
|
|
913
|
+
throw new InvalidOAuthCallbackError("Invalid or missing code in linkAccount");
|
|
752
914
|
}
|
|
753
915
|
const providerType = String(provider).toLowerCase();
|
|
754
916
|
const providerConfig = this.findProviderByType(providerType);
|
|
755
917
|
if (!providerConfig) {
|
|
756
|
-
throw new
|
|
918
|
+
throw new ProviderNotConfiguredError(String(provider));
|
|
757
919
|
}
|
|
758
920
|
const providerImpl = this.getProvider(providerType, providerConfig);
|
|
759
|
-
let codeVerifier
|
|
921
|
+
let codeVerifier;
|
|
760
922
|
if (state) {
|
|
761
|
-
const stateStorage = this.stateHandler.stateStorage ||
|
|
923
|
+
const stateStorage = this.stateHandler.stateStorage || this.localStateHandler.stateStorage;
|
|
762
924
|
const cachedState = await stateStorage.getState(state);
|
|
763
|
-
if (cachedState) {
|
|
764
|
-
|
|
765
|
-
await stateStorage.deleteState(state);
|
|
925
|
+
if (!cachedState) {
|
|
926
|
+
throw new InvalidStateError("Invalid or expired state during account linking", { state });
|
|
766
927
|
}
|
|
928
|
+
codeVerifier = cachedState.codeVerifier;
|
|
929
|
+
await stateStorage.deleteState(state);
|
|
767
930
|
}
|
|
768
|
-
const tokens = await this.exchangeCodeForToken(
|
|
931
|
+
const tokens = await this.exchangeCodeForToken(code, providerConfig, providerImpl, codeVerifier || "");
|
|
769
932
|
const providerMetadata = {
|
|
770
933
|
name: providerType,
|
|
771
934
|
endpoints: {
|
|
@@ -803,14 +966,17 @@ var Lixa = class _Lixa {
|
|
|
803
966
|
* @returns Promise resolving to true on successful unlink
|
|
804
967
|
*/
|
|
805
968
|
async unlinkAccount(sessionId, providerToUnlink) {
|
|
806
|
-
const sessionStorage = this.sessionHandler.sessionStorage ||
|
|
969
|
+
const sessionStorage = this.sessionHandler.sessionStorage || this.localSessionHandler.sessionStorage;
|
|
807
970
|
const session = await sessionStorage.getSession(sessionId);
|
|
808
971
|
if (!session || !session.accounts) {
|
|
809
|
-
throw new
|
|
972
|
+
throw new AccountUnlinkError("Session not found or has no linked accounts.", { sessionId });
|
|
810
973
|
}
|
|
811
974
|
const linkedProviders = Object.keys(session.accounts);
|
|
812
975
|
if (linkedProviders.length <= 1) {
|
|
813
|
-
throw new
|
|
976
|
+
throw new AccountUnlinkError("Cannot unlink the only authentication provider for this account.", {
|
|
977
|
+
sessionId,
|
|
978
|
+
provider: providerToUnlink
|
|
979
|
+
});
|
|
814
980
|
}
|
|
815
981
|
delete session.accounts[providerToUnlink.toLowerCase()];
|
|
816
982
|
await sessionStorage.saveSession(sessionId, session, 86400);
|
|
@@ -829,20 +995,20 @@ var Lixa = class _Lixa {
|
|
|
829
995
|
*/
|
|
830
996
|
async getResourceAuthUrl(params) {
|
|
831
997
|
const { sessionId, provider, scopes, state, prompt, extraConfig } = params;
|
|
832
|
-
const sessionStorage = this.sessionHandler.sessionStorage ||
|
|
998
|
+
const sessionStorage = this.sessionHandler.sessionStorage || this.localSessionHandler.sessionStorage;
|
|
833
999
|
const activeSession = await sessionStorage.getSession(sessionId);
|
|
834
1000
|
if (!activeSession) {
|
|
835
|
-
throw new
|
|
1001
|
+
throw new SessionNotFoundError("Authentication required. Active session must exist to connect resource providers.", { sessionId });
|
|
836
1002
|
}
|
|
837
1003
|
const providerType = String(provider).toLowerCase();
|
|
838
1004
|
const providerConfig = this.findProviderByType(providerType);
|
|
839
1005
|
if (!providerConfig) {
|
|
840
|
-
throw new
|
|
1006
|
+
throw new ProviderNotConfiguredError(String(provider));
|
|
841
1007
|
}
|
|
842
1008
|
const providerImpl = this.getProvider(providerType, providerConfig);
|
|
843
1009
|
const stateValue = state || randomBytes2(16).toString("hex");
|
|
844
1010
|
const codeVerifier = randomBytes2(32).toString("hex");
|
|
845
|
-
const storage = this.stateHandler.stateStorage ||
|
|
1011
|
+
const storage = this.stateHandler.stateStorage || this.localStateHandler.stateStorage;
|
|
846
1012
|
await storage.saveState(
|
|
847
1013
|
stateValue,
|
|
848
1014
|
{
|
|
@@ -867,77 +1033,245 @@ var Lixa = class _Lixa {
|
|
|
867
1033
|
});
|
|
868
1034
|
return `${providerImpl.authorizationEndpoint}?${searchParams.toString()}`;
|
|
869
1035
|
}
|
|
1036
|
+
getUserKeyFromSession(session) {
|
|
1037
|
+
return session.userId || session.email || session.id || "anonymous";
|
|
1038
|
+
}
|
|
870
1039
|
/**
|
|
871
|
-
* Handles the OAuth callback for a connected resource provider and stores resource tokens
|
|
872
|
-
*
|
|
873
|
-
* @param params - Object containing sessionId, provider, code, state, and requested scopes
|
|
874
|
-
* @returns Updated Session containing stored resource tokens under session.resources[provider]
|
|
1040
|
+
* Handles the OAuth callback for a connected resource provider and stores resource tokens bound to user account.
|
|
875
1041
|
*/
|
|
876
1042
|
async handleResourceCallback(params) {
|
|
877
1043
|
const { sessionId, provider, code, state, scopes } = params;
|
|
878
|
-
const sessionStorage = this.sessionHandler.sessionStorage ||
|
|
1044
|
+
const sessionStorage = this.sessionHandler.sessionStorage || this.localSessionHandler.sessionStorage;
|
|
879
1045
|
const activeSession = await sessionStorage.getSession(sessionId);
|
|
880
1046
|
if (!activeSession) {
|
|
881
|
-
throw new
|
|
1047
|
+
throw new SessionNotFoundError("Authentication required. Active session not found for resource connection.", { sessionId });
|
|
882
1048
|
}
|
|
883
1049
|
const providerType = String(provider).toLowerCase();
|
|
884
1050
|
const providerConfig = this.findProviderByType(providerType);
|
|
885
1051
|
if (!providerConfig) {
|
|
886
|
-
throw new
|
|
1052
|
+
throw new ProviderNotConfiguredError(String(provider));
|
|
887
1053
|
}
|
|
888
1054
|
const providerImpl = this.getProvider(providerType, providerConfig);
|
|
889
1055
|
let codeVerifier = randomBytes2(32).toString("hex");
|
|
890
1056
|
if (state) {
|
|
891
|
-
const stateStorage = this.stateHandler.stateStorage ||
|
|
1057
|
+
const stateStorage = this.stateHandler.stateStorage || this.localStateHandler.stateStorage;
|
|
892
1058
|
const cachedState = await stateStorage.getState(state);
|
|
893
|
-
if (cachedState) {
|
|
894
|
-
|
|
895
|
-
await stateStorage.deleteState(state);
|
|
1059
|
+
if (!cachedState) {
|
|
1060
|
+
throw new InvalidStateError("Invalid or expired state during resource connection callback", { state });
|
|
896
1061
|
}
|
|
1062
|
+
codeVerifier = cachedState.codeVerifier;
|
|
1063
|
+
await stateStorage.deleteState(state);
|
|
897
1064
|
}
|
|
898
1065
|
const tokens = await this.exchangeCodeForToken(code, providerConfig, providerImpl, codeVerifier);
|
|
899
|
-
|
|
900
|
-
|
|
1066
|
+
const expiresAt = tokens.expires_in ? Date.now() + tokens.expires_in * 1e3 : void 0;
|
|
1067
|
+
const connectedResource = {
|
|
901
1068
|
provider: providerType,
|
|
902
1069
|
accessToken: tokens.access_token,
|
|
903
1070
|
refreshToken: tokens.refresh_token,
|
|
1071
|
+
expiresAt,
|
|
904
1072
|
scopes: scopes || (tokens.scope ? tokens.scope.split(" ") : []),
|
|
905
1073
|
raw: tokens,
|
|
906
1074
|
connectedAt: Date.now()
|
|
907
1075
|
};
|
|
1076
|
+
const userKey = this.getUserKeyFromSession(activeSession);
|
|
1077
|
+
const resourceStorage = this.resourceHandler.resourceStorage || this.localResourceHandler.resourceStorage;
|
|
1078
|
+
await resourceStorage.saveResource(userKey, providerType, connectedResource);
|
|
1079
|
+
activeSession.resources = activeSession.resources || {};
|
|
1080
|
+
activeSession.resources[providerType] = connectedResource;
|
|
908
1081
|
await sessionStorage.saveSession(sessionId, activeSession, 86400);
|
|
909
1082
|
return activeSession;
|
|
910
1083
|
}
|
|
911
1084
|
/**
|
|
912
|
-
* Retrieves a connected resource
|
|
1085
|
+
* Retrieves a connected resource for a specific User ID / Email directly (independent of session IDs).
|
|
1086
|
+
* Automatically refreshes expired access tokens if a refresh token is present.
|
|
1087
|
+
*
|
|
1088
|
+
* @param userIdOrEmail - User identifier or email
|
|
1089
|
+
* @param provider - Resource provider identifier (e.g. 'github', 'google')
|
|
1090
|
+
*/
|
|
1091
|
+
async getUserResource(userIdOrEmail, provider) {
|
|
1092
|
+
const resourceStorage = this.resourceHandler.resourceStorage || this.localResourceHandler.resourceStorage;
|
|
1093
|
+
const providerType = provider.toLowerCase();
|
|
1094
|
+
const resource = await resourceStorage.getResource(userIdOrEmail, providerType);
|
|
1095
|
+
if (!resource) return null;
|
|
1096
|
+
if (resource.refreshToken && resource.expiresAt && Date.now() >= resource.expiresAt - 6e4) {
|
|
1097
|
+
this.log("INFO", "Token", `Resource access token for user '${userIdOrEmail}' on '${providerType}' is expired. Auto-refreshing...`);
|
|
1098
|
+
try {
|
|
1099
|
+
return await this.refreshUserResourceToken(userIdOrEmail, providerType);
|
|
1100
|
+
} catch (error) {
|
|
1101
|
+
this.log("ERROR", "Token", `Failed to auto-refresh resource token for user '${userIdOrEmail}' on '${providerType}'`, { error: String(error) });
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
return resource;
|
|
1105
|
+
}
|
|
1106
|
+
/**
|
|
1107
|
+
* Retrieves all connected resources for a specific User ID / Email.
|
|
1108
|
+
*/
|
|
1109
|
+
async getUserResources(userIdOrEmail) {
|
|
1110
|
+
const resourceStorage = this.resourceHandler.resourceStorage || this.localResourceHandler.resourceStorage;
|
|
1111
|
+
return await resourceStorage.getUserResources(userIdOrEmail);
|
|
1112
|
+
}
|
|
1113
|
+
/**
|
|
1114
|
+
* Retrieves a connected resource provider token for an active session, auto-refreshing expired tokens if possible.
|
|
913
1115
|
*
|
|
914
1116
|
* @param sessionId - Active session ID
|
|
915
|
-
* @param provider - Provider identifier (e.g. 'github')
|
|
1117
|
+
* @param provider - Provider identifier (e.g. 'github', 'google')
|
|
916
1118
|
*/
|
|
917
1119
|
async getConnectedResource(sessionId, provider) {
|
|
918
|
-
const sessionStorage = this.sessionHandler.sessionStorage ||
|
|
1120
|
+
const sessionStorage = this.sessionHandler.sessionStorage || this.localSessionHandler.sessionStorage;
|
|
919
1121
|
const activeSession = await sessionStorage.getSession(sessionId);
|
|
920
|
-
if (!activeSession
|
|
921
|
-
|
|
1122
|
+
if (!activeSession) return null;
|
|
1123
|
+
const providerType = provider.toLowerCase();
|
|
1124
|
+
const userKey = this.getUserKeyFromSession(activeSession);
|
|
1125
|
+
let resource = await this.getUserResource(userKey, providerType);
|
|
1126
|
+
if (!resource && activeSession.resources) {
|
|
1127
|
+
resource = activeSession.resources[providerType] || null;
|
|
1128
|
+
}
|
|
1129
|
+
if (resource) {
|
|
1130
|
+
if (resource.refreshToken && resource.expiresAt && Date.now() >= resource.expiresAt - 6e4) {
|
|
1131
|
+
this.log("INFO", "Token", `Resource access token for user '${userKey}' on '${providerType}' is expired. Auto-refreshing...`);
|
|
1132
|
+
try {
|
|
1133
|
+
resource = await this.refreshResourceToken(sessionId, providerType);
|
|
1134
|
+
} catch (error) {
|
|
1135
|
+
this.log("ERROR", "Token", `Failed to auto-refresh resource token for user '${userKey}' on '${providerType}'`, { error: String(error) });
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
activeSession.resources = activeSession.resources || {};
|
|
1139
|
+
activeSession.resources[providerType] = resource;
|
|
1140
|
+
}
|
|
1141
|
+
return resource;
|
|
922
1142
|
}
|
|
923
1143
|
/**
|
|
924
|
-
*
|
|
1144
|
+
* Refreshes a user's resource access token using its refresh token.
|
|
1145
|
+
* Deduplicates concurrent refresh requests via an in-flight promise mutex.
|
|
925
1146
|
*
|
|
926
|
-
* @param
|
|
927
|
-
* @param provider - Provider identifier
|
|
1147
|
+
* @param userIdOrEmail - User identifier or email
|
|
1148
|
+
* @param provider - Provider identifier (e.g. 'google', 'github')
|
|
928
1149
|
*/
|
|
929
|
-
async
|
|
930
|
-
const
|
|
1150
|
+
async refreshUserResourceToken(userIdOrEmail, provider, existingResource) {
|
|
1151
|
+
const mutexKey = `${userIdOrEmail.toLowerCase()}:${provider.toLowerCase()}`;
|
|
1152
|
+
const existingPromise = this.refreshMutexes.get(mutexKey);
|
|
1153
|
+
if (existingPromise) {
|
|
1154
|
+
this.log("INFO", "Token", `Concurrent refresh request detected for '${mutexKey}'. Reusing in-flight promise.`);
|
|
1155
|
+
return existingPromise;
|
|
1156
|
+
}
|
|
1157
|
+
const refreshPromise = this.executeRefreshUserResourceToken(userIdOrEmail, provider, existingResource).finally(() => {
|
|
1158
|
+
this.refreshMutexes.delete(mutexKey);
|
|
1159
|
+
});
|
|
1160
|
+
this.refreshMutexes.set(mutexKey, refreshPromise);
|
|
1161
|
+
return refreshPromise;
|
|
1162
|
+
}
|
|
1163
|
+
/**
|
|
1164
|
+
* Internal execution of refresh token exchange.
|
|
1165
|
+
*/
|
|
1166
|
+
async executeRefreshUserResourceToken(userIdOrEmail, provider, existingResource) {
|
|
1167
|
+
const resourceStorage = this.resourceHandler.resourceStorage || this.localResourceHandler.resourceStorage;
|
|
1168
|
+
const providerType = provider.toLowerCase();
|
|
1169
|
+
let resource = existingResource || await resourceStorage.getResource(userIdOrEmail, providerType);
|
|
1170
|
+
if (!resource || !resource.refreshToken) {
|
|
1171
|
+
throw new RefreshTokenError(
|
|
1172
|
+
`No refresh token available for user '${userIdOrEmail}' on connected resource '${provider}'`,
|
|
1173
|
+
{ userId: userIdOrEmail, provider }
|
|
1174
|
+
);
|
|
1175
|
+
}
|
|
1176
|
+
const providerConfig = this.findProviderByType(providerType);
|
|
1177
|
+
if (!providerConfig) {
|
|
1178
|
+
throw new ProviderNotConfiguredError(provider);
|
|
1179
|
+
}
|
|
1180
|
+
const providerImpl = this.getProvider(providerType, providerConfig);
|
|
1181
|
+
const body = {
|
|
1182
|
+
client_id: providerConfig.clientId,
|
|
1183
|
+
client_secret: providerConfig.clientSecret,
|
|
1184
|
+
grant_type: "refresh_token",
|
|
1185
|
+
refresh_token: resource.refreshToken
|
|
1186
|
+
};
|
|
1187
|
+
const response = await fetch(providerImpl.tokenEndpoint, {
|
|
1188
|
+
method: "POST",
|
|
1189
|
+
headers: {
|
|
1190
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
1191
|
+
Accept: "application/json"
|
|
1192
|
+
},
|
|
1193
|
+
body: new URLSearchParams(body).toString()
|
|
1194
|
+
});
|
|
1195
|
+
if (!response.ok) {
|
|
1196
|
+
const errorText = await response.text();
|
|
1197
|
+
throw new RefreshTokenError(
|
|
1198
|
+
`Failed to refresh resource token for user '${userIdOrEmail}' on '${provider}': ${response.status} - ${errorText}`,
|
|
1199
|
+
{ userId: userIdOrEmail, provider, status: response.status }
|
|
1200
|
+
);
|
|
1201
|
+
}
|
|
1202
|
+
const tokens = await response.json();
|
|
1203
|
+
const expiresAt = tokens.expires_in ? Date.now() + tokens.expires_in * 1e3 : void 0;
|
|
1204
|
+
resource.accessToken = tokens.access_token;
|
|
1205
|
+
if (tokens.refresh_token) {
|
|
1206
|
+
resource.refreshToken = tokens.refresh_token;
|
|
1207
|
+
}
|
|
1208
|
+
if (expiresAt) {
|
|
1209
|
+
resource.expiresAt = expiresAt;
|
|
1210
|
+
}
|
|
1211
|
+
resource.raw = tokens;
|
|
1212
|
+
resource.connectedAt = Date.now();
|
|
1213
|
+
await resourceStorage.saveResource(userIdOrEmail, providerType, resource);
|
|
1214
|
+
return resource;
|
|
1215
|
+
}
|
|
1216
|
+
/**
|
|
1217
|
+
* Refreshes a connected resource access token for an active session.
|
|
1218
|
+
*/
|
|
1219
|
+
async refreshResourceToken(sessionId, provider) {
|
|
1220
|
+
const sessionStorage = this.sessionHandler.sessionStorage || this.localSessionHandler.sessionStorage;
|
|
931
1221
|
const activeSession = await sessionStorage.getSession(sessionId);
|
|
932
|
-
if (!activeSession
|
|
933
|
-
|
|
1222
|
+
if (!activeSession) {
|
|
1223
|
+
throw new SessionNotFoundError("Active session not found.", { sessionId });
|
|
1224
|
+
}
|
|
1225
|
+
const providerType = provider.toLowerCase();
|
|
1226
|
+
const userKey = this.getUserKeyFromSession(activeSession);
|
|
1227
|
+
const existingResource = activeSession.resources ? activeSession.resources[providerType] : void 0;
|
|
1228
|
+
const refreshed = await this.refreshUserResourceToken(userKey, providerType, existingResource);
|
|
1229
|
+
activeSession.resources = activeSession.resources || {};
|
|
1230
|
+
activeSession.resources[providerType] = refreshed;
|
|
934
1231
|
await sessionStorage.saveSession(sessionId, activeSession, 86400);
|
|
1232
|
+
return refreshed;
|
|
1233
|
+
}
|
|
1234
|
+
/**
|
|
1235
|
+
* Disconnects a resource provider for a specific User ID / Email.
|
|
1236
|
+
*/
|
|
1237
|
+
async disconnectUserResource(userIdOrEmail, provider) {
|
|
1238
|
+
const resourceStorage = this.resourceHandler.resourceStorage || this.localResourceHandler.resourceStorage;
|
|
1239
|
+
await resourceStorage.deleteResource(userIdOrEmail, provider.toLowerCase());
|
|
935
1240
|
return true;
|
|
936
1241
|
}
|
|
1242
|
+
/**
|
|
1243
|
+
* Disconnects a resource provider from an active session and user account.
|
|
1244
|
+
*/
|
|
1245
|
+
async disconnectResource(sessionId, provider) {
|
|
1246
|
+
const sessionStorage = this.sessionHandler.sessionStorage || this.localSessionHandler.sessionStorage;
|
|
1247
|
+
const activeSession = await sessionStorage.getSession(sessionId);
|
|
1248
|
+
if (activeSession) {
|
|
1249
|
+
const userKey = this.getUserKeyFromSession(activeSession);
|
|
1250
|
+
await this.disconnectUserResource(userKey, provider);
|
|
1251
|
+
if (activeSession.resources) {
|
|
1252
|
+
delete activeSession.resources[provider.toLowerCase()];
|
|
1253
|
+
await sessionStorage.saveSession(sessionId, activeSession, 86400);
|
|
1254
|
+
}
|
|
1255
|
+
return true;
|
|
1256
|
+
}
|
|
1257
|
+
return false;
|
|
1258
|
+
}
|
|
1259
|
+
/**
|
|
1260
|
+
* Retrieves active session details from session storage.
|
|
1261
|
+
*/
|
|
937
1262
|
async fetchSessionInfo(sessionId) {
|
|
938
|
-
const sessionStorage = this.sessionHandler.sessionStorage ||
|
|
1263
|
+
const sessionStorage = this.sessionHandler.sessionStorage || this.localSessionHandler.sessionStorage;
|
|
939
1264
|
return await sessionStorage.getSession(sessionId);
|
|
940
1265
|
}
|
|
1266
|
+
/**
|
|
1267
|
+
* Deletes a session from session storage (e.g. on logout).
|
|
1268
|
+
*
|
|
1269
|
+
* @param sessionId - Active session identifier
|
|
1270
|
+
*/
|
|
1271
|
+
async deleteSession(sessionId) {
|
|
1272
|
+
const sessionStorage = this.sessionHandler.sessionStorage || this.localSessionHandler.sessionStorage;
|
|
1273
|
+
await sessionStorage.deleteSession(sessionId);
|
|
1274
|
+
}
|
|
941
1275
|
async exchangeCodeForToken(code, providerConfig, providerImpl, codeVerifier) {
|
|
942
1276
|
const body = {
|
|
943
1277
|
client_id: providerConfig.clientId,
|
|
@@ -968,8 +1302,10 @@ var Lixa = class _Lixa {
|
|
|
968
1302
|
statusText: response.statusText,
|
|
969
1303
|
error: errorBody
|
|
970
1304
|
});
|
|
971
|
-
throw new
|
|
972
|
-
`Token exchange failed: ${response.status} ${response.statusText} - ${errorBody}
|
|
1305
|
+
throw new TokenExchangeError(
|
|
1306
|
+
`Token exchange failed: ${response.status} ${response.statusText} - ${errorBody}`,
|
|
1307
|
+
response.status,
|
|
1308
|
+
{ statusText: response.statusText, errorBody }
|
|
973
1309
|
);
|
|
974
1310
|
}
|
|
975
1311
|
this.log("INFO", "Token", "Token exchange response received successfully");
|
|
@@ -984,12 +1320,151 @@ var Lixa = class _Lixa {
|
|
|
984
1320
|
return void 0;
|
|
985
1321
|
}
|
|
986
1322
|
};
|
|
1323
|
+
|
|
1324
|
+
// src/utils/cookies.ts
|
|
1325
|
+
var DEFAULT_SESSION_COOKIE_NAME = "lixa_session";
|
|
1326
|
+
var DEFAULT_STATE_COOKIE_NAME = "lixa_oauth_state";
|
|
1327
|
+
var DEFAULT_SESSION_MAX_AGE_SECONDS = 24 * 60 * 60;
|
|
1328
|
+
var DEFAULT_STATE_MAX_AGE_SECONDS = 5 * 60;
|
|
1329
|
+
function isProductionEnvironment() {
|
|
1330
|
+
return typeof process !== "undefined" && process.env?.NODE_ENV === "production";
|
|
1331
|
+
}
|
|
1332
|
+
function serializeCookie(name, value, options) {
|
|
1333
|
+
const isProd = isProductionEnvironment();
|
|
1334
|
+
const path = options?.path ?? "/";
|
|
1335
|
+
const httpOnly = options?.httpOnly ?? true;
|
|
1336
|
+
const secure = options?.secure ?? isProd;
|
|
1337
|
+
const sameSite = options?.sameSite ?? "lax";
|
|
1338
|
+
const parts = [`${encodeURIComponent(name)}=${encodeURIComponent(value)}`];
|
|
1339
|
+
if (path) {
|
|
1340
|
+
parts.push(`Path=${path}`);
|
|
1341
|
+
}
|
|
1342
|
+
if (typeof options?.maxAge === "number") {
|
|
1343
|
+
parts.push(`Max-Age=${Math.floor(options.maxAge)}`);
|
|
1344
|
+
const expires = new Date(Date.now() + options.maxAge * 1e3).toUTCString();
|
|
1345
|
+
parts.push(`Expires=${expires}`);
|
|
1346
|
+
}
|
|
1347
|
+
if (options?.domain) {
|
|
1348
|
+
parts.push(`Domain=${options.domain}`);
|
|
1349
|
+
}
|
|
1350
|
+
if (httpOnly) {
|
|
1351
|
+
parts.push("HttpOnly");
|
|
1352
|
+
}
|
|
1353
|
+
if (secure) {
|
|
1354
|
+
parts.push("Secure");
|
|
1355
|
+
}
|
|
1356
|
+
if (sameSite) {
|
|
1357
|
+
const capitalized = sameSite.charAt(0).toUpperCase() + sameSite.slice(1).toLowerCase();
|
|
1358
|
+
parts.push(`SameSite=${capitalized}`);
|
|
1359
|
+
}
|
|
1360
|
+
return parts.join("; ");
|
|
1361
|
+
}
|
|
1362
|
+
function createSessionCookie(sessionId, options) {
|
|
1363
|
+
const isProd = isProductionEnvironment();
|
|
1364
|
+
const resolvedOptions = {
|
|
1365
|
+
name: options?.name || DEFAULT_SESSION_COOKIE_NAME,
|
|
1366
|
+
path: options?.path ?? "/",
|
|
1367
|
+
maxAge: options?.maxAge ?? DEFAULT_SESSION_MAX_AGE_SECONDS,
|
|
1368
|
+
httpOnly: options?.httpOnly ?? true,
|
|
1369
|
+
secure: options?.secure ?? isProd,
|
|
1370
|
+
sameSite: options?.sameSite ?? "lax",
|
|
1371
|
+
domain: options?.domain
|
|
1372
|
+
};
|
|
1373
|
+
const name = resolvedOptions.name;
|
|
1374
|
+
const header = serializeCookie(name, sessionId, resolvedOptions);
|
|
1375
|
+
return {
|
|
1376
|
+
name,
|
|
1377
|
+
value: sessionId,
|
|
1378
|
+
options: resolvedOptions,
|
|
1379
|
+
header
|
|
1380
|
+
};
|
|
1381
|
+
}
|
|
1382
|
+
function clearSessionCookie(options) {
|
|
1383
|
+
const isProd = isProductionEnvironment();
|
|
1384
|
+
const resolvedOptions = {
|
|
1385
|
+
name: options?.name || DEFAULT_SESSION_COOKIE_NAME,
|
|
1386
|
+
path: options?.path ?? "/",
|
|
1387
|
+
maxAge: 0,
|
|
1388
|
+
httpOnly: options?.httpOnly ?? true,
|
|
1389
|
+
secure: options?.secure ?? isProd,
|
|
1390
|
+
sameSite: options?.sameSite ?? "lax",
|
|
1391
|
+
domain: options?.domain
|
|
1392
|
+
};
|
|
1393
|
+
const name = resolvedOptions.name;
|
|
1394
|
+
const header = serializeCookie(name, "", resolvedOptions);
|
|
1395
|
+
return {
|
|
1396
|
+
name,
|
|
1397
|
+
value: "",
|
|
1398
|
+
options: resolvedOptions,
|
|
1399
|
+
header
|
|
1400
|
+
};
|
|
1401
|
+
}
|
|
1402
|
+
function createStateCookie(state, options) {
|
|
1403
|
+
const isProd = isProductionEnvironment();
|
|
1404
|
+
const resolvedOptions = {
|
|
1405
|
+
name: options?.name || DEFAULT_STATE_COOKIE_NAME,
|
|
1406
|
+
path: options?.path ?? "/",
|
|
1407
|
+
maxAge: options?.maxAge ?? DEFAULT_STATE_MAX_AGE_SECONDS,
|
|
1408
|
+
httpOnly: options?.httpOnly ?? true,
|
|
1409
|
+
secure: options?.secure ?? isProd,
|
|
1410
|
+
sameSite: options?.sameSite ?? "lax",
|
|
1411
|
+
domain: options?.domain
|
|
1412
|
+
};
|
|
1413
|
+
const name = resolvedOptions.name;
|
|
1414
|
+
const header = serializeCookie(name, state, resolvedOptions);
|
|
1415
|
+
return {
|
|
1416
|
+
name,
|
|
1417
|
+
value: state,
|
|
1418
|
+
options: resolvedOptions,
|
|
1419
|
+
header
|
|
1420
|
+
};
|
|
1421
|
+
}
|
|
1422
|
+
function clearStateCookie(options) {
|
|
1423
|
+
const isProd = isProductionEnvironment();
|
|
1424
|
+
const resolvedOptions = {
|
|
1425
|
+
name: options?.name || DEFAULT_STATE_COOKIE_NAME,
|
|
1426
|
+
path: options?.path ?? "/",
|
|
1427
|
+
maxAge: 0,
|
|
1428
|
+
httpOnly: options?.httpOnly ?? true,
|
|
1429
|
+
secure: options?.secure ?? isProd,
|
|
1430
|
+
sameSite: options?.sameSite ?? "lax",
|
|
1431
|
+
domain: options?.domain
|
|
1432
|
+
};
|
|
1433
|
+
const name = resolvedOptions.name;
|
|
1434
|
+
const header = serializeCookie(name, "", resolvedOptions);
|
|
1435
|
+
return {
|
|
1436
|
+
name,
|
|
1437
|
+
value: "",
|
|
1438
|
+
options: resolvedOptions,
|
|
1439
|
+
header
|
|
1440
|
+
};
|
|
1441
|
+
}
|
|
987
1442
|
export {
|
|
988
1443
|
AccountLinkingStrategy,
|
|
1444
|
+
AccountUnlinkError,
|
|
1445
|
+
DEFAULT_SESSION_COOKIE_NAME,
|
|
1446
|
+
DEFAULT_SESSION_MAX_AGE_SECONDS,
|
|
1447
|
+
DEFAULT_STATE_COOKIE_NAME,
|
|
1448
|
+
DEFAULT_STATE_MAX_AGE_SECONDS,
|
|
1449
|
+
EmailNotVerifiedError,
|
|
1450
|
+
InvalidOAuthCallbackError,
|
|
1451
|
+
InvalidProviderConfigError,
|
|
1452
|
+
InvalidStateError,
|
|
989
1453
|
Lixa,
|
|
1454
|
+
LixaError,
|
|
1455
|
+
ProviderNotConfiguredError,
|
|
1456
|
+
RefreshTokenError,
|
|
1457
|
+
SessionNotFoundError,
|
|
1458
|
+
TokenExchangeError,
|
|
1459
|
+
clearSessionCookie,
|
|
1460
|
+
clearStateCookie,
|
|
1461
|
+
createSessionCookie,
|
|
1462
|
+
createStateCookie,
|
|
990
1463
|
decodeIdToken,
|
|
991
1464
|
determineProviderFromIssuer,
|
|
992
1465
|
extractUserInfo,
|
|
993
|
-
fetchUserInfo
|
|
1466
|
+
fetchUserInfo,
|
|
1467
|
+
isProductionEnvironment,
|
|
1468
|
+
serializeCookie
|
|
994
1469
|
};
|
|
995
1470
|
//# sourceMappingURL=index.js.map
|