diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..788b1f4 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,10 @@ +node_modules +.git +.github +.next +dist +*.md +!README.md +.env* +.turbo +apps/desktop diff --git a/apps/api/Dockerfile b/apps/api/Dockerfile new file mode 100644 index 0000000..8501023 --- /dev/null +++ b/apps/api/Dockerfile @@ -0,0 +1,16 @@ +FROM node:22-slim +ENV PNPM_HOME="/pnpm" +ENV PATH="$PNPM_HOME:$PATH" +RUN corepack enable && corepack prepare pnpm@9.0.0 --activate + +WORKDIR /app +COPY pnpm-lock.yaml pnpm-workspace.yaml package.json turbo.json ./ +COPY apps/api/ ./apps/api/ +COPY packages/ ./packages/ +RUN pnpm install --frozen-lockfile +RUN pnpm --filter @repo/api build + +ENV NODE_ENV=production +USER node +EXPOSE 8080 +CMD ["node", "apps/api/dist/index.js"] diff --git a/apps/api/package.json b/apps/api/package.json index 892e65e..c6c6542 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -7,7 +7,9 @@ "dev": "tsx watch src/index.ts", "build": "tsup && tsc --declaration --emitDeclarationOnly --outDir dist", "start": "node dist/index.js", - "check-types": "tsc --noEmit" + "check-types": "tsc --noEmit", + "test": "vitest run", + "test:watch": "vitest" }, "dependencies": { "@aws-sdk/client-s3": "^3.1004.0", diff --git a/apps/api/src/__tests__/auth-guard.test.ts b/apps/api/src/__tests__/auth-guard.test.ts new file mode 100644 index 0000000..f3e897f --- /dev/null +++ b/apps/api/src/__tests__/auth-guard.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, it } from "vitest" +import { client } from "./helpers" + +describe("Auth Guard — unauthenticated requests return 401", () => { + it("GET /v1/allies", async () => { + const res = await client.v1.allies.$get() + expect(res.status).toBe(401) + }) + + it("GET /v1/dms", async () => { + const res = await client.v1.dms.$get({ + query: { + page: 1, + perPage: 50, + }, + }) + expect(res.status).toBe(401) + }) + + it("GET /v1/blocks", async () => { + const res = await client.v1.blocks.$get() + expect(res.status).toBe(401) + }) + + it("GET /v1/notification-settings", async () => { + const res = await client.v1["notification-settings"].$get() + expect(res.status).toBe(401) + }) + + it("GET /v1/privacy-settings", async () => { + const res = await client.v1["privacy-settings"].$get() + expect(res.status).toBe(401) + }) + + it("POST /v1/allies/requests", async () => { + const res = await client.v1.allies.requests.$post({ + json: { userId: "fake-id" }, + }) + expect(res.status).toBe(401) + }) +}) diff --git a/apps/api/src/__tests__/health.test.ts b/apps/api/src/__tests__/health.test.ts new file mode 100644 index 0000000..8e370e1 --- /dev/null +++ b/apps/api/src/__tests__/health.test.ts @@ -0,0 +1,15 @@ +import { describe, expect, it } from "vitest" +import app from "@/app" + +describe("Health Check", () => { + it("GET / returns 200 with status ok", async () => { + const res = await app.request("/") + expect(res.status).toBe(200) + expect(await res.json()).toEqual({ status: "ok" }) + }) + + it("GET /unknown returns 404", async () => { + const res = await app.request("/this-does-not-exist") + expect(res.status).toBe(404) + }) +}) diff --git a/apps/api/src/__tests__/helpers.ts b/apps/api/src/__tests__/helpers.ts new file mode 100644 index 0000000..1728561 --- /dev/null +++ b/apps/api/src/__tests__/helpers.ts @@ -0,0 +1,22 @@ +import type { ClientRequestOptions } from "hono/client" +import { testClient } from "hono/testing" +import app, { type AppType } from "@/app" + +/** + * Typed test client for the Townhall API. + * Provides autocomplete on routes, params, and response bodies. + */ +export const client = testClient(app as unknown as AppType) +/** + * Creates request options with a session cookie for authenticated requests. + * + * Usage: + * const res = await client.v1.allies.$get(undefined, withSession(cookie)) + */ +export function withSession(sessionCookie: string): ClientRequestOptions { + return { + headers: { + Cookie: sessionCookie, + }, + } +} diff --git a/apps/api/vitest.config.ts b/apps/api/vitest.config.ts new file mode 100644 index 0000000..0d11a37 --- /dev/null +++ b/apps/api/vitest.config.ts @@ -0,0 +1,14 @@ +import { resolve } from "node:path" +import { defineConfig } from "vitest/config" + +export default defineConfig({ + test: { + environment: "node", + include: ["src/**/*.test.ts"], + }, + resolve: { + alias: { + "@": resolve(__dirname, "src"), + }, + }, +}) diff --git a/apps/realtime/Dockerfile b/apps/realtime/Dockerfile new file mode 100644 index 0000000..11a155d --- /dev/null +++ b/apps/realtime/Dockerfile @@ -0,0 +1,16 @@ +FROM node:22-slim +ENV PNPM_HOME="/pnpm" +ENV PATH="$PNPM_HOME:$PATH" +RUN corepack enable && corepack prepare pnpm@9.0.0 --activate + +WORKDIR /app +COPY pnpm-lock.yaml pnpm-workspace.yaml package.json turbo.json ./ +COPY apps/realtime/ ./apps/realtime/ +COPY packages/ ./packages/ +RUN pnpm install --frozen-lockfile +RUN pnpm --filter @repo/realtime build + +ENV NODE_ENV=production +USER node +EXPOSE 8000 +CMD ["node", "apps/realtime/dist/index.js"] diff --git a/apps/worker/Dockerfile b/apps/worker/Dockerfile new file mode 100644 index 0000000..22fc8d5 --- /dev/null +++ b/apps/worker/Dockerfile @@ -0,0 +1,15 @@ +FROM node:22-slim +ENV PNPM_HOME="/pnpm" +ENV PATH="$PNPM_HOME:$PATH" +RUN corepack enable && corepack prepare pnpm@9.0.0 --activate + +WORKDIR /app +COPY pnpm-lock.yaml pnpm-workspace.yaml package.json turbo.json ./ +COPY apps/worker/ ./apps/worker/ +COPY packages/ ./packages/ +RUN pnpm install --frozen-lockfile +RUN pnpm --filter @repo/worker build + +ENV NODE_ENV=production +USER node +CMD ["node", "apps/worker/dist/index.js"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..cca16ac --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,116 @@ +services: + postgres: + image: postgres:16-alpine + restart: unless-stopped + environment: + POSTGRES_USER: townhall + POSTGRES_PASSWORD: townhall + POSTGRES_DB: townhall + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U townhall"] + interval: 5s + timeout: 3s + retries: 5 + + redis: + image: redis:7-alpine + restart: unless-stopped + ports: + - "6379:6379" + volumes: + - redis_data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 3s + retries: 5 + + api: + build: + context: . + dockerfile: apps/api/Dockerfile + restart: unless-stopped + ports: + - "8080:8080" + environment: + NODE_ENV: production + PORT: 8080 + DATABASE_URL: postgresql://townhall:townhall@postgres:5432/townhall + REDIS_URL: redis://redis:6379 + BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET} + NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-http://localhost:8080} + S3_ENDPOINT: ${S3_ENDPOINT} + S3_ACCESS_KEY_ID: ${S3_ACCESS_KEY_ID} + S3_SECRET_ACCESS_KEY: ${S3_SECRET_ACCESS_KEY} + S3_BUCKET_NAME: ${S3_BUCKET_NAME} + S3_REGION: ${S3_REGION:-auto} + S3_PUBLIC_URL: ${S3_PUBLIC_URL} + RESEND_API_KEY: ${RESEND_API_KEY} + EMAIL_FROM: ${EMAIL_FROM:-Townhall } + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + + realtime: + build: + context: . + dockerfile: apps/realtime/Dockerfile + restart: unless-stopped + ports: + - "8000:8000" + environment: + NODE_ENV: production + REALTIME_PORT: 8000 + DATABASE_URL: postgresql://townhall:townhall@postgres:5432/townhall + REDIS_URL: redis://redis:6379 + BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET} + NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-http://localhost:8080} + REALTIME_CORS_ORIGIN: ${REALTIME_CORS_ORIGIN:-http://localhost:3000,http://localhost:3001} + S3_ENDPOINT: ${S3_ENDPOINT} + S3_ACCESS_KEY_ID: ${S3_ACCESS_KEY_ID} + S3_SECRET_ACCESS_KEY: ${S3_SECRET_ACCESS_KEY} + S3_BUCKET_NAME: ${S3_BUCKET_NAME} + S3_REGION: ${S3_REGION:-auto} + S3_PUBLIC_URL: ${S3_PUBLIC_URL} + RESEND_API_KEY: ${RESEND_API_KEY} + EMAIL_FROM: ${EMAIL_FROM:-Townhall } + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + + worker: + build: + context: . + dockerfile: apps/worker/Dockerfile + restart: unless-stopped + environment: + NODE_ENV: production + DATABASE_URL: postgresql://townhall:townhall@postgres:5432/townhall + REDIS_URL: redis://redis:6379 + NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-http://localhost:8080} + S3_ENDPOINT: ${S3_ENDPOINT} + S3_ACCESS_KEY_ID: ${S3_ACCESS_KEY_ID} + S3_SECRET_ACCESS_KEY: ${S3_SECRET_ACCESS_KEY} + S3_BUCKET_NAME: ${S3_BUCKET_NAME} + S3_REGION: ${S3_REGION:-auto} + S3_PUBLIC_URL: ${S3_PUBLIC_URL} + BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET} + RESEND_API_KEY: ${RESEND_API_KEY} + EMAIL_FROM: ${EMAIL_FROM:-Townhall } + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + +volumes: + postgres_data: + redis_data: diff --git a/package.json b/package.json index 592800e..7b01daf 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "generate:auth-schema": "pnpm --filter @repo/auth exec npx @better-auth/cli@latest generate --output ../../packages/db/src/generated-schema.ts", "db:push": "pnpm --filter @repo/db db:push", "db:studio": "pnpm --filter @repo/db db:studio", + "test": "turbo run test", "desktop": "pnpm --filter desktop dev", "prepare": "husky" }, @@ -18,7 +19,8 @@ "@types/node": "^25.2.2", "husky": "^9.1.7", "turbo": "^2.8.3", - "typescript": "5.9.3" + "typescript": "5.9.3", + "vitest": "^4.1.2" }, "packageManager": "pnpm@9.0.0", "engines": { diff --git a/packages/auth/src/lib/auth.ts b/packages/auth/src/lib/auth.ts index 5ba5c14..821f6d3 100644 --- a/packages/auth/src/lib/auth.ts +++ b/packages/auth/src/lib/auth.ts @@ -149,7 +149,7 @@ export const auth = betterAuth({

Reset Your Password

Click the button below to reset your password.

- Reset Password + Reset Password

If you didn't request a password reset, you can safely ignore this email.

`, @@ -177,7 +177,7 @@ export const auth = betterAuth({

Welcome to Townhall

Click the button below to verify your email address and get started.

- Verify Email + Verify Email

If you didn't create a Townhall account, you can safely ignore this email.

`, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4eab904..30ca8ed 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,9 @@ importers: typescript: specifier: 5.9.3 version: 5.9.3 + vitest: + specifier: ^4.1.2 + version: 4.1.2(@types/node@25.2.2)(msw@2.12.9(@types/node@25.2.2)(typescript@5.9.3))(vite@6.4.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)) apps/api: dependencies: @@ -430,7 +433,7 @@ importers: version: link:../logger better-auth: specifier: ^1.4.18 - version: 1.4.18(drizzle-kit@0.31.9)(drizzle-orm@0.45.1(kysely@0.28.11)(postgres@3.4.8))(next@16.1.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 1.4.18(drizzle-kit@0.31.9)(drizzle-orm@0.45.1(kysely@0.28.11)(postgres@3.4.8))(next@16.1.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.1.2(@types/node@25.2.2)(msw@2.12.9(@types/node@25.2.2)(typescript@5.9.3))(vite@6.4.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2))) ioredis: specifier: ^5.10.0 version: 5.10.0 @@ -3428,12 +3431,18 @@ packages: '@types/babel__traverse@7.28.0': resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@types/cors@2.8.19': resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==} '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/estree-jsx@1.0.5': resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} @@ -3496,6 +3505,35 @@ packages: peerDependencies: vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + '@vitest/expect@4.1.2': + resolution: {integrity: sha512-gbu+7B0YgUJ2nkdsRJrFFW6X7NTP44WlhiclHniUhxADQJH5Szt9mZ9hWnJPJ8YwOK5zUOSSlSvyzRf0u1DSBQ==} + + '@vitest/mocker@4.1.2': + resolution: {integrity: sha512-Ize4iQtEALHDttPRCmN+FKqOl2vxTiNUhzobQFFt/BM1lRUTG7zRCLOykG/6Vo4E4hnUdfVLo5/eqKPukcWW7Q==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.2': + resolution: {integrity: sha512-dwQga8aejqeuB+TvXCMzSQemvV9hNEtDDpgUKDzOmNQayl2OG241PSWeJwKRH3CiC+sESrmoFd49rfnq7T4RnA==} + + '@vitest/runner@4.1.2': + resolution: {integrity: sha512-Gr+FQan34CdiYAwpGJmQG8PgkyFVmARK8/xSijia3eTFgVfpcpztWLuP6FttGNfPLJhaZVP/euvujeNYar36OQ==} + + '@vitest/snapshot@4.1.2': + resolution: {integrity: sha512-g7yfUmxYS4mNxk31qbOYsSt2F4m1E02LFqO53Xpzg3zKMhLAPZAjjfyl9e6z7HrW6LvUdTwAQR3HHfLjpko16A==} + + '@vitest/spy@4.1.2': + resolution: {integrity: sha512-DU4fBnbVCJGNBwVA6xSToNXrkZNSiw59H8tcuUspVMsBDBST4nfvsPsEHDHGtWRRnqBERBQu7TrTKskmjqTXKA==} + + '@vitest/utils@4.1.2': + resolution: {integrity: sha512-xw2/TiX82lQHA06cgbqRKFb5lCAy3axQ4H4SoUFhUsg+wztiet+co86IAMDtF6Vm1hc7J6j09oh/rgDn+JdKIQ==} + accepts@1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} @@ -3554,6 +3592,10 @@ packages: resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} engines: {node: '>=10'} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + ast-types@0.16.1: resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==} engines: {node: '>=4'} @@ -3715,6 +3757,10 @@ packages: ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + chalk@5.6.2: resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} @@ -4169,6 +4215,9 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} + es-module-lexer@2.0.0: + resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==} + es-object-atoms@1.1.1: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} @@ -4216,6 +4265,9 @@ packages: estree-util-is-identifier-name@3.0.0: resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + etag@1.8.1: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} @@ -4236,6 +4288,10 @@ packages: resolution: {integrity: sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==} engines: {node: ^18.19.0 || >=20.5.0} + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + express-rate-limit@8.2.1: resolution: {integrity: sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==} engines: {node: '>= 16'} @@ -5147,6 +5203,9 @@ packages: resolution: {integrity: sha512-EFVjAYfzWqWsBMRHPMAXLCDIJnpMhdWAqR7xG6M6a2cs6PMFpl/+Z20w9zDW4vkxOFfddegBKq9Rehd0bxWE7A==} engines: {node: '>= 10'} + obug@2.1.1: + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + on-exit-leak-free@2.1.2: resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} engines: {node: '>=14.0.0'} @@ -5700,6 +5759,9 @@ packages: resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} engines: {node: '>= 0.4'} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} @@ -5756,6 +5818,9 @@ packages: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + standard-as-callback@2.1.0: resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} @@ -5766,6 +5831,9 @@ packages: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} + std-env@4.0.0: + resolution: {integrity: sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==} + stdin-discarder@0.2.2: resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==} engines: {node: '>=18'} @@ -5875,6 +5943,9 @@ packages: tiny-warning@1.0.3: resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==} + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + tinyexec@0.3.2: resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} @@ -5886,6 +5957,10 @@ packages: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} + engines: {node: '>=14.0.0'} + tldts-core@7.0.23: resolution: {integrity: sha512-0g9vrtDQLrNIiCj22HSe9d4mLVG3g5ph5DZ8zCKBr4OtrspmNB6ss7hVyzArAeE88ceZocIEGkyW1Ime7fxPtQ==} @@ -6155,6 +6230,41 @@ packages: yaml: optional: true + vitest@4.1.2: + resolution: {integrity: sha512-xjR1dMTVHlFLh98JE3i/f/WePqJsah4A0FK9cc8Ehp9Udk0AZk6ccpIZhh1qJ/yxVWRZ+Q54ocnD8TXmkhspGg==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.2 + '@vitest/browser-preview': 4.1.2 + '@vitest/browser-webdriverio': 4.1.2 + '@vitest/ui': 4.1.2 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + w3c-keyname@2.2.8: resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} @@ -6184,6 +6294,11 @@ packages: engines: {node: ^16.13.0 || >=18.0.0} hasBin: true + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + wrap-ansi@6.2.0: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} engines: {node: '>=8'} @@ -9290,6 +9405,11 @@ snapshots: dependencies: '@babel/types': 7.29.0 + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + '@types/cors@2.8.19': dependencies: '@types/node': 25.2.2 @@ -9298,6 +9418,8 @@ snapshots: dependencies: '@types/ms': 2.1.0 + '@types/deep-eql@4.0.2': {} + '@types/estree-jsx@1.0.5': dependencies: '@types/estree': 1.0.8 @@ -9361,6 +9483,48 @@ snapshots: transitivePeerDependencies: - supports-color + '@vitest/expect@4.1.2': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.2 + '@vitest/utils': 4.1.2 + chai: 6.2.2 + tinyrainbow: 3.1.0 + + '@vitest/mocker@4.1.2(msw@2.12.9(@types/node@25.2.2)(typescript@5.9.3))(vite@6.4.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2))': + dependencies: + '@vitest/spy': 4.1.2 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + msw: 2.12.9(@types/node@25.2.2)(typescript@5.9.3) + vite: 6.4.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2) + + '@vitest/pretty-format@4.1.2': + dependencies: + tinyrainbow: 3.1.0 + + '@vitest/runner@4.1.2': + dependencies: + '@vitest/utils': 4.1.2 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.2': + dependencies: + '@vitest/pretty-format': 4.1.2 + '@vitest/utils': 4.1.2 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.2': {} + + '@vitest/utils@4.1.2': + dependencies: + '@vitest/pretty-format': 4.1.2 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 + accepts@1.3.8: dependencies: mime-types: 2.1.35 @@ -9409,6 +9573,8 @@ snapshots: dependencies: tslib: 2.8.1 + assertion-error@2.0.1: {} + ast-types@0.16.1: dependencies: tslib: 2.8.1 @@ -9432,7 +9598,7 @@ snapshots: baseline-browser-mapping@2.9.19: {} - better-auth@1.4.18(drizzle-kit@0.31.9)(drizzle-orm@0.45.1(kysely@0.28.11)(postgres@3.4.8))(next@16.1.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + better-auth@1.4.18(drizzle-kit@0.31.9)(drizzle-orm@0.45.1(kysely@0.28.11)(postgres@3.4.8))(next@16.1.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.1.2(@types/node@25.2.2)(msw@2.12.9(@types/node@25.2.2)(typescript@5.9.3))(vite@6.4.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2))): dependencies: '@better-auth/core': 1.4.18(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0) '@better-auth/telemetry': 1.4.18(@better-auth/core@1.4.18(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0)) @@ -9452,6 +9618,7 @@ snapshots: next: 16.1.6(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react: 19.2.4 react-dom: 19.2.4(react@19.2.4) + vitest: 4.1.2(@types/node@25.2.2)(msw@2.12.9(@types/node@25.2.2)(typescript@5.9.3))(vite@6.4.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)) better-call@1.1.8(zod@4.3.6): dependencies: @@ -9537,6 +9704,8 @@ snapshots: ccount@2.0.1: {} + chai@6.2.2: {} + chalk@5.6.2: {} character-entities-html4@2.1.0: {} @@ -9878,6 +10047,8 @@ snapshots: es-errors@1.3.0: {} + es-module-lexer@2.0.0: {} + es-object-atoms@1.1.1: dependencies: es-errors: 1.3.0 @@ -9984,6 +10155,10 @@ snapshots: estree-util-is-identifier-name@3.0.0: {} + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + etag@1.8.1: {} eventsource-parser@3.0.6: {} @@ -10019,6 +10194,8 @@ snapshots: strip-final-newline: 4.0.0 yoctocolors: 2.1.2 + expect-type@1.3.0: {} + express-rate-limit@8.2.1(express@5.2.1): dependencies: express: 5.2.1 @@ -11093,6 +11270,8 @@ snapshots: object-treeify@1.1.33: {} + obug@2.1.1: {} + on-exit-leak-free@2.1.2: {} on-finished@2.4.1: @@ -11890,6 +12069,8 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} + signal-exit@3.0.7: {} signal-exit@4.1.0: {} @@ -11961,6 +12142,8 @@ snapshots: split2@4.2.0: {} + stackback@0.0.2: {} + standard-as-callback@2.1.0: {} standardwebhooks@1.0.0: @@ -11970,6 +12153,8 @@ snapshots: statuses@2.0.2: {} + std-env@4.0.0: {} + stdin-discarder@0.2.2: {} strict-event-emitter@0.5.1: {} @@ -12071,6 +12256,8 @@ snapshots: tiny-warning@1.0.3: {} + tinybench@2.9.0: {} + tinyexec@0.3.2: {} tinyexec@1.0.2: {} @@ -12080,6 +12267,8 @@ snapshots: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 + tinyrainbow@3.1.0: {} + tldts-core@7.0.23: {} tldts@7.0.23: @@ -12317,6 +12506,33 @@ snapshots: tsx: 4.21.0 yaml: 2.8.2 + vitest@4.1.2(@types/node@25.2.2)(msw@2.12.9(@types/node@25.2.2)(typescript@5.9.3))(vite@6.4.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)): + dependencies: + '@vitest/expect': 4.1.2 + '@vitest/mocker': 4.1.2(msw@2.12.9(@types/node@25.2.2)(typescript@5.9.3))(vite@6.4.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)) + '@vitest/pretty-format': 4.1.2 + '@vitest/runner': 4.1.2 + '@vitest/snapshot': 4.1.2 + '@vitest/spy': 4.1.2 + '@vitest/utils': 4.1.2 + es-module-lexer: 2.0.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.3 + std-env: 4.0.0 + tinybench: 2.9.0 + tinyexec: 1.0.2 + tinyglobby: 0.2.15 + tinyrainbow: 3.1.0 + vite: 6.4.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 25.2.2 + transitivePeerDependencies: + - msw + w3c-keyname@2.2.8: {} web-streams-polyfill@3.3.3: {} @@ -12337,6 +12553,11 @@ snapshots: dependencies: isexe: 3.1.5 + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + wrap-ansi@6.2.0: dependencies: ansi-styles: 4.3.0 diff --git a/turbo.json b/turbo.json index d51f876..7e636d6 100644 --- a/turbo.json +++ b/turbo.json @@ -14,6 +14,9 @@ "check-types": { "dependsOn": ["^check-types"] }, + "test": { + "dependsOn": ["^build"] + }, "dev": { "cache": false, "persistent": true