diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 528d107..1c298d4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,9 +6,28 @@ on: pull_request: branches: [main] +env: + DATABASE_URL: "postgresql://ci:ci@localhost:5432/batchcooking_ci?schema=public" + # Test-only secret, never used outside CI — real deployments must set their own. + JWT_SECRET: "ci-only-secret-not-used-anywhere-else-32chars+" + jobs: lint-and-test: runs-on: ubuntu-latest + services: + postgres: + image: postgres:16-alpine + env: + POSTGRES_USER: ci + POSTGRES_PASSWORD: ci + POSTGRES_DB: batchcooking_ci + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 5s + --health-timeout 5s + --health-retries 10 steps: - uses: actions/checkout@v4 @@ -21,6 +40,7 @@ jobs: - run: pnpm install --frozen-lockfile - run: pnpm lint + - run: pnpm --filter api exec prisma migrate deploy - run: pnpm --filter api test - run: pnpm --filter api test:bdd - run: pnpm build diff --git a/README.md b/README.md index 1532820..4858067 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,8 @@ Puis **édite ces deux `.env`** pour renseigner de vrais `POSTGRES_USER`/`POSTGR ne contiennent volontairement aucun identifiant réel (juste `changeme`), et `docker-compose.yml` refuse de démarrer tant que `POSTGRES_USER`/`PASSWORD`/`DB` ne sont pas définis dans `.env` — pas de valeur par défaut en dur dans les fichiers commités. +Même règle pour `apps/api/.env` : `JWT_SECRET` est **requis, sans défaut** (génère le +tien, voir le commentaire dans `apps/api/.env.example`). ### Cypress : téléchargement du binaire @@ -112,3 +114,33 @@ qui n'existent pas encore). > `package.json`. `cucumber.cjs` évite le problème *pour sa propre config* en étant > explicitement CommonJS ; les steps/world restent en `.ts` ESM classique et sont > chargés via `tsx` (`NODE_OPTIONS=--import=tsx`, voir le script `test:bdd`). + +## Auth (apps/api) + +Inscription (création de profil + foyer) et connexion, JWT dans un cookie httpOnly. + +- `POST /auth/signup` — `{ firstName, lastName, email, password }` → crée le foyer + (`house`) et le profil (`user_profiles`) en une transaction, pose le cookie de + session, renvoie le profil (201) +- `POST /auth/login` — `{ email, password }` → pose le cookie de session, renvoie le + profil (200) ; message d'erreur volontairement générique (401) que ce soit + l'email ou le mot de passe qui soit incorrect +- `POST /auth/logout` — efface le cookie (204) +- `GET /auth/me` — profil courant, nécessite le cookie de session (401 sinon) + +Mots de passe hachés avec argon2. Le hash est indépendant du foyer : un profil crée +toujours son propre foyer à l'inscription (rejoindre un foyer existant n'est pas +encore implémenté). + +> **argon2 : version pinnée à `0.31.2`, pas de `^`.** La version `0.45.1` (dernière au +> moment de l'écriture) segfault au runtime sur au moins une configuration Windows — +> reproduit de façon stable (bash sandboxé, bash non-sandboxé, PowerShell), alors que +> `0.31.2` fonctionne parfaitement avec la même API. Si tu montes la version, revérifie +> concrètement (`argon2.hash(...)` dans un `node -e`) avant de merger, un `pnpm build` +> qui passe ne suffit pas à détecter un crash runtime. + +Les tests (Mocha + Cucumber) tournent avec un coût argon2 réduit +(`NODE_ENV=test`, voir `auth.service.ts`) — le coût par défaut est volontairement +élevé (sécurité), ce qui rendrait la suite de tests lente/instable sinon. La CI +provisionne un vrai Postgres de service (`.github/workflows/ci.yml`) et exécute +`prisma migrate deploy` avant les tests. diff --git a/apps/api/.env.example b/apps/api/.env.example index 1cb9717..7dbf7af 100644 --- a/apps/api/.env.example +++ b/apps/api/.env.example @@ -3,3 +3,12 @@ PORT=3000 # Match whatever you set in the root .env (POSTGRES_USER/PASSWORD/DB) — # do not commit the real value. DATABASE_URL="postgresql://changeme:changeme@localhost:5432/batchcooking?schema=public" + +# Required, no default on purpose — generate your own, e.g.: +# node -e "console.log(require('crypto').randomBytes(48).toString('hex'))" +JWT_SECRET=changeme-generate-a-real-random-secret-at-least-32-chars + +# Optional — defaults shown (see src/config/env.ts) +# JWT_EXPIRES_IN=7d +# AUTH_COOKIE_NAME=session +# CORS_ORIGIN=http://localhost:5173 diff --git a/apps/api/.mocharc.json b/apps/api/.mocharc.json index 657e5a5..193fdb9 100644 --- a/apps/api/.mocharc.json +++ b/apps/api/.mocharc.json @@ -1,5 +1,6 @@ { "extension": ["ts"], "spec": "test/**/*.test.ts", - "node-option": ["import=tsx"] + "node-option": ["import=tsx"], + "timeout": 10000 } diff --git a/apps/api/features/auth.feature b/apps/api/features/auth.feature new file mode 100644 index 0000000..cb71a22 --- /dev/null +++ b/apps/api/features/auth.feature @@ -0,0 +1,33 @@ +Feature: Account creation and login + As a new user + I want to create a profile and log in + So that I can access my household's batch-cooking planning + + Scenario: A visitor creates a new profile + When I sign up with the following details: + | firstName | Alice | + | lastName | Martin | + | email | alice@example.com | + | password | correct-horse-battery-staple | + Then the response status should be 201 + And I am authenticated as "alice@example.com" + + Scenario: A visitor cannot sign up twice with the same email + Given a profile already exists with email "alice@example.com" + When I sign up with the following details: + | firstName | Alice | + | lastName | Martin | + | email | alice@example.com | + | password | correct-horse-battery-staple | + Then the response status should be 409 + + Scenario: A registered user logs in with correct credentials + Given a profile already exists with email "alice@example.com" and password "correct-horse-battery-staple" + When I log in with email "alice@example.com" and password "correct-horse-battery-staple" + Then the response status should be 200 + And I am authenticated as "alice@example.com" + + Scenario: A registered user cannot log in with the wrong password + Given a profile already exists with email "alice@example.com" and password "correct-horse-battery-staple" + When I log in with email "alice@example.com" and password "wrong-password" + Then the response status should be 401 diff --git a/apps/api/features/step-definitions/auth.steps.ts b/apps/api/features/step-definitions/auth.steps.ts new file mode 100644 index 0000000..eda2041 --- /dev/null +++ b/apps/api/features/step-definitions/auth.steps.ts @@ -0,0 +1,44 @@ +import assert from "node:assert/strict"; +import type { DataTable } from "@cucumber/cucumber"; +import { Given, Then, When } from "@cucumber/cucumber"; +import { signup } from "../../src/modules/auth/auth.service.js"; +import type { CustomWorld } from "../support/world.js"; + +Given("a profile already exists with email {string}", async (email: string) => { + await signup({ + firstName: "Existing", + lastName: "User", + email, + password: "some-existing-password", + }); +}); + +Given( + "a profile already exists with email {string} and password {string}", + async (email: string, password: string) => { + await signup({ firstName: "Existing", lastName: "User", email, password }); + }, +); + +When("I sign up with the following details:", async function (this: CustomWorld, table: DataTable) { + const details = table.rowsHash(); + this.response = await this.agent.post("/auth/signup").send({ + firstName: details.firstName, + lastName: details.lastName, + email: details.email, + password: details.password, + }); +}); + +When( + "I log in with email {string} and password {string}", + async function (this: CustomWorld, email: string, password: string) { + this.response = await this.agent.post("/auth/login").send({ email, password }); + }, +); + +Then("I am authenticated as {string}", async function (this: CustomWorld, email: string) { + const res = await this.agent.get("/auth/me"); + assert.equal(res.status, 200); + assert.equal(res.body.email, email); +}); diff --git a/apps/api/features/support/hooks.ts b/apps/api/features/support/hooks.ts new file mode 100644 index 0000000..57b15a7 --- /dev/null +++ b/apps/api/features/support/hooks.ts @@ -0,0 +1,10 @@ +import { Before, setDefaultTimeout } from "@cucumber/cucumber"; +import { resetDatabase } from "../../test-support/reset-db.js"; + +// Integration tests hitting a real Postgres + argon2 need more than +// Cucumber's 5s default, especially on a cold Prisma connection. +setDefaultTimeout(10_000); + +Before(async () => { + await resetDatabase(); +}); diff --git a/apps/api/features/support/world.ts b/apps/api/features/support/world.ts index a56b22f..d8460b5 100644 --- a/apps/api/features/support/world.ts +++ b/apps/api/features/support/world.ts @@ -1,17 +1,21 @@ import { type IWorldOptions, World, setWorldConstructor } from "@cucumber/cucumber"; import type { Express } from "express"; -import type request from "supertest"; +import request from "supertest"; import { createApp } from "../../src/app.js"; // Fresh Express app per scenario (in-process, via supertest — no server to // spin up/tear down) plus the last HTTP response, available to every step. +// `agent` persists cookies across requests within a scenario (needed for +// "sign up then check I'm authenticated" style flows). export class CustomWorld extends World { app: Express; + agent: ReturnType; response!: request.Response; constructor(options: IWorldOptions) { super(options); this.app = createApp(); + this.agent = request.agent(this.app); } } diff --git a/apps/api/package.json b/apps/api/package.json index 304e91a..94db4a2 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -7,20 +7,28 @@ "dev": "tsx watch src/server.ts", "build": "tsc -p tsconfig.json", "start": "node dist/server.js", - "test": "mocha", - "test:bdd": "cross-env NODE_OPTIONS=--import=tsx cucumber-js", + "test": "cross-env NODE_ENV=test mocha", + "test:bdd": "cross-env NODE_ENV=test NODE_OPTIONS=--import=tsx cucumber-js", "prisma:generate": "prisma generate", - "prisma:migrate": "prisma migrate dev" + "prisma:migrate": "prisma migrate dev", + "postinstall": "prisma generate" }, "dependencies": { "@prisma/client": "^5.22.0", + "argon2": "0.31.2", + "cookie-parser": "^1.4.7", + "cors": "^2.8.6", "dotenv": "^16.4.5", "express": "^4.21.1", + "jsonwebtoken": "^9.0.3", "zod": "^3.23.8" }, "devDependencies": { "@cucumber/cucumber": "^13.2.1", + "@types/cookie-parser": "^1.4.10", + "@types/cors": "^2.8.19", "@types/express": "^4.17.21", + "@types/jsonwebtoken": "^9.0.10", "@types/node": "^22.9.0", "@types/supertest": "^6.0.2", "chai": "^5.1.2", diff --git a/apps/api/prisma/migrations/20260816105101_add_user_auth_fields/migration.sql b/apps/api/prisma/migrations/20260816105101_add_user_auth_fields/migration.sql new file mode 100644 index 0000000..d54eaa3 --- /dev/null +++ b/apps/api/prisma/migrations/20260816105101_add_user_auth_fields/migration.sql @@ -0,0 +1,12 @@ +/* + Warnings: + + - Added the required column `password_hash` to the `user_profiles` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "user_profiles" ADD COLUMN "password_hash" TEXT NOT NULL, +ADD COLUMN "token_version" INTEGER NOT NULL DEFAULT 0; + +COMMENT ON COLUMN "user_profiles"."password_hash" IS 'argon2 hash of the account password (not in the original spec doc — added for authentication)'; +COMMENT ON COLUMN "user_profiles"."token_version" IS 'Bumped to invalidate previously-issued JWTs, e.g. on password change (not in the original spec doc — required for stateless JWT auth)'; diff --git a/apps/api/prisma/schema.prisma b/apps/api/prisma/schema.prisma index ba92c96..c94073e 100644 --- a/apps/api/prisma/schema.prisma +++ b/apps/api/prisma/schema.prisma @@ -52,12 +52,18 @@ model Allergy { } model UserProfile { - id Int @id @default(autoincrement()) - firstName String @map("first_name") - lastName String @map("last_name") - email String @unique - houseId Int? @map("house_id") - dietId Int? @map("diet_id") + id Int @id @default(autoincrement()) + firstName String @map("first_name") + lastName String @map("last_name") + email String @unique + /// argon2 hash of the account password. Not in the original spec doc — + /// added for authentication (login page / profile creation). + passwordHash String @map("password_hash") + /// Bumped to invalidate previously-issued JWTs (e.g. on password change). + /// Not in the original spec doc — required for stateless JWT auth. + tokenVersion Int @default(0) @map("token_version") + houseId Int? @map("house_id") + dietId Int? @map("diet_id") house House? @relation(fields: [houseId], references: [id], onDelete: SetNull) diet Diet? @relation(fields: [dietId], references: [id], onDelete: SetNull) diff --git a/apps/api/src/app.ts b/apps/api/src/app.ts index a9406f6..7c02179 100644 --- a/apps/api/src/app.ts +++ b/apps/api/src/app.ts @@ -1,22 +1,39 @@ +import cookieParser from "cookie-parser"; +import cors from "cors"; import express, { type NextFunction, type Request, type Response } from "express"; +import { ZodError } from "zod"; +import { env } from "./config/env.js"; +import { HttpError } from "./lib/http-error.js"; +import { authRouter } from "./modules/auth/auth.routes.js"; -// Application factory — no business modules yet, only generic infrastructure -// (health check, JSON parsing, 404 handler, error handler). Feature modules -// will be added once the data model / specs are defined. +// Application factory. Feature modules are added under src/modules/* as +// specs land; auth is the first one (login page / profile creation). export function createApp() { const app = express(); + app.use(cors({ origin: env.CORS_ORIGIN, credentials: true })); app.use(express.json()); + app.use(cookieParser()); app.get("/health", (_req: Request, res: Response) => { res.status(200).json({ status: "ok" }); }); + app.use("/auth", authRouter); + app.use((_req: Request, res: Response) => { res.status(404).json({ error: "Not found" }); }); app.use((err: unknown, _req: Request, res: Response, _next: NextFunction) => { + if (err instanceof ZodError) { + res.status(400).json({ error: "Validation error", details: err.flatten() }); + return; + } + if (err instanceof HttpError) { + res.status(err.status).json({ error: err.message }); + return; + } console.error(err); res.status(500).json({ error: "Internal server error" }); }); diff --git a/apps/api/src/config/env.ts b/apps/api/src/config/env.ts index dd3511e..05ffff9 100644 --- a/apps/api/src/config/env.ts +++ b/apps/api/src/config/env.ts @@ -5,6 +5,13 @@ const envSchema = z.object({ NODE_ENV: z.enum(["development", "test", "production"]).default("development"), PORT: z.coerce.number().int().positive().default(3000), DATABASE_URL: z.string().url().optional(), + // Auth — no default on purpose, same reasoning as docker-compose.yml's + // POSTGRES_USER/PASSWORD: a secret must never have a working fallback + // baked into committed code. + JWT_SECRET: z.string().min(32, "JWT_SECRET must be at least 32 characters"), + JWT_EXPIRES_IN: z.string().default("7d"), + AUTH_COOKIE_NAME: z.string().default("session"), + CORS_ORIGIN: z.string().default("http://localhost:5173"), }); export const env = envSchema.parse(process.env); diff --git a/apps/api/src/db/prisma.ts b/apps/api/src/db/prisma.ts new file mode 100644 index 0000000..19fb86f --- /dev/null +++ b/apps/api/src/db/prisma.ts @@ -0,0 +1,5 @@ +import { PrismaClient } from "@prisma/client"; + +// Single shared instance — Prisma manages its own connection pool +// internally, a new PrismaClient per request would exhaust connections. +export const prisma = new PrismaClient(); diff --git a/apps/api/src/lib/http-error.ts b/apps/api/src/lib/http-error.ts new file mode 100644 index 0000000..b1931ad --- /dev/null +++ b/apps/api/src/lib/http-error.ts @@ -0,0 +1,11 @@ +/** Typed error carrying the HTTP status it should map to, so the central + * error handler in app.ts can respond correctly instead of always 500ing. */ +export class HttpError extends Error { + status: number; + + constructor(status: number, message: string) { + super(message); + this.name = "HttpError"; + this.status = status; + } +} diff --git a/apps/api/src/lib/jwt.ts b/apps/api/src/lib/jwt.ts new file mode 100644 index 0000000..bcd2c2d --- /dev/null +++ b/apps/api/src/lib/jwt.ts @@ -0,0 +1,30 @@ +import jwt from "jsonwebtoken"; +import { env } from "../config/env.js"; + +export interface AuthTokenPayload { + userProfileId: number; + tokenVersion: number; +} + +export function signAuthToken(payload: AuthTokenPayload): string { + // "sub" follows the JWT convention (RFC 7519) of identifying the + // principal as a string; userProfileId/tokenVersion are our own claims. + return jwt.sign( + { sub: String(payload.userProfileId), tokenVersion: payload.tokenVersion }, + env.JWT_SECRET, + { expiresIn: env.JWT_EXPIRES_IN as jwt.SignOptions["expiresIn"] }, + ); +} + +export function verifyAuthToken(token: string): AuthTokenPayload { + const decoded = jwt.verify(token, env.JWT_SECRET); + const userProfileId = typeof decoded === "object" ? Number(decoded.sub) : Number.NaN; + if ( + typeof decoded !== "object" || + Number.isNaN(userProfileId) || + typeof decoded.tokenVersion !== "number" + ) { + throw new Error("Malformed auth token payload"); + } + return { userProfileId, tokenVersion: decoded.tokenVersion }; +} diff --git a/apps/api/src/middlewares/require-auth.ts b/apps/api/src/middlewares/require-auth.ts new file mode 100644 index 0000000..1ed0e11 --- /dev/null +++ b/apps/api/src/middlewares/require-auth.ts @@ -0,0 +1,35 @@ +import type { NextFunction, Request, Response } from "express"; +import { env } from "../config/env.js"; +import { prisma } from "../db/prisma.js"; +import { HttpError } from "../lib/http-error.js"; +import { verifyAuthToken } from "../lib/jwt.js"; + +/** Reads the session cookie, verifies the JWT, and re-checks tokenVersion + * against the database (so a password change / logout-everywhere can + * invalidate previously-issued tokens despite JWT being stateless). */ +export async function requireAuth(req: Request, _res: Response, next: NextFunction) { + try { + const token = req.cookies?.[env.AUTH_COOKIE_NAME]; + if (typeof token !== "string") { + throw new HttpError(401, "Not authenticated"); + } + + const payload = verifyAuthToken(token); + const profile = await prisma.userProfile.findUnique({ where: { id: payload.userProfileId } }); + + if (!profile || profile.tokenVersion !== payload.tokenVersion) { + throw new HttpError(401, "Not authenticated"); + } + + const { passwordHash: _passwordHash, ...safeProfile } = profile; + req.userProfile = safeProfile; + next(); + } catch (err) { + if (err instanceof HttpError) { + next(err); + } else { + // Covers jwt.verify failures (expired/invalid/malformed token). + next(new HttpError(401, "Not authenticated")); + } + } +} diff --git a/apps/api/src/modules/auth/auth.routes.ts b/apps/api/src/modules/auth/auth.routes.ts new file mode 100644 index 0000000..5eacfbe --- /dev/null +++ b/apps/api/src/modules/auth/auth.routes.ts @@ -0,0 +1,51 @@ +import { Router } from "express"; +import type { CookieOptions } from "express"; +import { env } from "../../config/env.js"; +import { requireAuth } from "../../middlewares/require-auth.js"; +import { loginSchema, signupSchema } from "./auth.schema.js"; +import { login, signup } from "./auth.service.js"; + +export const authRouter = Router(); + +// Independent from JWT_EXPIRES_IN on purpose (see auth.routes.ts) — the JWT's +// own expiry is what's actually enforced by requireAuth, this only bounds +// how long the browser keeps sending the cookie. +const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000; + +const cookieOptions: CookieOptions = { + httpOnly: true, + secure: env.NODE_ENV === "production", + sameSite: "lax", + maxAge: SEVEN_DAYS_MS, +}; + +authRouter.post("/signup", async (req, res, next) => { + try { + const input = signupSchema.parse(req.body); + const { profile, token } = await signup(input); + res.cookie(env.AUTH_COOKIE_NAME, token, cookieOptions); + res.status(201).json(profile); + } catch (err) { + next(err); + } +}); + +authRouter.post("/login", async (req, res, next) => { + try { + const input = loginSchema.parse(req.body); + const { profile, token } = await login(input); + res.cookie(env.AUTH_COOKIE_NAME, token, cookieOptions); + res.status(200).json(profile); + } catch (err) { + next(err); + } +}); + +authRouter.post("/logout", (_req, res) => { + res.clearCookie(env.AUTH_COOKIE_NAME, cookieOptions); + res.status(204).end(); +}); + +authRouter.get("/me", requireAuth, (req, res) => { + res.status(200).json(req.userProfile); +}); diff --git a/apps/api/src/modules/auth/auth.schema.ts b/apps/api/src/modules/auth/auth.schema.ts new file mode 100644 index 0000000..3e3574b --- /dev/null +++ b/apps/api/src/modules/auth/auth.schema.ts @@ -0,0 +1,18 @@ +import { z } from "zod"; + +export const signupSchema = z.object({ + firstName: z.string().trim().min(1).max(100), + lastName: z.string().trim().min(1).max(100), + email: z.string().trim().toLowerCase().email(), + // Length only — not the place to enforce complexity rules; argon2 already + // makes brute-forcing short-but-random passwords impractical, and + // complexity rules mostly push users toward predictable patterns. + password: z.string().min(8).max(200), +}); +export type SignupInput = z.infer; + +export const loginSchema = z.object({ + email: z.string().trim().toLowerCase().email(), + password: z.string().min(1), +}); +export type LoginInput = z.infer; diff --git a/apps/api/src/modules/auth/auth.service.ts b/apps/api/src/modules/auth/auth.service.ts new file mode 100644 index 0000000..da25dae --- /dev/null +++ b/apps/api/src/modules/auth/auth.service.ts @@ -0,0 +1,65 @@ +import type { UserProfile } from "@prisma/client"; +import argon2 from "argon2"; +import { env } from "../../config/env.js"; +import { prisma } from "../../db/prisma.js"; +import { HttpError } from "../../lib/http-error.js"; +import { signAuthToken } from "../../lib/jwt.js"; +import type { LoginInput, SignupInput } from "./auth.schema.js"; + +type SafeProfile = Omit; + +// argon2's defaults (64 MB memory, 3 passes) are deliberately expensive — +// that's the point, for real passwords. In tests we hash/verify dozens of +// times per run against throwaway data, so a much cheaper cost keeps the +// suite fast without weakening anything real. Never applies outside +// NODE_ENV=test. +const testHashOptions = { memoryCost: 8192, timeCost: 2, parallelism: 1 }; +const hashOptions = env.NODE_ENV === "test" ? testHashOptions : undefined; + +function toSafeProfile(profile: UserProfile): SafeProfile { + const { passwordHash: _passwordHash, ...safeProfile } = profile; + return safeProfile; +} + +export async function signup(input: SignupInput): Promise<{ profile: SafeProfile; token: string }> { + const existing = await prisma.userProfile.findUnique({ where: { email: input.email } }); + if (existing) { + throw new HttpError(409, "Email already in use"); + } + + const passwordHash = await argon2.hash(input.password, hashOptions); + + // A profile always belongs to a house; signup creates one (named after + // the new user for now — renaming/joining an existing house is a + // separate, not-yet-built feature). + const profile = await prisma.$transaction(async (tx) => { + const house = await tx.house.create({ + data: { name: `Foyer de ${input.firstName}` }, + }); + return tx.userProfile.create({ + data: { + firstName: input.firstName, + lastName: input.lastName, + email: input.email, + passwordHash, + houseId: house.id, + }, + }); + }); + + const token = signAuthToken({ userProfileId: profile.id, tokenVersion: profile.tokenVersion }); + return { profile: toSafeProfile(profile), token }; +} + +export async function login(input: LoginInput): Promise<{ profile: SafeProfile; token: string }> { + const profile = await prisma.userProfile.findUnique({ where: { email: input.email } }); + + // Deliberately generic error/message for both "no such email" and "wrong + // password" — don't leak which one it was. + if (!profile || !(await argon2.verify(profile.passwordHash, input.password))) { + throw new HttpError(401, "Invalid email or password"); + } + + const token = signAuthToken({ userProfileId: profile.id, tokenVersion: profile.tokenVersion }); + return { profile: toSafeProfile(profile), token }; +} diff --git a/apps/api/src/types/express.d.ts b/apps/api/src/types/express.d.ts new file mode 100644 index 0000000..a3db624 --- /dev/null +++ b/apps/api/src/types/express.d.ts @@ -0,0 +1,10 @@ +import type { UserProfile } from "@prisma/client"; + +declare global { + namespace Express { + interface Request { + /** Set by requireAuth once the session cookie's JWT has been verified. */ + userProfile?: Omit; + } + } +} diff --git a/apps/api/test-support/reset-db.ts b/apps/api/test-support/reset-db.ts new file mode 100644 index 0000000..cdbf1f2 --- /dev/null +++ b/apps/api/test-support/reset-db.ts @@ -0,0 +1,15 @@ +import { prisma } from "../src/db/prisma.js"; + +// Single TRUNCATE ... CASCADE covers FK ordering and resets identity +// sequences — used between tests/scenarios to start from a clean slate. +export async function resetDatabase() { + await prisma.$executeRawUnsafe(` + TRUNCATE TABLE + "user_profile_allergy", "allergy", "category", + "planning_item", "planning", + "recipe_ingredient", "step", "tech_step_mapping", "tech_step", + "recipe", "ingredients", "sources", + "user_profiles", "diet", "house" + RESTART IDENTITY CASCADE; + `); +} diff --git a/apps/api/test/auth.test.ts b/apps/api/test/auth.test.ts new file mode 100644 index 0000000..a3523bc --- /dev/null +++ b/apps/api/test/auth.test.ts @@ -0,0 +1,103 @@ +import { expect } from "chai"; +import request from "supertest"; +import { createApp } from "../src/app.js"; +import { prisma } from "../src/db/prisma.js"; +import { resetDatabase } from "../test-support/reset-db.js"; + +const validSignup = { + firstName: "Nicolas", + lastName: "Lefevre", + email: "nicolas@example.com", + password: "correct-horse-battery-staple", +}; + +describe("Auth", () => { + const app = createApp(); + + beforeEach(async () => { + await resetDatabase(); + }); + + after(async () => { + await prisma.$disconnect(); + }); + + describe("POST /auth/signup", () => { + it("creates a profile and its house, and sets a session cookie", async () => { + const res = await request(app).post("/auth/signup").send(validSignup); + + expect(res.status).to.equal(201); + expect(res.body).to.include({ + firstName: "Nicolas", + lastName: "Lefevre", + email: "nicolas@example.com", + }); + expect(res.body).to.not.have.property("passwordHash"); + expect(res.body.houseId).to.be.a("number"); + expect(res.headers["set-cookie"]?.[0]).to.include("session="); + }); + + it("rejects a duplicate email with 409", async () => { + await request(app).post("/auth/signup").send(validSignup); + const res = await request(app).post("/auth/signup").send(validSignup); + + expect(res.status).to.equal(409); + }); + + it("rejects an invalid payload with 400", async () => { + const res = await request(app) + .post("/auth/signup") + .send({ firstName: "X", lastName: "Y", email: "not-an-email", password: "short" }); + + expect(res.status).to.equal(400); + }); + }); + + describe("POST /auth/login", () => { + beforeEach(async () => { + await request(app).post("/auth/signup").send(validSignup); + }); + + it("logs in with correct credentials", async () => { + const res = await request(app) + .post("/auth/login") + .send({ email: validSignup.email, password: validSignup.password }); + + expect(res.status).to.equal(200); + expect(res.body.email).to.equal(validSignup.email); + }); + + it("rejects a wrong password with 401", async () => { + const res = await request(app) + .post("/auth/login") + .send({ email: validSignup.email, password: "wrong-password" }); + + expect(res.status).to.equal(401); + }); + + it("rejects an unknown email with 401", async () => { + const res = await request(app) + .post("/auth/login") + .send({ email: "nobody@example.com", password: validSignup.password }); + + expect(res.status).to.equal(401); + }); + }); + + describe("GET /auth/me", () => { + it("rejects requests without a session cookie", async () => { + const res = await request(app).get("/auth/me"); + expect(res.status).to.equal(401); + }); + + it("returns the current profile when authenticated", async () => { + const agent = request.agent(app); + await agent.post("/auth/signup").send(validSignup); + + const res = await agent.get("/auth/me"); + + expect(res.status).to.equal(200); + expect(res.body.email).to.equal(validSignup.email); + }); + }); +}); diff --git a/package.json b/package.json index 7075cb1..b8a073d 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "@biomejs/biome", "@prisma/client", "@prisma/engines", + "argon2", "cypress", "esbuild", "prisma" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index eea4b6d..85fa005 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,12 +20,24 @@ importers: '@prisma/client': specifier: ^5.22.0 version: 5.22.0(prisma@5.22.0) + argon2: + specifier: 0.31.2 + version: 0.31.2 + cookie-parser: + specifier: ^1.4.7 + version: 1.4.7 + cors: + specifier: ^2.8.6 + version: 2.8.6 dotenv: specifier: ^16.4.5 version: 16.6.1 express: specifier: ^4.21.1 version: 4.22.2 + jsonwebtoken: + specifier: ^9.0.3 + version: 9.0.3 zod: specifier: ^3.23.8 version: 3.25.76 @@ -33,9 +45,18 @@ importers: '@cucumber/cucumber': specifier: ^13.2.1 version: 13.2.1 + '@types/cookie-parser': + specifier: ^1.4.10 + version: 1.4.10(@types/express@4.17.25) + '@types/cors': + specifier: ^2.8.19 + version: 2.8.19 '@types/express': specifier: ^4.17.21 version: 4.17.25 + '@types/jsonwebtoken': + specifier: ^9.0.10 + version: 9.0.10 '@types/node': specifier: ^22.9.0 version: 22.20.1 @@ -654,6 +675,10 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==, tarball: https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz} + '@mapbox/node-pre-gyp@1.0.11': + resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==, tarball: https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz} + hasBin: true + '@napi-rs/lzma-linux-x64-gnu@1.5.1': resolution: {integrity: sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==, tarball: https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz} engines: {node: ^22.20 || ^24.12 || >=25} @@ -667,6 +692,10 @@ packages: '@paralleldrive/cuid2@2.3.1': resolution: {integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==, tarball: https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz} + '@phc/format@1.0.0': + resolution: {integrity: sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==, tarball: https://registry.npmjs.org/@phc/format/-/format-1.0.0.tgz} + engines: {node: '>=10'} + '@prisma/client@5.22.0': resolution: {integrity: sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==, tarball: https://registry.npmjs.org/@prisma/client/-/client-5.22.0.tgz} engines: {node: '>=16.13'} @@ -844,9 +873,17 @@ packages: '@types/connect@3.4.38': resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==, tarball: https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz} + '@types/cookie-parser@1.4.10': + resolution: {integrity: sha512-B4xqkqfZ8Wek+rCOeRxsjMS9OgvzebEzzLYw7NHYuvzb7IdxOkI0ZHGgeEBX4PUM7QGVvNSK60T3OvWj3YfBRg==, tarball: https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.10.tgz} + peerDependencies: + '@types/express': '*' + '@types/cookiejar@2.1.5': resolution: {integrity: sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==, tarball: https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz} + '@types/cors@2.8.19': + resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==, tarball: https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz} + '@types/estree@1.0.9': resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==, tarball: https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz} @@ -859,12 +896,18 @@ packages: '@types/http-errors@2.0.5': resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==, tarball: https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz} + '@types/jsonwebtoken@9.0.10': + resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==, tarball: https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz} + '@types/methods@1.1.4': resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==, tarball: https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz} '@types/mime@1.3.5': resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==, tarball: https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz} + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==, tarball: https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz} + '@types/node@22.20.1': resolution: {integrity: sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==, tarball: https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz} @@ -918,6 +961,9 @@ packages: peerDependencies: vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + abbrev@1.1.1: + resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==, tarball: https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz} + accepts@1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==, tarball: https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz} engines: {node: '>= 0.6'} @@ -954,12 +1000,24 @@ packages: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==, tarball: https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz} engines: {node: '>= 8'} + aproba@2.1.0: + resolution: {integrity: sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==, tarball: https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz} + arch@2.2.0: resolution: {integrity: sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==, tarball: https://registry.npmjs.org/arch/-/arch-2.2.0.tgz} + are-we-there-yet@2.0.0: + resolution: {integrity: sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==, tarball: https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz} + engines: {node: '>=10'} + deprecated: This package is no longer supported. + arg@5.0.2: resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==, tarball: https://registry.npmjs.org/arg/-/arg-5.0.2.tgz} + argon2@0.31.2: + resolution: {integrity: sha512-QSnJ8By5Mth60IEte45w9Y7v6bWcQw3YhRtJKKN8oNCxnTLDiv/AXXkDPf2srTMfxFVn3QJdVv2nhXESsUa+Yg==, tarball: https://registry.npmjs.org/argon2/-/argon2-0.31.2.tgz} + engines: {node: '>=14.0.0'} + argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==, tarball: https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz} @@ -1034,6 +1092,9 @@ packages: resolution: {integrity: sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==, tarball: https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + brace-expansion@1.1.18: + resolution: {integrity: sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==, tarball: https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz} + brace-expansion@2.1.4: resolution: {integrity: sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==, tarball: https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz} @@ -1052,6 +1113,9 @@ packages: buffer-crc32@0.2.13: resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==, tarball: https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz} + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==, tarball: https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz} + buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==, tarball: https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz} @@ -1104,6 +1168,10 @@ packages: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==, tarball: https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz} engines: {node: '>= 8.10.0'} + chownr@2.0.0: + resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==, tarball: https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz} + engines: {node: '>=10'} + ci-info@4.4.0: resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==, tarball: https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz} engines: {node: '>=8'} @@ -1134,6 +1202,10 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==, tarball: https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz} + color-support@1.1.3: + resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==, tarball: https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz} + hasBin: true + colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==, tarball: https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz} @@ -1156,6 +1228,12 @@ packages: component-emitter@1.3.1: resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==, tarball: https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz} + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==, tarball: https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz} + + console-control-strings@1.1.0: + resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==, tarball: https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz} + content-disposition@0.5.4: resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==, tarball: https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz} engines: {node: '>= 0.6'} @@ -1167,6 +1245,13 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==, tarball: https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz} + cookie-parser@1.4.7: + resolution: {integrity: sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==, tarball: https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz} + engines: {node: '>= 0.8.0'} + + cookie-signature@1.0.6: + resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==, tarball: https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz} + cookie-signature@1.0.7: resolution: {integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==, tarball: https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz} @@ -1184,6 +1269,10 @@ packages: core-util-is@1.0.2: resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==, tarball: https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz} + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==, tarball: https://registry.npmjs.org/cors/-/cors-2.8.6.tgz} + engines: {node: '>= 0.10'} + cross-env@10.1.0: resolution: {integrity: sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==, tarball: https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz} engines: {node: '>=20'} @@ -1245,6 +1334,9 @@ packages: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==, tarball: https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz} engines: {node: '>=0.4.0'} + delegates@1.0.0: + resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==, tarball: https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz} + depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==, tarball: https://registry.npmjs.org/depd/-/depd-2.0.0.tgz} engines: {node: '>= 0.8'} @@ -1253,6 +1345,10 @@ packages: resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==, tarball: https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==, tarball: https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz} + engines: {node: '>=8'} + dezalgo@1.0.4: resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==, tarball: https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz} @@ -1278,6 +1374,9 @@ packages: ecc-jsbn@0.1.2: resolution: {integrity: sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==, tarball: https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz} + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==, tarball: https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz} + ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==, tarball: https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz} @@ -1449,6 +1548,10 @@ packages: resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==, tarball: https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz} engines: {node: '>=10'} + fs-minipass@2.1.0: + resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==, tarball: https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz} + engines: {node: '>= 8'} + fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==, tarball: https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz} @@ -1460,6 +1563,11 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==, tarball: https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz} + gauge@3.0.2: + resolution: {integrity: sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==, tarball: https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz} + engines: {node: '>=10'} + deprecated: This package is no longer supported. + gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==, tarball: https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz} engines: {node: '>=6.9.0'} @@ -1494,6 +1602,10 @@ packages: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==, tarball: https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz} engines: {node: '>= 6'} + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==, tarball: https://registry.npmjs.org/glob/-/glob-7.2.3.tgz} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + glob@8.1.0: resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==, tarball: https://registry.npmjs.org/glob/-/glob-8.1.0.tgz} engines: {node: '>=12'} @@ -1530,6 +1642,9 @@ packages: resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==, tarball: https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz} engines: {node: '>= 0.4'} + has-unicode@2.0.1: + resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==, tarball: https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz} + hasown@2.0.4: resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==, tarball: https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz} engines: {node: '>= 0.4'} @@ -1694,10 +1809,20 @@ packages: jsonfile@6.2.1: resolution: {integrity: sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==, tarball: https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz} + jsonwebtoken@9.0.3: + resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==, tarball: https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz} + engines: {node: '>=12', npm: '>=6'} + jsprim@2.0.2: resolution: {integrity: sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==, tarball: https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz} engines: {'0': node >=0.6.0} + jwa@2.0.1: + resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==, tarball: https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz} + + jws@4.0.1: + resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==, tarball: https://registry.npmjs.org/jws/-/jws-4.0.1.tgz} + knuth-shuffle-seeded@1.0.6: resolution: {integrity: sha512-9pFH0SplrfyKyojCLxZfMcvkhf5hH0d+UwR9nTVJ/DDQJGuzcXjTwB7TP7sDfehSudlGGaOLblmEWqv04ERVWg==, tarball: https://registry.npmjs.org/knuth-shuffle-seeded/-/knuth-shuffle-seeded-1.0.6.tgz} @@ -1718,6 +1843,24 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==, tarball: https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz} engines: {node: '>=10'} + lodash.includes@4.3.0: + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==, tarball: https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz} + + lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==, tarball: https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz} + + lodash.isinteger@4.0.4: + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==, tarball: https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz} + + lodash.isnumber@3.0.3: + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==, tarball: https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==, tarball: https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz} + + lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==, tarball: https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz} + lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==, tarball: https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz} @@ -1759,6 +1902,10 @@ packages: resolution: {integrity: sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==, tarball: https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz} engines: {node: '>=12'} + make-dir@3.1.0: + resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==, tarball: https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz} + engines: {node: '>=8'} + map-stream@0.1.0: resolution: {integrity: sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g==, tarball: https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz} @@ -1807,6 +1954,9 @@ packages: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==, tarball: https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz} engines: {node: '>=6'} + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==, tarball: https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz} + minimatch@5.1.9: resolution: {integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==, tarball: https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz} engines: {node: '>=10'} @@ -1814,6 +1964,23 @@ packages: minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==, tarball: https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz} + minipass@3.3.6: + resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==, tarball: https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz} + engines: {node: '>=8'} + + minipass@5.0.0: + resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==, tarball: https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz} + engines: {node: '>=8'} + + minizlib@2.1.2: + resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==, tarball: https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz} + engines: {node: '>= 8'} + + mkdirp@1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==, tarball: https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz} + engines: {node: '>=10'} + hasBin: true + mocha@10.8.2: resolution: {integrity: sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg==, tarball: https://registry.npmjs.org/mocha/-/mocha-10.8.2.tgz} engines: {node: '>= 14.0.0'} @@ -1834,10 +2001,27 @@ packages: resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==, tarball: https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz} engines: {node: '>= 0.6'} + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==, tarball: https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz} + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==, tarball: https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + node-releases@2.0.53: resolution: {integrity: sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==, tarball: https://registry.npmjs.org/node-releases/-/node-releases-2.0.53.tgz} engines: {node: '>=18'} + nopt@5.0.0: + resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==, tarball: https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz} + engines: {node: '>=6'} + hasBin: true + normalize-package-data@8.0.0: resolution: {integrity: sha512-RWk+PI433eESQ7ounYxIp67CYuVsS1uYSonX3kA6ps/3LWfjVQa/ptEg6Y3T6uAMq1mWpX9PQ+qx+QaHpsc7gQ==, tarball: https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-8.0.0.tgz} engines: {node: ^20.17.0 || >=22.9.0} @@ -1850,6 +2034,14 @@ packages: resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==, tarball: https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz} engines: {node: '>=8'} + npmlog@5.0.1: + resolution: {integrity: sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==, tarball: https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz} + deprecated: This package is no longer supported. + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==, tarball: https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz} + engines: {node: '>=0.10.0'} + object-inspect@1.13.4: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==, tarball: https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz} engines: {node: '>= 0.4'} @@ -1896,6 +2088,10 @@ packages: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==, tarball: https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz} engines: {node: '>=8'} + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==, tarball: https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz} + engines: {node: '>=0.10.0'} + path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==, tarball: https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz} engines: {node: '>=8'} @@ -2006,6 +2202,10 @@ packages: resolution: {integrity: sha512-I8g2lArQiP78ll51UeMZojewtYgIRCKCWqZEgOO8c/uefTI+XDXvCSXu3+YNUaTNvZzobrL5+SqHjBrByRRTdg==, tarball: https://registry.npmjs.org/read-pkg/-/read-pkg-10.1.0.tgz} engines: {node: '>=20'} + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==, tarball: https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz} + engines: {node: '>= 6'} + readdirp@3.6.0: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==, tarball: https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz} engines: {node: '>=8.10.0'} @@ -2035,6 +2235,11 @@ packages: rfdc@1.4.1: resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==, tarball: https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz} + rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==, tarball: https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + rollup@4.62.4: resolution: {integrity: sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg==, tarball: https://registry.npmjs.org/rollup/-/rollup-4.62.4.tgz} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -2075,6 +2280,9 @@ packages: resolution: {integrity: sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==, tarball: https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz} engines: {node: '>= 0.8.0'} + set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==, tarball: https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz} + setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==, tarball: https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz} @@ -2167,6 +2375,9 @@ packages: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==, tarball: https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz} engines: {node: '>=8'} + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==, tarball: https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz} + strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==, tarball: https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz} engines: {node: '>=8'} @@ -2203,6 +2414,11 @@ packages: resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==, tarball: https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz} engines: {node: '>=20'} + tar@6.2.1: + resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==, tarball: https://registry.npmjs.org/tar/-/tar-6.2.1.tgz} + engines: {node: '>=10'} + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + throttleit@1.0.1: resolution: {integrity: sha512-vDZpf9Chs9mAdfY046mcPt8fg5QSZr37hEH4TXYBnDF+izxgrbRGUAAaBvIk/fJm9aOFCGFd1EsNg5AZCbnQCQ==, tarball: https://registry.npmjs.org/throttleit/-/throttleit-1.0.1.tgz} @@ -2238,6 +2454,9 @@ packages: resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==, tarball: https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz} engines: {node: '>=16'} + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==, tarball: https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz} + tree-kill@1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==, tarball: https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz} hasBin: true @@ -2309,6 +2528,9 @@ packages: util-arity@1.1.0: resolution: {integrity: sha512-kkyIsXKwemfSy8ZEoaIz06ApApnWsk5hQO0vLjZS6UkBiGiW++Jsyb8vSBoc0WKlffGoGs5yYy/j5pp8zckrFA==, tarball: https://registry.npmjs.org/util-arity/-/util-arity-1.1.0.tgz} + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==, tarball: https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz} + utils-merge@1.0.1: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==, tarball: https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz} engines: {node: '>= 0.4.0'} @@ -2365,11 +2587,20 @@ packages: engines: {node: '>=20.0.0'} hasBin: true + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==, tarball: https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==, tarball: https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==, tarball: https://registry.npmjs.org/which/-/which-2.0.2.tgz} engines: {node: '>= 8'} hasBin: true + wide-align@1.1.5: + resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==, tarball: https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz} + workerpool@6.5.1: resolution: {integrity: sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==, tarball: https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz} @@ -2395,6 +2626,9 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==, tarball: https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz} + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==, tarball: https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz} + yaml@2.9.0: resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==, tarball: https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz} engines: {node: '>= 14.6'} @@ -2895,6 +3129,21 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@mapbox/node-pre-gyp@1.0.11': + dependencies: + detect-libc: 2.1.2 + https-proxy-agent: 5.0.1 + make-dir: 3.1.0 + node-fetch: 2.7.0 + nopt: 5.0.0 + npmlog: 5.0.1 + rimraf: 3.0.2 + semver: 7.8.5 + tar: 6.2.1 + transitivePeerDependencies: + - encoding + - supports-color + '@napi-rs/lzma-linux-x64-gnu@1.5.1': optional: true @@ -2904,6 +3153,8 @@ snapshots: dependencies: '@noble/hashes': 1.8.0 + '@phc/format@1.0.0': {} + '@prisma/client@5.22.0(prisma@5.22.0)': optionalDependencies: prisma: 5.22.0 @@ -3040,8 +3291,16 @@ snapshots: dependencies: '@types/node': 22.20.1 + '@types/cookie-parser@1.4.10(@types/express@4.17.25)': + dependencies: + '@types/express': 4.17.25 + '@types/cookiejar@2.1.5': {} + '@types/cors@2.8.19': + dependencies: + '@types/node': 22.20.1 + '@types/estree@1.0.9': {} '@types/express-serve-static-core@4.19.9': @@ -3060,10 +3319,17 @@ snapshots: '@types/http-errors@2.0.5': {} + '@types/jsonwebtoken@9.0.10': + dependencies: + '@types/ms': 2.1.0 + '@types/node': 22.20.1 + '@types/methods@1.1.4': {} '@types/mime@1.3.5': {} + '@types/ms@2.1.0': {} + '@types/node@22.20.1': dependencies: undici-types: 6.21.0 @@ -3133,6 +3399,8 @@ snapshots: transitivePeerDependencies: - supports-color + abbrev@1.1.1: {} + accepts@1.3.8: dependencies: mime-types: 2.1.35 @@ -3168,10 +3436,26 @@ snapshots: normalize-path: 3.0.0 picomatch: 2.3.2 + aproba@2.1.0: {} + arch@2.2.0: {} + are-we-there-yet@2.0.0: + dependencies: + delegates: 1.0.0 + readable-stream: 3.6.2 + arg@5.0.2: {} + argon2@0.31.2: + dependencies: + '@mapbox/node-pre-gyp': 1.0.11 + '@phc/format': 1.0.0 + node-addon-api: 7.1.1 + transitivePeerDependencies: + - encoding + - supports-color + argparse@2.0.1: {} array-flatten@1.1.1: {} @@ -3247,6 +3531,11 @@ snapshots: transitivePeerDependencies: - supports-color + brace-expansion@1.1.18: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + brace-expansion@2.1.4: dependencies: balanced-match: 1.0.2 @@ -3267,6 +3556,8 @@ snapshots: buffer-crc32@0.2.13: {} + buffer-equal-constant-time@1.0.1: {} + buffer-from@1.1.2: {} buffer@5.7.1: @@ -3323,6 +3614,8 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + chownr@2.0.0: {} + ci-info@4.4.0: {} clean-stack@2.2.0: {} @@ -3354,6 +3647,8 @@ snapshots: color-name@1.1.4: {} + color-support@1.1.3: {} + colorette@2.0.20: {} combined-stream@1.0.8: @@ -3368,6 +3663,10 @@ snapshots: component-emitter@1.3.1: {} + concat-map@0.0.1: {} + + console-control-strings@1.1.0: {} + content-disposition@0.5.4: dependencies: safe-buffer: 5.2.1 @@ -3376,6 +3675,13 @@ snapshots: convert-source-map@2.0.0: {} + cookie-parser@1.4.7: + dependencies: + cookie: 0.7.2 + cookie-signature: 1.0.6 + + cookie-signature@1.0.6: {} + cookie-signature@1.0.7: {} cookie-signature@1.2.2: {} @@ -3386,6 +3692,11 @@ snapshots: core-util-is@1.0.2: {} + cors@2.8.6: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + cross-env@10.1.0: dependencies: '@epic-web/invariant': 1.0.0 @@ -3483,10 +3794,14 @@ snapshots: delayed-stream@1.0.0: {} + delegates@1.0.0: {} + depd@2.0.0: {} destroy@1.2.0: {} + detect-libc@2.1.2: {} + dezalgo@1.0.4: dependencies: asap: 2.0.6 @@ -3511,6 +3826,10 @@ snapshots: jsbn: 0.1.1 safer-buffer: 2.1.2 + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + ee-first@1.1.1: {} electron-to-chromium@1.5.407: {} @@ -3774,6 +4093,10 @@ snapshots: jsonfile: 6.2.1 universalify: 2.0.1 + fs-minipass@2.1.0: + dependencies: + minipass: 3.3.6 + fs.realpath@1.0.0: {} fsevents@2.3.3: @@ -3781,6 +4104,18 @@ snapshots: function-bind@1.1.2: {} + gauge@3.0.2: + dependencies: + aproba: 2.1.0 + color-support: 1.1.3 + console-control-strings: 1.1.0 + has-unicode: 2.0.1 + object-assign: 4.1.1 + signal-exit: 3.0.7 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wide-align: 1.1.5 + gensync@1.0.0-beta.2: {} get-caller-file@2.0.5: {} @@ -3821,6 +4156,15 @@ snapshots: dependencies: is-glob: 4.0.3 + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.5 + once: 1.4.0 + path-is-absolute: 1.0.1 + glob@8.1.0: dependencies: fs.realpath: 1.0.0 @@ -3853,6 +4197,8 @@ snapshots: dependencies: has-symbols: 1.1.0 + has-unicode@2.0.1: {} + hasown@2.0.4: dependencies: function-bind: 1.1.2 @@ -3987,6 +4333,19 @@ snapshots: optionalDependencies: graceful-fs: 4.2.11 + jsonwebtoken@9.0.3: + dependencies: + jws: 4.0.1 + lodash.includes: 4.3.0 + lodash.isboolean: 3.0.3 + lodash.isinteger: 4.0.4 + lodash.isnumber: 3.0.3 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.once: 4.1.1 + ms: 2.1.3 + semver: 7.8.5 + jsprim@2.0.2: dependencies: assert-plus: 1.0.0 @@ -3994,6 +4353,17 @@ snapshots: json-schema: 0.4.0 verror: 1.10.0 + jwa@2.0.1: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@4.0.1: + dependencies: + jwa: 2.0.1 + safe-buffer: 5.2.1 + knuth-shuffle-seeded@1.0.6: dependencies: seed-random: 2.2.0 @@ -4017,6 +4387,18 @@ snapshots: dependencies: p-locate: 5.0.0 + lodash.includes@4.3.0: {} + + lodash.isboolean@3.0.3: {} + + lodash.isinteger@4.0.4: {} + + lodash.isnumber@3.0.3: {} + + lodash.isplainobject@4.0.6: {} + + lodash.isstring@4.0.1: {} + lodash.merge@4.6.2: {} lodash.mergewith@4.6.2: {} @@ -4053,6 +4435,10 @@ snapshots: luxon@3.7.2: {} + make-dir@3.1.0: + dependencies: + semver: 6.3.1 + map-stream@0.1.0: {} math-intrinsics@1.1.0: {} @@ -4079,12 +4465,29 @@ snapshots: mimic-fn@2.1.0: {} + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.18 + minimatch@5.1.9: dependencies: brace-expansion: 2.1.4 minimist@1.2.8: {} + minipass@3.3.6: + dependencies: + yallist: 4.0.0 + + minipass@5.0.0: {} + + minizlib@2.1.2: + dependencies: + minipass: 3.3.6 + yallist: 4.0.0 + + mkdirp@1.0.4: {} + mocha@10.8.2: dependencies: ansi-colors: 4.1.3 @@ -4116,8 +4519,18 @@ snapshots: negotiator@0.6.3: {} + node-addon-api@7.1.1: {} + + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + node-releases@2.0.53: {} + nopt@5.0.0: + dependencies: + abbrev: 1.1.1 + normalize-package-data@8.0.0: dependencies: hosted-git-info: 9.0.3 @@ -4130,6 +4543,15 @@ snapshots: dependencies: path-key: 3.1.1 + npmlog@5.0.1: + dependencies: + are-we-there-yet: 2.0.0 + console-control-strings: 1.1.0 + gauge: 3.0.2 + set-blocking: 2.0.0 + + object-assign@4.1.1: {} + object-inspect@1.13.4: {} on-finished@2.4.1: @@ -4172,6 +4594,8 @@ snapshots: path-exists@4.0.0: {} + path-is-absolute@1.0.1: {} + path-key@3.1.1: {} path-to-regexp@0.1.13: {} @@ -4276,6 +4700,12 @@ snapshots: type-fest: 5.8.0 unicorn-magic: 0.4.0 + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + readdirp@3.6.0: dependencies: picomatch: 2.3.2 @@ -4301,6 +4731,10 @@ snapshots: rfdc@1.4.1: {} + rimraf@3.0.2: + dependencies: + glob: 7.2.3 + rollup@4.62.4: dependencies: '@types/estree': 1.0.9 @@ -4382,6 +4816,8 @@ snapshots: transitivePeerDependencies: - supports-color + set-blocking@2.0.0: {} + setprototypeof@1.2.0: {} shebang-command@2.0.0: @@ -4500,6 +4936,10 @@ snapshots: is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 @@ -4542,6 +4982,15 @@ snapshots: tagged-tag@1.0.0: {} + tar@6.2.1: + dependencies: + chownr: 2.0.0 + fs-minipass: 2.1.0 + minipass: 5.0.0 + minizlib: 2.1.2 + mkdirp: 1.0.4 + yallist: 4.0.0 + throttleit@1.0.1: {} through@2.3.8: {} @@ -4568,6 +5017,8 @@ snapshots: dependencies: tldts: 6.1.86 + tr46@0.0.3: {} + tree-kill@1.2.2: {} tslib@2.8.1: {} @@ -4619,6 +5070,8 @@ snapshots: util-arity@1.1.0: {} + util-deprecate@1.0.2: {} + utils-merge@1.0.1: {} uuid@8.3.2: {} @@ -4656,10 +5109,21 @@ snapshots: - debug - supports-color + webidl-conversions@3.0.1: {} + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + which@2.0.2: dependencies: isexe: 2.0.0 + wide-align@1.1.5: + dependencies: + string-width: 4.2.3 + workerpool@6.5.1: {} wrap-ansi@6.2.0: @@ -4682,6 +5146,8 @@ snapshots: yallist@3.1.1: {} + yallist@4.0.0: {} + yaml@2.9.0: {} yargs-parser@20.2.9: {}