fix tests isues

This commit is contained in:
root
2026-06-11 01:43:25 -04:00
parent 64df4439d3
commit b22983650d
16 changed files with 10 additions and 1485 deletions
+58
View File
@@ -0,0 +1,58 @@
# npm audit report
postcss <8.5.10
Severity: moderate
PostCSS has XSS via Unescaped </style> in its CSS Stringify Output - https://github.com/advisories/GHSA-qx2v-qp2m-jg93
fix available via `npm audit fix --force`
Will install next@9.3.3, which is a breaking change
node_modules/next/node_modules/postcss
next 9.3.4-canary.0 - 16.3.0-canary.5
Depends on vulnerable versions of postcss
node_modules/next
uuid <11.1.1
Severity: moderate
uuid: Missing buffer bounds check in v3/v5/v6 when buf is provided - https://github.com/advisories/GHSA-w5hq-g745-h8pq
fix available via `npm audit fix --force`
Will install node-cron@4.2.1, which is a breaking change
node_modules/gaxios/node_modules/uuid
node_modules/google-gax/node_modules/uuid
node_modules/node-cron/node_modules/uuid
node_modules/teeny-request/node_modules/uuid
node_modules/uuid
firebase-admin 7.0.0 - 8.2.0 || >=10.2.0
Depends on vulnerable versions of @google-cloud/firestore
Depends on vulnerable versions of @google-cloud/storage
Depends on vulnerable versions of uuid
node_modules/firebase-admin
gaxios 6.4.0 - 6.7.1
Depends on vulnerable versions of uuid
node_modules/gaxios
google-gax 4.0.5-experimental - 4.6.1
Depends on vulnerable versions of retry-request
Depends on vulnerable versions of uuid
node_modules/google-gax
@google-cloud/firestore 7.5.0-pre.0 || 7.6.0 - 7.11.6
Depends on vulnerable versions of google-gax
node_modules/@google-cloud/firestore
node-cron 3.0.2 - 3.0.3
Depends on vulnerable versions of uuid
node_modules/node-cron
teeny-request 3.9.1 - 9.0.0
Depends on vulnerable versions of uuid
node_modules/teeny-request
@google-cloud/storage 2.2.0 - 2.5.0 || >=5.19.0
Depends on vulnerable versions of retry-request
Depends on vulnerable versions of teeny-request
node_modules/@google-cloud/storage
retry-request 7.0.0 - 7.0.2
Depends on vulnerable versions of teeny-request
node_modules/retry-request
11 moderate severity vulnerabilities
To address issues that do not require attention, run:
npm audit fix
To address all issues (including breaking changes), run:
npm audit fix --force
+47
View File
@@ -0,0 +1,47 @@
docker compose -f docker-compose.dev.yml up -d postgres
docker compose -f docker-compose.dev.yml up -d redis
docker compose -p rentaldrivego-prod --env-file .env.docker.production -f docker-compose.production.yml up --build -d --no-deps marketplace
docker compose -p rentaldrivego-prod --env-file .env.docker.production -f docker-compose.production.yml up --build -d --no-deps admin
docker compose -f docker-compose.dev.yml --profile full up --build
docker compose -f docker-compose.dev.yml --profile dashboard up --build
docker compose -f docker-compose.dev.yml --profile marketplace up --build
docker compose -f docker-compose.dev.yml --profile api up --build
Default dev admin:
Email: platform.ops@rentaldrivego.com
Password: PlatformOps123!
What this means:
docker compose -f docker-compose.dev.yml --profile full up --build will no longer reseed or redeploy the DB on every fix.
Your data stays as long as you do not run docker compose ... down -v or manually delete the Docker volumes.
If you later change the schema and want to apply it without wiping data, run:
docker compose -f docker-compose.dev.yml run --rm migrate sh -c "npm run db:deploy"
Production was already using a persistent named Postgres volume, so the main issue was the dev bootstrap behavior. Restart the dev stack once for this change to take effect.
Done. The DOCKER.md production section now covers the full deployment flow from a fresh server. The short version:
# Once per server
docker network create traefik-proxy
# Clone & configure
git clone <repo> rentaldrivego && cd rentaldrivego
cp .env.docker.production.example .env.docker.production
# → fill in POSTGRES_PASSWORD, JWT_SECRET, ACME_EMAIL, RESEND_API_KEY
# Deploy
docker compose -f traefik.yaml up -d
docker compose -p rentaldrivego-prod --env-file .env.docker.production -f docker-compose.production.yml up --build -d
+45
View File
@@ -0,0 +1,45 @@
Deploy dev
Option A — Docker (recommended, full stack)
# First time or after dependency changes:
docker compose -f docker-compose.dev.yml build
# Start everything (api + all frontend apps + postgres + redis):
docker compose -f docker-compose.dev.yml --profile full up
# Or start only what you need (mix and match profiles):
docker compose -f docker-compose.dev.yml --profile api up # api only
docker compose -f docker-compose.dev.yml --profile api --profile dashboard up
First run seeds the database automatically; subsequent runs skip seeding.
Option B — Local (no Docker for apps)
# Start infra only:
docker compose -f docker-compose.dev.yml up postgres redis
# Then in a separate terminal:
npm run dev # starts all apps via turbo
Port map (after port reassignment)
┌─────────────┬───────────────────────────────────────────────┐
│ App │ URL │
├─────────────┼───────────────────────────────────────────────┤
│ public-site │ http://localhost:3000 │
├─────────────┼───────────────────────────────────────────────┤
│ marketplace │ http://localhost:3001/marketplace │
├─────────────┼───────────────────────────────────────────────┤
│ dashboard │ http://localhost:3002/dashboard │
├─────────────┼───────────────────────────────────────────────┤
│ admin │ http://localhost:3003/admin │
├─────────────┼───────────────────────────────────────────────┤
│ api │ http://localhost:4000 │
├─────────────┼───────────────────────────────────────────────┤
│ pgAdmin │ http://localhost:5050 (needs --profile tools) │
└─────────────┴───────────────────────────────────────────────┘
▎ Note: In Docker dev each app is directly accessible on its own port. The /marketplace, /dashboard, /admin proxy
▎ through port 3000 (public-site) only works when running locally (not in separate containers), since localhost inside
▎ a container refers to that container only.
+12
View File
@@ -0,0 +1,12 @@
apps/api/src/index.ts -> apps/api/src/index.ts differ
apps/api/src/middleware/requireApiKey.test.ts -> apps/api/src/middleware/requireApiKey.test.ts differ
apps/api/src/middleware/requireApiKey.ts -> apps/api/src/middleware/requireApiKey.ts differ
apps/api/src/middleware/requireCompanyAuth.test.ts -> apps/api/src/middleware/requireCompanyAuth.test.ts differ
apps/api/src/middleware/requireRenterAuth.test.ts -> apps/api/src/middleware/requireRenterAuth.test.ts differ
apps/api/src/modules/auth/auth.employee.service.ts -> apps/api/src/modules/auth/auth.employee.service.ts differ
apps/api/src/security/tokens.ts -> apps/api/src/security/tokens.ts differ
apps/api/src/tests/helpers/fixtures.ts -> apps/api/src/tests/helpers/fixtures.ts differ
packages/database/prisma/migrations: 20260609233000_drop_legacy_company_api_key
packages/database/prisma/schema.prisma -> packages/database/prisma/schema.prisma differ
packages/database/src/index.d.ts -> packages/database/src/index.d.ts differ
packages/database/src/index.ts -> packages/database/src/index.ts differ
+342
View File
@@ -0,0 +1,342 @@
diff -ru '--exclude=node_modules' car_project_original/apps/api/src/index.ts car_project_work/apps/api/src/index.ts
--- car_project_original/apps/api/src/index.ts 2026-06-09 19:40:36.000000000 +0000
+++ car_project_work/apps/api/src/index.ts 2026-06-09 23:18:30.521017321 +0000
@@ -1,11 +1,11 @@
import http from 'http'
import { Server as SocketIOServer } from 'socket.io'
import cron from 'node-cron'
-import jwt from 'jsonwebtoken'
import { redis } from './lib/redis'
import { prisma } from './lib/prisma'
import { assertStorageConfiguration } from './lib/storage'
import { createApp, corsOrigins } from './app'
+import { verifyAnyActorToken } from './security/tokens'
import { sendNotification } from './services/notificationService'
import {
runTrialExpirationJob,
@@ -29,7 +29,7 @@
const token = socket.handshake.auth?.token as string | undefined
if (!token) return next() // unauthenticated connections allowed; they just don't join rooms
try {
- const payload = jwt.verify(token, process.env.JWT_SECRET!) as { sub: string; type: string }
+ const payload = verifyAnyActorToken(token)
;(socket as any).authenticatedUserId = payload.sub
next()
} catch {
diff -ru '--exclude=node_modules' car_project_original/apps/api/src/middleware/requireApiKey.test.ts car_project_work/apps/api/src/middleware/requireApiKey.test.ts
--- car_project_original/apps/api/src/middleware/requireApiKey.test.ts 2026-06-09 19:40:36.000000000 +0000
+++ car_project_work/apps/api/src/middleware/requireApiKey.test.ts 2026-06-09 23:18:42.795559504 +0000
@@ -1,10 +1,12 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { NextFunction, Request, Response } from 'express'
+import { generateCompanyApiKey } from '../security/apiKeys'
vi.mock('../lib/prisma', () => ({
prisma: {
- company: {
+ companyApiKey: {
findUnique: vi.fn(),
+ update: vi.fn(),
},
},
}))
@@ -42,19 +44,18 @@
message: 'API key required in x-api-key header',
statusCode: 401,
})
- expect(prisma.company.findUnique).not.toHaveBeenCalled()
+ expect((prisma as any).companyApiKey.findUnique).not.toHaveBeenCalled()
expect(next).not.toHaveBeenCalled()
})
- it('rejects unknown API keys', async () => {
- vi.mocked(prisma.company.findUnique).mockResolvedValue(null)
+ it('rejects malformed API keys before database lookup', async () => {
const req = { headers: { 'x-api-key': 'bad-key' } } as unknown as Request
const res = createResponseStub()
const next = vi.fn() as NextFunction
await requireApiKey(req, res, next)
- expect(prisma.company.findUnique).toHaveBeenCalledWith({ where: { apiKey: 'bad-key' } })
+ expect((prisma as any).companyApiKey.findUnique).not.toHaveBeenCalled()
expect(res.status).toHaveBeenCalledWith(401)
expect(res.json).toHaveBeenCalledWith({
error: 'invalid_api_key',
@@ -64,15 +65,91 @@
expect(next).not.toHaveBeenCalled()
})
- it('attaches company context and calls next for valid API keys', async () => {
- const company = { id: 'company_1', apiKey: 'valid-key', name: 'Atlas Cars' }
- vi.mocked(prisma.company.findUnique).mockResolvedValue(company as any)
- const req = { headers: { 'x-api-key': 'valid-key' } } as unknown as Request
+ it('rejects unknown API key prefixes', async () => {
+ const generated = generateCompanyApiKey()
+ vi.mocked((prisma as any).companyApiKey.findUnique).mockResolvedValue(null)
+ const req = { headers: { 'x-api-key': generated.rawKey } } as unknown as Request
const res = createResponseStub()
const next = vi.fn() as NextFunction
await requireApiKey(req, res, next)
+ expect((prisma as any).companyApiKey.findUnique).toHaveBeenCalledWith({
+ where: { prefix: generated.prefix },
+ include: { company: true },
+ })
+ expect(res.status).toHaveBeenCalledWith(401)
+ expect(res.json).toHaveBeenCalledWith({
+ error: 'invalid_api_key',
+ message: 'Invalid API key',
+ statusCode: 401,
+ })
+ expect(next).not.toHaveBeenCalled()
+ })
+
+ it('rejects revoked API keys', async () => {
+ const generated = generateCompanyApiKey()
+ vi.mocked((prisma as any).companyApiKey.findUnique).mockResolvedValue({
+ id: 'key_1',
+ companyId: 'company_1',
+ prefix: generated.prefix,
+ keyHash: generated.keyHash,
+ revokedAt: new Date('2026-06-01T00:00:00.000Z'),
+ company: { id: 'company_1', name: 'Atlas Cars' },
+ })
+ const req = { headers: { 'x-api-key': generated.rawKey } } as unknown as Request
+ const res = createResponseStub()
+ const next = vi.fn() as NextFunction
+
+ await requireApiKey(req, res, next)
+
+ expect(res.status).toHaveBeenCalledWith(401)
+ expect(next).not.toHaveBeenCalled()
+ })
+
+ it('rejects API keys whose secret does not match the stored hash', async () => {
+ const generated = generateCompanyApiKey()
+ const other = generateCompanyApiKey()
+ vi.mocked((prisma as any).companyApiKey.findUnique).mockResolvedValue({
+ id: 'key_1',
+ companyId: 'company_1',
+ prefix: generated.prefix,
+ keyHash: other.keyHash,
+ revokedAt: null,
+ company: { id: 'company_1', name: 'Atlas Cars' },
+ })
+ const req = { headers: { 'x-api-key': generated.rawKey } } as unknown as Request
+ const res = createResponseStub()
+ const next = vi.fn() as NextFunction
+
+ await requireApiKey(req, res, next)
+
+ expect(res.status).toHaveBeenCalledWith(401)
+ expect(next).not.toHaveBeenCalled()
+ })
+
+ it('attaches company context, updates lastUsedAt, and calls next for valid API keys', async () => {
+ const generated = generateCompanyApiKey()
+ const company = { id: 'company_1', name: 'Atlas Cars' }
+ vi.mocked((prisma as any).companyApiKey.findUnique).mockResolvedValue({
+ id: 'key_1',
+ companyId: company.id,
+ prefix: generated.prefix,
+ keyHash: generated.keyHash,
+ revokedAt: null,
+ company,
+ })
+ vi.mocked((prisma as any).companyApiKey.update).mockResolvedValue({})
+ const req = { headers: { 'x-api-key': generated.rawKey } } as unknown as Request
+ const res = createResponseStub()
+ const next = vi.fn() as NextFunction
+
+ await requireApiKey(req, res, next)
+
+ expect((prisma as any).companyApiKey.update).toHaveBeenCalledWith({
+ where: { id: 'key_1' },
+ data: { lastUsedAt: expect.any(Date) },
+ })
expect(req.company).toEqual(company)
expect(req.companyId).toBe('company_1')
expect(next).toHaveBeenCalledTimes(1)
diff -ru '--exclude=node_modules' car_project_original/apps/api/src/middleware/requireApiKey.ts car_project_work/apps/api/src/middleware/requireApiKey.ts
--- car_project_original/apps/api/src/middleware/requireApiKey.ts 2026-06-09 19:44:15.000000000 +0000
+++ car_project_work/apps/api/src/middleware/requireApiKey.ts 2026-06-09 23:18:30.518795557 +0000
@@ -21,14 +21,6 @@
const hashed = hashApiKey(apiKey)
if (!keyRecord || keyRecord.revokedAt || !timingSafeEqualHex(hashed, keyRecord.keyHash)) {
- if (process.env.ALLOW_LEGACY_COMPANY_API_KEYS === 'true') {
- const company = await prisma.company.findUnique({ where: { apiKey } })
- if (company) {
- req.company = company
- req.companyId = company.id
- return next()
- }
- }
return res.status(401).json({ error: 'invalid_api_key', message: 'Invalid API key', statusCode: 401 })
}
diff -ru '--exclude=node_modules' car_project_original/apps/api/src/middleware/requireCompanyAuth.test.ts car_project_work/apps/api/src/middleware/requireCompanyAuth.test.ts
--- car_project_original/apps/api/src/middleware/requireCompanyAuth.test.ts 2026-06-09 20:03:48.000000000 +0000
+++ car_project_work/apps/api/src/middleware/requireCompanyAuth.test.ts 2026-06-09 23:18:50.351586123 +0000
@@ -63,7 +63,7 @@
await requireCompanyAuth(req, res, next)
expect(res.status).toHaveBeenCalledWith(401)
- expect(res.json).toHaveBeenCalledWith({ error: 'invalid_token', message: 'Invalid token type for this endpoint', statusCode: 401 })
+ expect(res.json).toHaveBeenCalledWith({ error: 'invalid_token', message: 'Invalid or expired session token', statusCode: 401 })
expect(prisma.employee.findUnique).not.toHaveBeenCalled()
})
@@ -108,7 +108,11 @@
await requireCompanyDocumentAuth(req, res, next)
- expect(jwt.verify).toHaveBeenCalledWith('cookie-token', 'test-secret')
+ expect(jwt.verify).toHaveBeenCalledWith('cookie-token', 'test-secret', {
+ algorithms: ['HS256'],
+ issuer: 'rentaldrivego-api',
+ audience: 'employee',
+ })
expect(req.companyId).toBe('company_2')
expect(next).toHaveBeenCalledTimes(1)
})
diff -ru '--exclude=node_modules' car_project_original/apps/api/src/middleware/requireRenterAuth.test.ts car_project_work/apps/api/src/middleware/requireRenterAuth.test.ts
--- car_project_original/apps/api/src/middleware/requireRenterAuth.test.ts 2026-06-09 19:40:36.000000000 +0000
+++ car_project_work/apps/api/src/middleware/requireRenterAuth.test.ts 2026-06-09 23:18:50.352660214 +0000
@@ -49,7 +49,7 @@
await requireRenterAuth(req, res, next)
expect(res.status).toHaveBeenCalledWith(401)
- expect(res.json).toHaveBeenCalledWith({ error: 'invalid_token', message: 'Invalid token type', statusCode: 401 })
+ expect(res.json).toHaveBeenCalledWith({ error: 'invalid_token', message: 'Invalid or expired token', statusCode: 401 })
expect(prisma.renter.findUnique).not.toHaveBeenCalled()
})
diff -ru '--exclude=node_modules' car_project_original/apps/api/src/modules/auth/auth.employee.service.ts car_project_work/apps/api/src/modules/auth/auth.employee.service.ts
--- car_project_original/apps/api/src/modules/auth/auth.employee.service.ts 2026-06-09 19:42:41.000000000 +0000
+++ car_project_work/apps/api/src/modules/auth/auth.employee.service.ts 2026-06-09 23:19:29.323222951 +0000
@@ -41,13 +41,22 @@
pwdv: getEmployeePasswordResetVersion(passwordHash),
},
process.env.JWT_SECRET!,
- { expiresIn: `${RESET_TOKEN_TTL_MINUTES}m` },
+ {
+ algorithm: 'HS256',
+ issuer: 'rentaldrivego-api',
+ audience: 'employee_password_reset',
+ expiresIn: `${RESET_TOKEN_TTL_MINUTES}m`,
+ },
)
}
function verifyEmployeePasswordResetToken(token: string): EmployeePasswordResetPayload | null {
try {
- const payload = jwt.verify(token, process.env.JWT_SECRET!) as jwt.JwtPayload
+ const payload = jwt.verify(token, process.env.JWT_SECRET!, {
+ algorithms: ['HS256'],
+ issuer: 'rentaldrivego-api',
+ audience: 'employee_password_reset',
+ }) as jwt.JwtPayload
if (
typeof payload.sub !== 'string' ||
diff -ru '--exclude=node_modules' car_project_original/apps/api/src/security/tokens.ts car_project_work/apps/api/src/security/tokens.ts
--- car_project_original/apps/api/src/security/tokens.ts 2026-06-09 20:20:10.000000000 +0000
+++ car_project_work/apps/api/src/security/tokens.ts 2026-06-09 23:18:30.519590128 +0000
@@ -54,3 +54,14 @@
return payload as ActorTokenPayload
}
+
+export function verifyAnyActorToken(token: string): ActorTokenPayload {
+ const decoded = jwt.decode(token) as jwt.JwtPayload | null
+ const actorType = decoded?.type
+
+ if (actorType !== 'admin' && actorType !== 'employee' && actorType !== 'renter') {
+ throw new Error('Invalid actor token')
+ }
+
+ return verifyActorToken(token, actorType)
+}
diff -ru '--exclude=node_modules' car_project_original/apps/api/src/tests/helpers/fixtures.ts car_project_work/apps/api/src/tests/helpers/fixtures.ts
--- car_project_original/apps/api/src/tests/helpers/fixtures.ts 2026-06-09 19:40:36.000000000 +0000
+++ car_project_work/apps/api/src/tests/helpers/fixtures.ts 2026-06-09 23:19:11.289635569 +0000
@@ -1,8 +1,6 @@
import bcrypt from 'bcryptjs'
-import jwt from 'jsonwebtoken'
import { prisma } from '../../lib/prisma'
-
-const JWT_SECRET = process.env.JWT_SECRET ?? 'test-secret'
+import { signActorToken } from '../../security/tokens'
let counter = 0
function uid() {
@@ -215,28 +213,16 @@
})
}
-export function signEmployeeToken(employeeId: string, companyId: string, role: string = 'OWNER') {
- return jwt.sign(
- { sub: employeeId, companyId, role, type: 'employee' },
- JWT_SECRET,
- { expiresIn: '1h' },
- )
+export function signEmployeeToken(employeeId: string, _companyId: string, _role: string = 'OWNER') {
+ return signActorToken(employeeId, 'employee', { expiresIn: '1h' })
}
export function signAdminToken(adminId: string) {
- return jwt.sign(
- { sub: adminId, type: 'admin' },
- JWT_SECRET,
- { expiresIn: '1h' },
- )
+ return signActorToken(adminId, 'admin', { expiresIn: '1h', last2faAt: Date.now() })
}
export function signRenterToken(renterId: string) {
- return jwt.sign(
- { sub: renterId, type: 'renter' },
- JWT_SECRET,
- { expiresIn: '1h' },
- )
+ return signActorToken(renterId, 'renter', { expiresIn: '1h' })
}
export function authHeader(token: string) {
Only in car_project_work/packages/database/prisma/migrations: 20260609233000_drop_legacy_company_api_key
diff -ru '--exclude=node_modules' car_project_original/packages/database/prisma/schema.prisma car_project_work/packages/database/prisma/schema.prisma
--- car_project_original/packages/database/prisma/schema.prisma 2026-06-09 20:18:52.000000000 +0000
+++ car_project_work/packages/database/prisma/schema.prisma 2026-06-09 23:18:30.516362264 +0000
@@ -502,7 +502,6 @@
address Json?
status CompanyStatus @default(PENDING)
subscriptionPaymentRef String?
- apiKey String @unique @default(cuid())
subscription Subscription?
billingAccounts BillingAccount[]
diff -ru '--exclude=node_modules' car_project_original/packages/database/src/index.d.ts car_project_work/packages/database/src/index.d.ts
--- car_project_original/packages/database/src/index.d.ts 2026-06-09 19:40:36.000000000 +0000
+++ car_project_work/packages/database/src/index.d.ts 2026-06-09 23:18:30.517981475 +0000
@@ -33,7 +33,6 @@
email: string
phone: string | null
status: string
- apiKey: string
}
export interface Employee {
diff -ru '--exclude=node_modules' car_project_original/packages/database/src/index.ts car_project_work/packages/database/src/index.ts
--- car_project_original/packages/database/src/index.ts 2026-06-09 19:40:36.000000000 +0000
+++ car_project_work/packages/database/src/index.ts 2026-06-09 23:18:30.517228009 +0000
@@ -33,7 +33,6 @@
email: string
phone: string | null
status: string
- apiKey: string
}
export interface Employee {
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,32 @@
A SECURITY_HARDENING_LEFTOVER_APPLIED_REPORT.md
M apps/admin/src/app/dashboard/admin-users/page.tsx
M apps/admin/src/app/dashboard/companies/[id]/page.tsx
M apps/admin/src/app/dashboard/containers/page.tsx
M apps/admin/src/app/dashboard/pricing/page.tsx
M apps/admin/src/app/dashboard/renters/page.tsx
M apps/admin/src/app/forgot-password/page.tsx
M apps/admin/src/app/reset-password/page.tsx
A apps/admin/src/middleware.ts
M apps/api/src/app.ts
M apps/api/src/index.ts
M apps/api/src/middleware/rateLimiter.ts
M apps/api/src/modules/admin/admin.repo.ts
M apps/api/src/modules/admin/admin.routes.ts
M apps/api/src/modules/admin/admin.schemas.ts
M apps/api/src/modules/admin/admin.service.ts
M apps/api/src/swagger/openapi.ts
M apps/dashboard/README.md
M apps/dashboard/src/app/(dashboard)/team/page.tsx
M apps/dashboard/src/app/sign-in/[[...sign-in]]/SignInPageClient.tsx
M apps/dashboard/src/components/layout/Sidebar.tsx
M apps/dashboard/src/components/layout/TopBar.tsx
M apps/dashboard/src/lib/api.ts
M apps/dashboard/src/middleware.test.ts
M apps/dashboard/src/middleware.ts
M apps/marketplace/src/middleware.test.ts
M apps/marketplace/src/middleware.ts
M docs/project-design/COOKIE_POLICY.md
M memory/project_auth_architecture.md
A packages/database/prisma/migrations/20260610001500_add_admin_recovery_codes/migration.sql
M packages/database/prisma/schema.prisma
M scripts/security-static-check.mjs