feat(backend): use stateless OAuth2 state store (#6098)
This commit is contained in:
parent
5d855f95c9
commit
76329eaf31
7 changed files with 289 additions and 60 deletions
|
|
@ -56,7 +56,6 @@
|
||||||
"cookie-parser": "1.4.7",
|
"cookie-parser": "1.4.7",
|
||||||
"dotenv": "17.3.1",
|
"dotenv": "17.3.1",
|
||||||
"express": "5.2.1",
|
"express": "5.2.1",
|
||||||
"express-session": "1.19.0",
|
|
||||||
"fp-ts": "2.16.11",
|
"fp-ts": "2.16.11",
|
||||||
"graphql": "16.13.1",
|
"graphql": "16.13.1",
|
||||||
"graphql-query-complexity": "1.1.0",
|
"graphql-query-complexity": "1.1.0",
|
||||||
|
|
|
||||||
265
packages/hoppscotch-backend/src/auth/stateless-state-store.ts
Normal file
265
packages/hoppscotch-backend/src/auth/stateless-state-store.ts
Normal file
|
|
@ -0,0 +1,265 @@
|
||||||
|
import * as crypto from 'crypto';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module-level fallback secret for single-instance deployments
|
||||||
|
* where INFRA.SESSION_SECRET is not configured.
|
||||||
|
*/
|
||||||
|
const FALLBACK_SECRET = crypto.randomBytes(32).toString('hex');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum serialized payload size in bytes.
|
||||||
|
* Prevents oversized state parameters that could break provider redirects
|
||||||
|
* or exceed URL length limits.
|
||||||
|
*/
|
||||||
|
const MAX_PAYLOAD_BYTES = 2048;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default cookie name used to bind the OAuth state to a specific browser session.
|
||||||
|
* Prevents login CSRF by ensuring the callback can only be completed
|
||||||
|
* by the same browser that initiated the OAuth flow.
|
||||||
|
*/
|
||||||
|
const DEFAULT_COOKIE_NAME = '__oauth_nonce';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A stateless OAuth state store for passport strategies.
|
||||||
|
*
|
||||||
|
* Instead of storing state in express-session (MemoryStore), this encodes
|
||||||
|
* the state as a signed, time-limited token passed through the OAuth `state`
|
||||||
|
* query parameter. The token format is: `base64url(payload).base64url(hmac)`.
|
||||||
|
*
|
||||||
|
* CSRF protection: A random nonce is generated per OAuth flow, stored in a
|
||||||
|
* SameSite=Lax HttpOnly cookie, and mixed into the HMAC signature. On
|
||||||
|
* callback, the nonce from the cookie is used to verify the signature,
|
||||||
|
* ensuring the same browser that started the flow completes it.
|
||||||
|
*
|
||||||
|
* This eliminates all server-side session state from the OAuth flow, making
|
||||||
|
* it fully compatible with horizontal scaling (multiple backend instances
|
||||||
|
* behind a load balancer).
|
||||||
|
*
|
||||||
|
* Supports both passport-oauth2 (Google, GitHub, Microsoft) which dispatches
|
||||||
|
* by `.length`, and passport-openidconnect (OIDC) which always calls with 5 args.
|
||||||
|
*
|
||||||
|
* IMPORTANT: For multi-instance deployments, `INFRA.SESSION_SECRET` must be
|
||||||
|
* set to the same value across all instances.
|
||||||
|
*/
|
||||||
|
export class StatelessStateStore {
|
||||||
|
private readonly secret: string;
|
||||||
|
private readonly maxAgeMs: number;
|
||||||
|
private readonly cookieName: string;
|
||||||
|
private readonly secureCookies: boolean;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
secret?: string,
|
||||||
|
maxAgeMs: number = 600_000,
|
||||||
|
cookieName?: string,
|
||||||
|
secureCookies?: boolean,
|
||||||
|
) {
|
||||||
|
if (!secret) {
|
||||||
|
if (process.env.NODE_ENV === 'production') {
|
||||||
|
throw new Error(
|
||||||
|
'StatelessStateStore: INFRA.SESSION_SECRET must be set in production. Without a shared secret, OAuth callbacks will fail across load-balanced instances.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
console.warn(
|
||||||
|
'[StatelessStateStore] INFRA.SESSION_SECRET not set; using ephemeral fallback. OAuth will fail in any multi-instance deployment.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.secret = secret || FALLBACK_SECRET;
|
||||||
|
this.maxAgeMs = maxAgeMs;
|
||||||
|
this.cookieName = cookieName || DEFAULT_COOKIE_NAME;
|
||||||
|
this.secureCookies = secureCookies ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by passport before redirecting to the OAuth provider.
|
||||||
|
*
|
||||||
|
* passport-oauth2 dispatches by `.length`:
|
||||||
|
* arity 5: store(req, verifier, state, meta, cb) — PKCE flow
|
||||||
|
* arity 4: store(req, state, meta, cb) — standard flow
|
||||||
|
* arity 3: store(req, meta, cb) — no state
|
||||||
|
*
|
||||||
|
* passport-openidconnect always calls with 5 args:
|
||||||
|
* store(req, ctx, appState, meta, cb)
|
||||||
|
*
|
||||||
|
* We declare 5 params so `.length === 5` satisfies both libraries.
|
||||||
|
*
|
||||||
|
* The token payload stores both `ctx` (OIDC context / PKCE verifier) and
|
||||||
|
* `state` (the app state, e.g. { redirect_uri }) so that verify() can
|
||||||
|
* return them both correctly to each library's restore/loaded callback.
|
||||||
|
*/
|
||||||
|
store(
|
||||||
|
req: any,
|
||||||
|
ctxOrState: any,
|
||||||
|
stateOrMeta: any,
|
||||||
|
metaOrCb: any,
|
||||||
|
cb?: Function,
|
||||||
|
): void {
|
||||||
|
let ctx: any;
|
||||||
|
let state: any;
|
||||||
|
let callback: Function;
|
||||||
|
|
||||||
|
if (typeof cb === 'function') {
|
||||||
|
// 5-arg: (req, verifier/ctx, state, meta, cb)
|
||||||
|
// For passport-oauth2 PKCE: verifier is a string, state is the app state
|
||||||
|
// For passport-openidconnect: ctx has { maxAge, nonce, issued }, state is app state
|
||||||
|
ctx = ctxOrState;
|
||||||
|
state = stateOrMeta;
|
||||||
|
callback = cb;
|
||||||
|
} else if (typeof metaOrCb === 'function') {
|
||||||
|
// 4-arg: (req, state, meta, cb)
|
||||||
|
ctx = undefined;
|
||||||
|
state = ctxOrState;
|
||||||
|
callback = metaOrCb;
|
||||||
|
} else if (typeof stateOrMeta === 'function') {
|
||||||
|
// 3-arg: (req, meta, cb)
|
||||||
|
ctx = undefined;
|
||||||
|
state = {};
|
||||||
|
callback = stateOrMeta;
|
||||||
|
} else {
|
||||||
|
throw new Error('StatelessStateStore.store: invalid arguments');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Generate a browser-bound nonce for CSRF protection
|
||||||
|
const nonce = crypto.randomBytes(16).toString('hex');
|
||||||
|
|
||||||
|
const payload: any = {
|
||||||
|
state: state,
|
||||||
|
exp: Date.now() + this.maxAgeMs,
|
||||||
|
nonce: nonce,
|
||||||
|
};
|
||||||
|
// Store ctx only if defined (OIDC context or PKCE verifier)
|
||||||
|
if (ctx !== undefined && ctx !== null) {
|
||||||
|
payload.ctx = ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
const serialized = JSON.stringify(payload);
|
||||||
|
if (Buffer.byteLength(serialized) > MAX_PAYLOAD_BYTES) {
|
||||||
|
return callback(
|
||||||
|
new Error('OAuth state payload exceeds maximum allowed size'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const encoded = this.encode(payload);
|
||||||
|
|
||||||
|
// Set the nonce as a SameSite cookie to bind the OAuth flow to this browser
|
||||||
|
if (req.res) {
|
||||||
|
req.res.cookie(this.cookieName, nonce, {
|
||||||
|
httpOnly: true,
|
||||||
|
sameSite: 'lax',
|
||||||
|
secure: this.secureCookies,
|
||||||
|
maxAge: this.maxAgeMs,
|
||||||
|
path: '/',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(null, encoded);
|
||||||
|
} catch (err) {
|
||||||
|
callback(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by passport on the callback to verify the state parameter.
|
||||||
|
*
|
||||||
|
* passport-oauth2 dispatches by `.length`:
|
||||||
|
* arity 4: verify(req, state, meta, cb)
|
||||||
|
* arity 3: verify(req, state, cb)
|
||||||
|
*
|
||||||
|
* passport-openidconnect always calls with 3 args:
|
||||||
|
* verify(req, handle, cb) → restored(err, ctx, state)
|
||||||
|
*
|
||||||
|
* We declare 3 params (matching both) since passport-oauth2 will dispatch
|
||||||
|
* to arity 3 when `.length === 3`.
|
||||||
|
*
|
||||||
|
* Return semantics (second arg → third arg of cb):
|
||||||
|
* passport-oauth2: cb(null, ok:truthy|string, appState)
|
||||||
|
* passport-openidconnect: cb(null, ctx:{maxAge,nonce,issued}, appState)
|
||||||
|
*
|
||||||
|
* We return the stored ctx (or `true` when ctx was not stored) as the
|
||||||
|
* second arg, and the app state as the third arg. This satisfies both:
|
||||||
|
* - passport-oauth2 just needs truthy (or a PKCE verifier string)
|
||||||
|
* - passport-openidconnect needs the OIDC context object
|
||||||
|
*/
|
||||||
|
verify(req: any, providedState: string, cb: Function): void {
|
||||||
|
try {
|
||||||
|
// Read the browser-bound nonce from the cookie
|
||||||
|
const cookieNonce = req.cookies?.[this.cookieName];
|
||||||
|
|
||||||
|
// Clear the nonce cookie regardless of outcome
|
||||||
|
if (req.res) {
|
||||||
|
req.res.clearCookie(this.cookieName, { path: '/' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cookieNonce) {
|
||||||
|
return cb(null, false, { message: 'Missing OAuth nonce cookie' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = this.decode(providedState, cookieNonce);
|
||||||
|
if (!payload) {
|
||||||
|
return cb(null, false, { message: 'Invalid state signature' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payload.exp && Date.now() > payload.exp) {
|
||||||
|
return cb(null, false, { message: 'State has expired' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return ctx (OIDC context or PKCE verifier) as second arg,
|
||||||
|
// falling back to `true` for plain OAuth2 flows without ctx.
|
||||||
|
// Return the app state (e.g. { redirect_uri }) as third arg.
|
||||||
|
const ctx = payload.ctx !== undefined ? payload.ctx : true;
|
||||||
|
cb(null, ctx, payload.state);
|
||||||
|
} catch (err) {
|
||||||
|
cb(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode and verify a signed state token.
|
||||||
|
* Returns null if the token is missing, malformed, or the signature is invalid.
|
||||||
|
*/
|
||||||
|
private decode(token: string, nonce?: string): any | null {
|
||||||
|
try {
|
||||||
|
if (!token) return null;
|
||||||
|
|
||||||
|
const dotIndex = token.indexOf('.');
|
||||||
|
if (dotIndex === -1) return null;
|
||||||
|
|
||||||
|
const payloadStr = token.substring(0, dotIndex);
|
||||||
|
const signature = token.substring(dotIndex + 1);
|
||||||
|
|
||||||
|
const expectedSignature = this.sign(payloadStr, nonce);
|
||||||
|
|
||||||
|
if (
|
||||||
|
signature.length !== expectedSignature.length ||
|
||||||
|
!crypto.timingSafeEqual(
|
||||||
|
Buffer.from(signature),
|
||||||
|
Buffer.from(expectedSignature),
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.parse(Buffer.from(payloadStr, 'base64url').toString());
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private encode(payload: any): string {
|
||||||
|
const payloadStr = Buffer.from(JSON.stringify(payload)).toString(
|
||||||
|
'base64url',
|
||||||
|
);
|
||||||
|
const signature = this.sign(payloadStr, payload.nonce);
|
||||||
|
return `${payloadStr}.${signature}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private sign(data: string, nonce?: string): string {
|
||||||
|
const hmac = crypto.createHmac('sha256', this.secret);
|
||||||
|
hmac.update(data);
|
||||||
|
if (nonce) {
|
||||||
|
hmac.update(nonce);
|
||||||
|
}
|
||||||
|
return hmac.digest('base64url');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,6 +8,7 @@ import * as E from 'fp-ts/Either';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { validateEmail } from 'src/utils';
|
import { validateEmail } from 'src/utils';
|
||||||
import { AUTH_EMAIL_NOT_PROVIDED_BY_OAUTH } from 'src/errors';
|
import { AUTH_EMAIL_NOT_PROVIDED_BY_OAUTH } from 'src/errors';
|
||||||
|
import { StatelessStateStore } from '../stateless-state-store';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GithubStrategy extends PassportStrategy(Strategy) {
|
export class GithubStrategy extends PassportStrategy(Strategy) {
|
||||||
|
|
@ -21,7 +22,13 @@ export class GithubStrategy extends PassportStrategy(Strategy) {
|
||||||
clientSecret: configService.get<string>('INFRA.GITHUB_CLIENT_SECRET'),
|
clientSecret: configService.get<string>('INFRA.GITHUB_CLIENT_SECRET'),
|
||||||
callbackURL: configService.get<string>('INFRA.GITHUB_CALLBACK_URL'),
|
callbackURL: configService.get<string>('INFRA.GITHUB_CALLBACK_URL'),
|
||||||
scope: [configService.get<string>('INFRA.GITHUB_SCOPE')],
|
scope: [configService.get<string>('INFRA.GITHUB_SCOPE')],
|
||||||
store: true,
|
store: new StatelessStateStore(
|
||||||
|
configService.get<string>('INFRA.SESSION_SECRET'),
|
||||||
|
undefined,
|
||||||
|
(configService.get<string>('INFRA.SESSION_COOKIE_NAME') ||
|
||||||
|
'__oauth_nonce') + '_github',
|
||||||
|
configService.get<string>('INFRA.ALLOW_SECURE_COOKIES') === 'true',
|
||||||
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import { ConfigService } from '@nestjs/config';
|
||||||
import { Request } from 'express';
|
import { Request } from 'express';
|
||||||
import { validateEmail } from 'src/utils';
|
import { validateEmail } from 'src/utils';
|
||||||
import { AUTH_EMAIL_NOT_PROVIDED_BY_OAUTH } from 'src/errors';
|
import { AUTH_EMAIL_NOT_PROVIDED_BY_OAUTH } from 'src/errors';
|
||||||
|
import { StatelessStateStore } from '../stateless-state-store';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GoogleStrategy extends PassportStrategy(Strategy) {
|
export class GoogleStrategy extends PassportStrategy(Strategy) {
|
||||||
|
|
@ -23,7 +24,13 @@ export class GoogleStrategy extends PassportStrategy(Strategy) {
|
||||||
callbackURL: configService.get<string>('INFRA.GOOGLE_CALLBACK_URL'),
|
callbackURL: configService.get<string>('INFRA.GOOGLE_CALLBACK_URL'),
|
||||||
scope: configService.get<string>('INFRA.GOOGLE_SCOPE').split(','),
|
scope: configService.get<string>('INFRA.GOOGLE_SCOPE').split(','),
|
||||||
passReqToCallback: true,
|
passReqToCallback: true,
|
||||||
store: true,
|
store: new StatelessStateStore(
|
||||||
|
configService.get<string>('INFRA.SESSION_SECRET'),
|
||||||
|
undefined,
|
||||||
|
(configService.get<string>('INFRA.SESSION_COOKIE_NAME') ||
|
||||||
|
'__oauth_nonce') + '_google',
|
||||||
|
configService.get<string>('INFRA.ALLOW_SECURE_COOKIES') === 'true',
|
||||||
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import * as E from 'fp-ts/Either';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { validateEmail } from 'src/utils';
|
import { validateEmail } from 'src/utils';
|
||||||
import { AUTH_EMAIL_NOT_PROVIDED_BY_OAUTH } from 'src/errors';
|
import { AUTH_EMAIL_NOT_PROVIDED_BY_OAUTH } from 'src/errors';
|
||||||
|
import { StatelessStateStore } from '../stateless-state-store';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MicrosoftStrategy extends PassportStrategy(Strategy) {
|
export class MicrosoftStrategy extends PassportStrategy(Strategy) {
|
||||||
|
|
@ -22,7 +23,13 @@ export class MicrosoftStrategy extends PassportStrategy(Strategy) {
|
||||||
callbackURL: configService.get<string>('INFRA.MICROSOFT_CALLBACK_URL'),
|
callbackURL: configService.get<string>('INFRA.MICROSOFT_CALLBACK_URL'),
|
||||||
scope: configService.get<string>('INFRA.MICROSOFT_SCOPE').split(','),
|
scope: configService.get<string>('INFRA.MICROSOFT_SCOPE').split(','),
|
||||||
tenant: configService.get<string>('INFRA.MICROSOFT_TENANT'),
|
tenant: configService.get<string>('INFRA.MICROSOFT_TENANT'),
|
||||||
store: true,
|
store: new StatelessStateStore(
|
||||||
|
configService.get<string>('INFRA.SESSION_SECRET'),
|
||||||
|
undefined,
|
||||||
|
(configService.get<string>('INFRA.SESSION_COOKIE_NAME') ||
|
||||||
|
'__oauth_nonce') + '_microsoft',
|
||||||
|
configService.get<string>('INFRA.ALLOW_SECURE_COOKIES') === 'true',
|
||||||
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,7 @@ import { json } from 'express';
|
||||||
import { AppModule } from './app.module';
|
import { AppModule } from './app.module';
|
||||||
import cookieParser from 'cookie-parser';
|
import cookieParser from 'cookie-parser';
|
||||||
import { ValidationPipe, VersioningType } from '@nestjs/common';
|
import { ValidationPipe, VersioningType } from '@nestjs/common';
|
||||||
import session from 'express-session';
|
|
||||||
import { emitGQLSchemaFile } from './gql-schema';
|
import { emitGQLSchemaFile } from './gql-schema';
|
||||||
import * as crypto from 'crypto';
|
|
||||||
import morgan from 'morgan';
|
import morgan from 'morgan';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
||||||
|
|
@ -50,21 +48,6 @@ async function bootstrap() {
|
||||||
console.log(`Running in production: ${isProduction}`);
|
console.log(`Running in production: ${isProduction}`);
|
||||||
console.log(`Port: ${configService.get('PORT')}`);
|
console.log(`Port: ${configService.get('PORT')}`);
|
||||||
|
|
||||||
app.use(
|
|
||||||
session({
|
|
||||||
// Allow overriding the default cookie name 'connect.sid' (which contains a dot).
|
|
||||||
// Some proxies/load balancers (like older Kong versions) cannot hash cookie names with dots,
|
|
||||||
// so we allow setting an alternative name via the INFRA.SESSION_COOKIE_NAME configuration.
|
|
||||||
name:
|
|
||||||
configService.get<string>('INFRA.SESSION_COOKIE_NAME') || 'connect.sid',
|
|
||||||
secret:
|
|
||||||
configService.get<string>('INFRA.SESSION_SECRET') ||
|
|
||||||
crypto.randomBytes(16).toString('hex'),
|
|
||||||
resave: false,
|
|
||||||
saveUninitialized: false,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Increase file upload limit to 100MB
|
// Increase file upload limit to 100MB
|
||||||
app.use(
|
app.use(
|
||||||
json({
|
json({
|
||||||
|
|
|
||||||
|
|
@ -258,9 +258,6 @@ importers:
|
||||||
express:
|
express:
|
||||||
specifier: 5.2.1
|
specifier: 5.2.1
|
||||||
version: 5.2.1
|
version: 5.2.1
|
||||||
express-session:
|
|
||||||
specifier: 1.19.0
|
|
||||||
version: 1.19.0
|
|
||||||
fp-ts:
|
fp-ts:
|
||||||
specifier: 2.16.11
|
specifier: 2.16.11
|
||||||
version: 2.16.11
|
version: 2.16.11
|
||||||
|
|
@ -6818,9 +6815,6 @@ packages:
|
||||||
cookie-signature@1.0.6:
|
cookie-signature@1.0.6:
|
||||||
resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==}
|
resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==}
|
||||||
|
|
||||||
cookie-signature@1.0.7:
|
|
||||||
resolution: {integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==}
|
|
||||||
|
|
||||||
cookie-signature@1.2.2:
|
cookie-signature@1.2.2:
|
||||||
resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==}
|
resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==}
|
||||||
engines: {node: '>=6.6.0'}
|
engines: {node: '>=6.6.0'}
|
||||||
|
|
@ -7761,10 +7755,6 @@ packages:
|
||||||
resolution: {integrity: sha512-1zQrciTiQfRdo7qJM1uG4navm8DayFa2TgCSRlzUyNkhcJ6XUZF3hjnpkyr3VhAqPH7i/9GkG7Tv5abz6fqz0Q==}
|
resolution: {integrity: sha512-1zQrciTiQfRdo7qJM1uG4navm8DayFa2TgCSRlzUyNkhcJ6XUZF3hjnpkyr3VhAqPH7i/9GkG7Tv5abz6fqz0Q==}
|
||||||
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
|
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
|
||||||
|
|
||||||
express-session@1.19.0:
|
|
||||||
resolution: {integrity: sha512-0csaMkGq+vaiZTmSMMGkfdCOabYv192VbytFypcvI0MANrp+4i/7yEkJ0sbAEhycQjntaKGzYfjfXQyVb7BHMA==}
|
|
||||||
engines: {node: '>= 0.8.0'}
|
|
||||||
|
|
||||||
express@5.2.1:
|
express@5.2.1:
|
||||||
resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==}
|
resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==}
|
||||||
engines: {node: '>= 18'}
|
engines: {node: '>= 18'}
|
||||||
|
|
@ -10716,10 +10706,6 @@ packages:
|
||||||
ramda@0.27.2:
|
ramda@0.27.2:
|
||||||
resolution: {integrity: sha512-SbiLPU40JuJniHexQSAgad32hfwd+DRUdwF2PlVuI5RZD0/vahUco7R8vD86J/tcEKKF9vZrUVwgtmGCqlCKyA==}
|
resolution: {integrity: sha512-SbiLPU40JuJniHexQSAgad32hfwd+DRUdwF2PlVuI5RZD0/vahUco7R8vD86J/tcEKKF9vZrUVwgtmGCqlCKyA==}
|
||||||
|
|
||||||
random-bytes@1.0.0:
|
|
||||||
resolution: {integrity: sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==}
|
|
||||||
engines: {node: '>= 0.8'}
|
|
||||||
|
|
||||||
randombytes@2.1.0:
|
randombytes@2.1.0:
|
||||||
resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==}
|
resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==}
|
||||||
|
|
||||||
|
|
@ -11838,10 +11824,6 @@ packages:
|
||||||
engines: {node: '>=0.8.0'}
|
engines: {node: '>=0.8.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
uid-safe@2.1.5:
|
|
||||||
resolution: {integrity: sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==}
|
|
||||||
engines: {node: '>= 0.8'}
|
|
||||||
|
|
||||||
uid2@0.0.4:
|
uid2@0.0.4:
|
||||||
resolution: {integrity: sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==}
|
resolution: {integrity: sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==}
|
||||||
|
|
||||||
|
|
@ -18833,8 +18815,6 @@ snapshots:
|
||||||
|
|
||||||
cookie-signature@1.0.6: {}
|
cookie-signature@1.0.6: {}
|
||||||
|
|
||||||
cookie-signature@1.0.7: {}
|
|
||||||
|
|
||||||
cookie-signature@1.2.2: {}
|
cookie-signature@1.2.2: {}
|
||||||
|
|
||||||
cookie@0.7.2: {}
|
cookie@0.7.2: {}
|
||||||
|
|
@ -19938,19 +19918,6 @@ snapshots:
|
||||||
jest-mock: 30.3.0
|
jest-mock: 30.3.0
|
||||||
jest-util: 30.3.0
|
jest-util: 30.3.0
|
||||||
|
|
||||||
express-session@1.19.0:
|
|
||||||
dependencies:
|
|
||||||
cookie: 0.7.2
|
|
||||||
cookie-signature: 1.0.7
|
|
||||||
debug: 2.6.9
|
|
||||||
depd: 2.0.0
|
|
||||||
on-headers: 1.1.0
|
|
||||||
parseurl: 1.3.3
|
|
||||||
safe-buffer: 5.2.1
|
|
||||||
uid-safe: 2.1.5
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- supports-color
|
|
||||||
|
|
||||||
express@5.2.1:
|
express@5.2.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
accepts: 2.0.0
|
accepts: 2.0.0
|
||||||
|
|
@ -23647,8 +23614,6 @@ snapshots:
|
||||||
|
|
||||||
ramda@0.27.2: {}
|
ramda@0.27.2: {}
|
||||||
|
|
||||||
random-bytes@1.0.0: {}
|
|
||||||
|
|
||||||
randombytes@2.1.0:
|
randombytes@2.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
safe-buffer: 5.2.1
|
safe-buffer: 5.2.1
|
||||||
|
|
@ -25016,10 +24981,6 @@ snapshots:
|
||||||
uglify-js@3.19.3:
|
uglify-js@3.19.3:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
uid-safe@2.1.5:
|
|
||||||
dependencies:
|
|
||||||
random-bytes: 1.0.0
|
|
||||||
|
|
||||||
uid2@0.0.4: {}
|
uid2@0.0.4: {}
|
||||||
|
|
||||||
uid@2.0.2:
|
uid@2.0.2:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue