diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..cf53c22
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,4 @@
+PORT=3001
+JWT_SECRET=replace-with-a-long-random-string-at-least-32-chars
+DATABASE_PATH=./stagepass.db
+NODE_ENV=development
diff --git a/packages/server/package.json b/packages/server/package.json
index 4caac47..172498a 100644
--- a/packages/server/package.json
+++ b/packages/server/package.json
@@ -4,21 +4,31 @@
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc",
- "seed": "tsx src/seed.ts"
+ "seed": "tsx src/seed.ts",
+ "test": "vitest run",
+ "test:coverage": "vitest run --coverage"
},
"dependencies": {
"@stagepass/common": "workspace:*",
+ "bcryptjs": "^3.0.3",
"better-sqlite3": "^11.0.0",
"cors": "^2.8.5",
+ "dotenv": "^17.3.1",
"express": "^4.18.2",
- "jsonwebtoken": "^9.0.2"
+ "express-rate-limit": "^8.2.1",
+ "jsonwebtoken": "^9.0.2",
+ "zod": "^4.3.6"
},
"devDependencies": {
+ "@types/bcryptjs": "^3.0.0",
"@types/better-sqlite3": "^7.6.8",
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/jsonwebtoken": "^9.0.5",
+ "@types/supertest": "^6.0.3",
+ "supertest": "^7.2.2",
"tsx": "^4.7.0",
- "typescript": "^5.3.3"
+ "typescript": "^5.3.3",
+ "vitest": "^4.0.18"
}
}
diff --git a/packages/server/src/app.ts b/packages/server/src/app.ts
new file mode 100644
index 0000000..ee506de
--- /dev/null
+++ b/packages/server/src/app.ts
@@ -0,0 +1,21 @@
+import 'dotenv/config';
+import express from 'express';
+import cors from 'cors';
+import moviesRouter from './routes/movies';
+import showtimesRouter from './routes/showtimes';
+import seatsRouter from './routes/seats';
+import bookingsRouter from './routes/bookings';
+import authRouter from './routes/auth';
+
+const app = express();
+
+app.use(cors());
+app.use(express.json());
+
+app.use('/api/movies', moviesRouter);
+app.use('/api/showtimes', showtimesRouter);
+app.use('/api/seats', seatsRouter);
+app.use('/api/bookings', bookingsRouter);
+app.use('/api/auth', authRouter);
+
+export default app;
diff --git a/packages/server/src/auth.integration.test.ts b/packages/server/src/auth.integration.test.ts
new file mode 100644
index 0000000..c01fa1d
--- /dev/null
+++ b/packages/server/src/auth.integration.test.ts
@@ -0,0 +1,75 @@
+/**
+ * Integration tests — full signup → login → protected route flow.
+ * Uses an in-memory SQLite database so no file I/O is needed.
+ */
+import { describe, it, expect, vi } from 'vitest';
+import request from 'supertest';
+
+// Set JWT_SECRET before any module loads
+process.env.JWT_SECRET = 'integration-test-secret';
+process.env.NODE_ENV = 'test';
+
+// Create an in-memory db with the full schema before anything imports the real db
+vi.mock('./db', async () => {
+ const Database = (await import('better-sqlite3')).default;
+ const db = new Database(':memory:');
+ db.exec(`
+ CREATE TABLE IF NOT EXISTS users (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ name TEXT NOT NULL,
+ email TEXT UNIQUE NOT NULL,
+ password TEXT NOT NULL
+ );
+ `);
+ return { default: db };
+});
+
+// Import app AFTER the mock is in place
+const { default: app } = await import('./app');
+
+describe('auth integration — signup → login → protected route', () => {
+ const testUser = {
+ name: 'Test User',
+ email: 'test@example.com',
+ // eslint-disable-next-line sonarjs/no-hardcoded-passwords
+ password: 'password123',
+ };
+
+ it('POST /api/auth/signup creates a user and returns a JWT', async () => {
+ const res = await request(app).post('/api/auth/signup').send(testUser);
+ expect(res.status).toBe(201);
+ expect(res.body.token).toBeTruthy();
+ expect(res.body.user.email).toBe(testUser.email);
+ });
+
+ it('POST /api/auth/login succeeds with correct credentials', async () => {
+ const res = await request(app).post('/api/auth/login').send({
+ email: testUser.email,
+ password: testUser.password,
+ });
+ expect(res.status).toBe(200);
+ expect(res.body.token).toBeTruthy();
+ });
+
+ it('POST /api/auth/login returns 401 with wrong password', async () => {
+ const res = await request(app).post('/api/auth/login').send({
+ email: testUser.email,
+ password: 'wrongpassword', // eslint-disable-line sonarjs/no-hardcoded-passwords
+ });
+ expect(res.status).toBe(401);
+ });
+
+ it('stored password is hashed — not the plain text value', async () => {
+ // Access the mocked db
+ const { default: db } = await import('./db');
+ const user = db.prepare('SELECT password FROM users WHERE email = ?').get(testUser.email) as { password: string };
+ expect(user.password).not.toBe(testUser.password);
+ expect(user.password).toMatch(/^\$2[ab]\$/); // bcrypt hash prefix
+ });
+
+ it('POST /api/auth/signup returns 400 for duplicate email', async () => {
+ const res = await request(app).post('/api/auth/signup').send(testUser);
+ expect(res.status).toBe(400);
+ expect(res.body.error).toMatch(/email already exists/i);
+ });
+});
diff --git a/packages/server/src/auth.test.ts b/packages/server/src/auth.test.ts
new file mode 100644
index 0000000..89ffdaf
--- /dev/null
+++ b/packages/server/src/auth.test.ts
@@ -0,0 +1,72 @@
+/**
+ * Unit tests for auth middleware (authenticateToken).
+ * The db and bcrypt are mocked so these run without a real database.
+ */
+import { describe, it, expect, vi } from 'vitest';
+import { Request, Response, NextFunction } from 'express';
+import jwt from 'jsonwebtoken';
+
+process.env.JWT_SECRET = 'unit-test-secret';
+process.env.NODE_ENV = 'test';
+
+vi.mock('./db', () => ({
+ default: {
+ prepare: vi.fn().mockReturnValue({ get: vi.fn(), run: vi.fn() }),
+ },
+}));
+
+const { authenticateToken } = await import('./routes/auth');
+
+function makeReqRes() {
+ const req = { headers: {} } as unknown as Request & { userId?: number };
+ const json = vi.fn();
+ const status = vi.fn().mockReturnValue({ json });
+ const res = { status, json } as unknown as Response;
+ const next = vi.fn() as NextFunction;
+ return { req, res, status, json, next };
+}
+
+describe('authenticateToken middleware', () => {
+ it('calls next() with userId set when token is valid', () => {
+ const token = jwt.sign({ userId: 42 }, 'unit-test-secret', { expiresIn: '1h' });
+ const { req, res, next } = makeReqRes();
+ req.headers['authorization'] = `Bearer ${token}`;
+
+ authenticateToken(req, res, next);
+
+ expect(next).toHaveBeenCalledOnce();
+ expect(req.userId).toBe(42);
+ });
+
+ it('returns 401 when no token is provided', () => {
+ const { req, res, next, status, json } = makeReqRes();
+
+ authenticateToken(req, res, next);
+
+ expect(status).toHaveBeenCalledWith(401);
+ expect(json).toHaveBeenCalledWith({ error: 'No token provided' });
+ expect(next).not.toHaveBeenCalled();
+ });
+
+ it('returns 403 when token is invalid', () => {
+ const { req, res, next, status, json } = makeReqRes();
+ req.headers['authorization'] = 'Bearer bad.token.here';
+
+ authenticateToken(req, res, next);
+
+ expect(status).toHaveBeenCalledWith(403);
+ expect(json).toHaveBeenCalledWith({ error: 'Invalid token' });
+ expect(next).not.toHaveBeenCalled();
+ });
+
+ it('returns 403 when token is signed with a different secret', () => {
+ const token = jwt.sign({ userId: 1 }, 'wrong-secret');
+ const { req, res, next, status } = makeReqRes();
+ req.headers['authorization'] = `Bearer ${token}`;
+
+ authenticateToken(req, res, next);
+
+ expect(status).toHaveBeenCalledWith(403);
+ expect(next).not.toHaveBeenCalled();
+ });
+});
diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts
index c3ecf5b..72a0352 100644
--- a/packages/server/src/index.ts
+++ b/packages/server/src/index.ts
@@ -1,22 +1,6 @@
-import express from 'express';
-import cors from 'cors';
-import moviesRouter from './routes/movies';
-import showtimesRouter from './routes/showtimes';
-import seatsRouter from './routes/seats';
-import bookingsRouter from './routes/bookings';
-import authRouter from './routes/auth';
+import app from './app';
-const app = express();
-const PORT = 3001;
-
-app.use(cors());
-app.use(express.json());
-
-app.use('/api/movies', moviesRouter);
-app.use('/api/showtimes', showtimesRouter);
-app.use('/api/seats', seatsRouter);
-app.use('/api/bookings', bookingsRouter);
-app.use('/api/auth', authRouter);
+const PORT = process.env.PORT ?? 3001;
app.listen(PORT, () => {
console.log(`StagePass server running on http://localhost:${PORT}`);
diff --git a/packages/server/src/routes/auth.ts b/packages/server/src/routes/auth.ts
index d5e84da..ec16638 100644
--- a/packages/server/src/routes/auth.ts
+++ b/packages/server/src/routes/auth.ts
@@ -1,37 +1,100 @@
import { Router, Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
+import bcrypt from 'bcryptjs';
+import rateLimit from 'express-rate-limit';
+import { z } from 'zod';
import db from '../db';
const router = Router();
-const JWT_SECRET = 'stagepass-secret-key-not-secure';
+
+const authLimiter = rateLimit({
+ windowMs: 15 * 60 * 1000, // 15 minutes
+ max: 20,
+ message: { error: 'Too many requests, please try again later' },
+ standardHeaders: true,
+ legacyHeaders: false,
+});
+
+router.use(authLimiter);
+
+const loginSchema = z.object({
+ // eslint-disable-next-line sonarjs/deprecation
+ email: z.string().email(),
+ password: z.string().min(1),
+});
+
+const signupSchema = z.object({
+ name: z.string().min(1),
+ // eslint-disable-next-line sonarjs/deprecation
+ email: z.string().email(),
+ password: z.string().min(8),
+});
+
+interface UserRow {
+ id: number;
+ name: string;
+ email: string;
+ password: string;
+}
+
+interface JwtPayload {
+ userId: number;
+}
+
+function getJwtSecret(): string {
+ const secret = process.env.JWT_SECRET;
+ if (!secret) throw new Error('JWT_SECRET environment variable is not set');
+ return secret;
+}
+
+// A dummy hash used when user is not found to prevent timing attacks.
+// bcrypt.compare will still run, making the response time consistent.
+const DUMMY_HASH = '$2b$10$abcdefghijklmnopqrstuvuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuu';
// POST login
-router.post('/login', (req: Request, res: Response) => {
- const { email, password } = req.body;
+router.post('/login', async (req: Request, res: Response) => {
+ const parsed = loginSchema.safeParse(req.body);
+ if (!parsed.success) {
+ return res.status(400).json({ errors: parsed.error.issues.map((i) => i.message) });
+ }
+ const { email, password } = parsed.data;
+
+ const user = db.prepare('SELECT * FROM users WHERE email = ?').get(email) as UserRow | undefined;
- const user = db.prepare('SELECT * FROM users WHERE email = ? AND password = ?').get(email, password) as any;
+ // Always compare to prevent timing attacks (constant-time response)
+ const hashToCompare = user?.password ?? DUMMY_HASH;
+ const passwordMatch = await bcrypt.compare(password, hashToCompare);
- if (!user) {
+ if (!user || !passwordMatch) {
return res.status(401).json({ error: 'Invalid credentials' });
}
- const token = jwt.sign({ userId: user.id }, JWT_SECRET, { expiresIn: '24h' });
+ const token = jwt.sign({ userId: user.id }, getJwtSecret(), { expiresIn: '24h' });
res.json({ token, user: { id: user.id, name: user.name, email: user.email } });
});
// POST signup
-router.post('/signup', (req: Request, res: Response) => {
- const { name, email, password } = req.body;
+router.post('/signup', async (req: Request, res: Response) => {
+ const parsed = signupSchema.safeParse(req.body);
+ if (!parsed.success) {
+ return res.status(400).json({ errors: parsed.error.issues.map((i) => i.message) });
+ }
+ const { name, email, password } = parsed.data;
try {
- const result = db.prepare('INSERT INTO users (name, email, password) VALUES (?, ?, ?)').run(name, email, password);
- const token = jwt.sign({ userId: result.lastInsertRowid }, JWT_SECRET, { expiresIn: '24h' });
+ const hashed = await bcrypt.hash(password, 10);
+ const result = db
+ .prepare('INSERT INTO users (name, email, password) VALUES (?, ?, ?)')
+ .run(name, email, hashed);
+ const token = jwt.sign({ userId: result.lastInsertRowid }, getJwtSecret(), {
+ expiresIn: '24h',
+ });
res.status(201).json({
token,
- user: { id: result.lastInsertRowid, name, email }
+ user: { id: result.lastInsertRowid, name, email },
});
- } catch (e: any) {
- if (e.message.includes('UNIQUE')) {
+ } catch (e: unknown) {
+ if (e instanceof Error && e.message.includes('UNIQUE')) {
return res.status(400).json({ error: 'Email already exists' });
}
return res.status(500).json({ error: 'Something went wrong' });
@@ -39,14 +102,18 @@ router.post('/signup', (req: Request, res: Response) => {
});
// Middleware
-export function authenticateToken(req: any, res: Response, next: NextFunction) {
+export function authenticateToken(
+ req: Request & { userId?: number },
+ res: Response,
+ next: NextFunction
+) {
const authHeader = req.headers['authorization'];
- const token = authHeader && authHeader.split(' ')[1];
+ const token = authHeader?.split(' ')[1];
if (!token) return res.status(401).json({ error: 'No token provided' });
try {
- const decoded = jwt.verify(token, JWT_SECRET) as any;
+ const decoded = jwt.verify(token, getJwtSecret()) as JwtPayload;
req.userId = decoded.userId;
next();
} catch {
diff --git a/packages/server/src/seed.ts b/packages/server/src/seed.ts
index e9168c4..163c36d 100644
--- a/packages/server/src/seed.ts
+++ b/packages/server/src/seed.ts
@@ -1,5 +1,12 @@
+import 'dotenv/config';
+import bcrypt from 'bcryptjs';
import db from './db';
+if (process.env.NODE_ENV === 'production') {
+ console.error('ERROR: seed.ts must not be run against a production database.');
+ process.exit(1);
+}
+
// Clear existing data
db.exec('DELETE FROM bookings');
db.exec('DELETE FROM seats');
@@ -8,11 +15,12 @@ db.exec('DELETE FROM movies');
db.exec('DELETE FROM users');
db.exec("DELETE FROM sqlite_sequence WHERE name IN ('bookings','seats','showtimes','movies','users')");
-// Seed users
+// Seed users — passwords are hashed (bcrypt, cost 10)
const insertUser = db.prepare('INSERT INTO users (name, email, password) VALUES (?, ?, ?)');
-insertUser.run('John Doe', 'john@example.com', 'password123');
-insertUser.run('Jane Smith', 'jane@example.com', 'password123');
-insertUser.run('Raj Patel', 'raj@example.com', 'password123');
+const seedPassword = bcrypt.hashSync('password123', 10);
+insertUser.run('John Doe', 'john@example.com', seedPassword);
+insertUser.run('Jane Smith', 'jane@example.com', seedPassword);
+insertUser.run('Raj Patel', 'raj@example.com', seedPassword);
// Seed movies - real movies with TMDB poster URLs
const insertMovie = db.prepare(`
diff --git a/packages/web/src/AuthContext.test.tsx b/packages/web/src/AuthContext.test.tsx
new file mode 100644
index 0000000..4c33d3d
--- /dev/null
+++ b/packages/web/src/AuthContext.test.tsx
@@ -0,0 +1,73 @@
+import { render, screen, act, waitFor } from '@testing-library/react';
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { AuthProvider, useAuth } from './AuthContext';
+
+// Simple consumer component
+function TestConsumer() {
+ const { user, token, login, logout } = useAuth();
+ return (
+
+ {user?.name ?? 'none'}
+ {token ?? 'none'}
+
+
+
+ );
+}
+
+function renderProvider() {
+ render(
+
+
+
+ );
+}
+
+beforeEach(() => {
+ localStorage.clear();
+ vi.restoreAllMocks();
+});
+
+describe('AuthContext', () => {
+ it('login stores token and user in localStorage', async () => {
+ global.fetch = vi.fn().mockResolvedValue({
+ ok: true,
+ json: () => Promise.resolve({ token: 'tok123', user: { id: 1, name: 'Alice', email: 'a@b.com' } }),
+ });
+
+ renderProvider();
+ screen.getByText('Login').click();
+
+ await waitFor(() => expect(screen.getByTestId('token').textContent).toBe('tok123'));
+ expect(localStorage.getItem('stagepass_token')).toBe('tok123');
+ expect(JSON.parse(localStorage.getItem('stagepass_user')!).name).toBe('Alice');
+ });
+
+ it('logout clears token and user from localStorage', async () => {
+ localStorage.setItem('stagepass_token', 'existing-tok');
+ localStorage.setItem('stagepass_user', JSON.stringify({ id: 1, name: 'Alice', email: 'a@b.com' }));
+
+ renderProvider();
+ await waitFor(() => expect(screen.getByTestId('token').textContent).toBe('existing-tok'));
+
+ act(() => {
+ screen.getByText('Logout').click();
+ });
+
+ await waitFor(() => expect(screen.getByTestId('token').textContent).toBe('none'));
+ expect(localStorage.getItem('stagepass_token')).toBeNull();
+ expect(localStorage.getItem('stagepass_user')).toBeNull();
+ });
+
+ it('rehydrates user from localStorage on mount', async () => {
+ localStorage.setItem('stagepass_token', 'saved-token');
+ localStorage.setItem('stagepass_user', JSON.stringify({ id: 2, name: 'Bob', email: 'b@c.com' }));
+
+ renderProvider();
+
+ await waitFor(() => {
+ expect(screen.getByTestId('user').textContent).toBe('Bob');
+ expect(screen.getByTestId('token').textContent).toBe('saved-token');
+ });
+ });
+});
diff --git a/packages/web/src/AuthContext.tsx b/packages/web/src/AuthContext.tsx
index 681c19a..3cf5b1e 100644
--- a/packages/web/src/AuthContext.tsx
+++ b/packages/web/src/AuthContext.tsx
@@ -1,4 +1,4 @@
-import React, { createContext, useContext, useState } from 'react';
+import React, { createContext, useContext, useState, useEffect } from 'react';
interface User {
id: number;
@@ -16,10 +16,28 @@ interface AuthContextType {
const AuthContext = createContext(null!);
+const STORAGE_KEY_TOKEN = 'stagepass_token';
+const STORAGE_KEY_USER = 'stagepass_user';
+
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState(null);
const [token, setToken] = useState(null);
+ // Rehydrate from localStorage on mount
+ useEffect(() => {
+ const storedToken = localStorage.getItem(STORAGE_KEY_TOKEN);
+ const storedUser = localStorage.getItem(STORAGE_KEY_USER);
+ if (storedToken && storedUser) {
+ try {
+ setToken(storedToken);
+ setUser(JSON.parse(storedUser) as User);
+ } catch {
+ localStorage.removeItem(STORAGE_KEY_TOKEN);
+ localStorage.removeItem(STORAGE_KEY_USER);
+ }
+ }
+ }, []);
+
const login = async (email: string, password: string) => {
const res = await fetch('/api/auth/login', {
method: 'POST',
@@ -27,7 +45,9 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
body: JSON.stringify({ email, password })
});
if (!res.ok) throw new Error('Login failed');
- const data = await res.json();
+ const data = await res.json() as { token: string; user: User };
+ localStorage.setItem(STORAGE_KEY_TOKEN, data.token);
+ localStorage.setItem(STORAGE_KEY_USER, JSON.stringify(data.user));
setUser(data.user);
setToken(data.token);
};
@@ -39,12 +59,16 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
body: JSON.stringify({ name, email, password })
});
if (!res.ok) throw new Error('Signup failed');
- const data = await res.json();
+ const data = await res.json() as { token: string; user: User };
+ localStorage.setItem(STORAGE_KEY_TOKEN, data.token);
+ localStorage.setItem(STORAGE_KEY_USER, JSON.stringify(data.user));
setUser(data.user);
setToken(data.token);
};
const logout = () => {
+ localStorage.removeItem(STORAGE_KEY_TOKEN);
+ localStorage.removeItem(STORAGE_KEY_USER);
setUser(null);
setToken(null);
};
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index feb17a3..a37d0cb 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -47,19 +47,34 @@ importers:
'@stagepass/common':
specifier: workspace:*
version: link:../common
+ bcryptjs:
+ specifier: ^3.0.3
+ version: 3.0.3
better-sqlite3:
specifier: ^11.0.0
version: 11.10.0
cors:
specifier: ^2.8.5
version: 2.8.6
+ dotenv:
+ specifier: ^17.3.1
+ version: 17.3.1
express:
specifier: ^4.18.2
version: 4.22.1
+ express-rate-limit:
+ specifier: ^8.2.1
+ version: 8.2.1(express@4.22.1)
jsonwebtoken:
specifier: ^9.0.2
version: 9.0.3
+ zod:
+ specifier: ^4.3.6
+ version: 4.3.6
devDependencies:
+ '@types/bcryptjs':
+ specifier: ^3.0.0
+ version: 3.0.0
'@types/better-sqlite3':
specifier: ^7.6.8
version: 7.6.13
@@ -72,12 +87,21 @@ importers:
'@types/jsonwebtoken':
specifier: ^9.0.5
version: 9.0.10
+ '@types/supertest':
+ specifier: ^6.0.3
+ version: 6.0.3
+ supertest:
+ specifier: ^7.2.2
+ version: 7.2.2
tsx:
specifier: ^4.7.0
version: 4.21.0
typescript:
specifier: ^5.3.3
version: 5.9.3
+ vitest:
+ specifier: ^4.0.18
+ version: 4.0.18(@types/node@25.2.3)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@1.8.0))(tsx@4.21.0)
packages/web:
dependencies:
@@ -114,13 +138,13 @@ importers:
version: 4.7.0(vite@5.4.21(@types/node@25.2.3))
'@vitest/coverage-v8':
specifier: ^4.0.18
- version: 4.0.18(vitest@4.0.18(@types/node@25.2.3)(jiti@1.21.7)(jsdom@28.1.0)(tsx@4.21.0))
+ version: 4.0.18(vitest@4.0.18(@types/node@25.2.3)(jiti@1.21.7)(jsdom@28.1.0(@noble/hashes@1.8.0))(tsx@4.21.0))
autoprefixer:
specifier: ^10.4.17
version: 10.4.24(postcss@8.5.6)
jsdom:
specifier: ^28.1.0
- version: 28.1.0
+ version: 28.1.0(@noble/hashes@1.8.0)
postcss:
specifier: ^8.4.35
version: 8.5.6
@@ -135,7 +159,7 @@ importers:
version: 5.4.21(@types/node@25.2.3)
vitest:
specifier: ^4.0.18
- version: 4.0.18(@types/node@25.2.3)(jiti@1.21.7)(jsdom@28.1.0)(tsx@4.21.0)
+ version: 4.0.18(@types/node@25.2.3)(jiti@1.21.7)(jsdom@28.1.0(@noble/hashes@1.8.0))(tsx@4.21.0)
packages:
@@ -734,6 +758,10 @@ packages:
'@jridgewell/trace-mapping@0.3.31':
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
+ '@noble/hashes@1.8.0':
+ resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==}
+ engines: {node: ^14.21.3 || >=16}
+
'@nodelib/fs.scandir@2.1.5':
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'}
@@ -746,6 +774,9 @@ packages:
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
engines: {node: '>= 8'}
+ '@paralleldrive/cuid2@2.3.1':
+ resolution: {integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==}
+
'@remix-run/router@1.23.2':
resolution: {integrity: sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==}
engines: {node: '>=14.0.0'}
@@ -938,6 +969,10 @@ packages:
'@types/babel__traverse@7.28.0':
resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==}
+ '@types/bcryptjs@3.0.0':
+ resolution: {integrity: sha512-WRZOuCuaz8UcZZE4R5HXTco2goQSI2XxjGY3hbM/xDvwmqFWd4ivooImsMx65OKM6CtNKbnZ5YL+YwAwK7c1dg==}
+ deprecated: This is a stub types definition. bcryptjs provides its own type definitions, so you do not need this installed.
+
'@types/better-sqlite3@7.6.13':
resolution: {integrity: sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==}
@@ -950,6 +985,9 @@ packages:
'@types/connect@3.4.38':
resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
+ '@types/cookiejar@2.1.5':
+ resolution: {integrity: sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==}
+
'@types/cors@2.8.19':
resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==}
@@ -974,6 +1012,9 @@ packages:
'@types/jsonwebtoken@9.0.10':
resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==}
+ '@types/methods@1.1.4':
+ resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==}
+
'@types/mime@1.3.5':
resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==}
@@ -1009,6 +1050,12 @@ packages:
'@types/serve-static@1.15.10':
resolution: {integrity: sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==}
+ '@types/superagent@8.1.9':
+ resolution: {integrity: sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==}
+
+ '@types/supertest@6.0.3':
+ resolution: {integrity: sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w==}
+
'@typescript-eslint/eslint-plugin@8.56.0':
resolution: {integrity: sha512-lRyPDLzNCuae71A3t9NEINBiTn7swyOhvUj3MyUOxb8x6g6vPEFoOU+ZRmGMusNC3X3YMhqMIX7i8ShqhT74Pw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -1174,6 +1221,9 @@ packages:
array-ify@1.0.0:
resolution: {integrity: sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==}
+ asap@2.0.6:
+ resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==}
+
assertion-error@2.0.1:
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
engines: {node: '>=12'}
@@ -1181,6 +1231,9 @@ packages:
ast-v8-to-istanbul@0.3.11:
resolution: {integrity: sha512-Qya9fkoofMjCBNVdWINMjB5KZvkYfaO9/anwkWnjxibpWUxo5iHl2sOdP7/uAqaRuUYuoo8rDwnbaaKVFxoUvw==}
+ asynckit@0.4.0:
+ resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
+
autoprefixer@10.4.24:
resolution: {integrity: sha512-uHZg7N9ULTVbutaIsDRoUkoS8/h3bdsmVJYZ5l3wv8Cp/6UIIoRDm90hZ+BwxUj/hGBEzLxdHNSKuFpn8WOyZw==}
engines: {node: ^10 || ^12 || >=14}
@@ -1198,6 +1251,10 @@ packages:
resolution: {integrity: sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==}
hasBin: true
+ bcryptjs@3.0.3:
+ resolution: {integrity: sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==}
+ hasBin: true
+
better-sqlite3@11.10.0:
resolution: {integrity: sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==}
@@ -1292,6 +1349,10 @@ packages:
color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
+ combined-stream@1.0.8:
+ resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
+ engines: {node: '>= 0.8'}
+
commander@4.1.1:
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
engines: {node: '>= 6'}
@@ -1299,6 +1360,9 @@ packages:
compare-func@2.0.0:
resolution: {integrity: sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==}
+ component-emitter@1.3.1:
+ resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==}
+
concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
@@ -1329,10 +1393,17 @@ packages:
cookie-signature@1.0.7:
resolution: {integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==}
+ cookie-signature@1.2.2:
+ resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==}
+ engines: {node: '>=6.6.0'}
+
cookie@0.7.2:
resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
engines: {node: '>= 0.6'}
+ cookiejar@2.1.4:
+ resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==}
+
cors@2.8.6:
resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==}
engines: {node: '>= 0.10'}
@@ -1416,6 +1487,10 @@ packages:
deep-is@0.1.4:
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
+ delayed-stream@1.0.0:
+ resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
+ engines: {node: '>=0.4.0'}
+
depd@2.0.0:
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
engines: {node: '>= 0.8'}
@@ -1432,6 +1507,9 @@ packages:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
+ dezalgo@1.0.4:
+ resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==}
+
didyoumean@1.2.2:
resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
@@ -1448,6 +1526,10 @@ packages:
resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==}
engines: {node: '>=8'}
+ dotenv@17.3.1:
+ resolution: {integrity: sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==}
+ engines: {node: '>=12'}
+
dunder-proto@1.0.1:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'}
@@ -1497,6 +1579,10 @@ packages:
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
engines: {node: '>= 0.4'}
+ es-set-tostringtag@2.1.0:
+ resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
+ engines: {node: '>= 0.4'}
+
esbuild@0.21.5:
resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==}
engines: {node: '>=12'}
@@ -1584,6 +1670,12 @@ packages:
resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==}
engines: {node: '>=12.0.0'}
+ express-rate-limit@8.2.1:
+ resolution: {integrity: sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==}
+ engines: {node: '>= 16'}
+ peerDependencies:
+ express: '>= 4.11'
+
express@4.22.1:
resolution: {integrity: sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==}
engines: {node: '>= 0.10.0'}
@@ -1601,6 +1693,9 @@ packages:
fast-levenshtein@2.0.6:
resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
+ fast-safe-stringify@2.1.1:
+ resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==}
+
fast-uri@3.1.0:
resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==}
@@ -1642,6 +1737,14 @@ packages:
flatted@3.3.3:
resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==}
+ form-data@4.0.5:
+ resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==}
+ engines: {node: '>= 6'}
+
+ formidable@3.5.4:
+ resolution: {integrity: sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==}
+ engines: {node: '>=14.0.0'}
+
forwarded@0.2.0:
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
engines: {node: '>= 0.6'}
@@ -1726,6 +1829,10 @@ packages:
resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
engines: {node: '>= 0.4'}
+ has-tostringtag@1.0.2:
+ resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
+ engines: {node: '>= 0.4'}
+
hasown@2.0.2:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
@@ -1794,6 +1901,10 @@ packages:
resolution: {integrity: sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
+ ip-address@10.0.1:
+ resolution: {integrity: sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==}
+ engines: {node: '>= 12'}
+
ipaddr.js@1.9.1:
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
engines: {node: '>= 0.10'}
@@ -2049,6 +2160,11 @@ packages:
engines: {node: '>=4'}
hasBin: true
+ mime@2.6.0:
+ resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==}
+ engines: {node: '>=4.0.0'}
+ hasBin: true
+
mimic-response@3.1.0:
resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==}
engines: {node: '>=10'}
@@ -2493,6 +2609,14 @@ packages:
engines: {node: '>=16 || 14 >=14.17'}
hasBin: true
+ superagent@10.3.0:
+ resolution: {integrity: sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ==}
+ engines: {node: '>=14.18.0'}
+
+ supertest@7.2.2:
+ resolution: {integrity: sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA==}
+ engines: {node: '>=14.18.0'}
+
supports-color@7.2.0:
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
engines: {node: '>=8'}
@@ -2797,6 +2921,9 @@ packages:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}
+ zod@4.3.6:
+ resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==}
+
snapshots:
'@acemir/cssom@0.9.31': {}
@@ -3267,7 +3394,9 @@ snapshots:
'@eslint/core': 0.17.0
levn: 0.4.1
- '@exodus/bytes@1.14.1': {}
+ '@exodus/bytes@1.14.1(@noble/hashes@1.8.0)':
+ optionalDependencies:
+ '@noble/hashes': 1.8.0
'@humanfs/core@0.19.1': {}
@@ -3305,6 +3434,8 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5
+ '@noble/hashes@1.8.0': {}
+
'@nodelib/fs.scandir@2.1.5':
dependencies:
'@nodelib/fs.stat': 2.0.5
@@ -3317,6 +3448,10 @@ snapshots:
'@nodelib/fs.scandir': 2.1.5
fastq: 1.20.1
+ '@paralleldrive/cuid2@2.3.1':
+ dependencies:
+ '@noble/hashes': 1.8.0
+
'@remix-run/router@1.23.2': {}
'@rolldown/pluginutils@1.0.0-beta.27': {}
@@ -3455,6 +3590,10 @@ snapshots:
dependencies:
'@babel/types': 7.29.0
+ '@types/bcryptjs@3.0.0':
+ dependencies:
+ bcryptjs: 3.0.3
+
'@types/better-sqlite3@7.6.13':
dependencies:
'@types/node': 25.2.3
@@ -3473,6 +3612,8 @@ snapshots:
dependencies:
'@types/node': 25.2.3
+ '@types/cookiejar@2.1.5': {}
+
'@types/cors@2.8.19':
dependencies:
'@types/node': 25.2.3
@@ -3504,6 +3645,8 @@ snapshots:
'@types/ms': 2.1.0
'@types/node': 25.2.3
+ '@types/methods@1.1.4': {}
+
'@types/mime@1.3.5': {}
'@types/ms@2.1.0': {}
@@ -3542,6 +3685,18 @@ snapshots:
'@types/node': 25.2.3
'@types/send': 0.17.6
+ '@types/superagent@8.1.9':
+ dependencies:
+ '@types/cookiejar': 2.1.5
+ '@types/methods': 1.1.4
+ '@types/node': 25.2.3
+ form-data: 4.0.5
+
+ '@types/supertest@6.0.3':
+ dependencies:
+ '@types/methods': 1.1.4
+ '@types/superagent': 8.1.9
+
'@typescript-eslint/eslint-plugin@8.56.0(@typescript-eslint/parser@8.56.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
dependencies:
'@eslint-community/regexpp': 4.12.2
@@ -3645,7 +3800,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@vitest/coverage-v8@4.0.18(vitest@4.0.18(@types/node@25.2.3)(jiti@1.21.7)(jsdom@28.1.0)(tsx@4.21.0))':
+ '@vitest/coverage-v8@4.0.18(vitest@4.0.18(@types/node@25.2.3)(jiti@1.21.7)(jsdom@28.1.0(@noble/hashes@1.8.0))(tsx@4.21.0))':
dependencies:
'@bcoe/v8-coverage': 1.0.2
'@vitest/utils': 4.0.18
@@ -3657,7 +3812,7 @@ snapshots:
obug: 2.1.1
std-env: 3.10.0
tinyrainbow: 3.0.3
- vitest: 4.0.18(@types/node@25.2.3)(jiti@1.21.7)(jsdom@28.1.0)(tsx@4.21.0)
+ vitest: 4.0.18(@types/node@25.2.3)(jiti@1.21.7)(jsdom@28.1.0(@noble/hashes@1.8.0))(tsx@4.21.0)
'@vitest/expect@4.0.18':
dependencies:
@@ -3676,6 +3831,14 @@ snapshots:
optionalDependencies:
vite: 7.3.1(@types/node@25.2.3)(jiti@1.21.7)(tsx@4.21.0)
+ '@vitest/mocker@4.0.18(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(tsx@4.21.0))':
+ dependencies:
+ '@vitest/spy': 4.0.18
+ estree-walker: 3.0.3
+ magic-string: 0.30.21
+ optionalDependencies:
+ vite: 7.3.1(@types/node@25.2.3)(jiti@2.6.1)(tsx@4.21.0)
+
'@vitest/pretty-format@4.0.18':
dependencies:
tinyrainbow: 3.0.3
@@ -3754,6 +3917,8 @@ snapshots:
array-ify@1.0.0: {}
+ asap@2.0.6: {}
+
assertion-error@2.0.1: {}
ast-v8-to-istanbul@0.3.11:
@@ -3762,6 +3927,8 @@ snapshots:
estree-walker: 3.0.3
js-tokens: 10.0.0
+ asynckit@0.4.0: {}
+
autoprefixer@10.4.24(postcss@8.5.6):
dependencies:
browserslist: 4.28.1
@@ -3777,6 +3944,8 @@ snapshots:
baseline-browser-mapping@2.9.19: {}
+ bcryptjs@3.0.3: {}
+
better-sqlite3@11.10.0:
dependencies:
bindings: 1.5.0
@@ -3896,6 +4065,10 @@ snapshots:
color-name@1.1.4: {}
+ combined-stream@1.0.8:
+ dependencies:
+ delayed-stream: 1.0.0
+
commander@4.1.1: {}
compare-func@2.0.0:
@@ -3903,6 +4076,8 @@ snapshots:
array-ify: 1.0.0
dot-prop: 5.3.0
+ component-emitter@1.3.1: {}
+
concat-map@0.0.1: {}
content-disposition@0.5.4:
@@ -3927,8 +4102,12 @@ snapshots:
cookie-signature@1.0.7: {}
+ cookie-signature@1.2.2: {}
+
cookie@0.7.2: {}
+ cookiejar@2.1.4: {}
+
cors@2.8.6:
dependencies:
object-assign: 4.1.1
@@ -3976,10 +4155,10 @@ snapshots:
dargs@8.1.0: {}
- data-urls@7.0.0:
+ data-urls@7.0.0(@noble/hashes@1.8.0):
dependencies:
whatwg-mimetype: 5.0.0
- whatwg-url: 16.0.1
+ whatwg-url: 16.0.1(@noble/hashes@1.8.0)
transitivePeerDependencies:
- '@noble/hashes'
@@ -4001,6 +4180,8 @@ snapshots:
deep-is@0.1.4: {}
+ delayed-stream@1.0.0: {}
+
depd@2.0.0: {}
dequal@2.0.3: {}
@@ -4009,6 +4190,11 @@ snapshots:
detect-libc@2.1.2: {}
+ dezalgo@1.0.4:
+ dependencies:
+ asap: 2.0.6
+ wrappy: 1.0.2
+
didyoumean@1.2.2: {}
dlv@1.1.3: {}
@@ -4021,6 +4207,8 @@ snapshots:
dependencies:
is-obj: 2.0.0
+ dotenv@17.3.1: {}
+
dunder-proto@1.0.1:
dependencies:
call-bind-apply-helpers: 1.0.2
@@ -4061,6 +4249,13 @@ snapshots:
dependencies:
es-errors: 1.3.0
+ es-set-tostringtag@2.1.0:
+ dependencies:
+ es-errors: 1.3.0
+ get-intrinsic: 1.3.0
+ has-tostringtag: 1.0.2
+ hasown: 2.0.2
+
esbuild@0.21.5:
optionalDependencies:
'@esbuild/aix-ppc64': 0.21.5
@@ -4216,6 +4411,11 @@ snapshots:
expect-type@1.3.0: {}
+ express-rate-limit@8.2.1(express@4.22.1):
+ dependencies:
+ express: 4.22.1
+ ip-address: 10.0.1
+
express@4.22.1:
dependencies:
accepts: 1.3.8
@@ -4266,6 +4466,8 @@ snapshots:
fast-levenshtein@2.0.6: {}
+ fast-safe-stringify@2.1.1: {}
+
fast-uri@3.1.0: {}
fastq@1.20.1:
@@ -4310,6 +4512,20 @@ snapshots:
flatted@3.3.3: {}
+ form-data@4.0.5:
+ dependencies:
+ asynckit: 0.4.0
+ combined-stream: 1.0.8
+ es-set-tostringtag: 2.1.0
+ hasown: 2.0.2
+ mime-types: 2.1.35
+
+ formidable@3.5.4:
+ dependencies:
+ '@paralleldrive/cuid2': 2.3.1
+ dezalgo: 1.0.4
+ once: 1.4.0
+
forwarded@0.2.0: {}
fraction.js@5.3.4: {}
@@ -4381,13 +4597,17 @@ snapshots:
has-symbols@1.1.0: {}
+ has-tostringtag@1.0.2:
+ dependencies:
+ has-symbols: 1.1.0
+
hasown@2.0.2:
dependencies:
function-bind: 1.1.2
- html-encoding-sniffer@6.0.0:
+ html-encoding-sniffer@6.0.0(@noble/hashes@1.8.0):
dependencies:
- '@exodus/bytes': 1.14.1
+ '@exodus/bytes': 1.14.1(@noble/hashes@1.8.0)
transitivePeerDependencies:
- '@noble/hashes'
@@ -4444,6 +4664,8 @@ snapshots:
ini@4.1.1: {}
+ ip-address@10.0.1: {}
+
ipaddr.js@1.9.1: {}
is-arrayish@0.2.1: {}
@@ -4499,16 +4721,16 @@ snapshots:
dependencies:
argparse: 2.0.1
- jsdom@28.1.0:
+ jsdom@28.1.0(@noble/hashes@1.8.0):
dependencies:
'@acemir/cssom': 0.9.31
'@asamuzakjp/dom-selector': 6.8.1
'@bramus/specificity': 2.4.2
- '@exodus/bytes': 1.14.1
+ '@exodus/bytes': 1.14.1(@noble/hashes@1.8.0)
cssstyle: 6.0.1
- data-urls: 7.0.0
+ data-urls: 7.0.0(@noble/hashes@1.8.0)
decimal.js: 10.6.0
- html-encoding-sniffer: 6.0.0
+ html-encoding-sniffer: 6.0.0(@noble/hashes@1.8.0)
http-proxy-agent: 7.0.2
https-proxy-agent: 7.0.6
is-potential-custom-element-name: 1.0.1
@@ -4520,7 +4742,7 @@ snapshots:
w3c-xmlserializer: 5.0.0
webidl-conversions: 8.0.1
whatwg-mimetype: 5.0.0
- whatwg-url: 16.0.1
+ whatwg-url: 16.0.1(@noble/hashes@1.8.0)
xml-name-validator: 5.0.0
transitivePeerDependencies:
- '@noble/hashes'
@@ -4666,6 +4888,8 @@ snapshots:
mime@1.6.0: {}
+ mime@2.6.0: {}
+
mimic-response@3.1.0: {}
min-indent@1.0.1: {}
@@ -5123,6 +5347,28 @@ snapshots:
tinyglobby: 0.2.15
ts-interface-checker: 0.1.13
+ superagent@10.3.0:
+ dependencies:
+ component-emitter: 1.3.1
+ cookiejar: 2.1.4
+ debug: 4.4.3
+ fast-safe-stringify: 2.1.1
+ form-data: 4.0.5
+ formidable: 3.5.4
+ methods: 1.1.2
+ mime: 2.6.0
+ qs: 6.14.2
+ transitivePeerDependencies:
+ - supports-color
+
+ supertest@7.2.2:
+ dependencies:
+ cookie-signature: 1.2.2
+ methods: 1.1.2
+ superagent: 10.3.0
+ transitivePeerDependencies:
+ - supports-color
+
supports-color@7.2.0:
dependencies:
has-flag: 4.0.0
@@ -5297,7 +5543,21 @@ snapshots:
jiti: 1.21.7
tsx: 4.21.0
- vitest@4.0.18(@types/node@25.2.3)(jiti@1.21.7)(jsdom@28.1.0)(tsx@4.21.0):
+ vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(tsx@4.21.0):
+ dependencies:
+ esbuild: 0.27.3
+ fdir: 6.5.0(picomatch@4.0.3)
+ picomatch: 4.0.3
+ postcss: 8.5.6
+ rollup: 4.57.1
+ tinyglobby: 0.2.15
+ optionalDependencies:
+ '@types/node': 25.2.3
+ fsevents: 2.3.3
+ jiti: 2.6.1
+ tsx: 4.21.0
+
+ vitest@4.0.18(@types/node@25.2.3)(jiti@1.21.7)(jsdom@28.1.0(@noble/hashes@1.8.0))(tsx@4.21.0):
dependencies:
'@vitest/expect': 4.0.18
'@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@25.2.3)(jiti@1.21.7)(tsx@4.21.0))
@@ -5321,7 +5581,45 @@ snapshots:
why-is-node-running: 2.3.0
optionalDependencies:
'@types/node': 25.2.3
- jsdom: 28.1.0
+ jsdom: 28.1.0(@noble/hashes@1.8.0)
+ transitivePeerDependencies:
+ - jiti
+ - less
+ - lightningcss
+ - msw
+ - sass
+ - sass-embedded
+ - stylus
+ - sugarss
+ - terser
+ - tsx
+ - yaml
+
+ vitest@4.0.18(@types/node@25.2.3)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@1.8.0))(tsx@4.21.0):
+ dependencies:
+ '@vitest/expect': 4.0.18
+ '@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(tsx@4.21.0))
+ '@vitest/pretty-format': 4.0.18
+ '@vitest/runner': 4.0.18
+ '@vitest/snapshot': 4.0.18
+ '@vitest/spy': 4.0.18
+ '@vitest/utils': 4.0.18
+ es-module-lexer: 1.7.0
+ expect-type: 1.3.0
+ magic-string: 0.30.21
+ obug: 2.1.1
+ pathe: 2.0.3
+ picomatch: 4.0.3
+ std-env: 3.10.0
+ tinybench: 2.9.0
+ tinyexec: 1.0.2
+ tinyglobby: 0.2.15
+ tinyrainbow: 3.0.3
+ vite: 7.3.1(@types/node@25.2.3)(jiti@2.6.1)(tsx@4.21.0)
+ why-is-node-running: 2.3.0
+ optionalDependencies:
+ '@types/node': 25.2.3
+ jsdom: 28.1.0(@noble/hashes@1.8.0)
transitivePeerDependencies:
- jiti
- less
@@ -5343,9 +5641,9 @@ snapshots:
whatwg-mimetype@5.0.0: {}
- whatwg-url@16.0.1:
+ whatwg-url@16.0.1(@noble/hashes@1.8.0):
dependencies:
- '@exodus/bytes': 1.14.1
+ '@exodus/bytes': 1.14.1(@noble/hashes@1.8.0)
tr46: 6.0.0
webidl-conversions: 8.0.1
transitivePeerDependencies:
@@ -5391,3 +5689,5 @@ snapshots:
yargs-parser: 21.1.1
yocto-queue@0.1.0: {}
+
+ zod@4.3.6: {}