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: {}