MeTube
+{channel?.name ?? "Channel"}
++ @{user.username} + · + Manage uploads and channel details +
+diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..68dff75
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,11 @@
+.git
+.gitignore
+node_modules
+.next
+npm-debug.log*
+
+.env
+.env.*
+
+videos
+thumbnails
diff --git a/.gitignore b/.gitignore
index 5ef6a52..e953f42 100644
--- a/.gitignore
+++ b/.gitignore
@@ -39,3 +39,11 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts
+
+/src/generated/prisma
+
+# local media uploads
+/videos/*
+!/videos/.gitkeep
+/thumbnails/*
+!/thumbnails/.gitkeep
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..1d8580d
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,20 @@
+ARG BASE_IMAGE=mirror.gcr.io/library/node:20-alpine
+FROM ${BASE_IMAGE}
+
+WORKDIR /app
+
+COPY package.json package-lock.json ./
+RUN npm ci
+
+COPY . .
+
+ARG DATABASE_URL="postgresql://postgres:postgres@localhost:5432/metube?schema=public"
+RUN DATABASE_URL="$DATABASE_URL" SESSION_SECRET="metube-build-session-secret-change-me-12345" npm run build
+
+ENV NODE_ENV=production
+ENV PORT=3000
+ENV HOSTNAME=0.0.0.0
+
+EXPOSE 3000
+
+CMD ["npm", "run", "start"]
diff --git a/README.md b/README.md
index e215bc4..f7d2ec8 100644
--- a/README.md
+++ b/README.md
@@ -1,36 +1 @@
-This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
-
-## Getting Started
-
-First, run the development server:
-
-```bash
-npm run dev
-# or
-yarn dev
-# or
-pnpm dev
-# or
-bun dev
-```
-
-Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
-
-You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
-
-This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
-
-## Learn More
-
-To learn more about Next.js, take a look at the following resources:
-
-- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
-- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
-
-You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
-
-## Deploy on Vercel
-
-The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
-
-Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
+# MeTube
\ No newline at end of file
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..0fb73ce
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,39 @@
+services:
+ app:
+ build:
+ context: .
+ dockerfile: Dockerfile
+ args:
+ BASE_IMAGE: ${BASE_IMAGE:-mirror.gcr.io/library/node:20-alpine}
+ DATABASE_URL: postgresql://postgres:postgres@db:5432/metube?schema=public
+ depends_on:
+ db:
+ condition: service_healthy
+ environment:
+ DATABASE_URL: postgresql://postgres:postgres@db:5432/metube?schema=public
+ env_file:
+ - .env
+ ports:
+ - "3000:3000"
+ volumes:
+ - ./videos:/app/videos
+ - ./thumbnails:/app/thumbnails
+
+ db:
+ image: ${POSTGRES_IMAGE:-mirror.gcr.io/library/postgres:16-alpine}
+ environment:
+ POSTGRES_DB: metube
+ POSTGRES_USER: postgres
+ POSTGRES_PASSWORD: postgres
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready -U postgres -d metube"]
+ interval: 5s
+ timeout: 5s
+ retries: 5
+ volumes:
+ - postgres_data:/var/lib/postgresql/data
+ - ./prisma/migrations/0001_init/migration.sql:/docker-entrypoint-initdb.d/001_init.sql:ro
+ - ./prisma/migrations/0002_channel_age_gate/migration.sql:/docker-entrypoint-initdb.d/002_channel_age_gate.sql:ro
+
+volumes:
+ postgres_data:
diff --git a/next.config.ts b/next.config.ts
index e9ffa30..078e9e1 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -1,7 +1,9 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
- /* config options here */
+ turbopack: {
+ root: process.cwd(),
+ },
};
export default nextConfig;
diff --git a/package-lock.json b/package-lock.json
index 71f84f0..67d0c92 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,9 +8,16 @@
"name": "metube",
"version": "0.1.0",
"dependencies": {
+ "@prisma/adapter-pg": "^7.7.0",
+ "@prisma/client": "^7.7.0",
+ "argon2": "^0.44.0",
+ "dotenv": "^17.4.2",
+ "jose": "^6.2.2",
"next": "16.2.3",
+ "pg": "^8.20.0",
"react": "19.2.4",
- "react-dom": "19.2.4"
+ "react-dom": "19.2.4",
+ "zod": "^4.3.6"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
@@ -19,6 +26,7 @@
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "16.2.3",
+ "prisma": "^7.7.0",
"tailwindcss": "^4",
"typescript": "^5"
}
@@ -276,6 +284,36 @@
"node": ">=6.9.0"
}
},
+ "node_modules/@electric-sql/pglite": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.4.1.tgz",
+ "integrity": "sha512-mZ9NzzUSYPOCnxHH1oAHPRzoMFJHY472raDKwXl/+6oPbpdJ7g8LsCN4FSaIIfkiCKHhb3iF/Zqo3NYxaIhU7Q==",
+ "devOptional": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/@electric-sql/pglite-socket": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/@electric-sql/pglite-socket/-/pglite-socket-0.1.1.tgz",
+ "integrity": "sha512-p2hoXw3Z3LQHwTeikdZNsFBOvXGqKY2hk51BBw+8NKND8eoH+8LFOtW9Z8CQKmTJ2qqGYu82ipqiyFZOTTXNfw==",
+ "devOptional": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "pglite-server": "dist/scripts/server.js"
+ },
+ "peerDependencies": {
+ "@electric-sql/pglite": "0.4.1"
+ }
+ },
+ "node_modules/@electric-sql/pglite-tools": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/@electric-sql/pglite-tools/-/pglite-tools-0.3.1.tgz",
+ "integrity": "sha512-C+T3oivmy9bpQvSxVqXA1UDY8cB9Eb9vZHL9zxWwEUfDixbXv4G3r2LjoTdR33LD8aomR3O9ZXEO3XEwr/cUCA==",
+ "devOptional": true,
+ "license": "Apache-2.0",
+ "peerDependencies": {
+ "@electric-sql/pglite": "0.4.1"
+ }
+ },
"node_modules/@emnapi/core": {
"version": "1.9.2",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz",
@@ -309,6 +347,12 @@
"tslib": "^2.4.0"
}
},
+ "node_modules/@epic-web/invariant": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz",
+ "integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==",
+ "license": "MIT"
+ },
"node_modules/@eslint-community/eslint-utils": {
"version": "4.9.1",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
@@ -453,6 +497,19 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
+ "node_modules/@hono/node-server": {
+ "version": "1.19.11",
+ "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.11.tgz",
+ "integrity": "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==",
+ "devOptional": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.14.1"
+ },
+ "peerDependencies": {
+ "hono": "^4"
+ }
+ },
"node_modules/@humanfs/core": {
"version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
@@ -1069,6 +1126,13 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
+ "node_modules/@kurkle/color": {
+ "version": "0.3.4",
+ "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
+ "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
+ "devOptional": true,
+ "license": "MIT"
+ },
"node_modules/@napi-rs/wasm-runtime": {
"version": "0.2.12",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz",
@@ -1286,6 +1350,396 @@
"node": ">=12.4.0"
}
},
+ "node_modules/@phc/format": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@phc/format/-/format-1.0.0.tgz",
+ "integrity": "sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@prisma/adapter-pg": {
+ "version": "7.7.0",
+ "resolved": "https://registry.npmjs.org/@prisma/adapter-pg/-/adapter-pg-7.7.0.tgz",
+ "integrity": "sha512-q33Ta8sKbgzEpAy0lx45tAq//yMv0qcb+8nj+TCA3P4wiAY+OBFEFk/NDkZncAfHaNJeGo5WJpJdpbL+ijYx8g==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@prisma/driver-adapter-utils": "7.7.0",
+ "@types/pg": "^8.16.0",
+ "pg": "^8.16.3",
+ "postgres-array": "3.0.4"
+ }
+ },
+ "node_modules/@prisma/client": {
+ "version": "7.7.0",
+ "resolved": "https://registry.npmjs.org/@prisma/client/-/client-7.7.0.tgz",
+ "integrity": "sha512-5Ar4OsZpJ54s21sy5oDNNW9gQtd4NuxCaiM7+JDTOU07D6VvlpLjYzAVCMB1+JzokN+08dAVomlx+b7bhJd3ww==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@prisma/client-runtime-utils": "7.7.0"
+ },
+ "engines": {
+ "node": "^20.19 || ^22.12 || >=24.0"
+ },
+ "peerDependencies": {
+ "prisma": "*",
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "prisma": {
+ "optional": true
+ },
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@prisma/client-runtime-utils": {
+ "version": "7.7.0",
+ "resolved": "https://registry.npmjs.org/@prisma/client-runtime-utils/-/client-runtime-utils-7.7.0.tgz",
+ "integrity": "sha512-BLyd0UpFYOtyJFTHm7jS9vesHW7P83abibodQMiIofqjBKzDHQ1VAsQkdfvXyYDkPlONPfOTz7/rv3x/+CQqvQ==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/@prisma/config": {
+ "version": "7.7.0",
+ "resolved": "https://registry.npmjs.org/@prisma/config/-/config-7.7.0.tgz",
+ "integrity": "sha512-hmPI3tKLO2aP0Y5vugbjcnA9qqlfJndiT6ds4tw28U5hNHLWg+mHJEWAhjsSPgxjtmxhJ/EDIeIlyh+3Us0OPg==",
+ "devOptional": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "c12": "3.1.0",
+ "deepmerge-ts": "7.1.5",
+ "effect": "3.20.0",
+ "empathic": "2.0.0"
+ }
+ },
+ "node_modules/@prisma/debug": {
+ "version": "7.7.0",
+ "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.7.0.tgz",
+ "integrity": "sha512-12J62XdqCmpiwJHhHdQxZeY3ckVCWIFmcJP8hg5dPTceeiQ0wiojXGFYTluKqFQfu46fRLgb/rLALZMAx3+dTA==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/@prisma/dev": {
+ "version": "0.24.3",
+ "resolved": "https://registry.npmjs.org/@prisma/dev/-/dev-0.24.3.tgz",
+ "integrity": "sha512-ffHlQuKXZiaDt9Go0OnCTdJZrHxK0k7omJKNV86/VjpsXu5EIHZLK0T7JSWgvNlJwh56kW9JFu9v0qJciFzepg==",
+ "devOptional": true,
+ "license": "ISC",
+ "dependencies": {
+ "@electric-sql/pglite": "0.4.1",
+ "@electric-sql/pglite-socket": "0.1.1",
+ "@electric-sql/pglite-tools": "0.3.1",
+ "@hono/node-server": "1.19.11",
+ "@prisma/get-platform": "7.2.0",
+ "@prisma/query-plan-executor": "7.2.0",
+ "@prisma/streams-local": "0.1.2",
+ "foreground-child": "3.3.1",
+ "get-port-please": "3.2.0",
+ "hono": "^4.12.8",
+ "http-status-codes": "2.3.0",
+ "pathe": "2.0.3",
+ "proper-lockfile": "4.1.2",
+ "remeda": "2.33.4",
+ "std-env": "3.10.0",
+ "valibot": "1.2.0",
+ "zeptomatch": "2.1.0"
+ }
+ },
+ "node_modules/@prisma/driver-adapter-utils": {
+ "version": "7.7.0",
+ "resolved": "https://registry.npmjs.org/@prisma/driver-adapter-utils/-/driver-adapter-utils-7.7.0.tgz",
+ "integrity": "sha512-gZXREeu6mOk7zXfGFJgh86p7Vhj0sXNKp+4Cg1tWYo7V2dfncP2qxS2BiTmbIIha8xPqItkl0WSw38RuSq1HoQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@prisma/debug": "7.7.0"
+ }
+ },
+ "node_modules/@prisma/engines": {
+ "version": "7.7.0",
+ "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-7.7.0.tgz",
+ "integrity": "sha512-7fmcbT7HHXBq/b+3h/dO1JI3fd8l8q7erf7xP7pRprh58hmSSnG8mg9K3yjW3h9WaHWUwngVFpSxxxivaitQ2w==",
+ "devOptional": true,
+ "hasInstallScript": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@prisma/debug": "7.7.0",
+ "@prisma/engines-version": "7.6.0-1.75cbdc1eb7150937890ad5465d861175c6624711",
+ "@prisma/fetch-engine": "7.7.0",
+ "@prisma/get-platform": "7.7.0"
+ }
+ },
+ "node_modules/@prisma/engines-version": {
+ "version": "7.6.0-1.75cbdc1eb7150937890ad5465d861175c6624711",
+ "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.6.0-1.75cbdc1eb7150937890ad5465d861175c6624711.tgz",
+ "integrity": "sha512-r51DLcJ8bDRSrBEJF3J4cinoWyGA7rfP2mG6lD90VqIbGNOkbfcLcXalSVjq5Y6brQS3vcjrq4GbyUb1Cb7vkw==",
+ "devOptional": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/@prisma/engines/node_modules/@prisma/get-platform": {
+ "version": "7.7.0",
+ "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.7.0.tgz",
+ "integrity": "sha512-MEUNzvKxvYnJ7kgvd6oNRnMmmiGNS9TYLB2weMeIXplnHdL/UWEGnvavYGnN7KLJ2n0iI4dDAyzSkHI3c7AscQ==",
+ "devOptional": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@prisma/debug": "7.7.0"
+ }
+ },
+ "node_modules/@prisma/fetch-engine": {
+ "version": "7.7.0",
+ "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-7.7.0.tgz",
+ "integrity": "sha512-TfyzveBQoK4xALzsTpVhB/0KG1N8zOK0ap+RnBMkzGUu3f98fnQ4QtXa2wlKPhsO2X8a3N5ugFQgcKNoHGmDfw==",
+ "devOptional": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@prisma/debug": "7.7.0",
+ "@prisma/engines-version": "7.6.0-1.75cbdc1eb7150937890ad5465d861175c6624711",
+ "@prisma/get-platform": "7.7.0"
+ }
+ },
+ "node_modules/@prisma/fetch-engine/node_modules/@prisma/get-platform": {
+ "version": "7.7.0",
+ "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.7.0.tgz",
+ "integrity": "sha512-MEUNzvKxvYnJ7kgvd6oNRnMmmiGNS9TYLB2weMeIXplnHdL/UWEGnvavYGnN7KLJ2n0iI4dDAyzSkHI3c7AscQ==",
+ "devOptional": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@prisma/debug": "7.7.0"
+ }
+ },
+ "node_modules/@prisma/get-platform": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.2.0.tgz",
+ "integrity": "sha512-k1V0l0Td1732EHpAfi2eySTezyllok9dXb6UQanajkJQzPUGi3vO2z7jdkz67SypFTdmbnyGYxvEvYZdZsMAVA==",
+ "devOptional": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@prisma/debug": "7.2.0"
+ }
+ },
+ "node_modules/@prisma/get-platform/node_modules/@prisma/debug": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.2.0.tgz",
+ "integrity": "sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw==",
+ "devOptional": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/@prisma/query-plan-executor": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/@prisma/query-plan-executor/-/query-plan-executor-7.2.0.tgz",
+ "integrity": "sha512-EOZmNzcV8uJ0mae3DhTsiHgoNCuu1J9mULQpGCh62zN3PxPTd+qI9tJvk5jOst8WHKQNwJWR3b39t0XvfBB0WQ==",
+ "devOptional": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/@prisma/streams-local": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/@prisma/streams-local/-/streams-local-0.1.2.tgz",
+ "integrity": "sha512-l49yTxKKF2odFxaAXTmwmkBKL3+bVQ1tFOooGifu4xkdb9NMNLxHj27XAhTylWZod8I+ISGM5erU1xcl/oBCtg==",
+ "devOptional": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "ajv": "^8.12.0",
+ "better-result": "^2.7.0",
+ "env-paths": "^3.0.0",
+ "proper-lockfile": "^4.1.2"
+ },
+ "engines": {
+ "bun": ">=1.3.6",
+ "node": ">=22.0.0"
+ }
+ },
+ "node_modules/@prisma/streams-local/node_modules/ajv": {
+ "version": "8.18.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
+ "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3",
+ "fast-uri": "^3.0.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/@prisma/streams-local/node_modules/json-schema-traverse": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/@prisma/studio-core": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@prisma/studio-core/-/studio-core-0.27.3.tgz",
+ "integrity": "sha512-AADjNFPdsrglxHQVTmHFqv6DuKQZ5WY4p5/gVFY017twvNrSwpLJ9lqUbYYxEu2W7nbvVxTZA8deJ8LseNALsw==",
+ "devOptional": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@radix-ui/react-toggle": "1.1.10",
+ "chart.js": "4.5.1"
+ },
+ "engines": {
+ "node": "^20.19 || ^22.12 || >=24.0",
+ "pnpm": "8"
+ },
+ "peerDependencies": {
+ "@types/react": "^18.0.0 || ^19.0.0",
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/@radix-ui/primitive": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
+ "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/@radix-ui/react-compose-refs": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
+ "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
+ "devOptional": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
+ "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-toggle": {
+ "version": "1.1.10",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz",
+ "integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-controllable-state": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
+ "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-effect-event": "0.0.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-effect-event": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz",
+ "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-layout-effect": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
+ "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
+ "devOptional": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@rtsao/scc": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
@@ -1293,6 +1747,13 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@standard-schema/spec": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
+ "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
+ "devOptional": true,
+ "license": "MIT"
+ },
"node_modules/@swc/helpers": {
"version": "0.5.15",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
@@ -1621,17 +2082,27 @@
"version": "20.19.39",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.39.tgz",
"integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==",
- "dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
+ "node_modules/@types/pg": {
+ "version": "8.20.0",
+ "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.20.0.tgz",
+ "integrity": "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "pg-protocol": "*",
+ "pg-types": "^2.2.0"
+ }
+ },
"node_modules/@types/react": {
"version": "19.2.14",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"csstype": "^3.2.2"
@@ -1641,7 +2112,7 @@
"version": "19.2.3",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"peerDependencies": {
"@types/react": "^19.2.0"
@@ -2291,6 +2762,22 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
+ "node_modules/argon2": {
+ "version": "0.44.0",
+ "resolved": "https://registry.npmjs.org/argon2/-/argon2-0.44.0.tgz",
+ "integrity": "sha512-zHPGN3S55sihSQo0dBbK0A5qpi2R31z7HZDZnry3ifOyj8bZZnpZND2gpmhnRGO1V/d555RwBqIK5W4Mrmv3ig==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "dependencies": {
+ "@phc/format": "^1.0.0",
+ "cross-env": "^10.0.0",
+ "node-addon-api": "^8.5.0",
+ "node-gyp-build": "^4.8.4"
+ },
+ "engines": {
+ "node": ">=16.17.0"
+ }
+ },
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
@@ -2501,6 +2988,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/aws-ssl-profiles": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz",
+ "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==",
+ "devOptional": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6.0.0"
+ }
+ },
"node_modules/axe-core": {
"version": "4.11.3",
"resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.3.tgz",
@@ -2540,6 +3037,13 @@
"node": ">=6.0.0"
}
},
+ "node_modules/better-result": {
+ "version": "2.8.2",
+ "resolved": "https://registry.npmjs.org/better-result/-/better-result-2.8.2.tgz",
+ "integrity": "sha512-YOf0VSj5nUPI27doTtXF+BBnsiRq3qY7avHqfIWnppxTLGyvkLq1QV2RTxkwoZwJ60ywLfZ0raFF4J/G886i7A==",
+ "devOptional": true,
+ "license": "MIT"
+ },
"node_modules/brace-expansion": {
"version": "1.1.14",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
@@ -2598,6 +3102,48 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
+ "node_modules/c12": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz",
+ "integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "chokidar": "^4.0.3",
+ "confbox": "^0.2.2",
+ "defu": "^6.1.4",
+ "dotenv": "^16.6.1",
+ "exsolve": "^1.0.7",
+ "giget": "^2.0.0",
+ "jiti": "^2.4.2",
+ "ohash": "^2.0.11",
+ "pathe": "^2.0.3",
+ "perfect-debounce": "^1.0.0",
+ "pkg-types": "^2.2.0",
+ "rc9": "^2.1.2"
+ },
+ "peerDependencies": {
+ "magicast": "^0.3.5"
+ },
+ "peerDependenciesMeta": {
+ "magicast": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/c12/node_modules/dotenv": {
+ "version": "16.6.1",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
+ "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
+ "devOptional": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
"node_modules/call-bind": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz",
@@ -2695,6 +3241,45 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
+ "node_modules/chart.js": {
+ "version": "4.5.1",
+ "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz",
+ "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "@kurkle/color": "^0.3.0"
+ },
+ "engines": {
+ "pnpm": ">=8"
+ }
+ },
+ "node_modules/chokidar": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
+ "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "readdirp": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 14.16.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/citty": {
+ "version": "0.1.6",
+ "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz",
+ "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "consola": "^3.2.3"
+ }
+ },
"node_modules/client-only": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
@@ -2728,6 +3313,23 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/confbox": {
+ "version": "0.2.4",
+ "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz",
+ "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/consola": {
+ "version": "3.4.2",
+ "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz",
+ "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==",
+ "devOptional": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^14.18.0 || >=16.10.0"
+ }
+ },
"node_modules/convert-source-map": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
@@ -2735,11 +3337,27 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/cross-env": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz",
+ "integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==",
+ "license": "MIT",
+ "dependencies": {
+ "@epic-web/invariant": "^1.0.0",
+ "cross-spawn": "^7.0.6"
+ },
+ "bin": {
+ "cross-env": "dist/bin/cross-env.js",
+ "cross-env-shell": "dist/bin/cross-env-shell.js"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
@@ -2754,7 +3372,7 @@
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
- "dev": true,
+ "devOptional": true,
"license": "MIT"
},
"node_modules/damerau-levenshtein": {
@@ -2843,6 +3461,16 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/deepmerge-ts": {
+ "version": "7.1.5",
+ "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz",
+ "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==",
+ "devOptional": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
"node_modules/define-data-property": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
@@ -2879,6 +3507,30 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/defu": {
+ "version": "6.1.7",
+ "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz",
+ "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/denque": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
+ "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
+ "devOptional": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/destr": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz",
+ "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==",
+ "devOptional": true,
+ "license": "MIT"
+ },
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
@@ -2902,6 +3554,18 @@
"node": ">=0.10.0"
}
},
+ "node_modules/dotenv": {
+ "version": "17.4.2",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz",
+ "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -2917,6 +3581,17 @@
"node": ">= 0.4"
}
},
+ "node_modules/effect": {
+ "version": "3.20.0",
+ "resolved": "https://registry.npmjs.org/effect/-/effect-3.20.0.tgz",
+ "integrity": "sha512-qMLfDJscrNG8p/aw+IkT9W7fgj50Z4wG5bLBy0Txsxz8iUHjDIkOgO3SV0WZfnQbNG2VJYb0b+rDLMrhM4+Krw==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "@standard-schema/spec": "^1.0.0",
+ "fast-check": "^3.23.1"
+ }
+ },
"node_modules/electron-to-chromium": {
"version": "1.5.336",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.336.tgz",
@@ -2931,6 +3606,16 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/empathic": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz",
+ "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==",
+ "devOptional": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ }
+ },
"node_modules/enhanced-resolve": {
"version": "5.20.1",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz",
@@ -2945,6 +3630,19 @@
"node": ">=10.13.0"
}
},
+ "node_modules/env-paths": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz",
+ "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==",
+ "devOptional": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/es-abstract": {
"version": "1.24.2",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz",
@@ -3551,11 +4249,41 @@
"node": ">=0.10.0"
}
},
+ "node_modules/exsolve": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz",
+ "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-check": {
+ "version": "3.23.2",
+ "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz",
+ "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==",
+ "devOptional": true,
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/dubzzz"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fast-check"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "pure-rand": "^6.1.0"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
- "dev": true,
+ "devOptional": true,
"license": "MIT"
},
"node_modules/fast-glob": {
@@ -3602,6 +4330,23 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/fast-uri": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
+ "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
+ "devOptional": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
"node_modules/fastq": {
"version": "1.20.1",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
@@ -3692,6 +4437,23 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/foreground-child": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
+ "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
+ "devOptional": true,
+ "license": "ISC",
+ "dependencies": {
+ "cross-spawn": "^7.0.6",
+ "signal-exit": "^4.0.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@@ -3733,6 +4495,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/generate-function": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz",
+ "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-property": "^1.0.2"
+ }
+ },
"node_modules/generator-function": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz",
@@ -3778,6 +4550,13 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/get-port-please": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/get-port-please/-/get-port-please-3.2.0.tgz",
+ "integrity": "sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A==",
+ "devOptional": true,
+ "license": "MIT"
+ },
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
@@ -3823,6 +4602,24 @@
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
}
},
+ "node_modules/giget": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz",
+ "integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "citty": "^0.1.6",
+ "consola": "^3.4.0",
+ "defu": "^6.1.4",
+ "node-fetch-native": "^1.6.6",
+ "nypm": "^0.6.0",
+ "pathe": "^2.0.3"
+ },
+ "bin": {
+ "giget": "dist/cli.mjs"
+ }
+ },
"node_modules/glob-parent": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
@@ -3883,9 +4680,23 @@
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
- "dev": true,
+ "devOptional": true,
"license": "ISC"
},
+ "node_modules/grammex": {
+ "version": "3.1.12",
+ "resolved": "https://registry.npmjs.org/grammex/-/grammex-3.1.12.tgz",
+ "integrity": "sha512-6ufJOsSA7LcQehIJNCO7HIBykfM7DXQual0Ny780/DEcJIpBlHRvcqEBWGPYd7hrXL2GJ3oJI1MIhaXjWmLQOQ==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/graphmatch": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/graphmatch/-/graphmatch-1.1.1.tgz",
+ "integrity": "sha512-5ykVn/EXM1hF0XCaWh05VbYvEiOL2lY1kBxZtaYsyvjp7cmWOU1XsAdfQBwClraEofXDT197lFbXOEVMHpvQOg==",
+ "devOptional": true,
+ "license": "MIT"
+ },
"node_modules/has-bigints": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz",
@@ -3997,6 +4808,40 @@
"hermes-estree": "0.25.1"
}
},
+ "node_modules/hono": {
+ "version": "4.12.12",
+ "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.12.tgz",
+ "integrity": "sha512-p1JfQMKaceuCbpJKAPKVqyqviZdS0eUxH9v82oWo1kb9xjQ5wA6iP3FNVAPDFlz5/p7d45lO+BpSk1tuSZMF4Q==",
+ "devOptional": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=16.9.0"
+ }
+ },
+ "node_modules/http-status-codes": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/http-status-codes/-/http-status-codes-2.3.0.tgz",
+ "integrity": "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
+ "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -4319,6 +5164,13 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/is-property": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz",
+ "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==",
+ "devOptional": true,
+ "license": "MIT"
+ },
"node_modules/is-regex": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
@@ -4475,7 +5327,6 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
- "dev": true,
"license": "ISC"
},
"node_modules/iterator.prototype": {
@@ -4500,12 +5351,21 @@
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
"integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"bin": {
"jiti": "lib/jiti-cli.mjs"
}
},
+ "node_modules/jose": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.2.tgz",
+ "integrity": "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/panva"
+ }
+ },
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -4929,6 +5789,13 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/long": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
+ "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
+ "devOptional": true,
+ "license": "Apache-2.0"
+ },
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
@@ -4952,6 +5819,22 @@
"yallist": "^3.0.2"
}
},
+ "node_modules/lru.min": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz",
+ "integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==",
+ "devOptional": true,
+ "license": "MIT",
+ "engines": {
+ "bun": ">=1.0.0",
+ "deno": ">=1.30.0",
+ "node": ">=8.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wellwelwel"
+ }
+ },
"node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
@@ -5026,6 +5909,40 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/mysql2": {
+ "version": "3.15.3",
+ "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.15.3.tgz",
+ "integrity": "sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "aws-ssl-profiles": "^1.1.1",
+ "denque": "^2.1.0",
+ "generate-function": "^2.3.1",
+ "iconv-lite": "^0.7.0",
+ "long": "^5.2.1",
+ "lru.min": "^1.0.0",
+ "named-placeholders": "^1.1.3",
+ "seq-queue": "^0.0.5",
+ "sqlstring": "^2.3.2"
+ },
+ "engines": {
+ "node": ">= 8.0"
+ }
+ },
+ "node_modules/named-placeholders": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz",
+ "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "lru.min": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
@@ -5148,6 +6065,15 @@
"node": "^10 || ^12 || >=14"
}
},
+ "node_modules/node-addon-api": {
+ "version": "8.7.0",
+ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.7.0.tgz",
+ "integrity": "sha512-9MdFxmkKaOYVTV+XVRG8ArDwwQ77XIgIPyKASB1k3JPq3M8fGQQQE3YpMOrKm6g//Ktx8ivZr8xo1Qmtqub+GA==",
+ "license": "MIT",
+ "engines": {
+ "node": "^18 || ^20 || >= 21"
+ }
+ },
"node_modules/node-exports-info": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz",
@@ -5167,6 +6093,24 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/node-fetch-native": {
+ "version": "1.6.7",
+ "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz",
+ "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/node-gyp-build": {
+ "version": "4.8.4",
+ "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
+ "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
+ "license": "MIT",
+ "bin": {
+ "node-gyp-build": "bin.js",
+ "node-gyp-build-optional": "optional.js",
+ "node-gyp-build-test": "build-test.js"
+ }
+ },
"node_modules/node-releases": {
"version": "2.0.37",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz",
@@ -5174,6 +6118,31 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/nypm": {
+ "version": "0.6.5",
+ "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.5.tgz",
+ "integrity": "sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "citty": "^0.2.0",
+ "pathe": "^2.0.3",
+ "tinyexec": "^1.0.2"
+ },
+ "bin": {
+ "nypm": "dist/cli.mjs"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/nypm/node_modules/citty": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/citty/-/citty-0.2.2.tgz",
+ "integrity": "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w==",
+ "devOptional": true,
+ "license": "MIT"
+ },
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -5297,6 +6266,13 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/ohash": {
+ "version": "2.0.11",
+ "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz",
+ "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==",
+ "devOptional": true,
+ "license": "MIT"
+ },
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@@ -5392,7 +6368,6 @@
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -5405,6 +6380,118 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/pathe": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
+ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/perfect-debounce": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
+ "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/pg": {
+ "version": "8.20.0",
+ "resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz",
+ "integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==",
+ "license": "MIT",
+ "dependencies": {
+ "pg-connection-string": "^2.12.0",
+ "pg-pool": "^3.13.0",
+ "pg-protocol": "^1.13.0",
+ "pg-types": "2.2.0",
+ "pgpass": "1.0.5"
+ },
+ "engines": {
+ "node": ">= 16.0.0"
+ },
+ "optionalDependencies": {
+ "pg-cloudflare": "^1.3.0"
+ },
+ "peerDependencies": {
+ "pg-native": ">=3.0.1"
+ },
+ "peerDependenciesMeta": {
+ "pg-native": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/pg-cloudflare": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz",
+ "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/pg-connection-string": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.12.0.tgz",
+ "integrity": "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==",
+ "license": "MIT"
+ },
+ "node_modules/pg-int8": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
+ "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/pg-pool": {
+ "version": "3.13.0",
+ "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.13.0.tgz",
+ "integrity": "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "pg": ">=8.0"
+ }
+ },
+ "node_modules/pg-protocol": {
+ "version": "1.13.0",
+ "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.13.0.tgz",
+ "integrity": "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==",
+ "license": "MIT"
+ },
+ "node_modules/pg-types": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
+ "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
+ "license": "MIT",
+ "dependencies": {
+ "pg-int8": "1.0.1",
+ "postgres-array": "~2.0.0",
+ "postgres-bytea": "~1.0.0",
+ "postgres-date": "~1.0.4",
+ "postgres-interval": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/pg-types/node_modules/postgres-array": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
+ "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/pgpass": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
+ "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
+ "license": "MIT",
+ "dependencies": {
+ "split2": "^4.1.0"
+ }
+ },
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -5424,6 +6511,18 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
+ "node_modules/pkg-types": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz",
+ "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "confbox": "^0.2.2",
+ "exsolve": "^1.0.7",
+ "pathe": "^2.0.3"
+ }
+ },
"node_modules/possible-typed-array-names": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
@@ -5463,6 +6562,59 @@
"node": "^10 || ^12 || >=14"
}
},
+ "node_modules/postgres": {
+ "version": "3.4.7",
+ "resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.7.tgz",
+ "integrity": "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==",
+ "devOptional": true,
+ "license": "Unlicense",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "type": "individual",
+ "url": "https://github.com/sponsors/porsager"
+ }
+ },
+ "node_modules/postgres-array": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.4.tgz",
+ "integrity": "sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/postgres-bytea": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz",
+ "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/postgres-date": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
+ "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/postgres-interval": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
+ "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
+ "license": "MIT",
+ "dependencies": {
+ "xtend": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -5473,6 +6625,40 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/prisma": {
+ "version": "7.7.0",
+ "resolved": "https://registry.npmjs.org/prisma/-/prisma-7.7.0.tgz",
+ "integrity": "sha512-HlgwRBt1uEFB9LStHL4HLYDvoi4BNu1rYA0hPG0zCAEyK9SaZBqp7E5Rjpc3Qh8Lex/ye/svoHZ0OWoFNhWxuQ==",
+ "devOptional": true,
+ "hasInstallScript": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@prisma/config": "7.7.0",
+ "@prisma/dev": "0.24.3",
+ "@prisma/engines": "7.7.0",
+ "@prisma/studio-core": "0.27.3",
+ "mysql2": "3.15.3",
+ "postgres": "3.4.7"
+ },
+ "bin": {
+ "prisma": "build/index.js"
+ },
+ "engines": {
+ "node": "^20.19 || ^22.12 || >=24.0"
+ },
+ "peerDependencies": {
+ "better-sqlite3": ">=9.0.0",
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "better-sqlite3": {
+ "optional": true
+ },
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
@@ -5485,6 +6671,25 @@
"react-is": "^16.13.1"
}
},
+ "node_modules/proper-lockfile": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz",
+ "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.4",
+ "retry": "^0.12.0",
+ "signal-exit": "^3.0.2"
+ }
+ },
+ "node_modules/proper-lockfile/node_modules/signal-exit": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+ "devOptional": true,
+ "license": "ISC"
+ },
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -5495,6 +6700,23 @@
"node": ">=6"
}
},
+ "node_modules/pure-rand": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz",
+ "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==",
+ "devOptional": true,
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/dubzzz"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fast-check"
+ }
+ ],
+ "license": "MIT"
+ },
"node_modules/queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@@ -5516,6 +6738,17 @@
],
"license": "MIT"
},
+ "node_modules/rc9": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz",
+ "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "defu": "^6.1.4",
+ "destr": "^2.0.3"
+ }
+ },
"node_modules/react": {
"version": "19.2.4",
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
@@ -5544,6 +6777,20 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/readdirp": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
+ "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
+ "devOptional": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14.18.0"
+ },
+ "funding": {
+ "type": "individual",
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
"node_modules/reflect.getprototypeof": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
@@ -5588,6 +6835,26 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/remeda": {
+ "version": "2.33.4",
+ "resolved": "https://registry.npmjs.org/remeda/-/remeda-2.33.4.tgz",
+ "integrity": "sha512-ygHswjlc/opg2VrtiYvUOPLjxjtdKvjGz1/plDhkG66hjNjFr1xmfrs2ClNFo/E6TyUFiwYNh53bKV26oBoMGQ==",
+ "devOptional": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/remeda"
+ }
+ },
+ "node_modules/require-from-string": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+ "devOptional": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/resolve": {
"version": "2.0.0-next.6",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.6.tgz",
@@ -5632,6 +6899,16 @@
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
}
},
+ "node_modules/retry": {
+ "version": "0.12.0",
+ "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
+ "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==",
+ "devOptional": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
"node_modules/reusify": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
@@ -5722,6 +6999,13 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "devOptional": true,
+ "license": "MIT"
+ },
"node_modules/scheduler": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
@@ -5738,6 +7022,12 @@
"semver": "bin/semver.js"
}
},
+ "node_modules/seq-queue": {
+ "version": "0.0.5",
+ "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz",
+ "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==",
+ "devOptional": true
+ },
"node_modules/set-function-length": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
@@ -5849,7 +7139,6 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"shebang-regex": "^3.0.0"
@@ -5862,7 +7151,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -5944,6 +7232,19 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/signal-exit": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+ "devOptional": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -5953,6 +7254,25 @@
"node": ">=0.10.0"
}
},
+ "node_modules/split2": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
+ "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">= 10.x"
+ }
+ },
+ "node_modules/sqlstring": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz",
+ "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==",
+ "devOptional": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/stable-hash": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz",
@@ -5960,6 +7280,13 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/std-env": {
+ "version": "3.10.0",
+ "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
+ "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",
+ "devOptional": true,
+ "license": "MIT"
+ },
"node_modules/stop-iteration-iterator": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
@@ -6180,6 +7507,16 @@
"url": "https://opencollective.com/webpack"
}
},
+ "node_modules/tinyexec": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.1.tgz",
+ "integrity": "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==",
+ "devOptional": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/tinyglobby": {
"version": "0.2.16",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
@@ -6381,7 +7718,7 @@
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
- "dev": true,
+ "devOptional": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
@@ -6438,7 +7775,6 @@
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
- "dev": true,
"license": "MIT"
},
"node_modules/unrs-resolver": {
@@ -6517,11 +7853,25 @@
"punycode": "^2.1.0"
}
},
+ "node_modules/valibot": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.2.0.tgz",
+ "integrity": "sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==",
+ "devOptional": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "typescript": ">=5"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
- "dev": true,
"license": "ISC",
"dependencies": {
"isexe": "^2.0.0"
@@ -6632,6 +7982,15 @@
"node": ">=0.10.0"
}
},
+ "node_modules/xtend": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
+ "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4"
+ }
+ },
"node_modules/yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
@@ -6652,11 +8011,21 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/zeptomatch": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/zeptomatch/-/zeptomatch-2.1.0.tgz",
+ "integrity": "sha512-KiGErG2J0G82LSpniV0CtIzjlJ10E04j02VOudJsPyPwNZgGnRKQy7I1R7GMyg/QswnE4l7ohSGrQbQbjXPPDA==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "grammex": "^3.1.11",
+ "graphmatch": "^1.1.0"
+ }
+ },
"node_modules/zod": {
"version": "4.3.6",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
- "dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
diff --git a/package.json b/package.json
index 8287870..cdebdb2 100644
--- a/package.json
+++ b/package.json
@@ -6,12 +6,22 @@
"dev": "next dev",
"build": "next build",
"start": "next start",
- "lint": "eslint"
+ "lint": "eslint",
+ "prisma:generate": "prisma generate",
+ "prisma:migrate": "prisma migrate dev",
+ "prisma:studio": "prisma studio"
},
"dependencies": {
+ "@prisma/adapter-pg": "^7.7.0",
+ "@prisma/client": "^7.7.0",
+ "argon2": "^0.44.0",
+ "dotenv": "^17.4.2",
+ "jose": "^6.2.2",
"next": "16.2.3",
+ "pg": "^8.20.0",
"react": "19.2.4",
- "react-dom": "19.2.4"
+ "react-dom": "19.2.4",
+ "zod": "^4.3.6"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
@@ -20,6 +30,7 @@
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "16.2.3",
+ "prisma": "^7.7.0",
"tailwindcss": "^4",
"typescript": "^5"
}
diff --git a/prisma.config.ts b/prisma.config.ts
new file mode 100644
index 0000000..831a20f
--- /dev/null
+++ b/prisma.config.ts
@@ -0,0 +1,14 @@
+// This file was generated by Prisma, and assumes you have installed the following:
+// npm install --save-dev prisma dotenv
+import "dotenv/config";
+import { defineConfig } from "prisma/config";
+
+export default defineConfig({
+ schema: "prisma/schema.prisma",
+ migrations: {
+ path: "prisma/migrations",
+ },
+ datasource: {
+ url: process.env["DATABASE_URL"],
+ },
+});
diff --git a/prisma/migrations/0001_init/migration.sql b/prisma/migrations/0001_init/migration.sql
new file mode 100644
index 0000000..c7d4ce3
--- /dev/null
+++ b/prisma/migrations/0001_init/migration.sql
@@ -0,0 +1,52 @@
+-- CreateSchema
+CREATE SCHEMA IF NOT EXISTS "public";
+
+-- CreateTable
+CREATE TABLE "Channel" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "description" TEXT,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "Channel_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "User" (
+ "id" TEXT NOT NULL,
+ "username" TEXT NOT NULL,
+ "passwordHash" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "User_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "Video" (
+ "id" TEXT NOT NULL,
+ "channelId" TEXT NOT NULL,
+ "title" TEXT NOT NULL,
+ "description" TEXT,
+ "videoFilename" TEXT NOT NULL,
+ "videoMimeType" TEXT NOT NULL,
+ "videoSizeBytes" INTEGER NOT NULL,
+ "thumbnailFilename" TEXT,
+ "thumbnailMimeType" TEXT,
+ "publishedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "Video_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "User_username_key" ON "User"("username");
+
+-- CreateIndex
+CREATE INDEX "Video_publishedAt_idx" ON "Video"("publishedAt" DESC);
+
+-- AddForeignKey
+ALTER TABLE "Video" ADD CONSTRAINT "Video_channelId_fkey" FOREIGN KEY ("channelId") REFERENCES "Channel"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
diff --git a/prisma/migrations/0002_channel_age_gate/migration.sql b/prisma/migrations/0002_channel_age_gate/migration.sql
new file mode 100644
index 0000000..8be9f8f
--- /dev/null
+++ b/prisma/migrations/0002_channel_age_gate/migration.sql
@@ -0,0 +1,3 @@
+-- AlterTable
+ALTER TABLE "Channel"
+ADD COLUMN "isAdult" BOOLEAN NOT NULL DEFAULT false;
diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml
new file mode 100644
index 0000000..2fe25d8
--- /dev/null
+++ b/prisma/migrations/migration_lock.toml
@@ -0,0 +1 @@
+provider = "postgresql"
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
new file mode 100644
index 0000000..7caa5cd
--- /dev/null
+++ b/prisma/schema.prisma
@@ -0,0 +1,49 @@
+// This is your Prisma schema file,
+// learn more about it in the docs: https://pris.ly/d/prisma-schema
+
+// Get a free hosted Postgres database in seconds: `npx create-db`
+
+generator client {
+ provider = "prisma-client"
+ output = "../src/generated/prisma"
+}
+
+datasource db {
+ provider = "postgresql"
+}
+
+model Channel {
+ id String @id
+ name String
+ description String?
+ isAdult Boolean @default(false)
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ videos Video[]
+}
+
+model User {
+ id String @id @default(cuid())
+ username String @unique
+ passwordHash String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+}
+
+model Video {
+ id String @id
+ channelId String
+ title String
+ description String?
+ videoFilename String
+ videoMimeType String
+ videoSizeBytes Int
+ thumbnailFilename String?
+ thumbnailMimeType String?
+ publishedAt DateTime @default(now())
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ channel Channel @relation(fields: [channelId], references: [id], onDelete: Cascade)
+
+ @@index([publishedAt(sort: Desc)])
+}
diff --git a/public/file.svg b/public/file.svg
deleted file mode 100644
index 004145c..0000000
--- a/public/file.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/public/globe.svg b/public/globe.svg
deleted file mode 100644
index 567f17b..0000000
--- a/public/globe.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/public/metube.png b/public/metube.png
new file mode 100644
index 0000000..436b44c
Binary files /dev/null and b/public/metube.png differ
diff --git a/public/next.svg b/public/next.svg
deleted file mode 100644
index 5174b28..0000000
--- a/public/next.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/public/vercel.svg b/public/vercel.svg
deleted file mode 100644
index 7705396..0000000
--- a/public/vercel.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/public/window.svg b/public/window.svg
deleted file mode 100644
index b2b2a44..0000000
--- a/public/window.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/src/app/api/auth/login/route.ts b/src/app/api/auth/login/route.ts
new file mode 100644
index 0000000..6230614
--- /dev/null
+++ b/src/app/api/auth/login/route.ts
@@ -0,0 +1,67 @@
+import { NextResponse } from "next/server";
+import { z } from "zod";
+
+import {
+ createSessionToken,
+ getSessionCookieOptions,
+ SESSION_COOKIE_NAME,
+ verifyPassword,
+} from "@/lib/auth";
+import { db } from "@/lib/db";
+import { isSetupComplete } from "@/lib/setup";
+
+export const runtime = "nodejs";
+
+const loginSchema = z.object({
+ username: z.string().trim().min(3).max(32),
+ password: z.string().min(8).max(128),
+});
+
+function redirectWithError(request: Request, error: string) {
+ const url = new URL("/login", request.url);
+ url.searchParams.set("error", error);
+ return NextResponse.redirect(url, 303);
+}
+
+export async function POST(request: Request) {
+ if (!(await isSetupComplete())) {
+ return NextResponse.redirect(new URL("/setup", request.url), 303);
+ }
+
+ const formData = await request.formData();
+ const parsed = loginSchema.safeParse({
+ username: formData.get("username"),
+ password: formData.get("password"),
+ });
+
+ if (!parsed.success) {
+ return redirectWithError(request, "Invalid credentials.");
+ }
+
+ const user = await db.user.findUnique({
+ where: {
+ username: parsed.data.username,
+ },
+ });
+
+ if (!user) {
+ return redirectWithError(request, "Invalid credentials.");
+ }
+
+ const isValidPassword = await verifyPassword(user.passwordHash, parsed.data.password);
+
+ if (!isValidPassword) {
+ return redirectWithError(request, "Invalid credentials.");
+ }
+
+ const sessionToken = await createSessionToken(user.id, user.username);
+ const response = NextResponse.redirect(new URL("/dashboard", request.url), 303);
+
+ response.cookies.set(
+ SESSION_COOKIE_NAME,
+ sessionToken,
+ getSessionCookieOptions(),
+ );
+
+ return response;
+}
diff --git a/src/app/api/auth/logout/route.ts b/src/app/api/auth/logout/route.ts
new file mode 100644
index 0000000..96ca365
--- /dev/null
+++ b/src/app/api/auth/logout/route.ts
@@ -0,0 +1,18 @@
+import { NextResponse } from "next/server";
+
+import { SESSION_COOKIE_NAME } from "@/lib/auth";
+import { isSetupComplete } from "@/lib/setup";
+
+export const runtime = "nodejs";
+
+export async function POST(request: Request) {
+ const setupComplete = await isSetupComplete();
+ const response = NextResponse.redirect(
+ new URL(setupComplete ? "/login" : "/setup", request.url),
+ 303,
+ );
+
+ response.cookies.delete(SESSION_COOKIE_NAME);
+
+ return response;
+}
diff --git a/src/app/api/channel/age-gate/route.ts b/src/app/api/channel/age-gate/route.ts
new file mode 100644
index 0000000..7832430
--- /dev/null
+++ b/src/app/api/channel/age-gate/route.ts
@@ -0,0 +1,48 @@
+import { NextResponse } from "next/server";
+import { z } from "zod";
+
+import {
+ ADULT_GATE_COOKIE_NAME,
+ getAdultGateCookieOptions,
+} from "@/lib/channel";
+import { getChannel, isSetupComplete } from "@/lib/setup";
+
+export const runtime = "nodejs";
+
+const ageGateSchema = z.object({
+ decision: z.enum(["yes", "no"]),
+});
+
+export async function POST(request: Request) {
+ if (!(await isSetupComplete())) {
+ return NextResponse.redirect(new URL("/setup", request.url), 303);
+ }
+
+ const channel = await getChannel();
+ if (!channel || !channel.isAdult) {
+ return NextResponse.redirect(new URL("/", request.url), 303);
+ }
+
+ const formData = await request.formData();
+ const parsed = ageGateSchema.safeParse({
+ decision: formData.get("decision"),
+ });
+
+ if (!parsed.success) {
+ return NextResponse.redirect(new URL("/", request.url), 303);
+ }
+
+ if (parsed.data.decision === "yes") {
+ const response = NextResponse.redirect(new URL("/", request.url), 303);
+
+ response.cookies.set(
+ ADULT_GATE_COOKIE_NAME,
+ channel.id,
+ getAdultGateCookieOptions(),
+ );
+
+ return response;
+ }
+
+ return NextResponse.redirect(new URL("https://google.com"), 303);
+}
diff --git a/src/app/api/channel/route.ts b/src/app/api/channel/route.ts
new file mode 100644
index 0000000..0c944b2
--- /dev/null
+++ b/src/app/api/channel/route.ts
@@ -0,0 +1,70 @@
+import { NextResponse } from "next/server";
+import { z } from "zod";
+
+import { getAuthenticatedUser } from "@/lib/auth";
+import { db } from "@/lib/db";
+import { getChannel, isSetupComplete } from "@/lib/setup";
+
+export const runtime = "nodejs";
+
+const channelSettingsSchema = z.object({
+ channelName: z.string().trim().min(2).max(80),
+ channelDescription: z.string().trim().max(500).optional(),
+ channelIsAdult: z.boolean(),
+});
+
+function redirectWithError(request: Request, error: string) {
+ const url = new URL("/dashboard/settings", request.url);
+ url.searchParams.set("error", error);
+ return NextResponse.redirect(url, 303);
+}
+
+function redirectWithSuccess(request: Request, success: string) {
+ const url = new URL("/dashboard/settings", request.url);
+ url.searchParams.set("success", success);
+ return NextResponse.redirect(url, 303);
+}
+
+export async function POST(request: Request) {
+ if (!(await isSetupComplete())) {
+ return NextResponse.redirect(new URL("/setup", request.url), 303);
+ }
+
+ const user = await getAuthenticatedUser();
+ if (!user) {
+ return NextResponse.redirect(new URL("/login", request.url), 303);
+ }
+
+ const channel = await getChannel();
+ if (!channel) {
+ return NextResponse.redirect(new URL("/setup", request.url), 303);
+ }
+
+ const formData = await request.formData();
+ const parsed = channelSettingsSchema.safeParse({
+ channelName: formData.get("channelName"),
+ channelDescription: formData.get("channelDescription") || undefined,
+ channelIsAdult: formData.get("channelIsAdult") === "on",
+ });
+
+ if (!parsed.success) {
+ return redirectWithError(request, "Please provide a valid channel name.");
+ }
+
+ try {
+ await db.channel.update({
+ where: {
+ id: channel.id,
+ },
+ data: {
+ name: parsed.data.channelName,
+ description: parsed.data.channelDescription || null,
+ isAdult: parsed.data.channelIsAdult,
+ },
+ });
+
+ return redirectWithSuccess(request, "Channel settings saved.");
+ } catch {
+ return redirectWithError(request, "Failed to save channel settings.");
+ }
+}
diff --git a/src/app/api/media/thumbnails/[videoId]/route.ts b/src/app/api/media/thumbnails/[videoId]/route.ts
new file mode 100644
index 0000000..381fd22
--- /dev/null
+++ b/src/app/api/media/thumbnails/[videoId]/route.ts
@@ -0,0 +1,43 @@
+import { readFile } from "node:fs/promises";
+
+import { NextResponse } from "next/server";
+
+import { db } from "@/lib/db";
+import { getThumbnailPath } from "@/lib/storage";
+
+export const runtime = "nodejs";
+export const dynamic = "force-dynamic";
+
+export async function GET(
+ _request: Request,
+ context: RouteContext<"/api/media/thumbnails/[videoId]">,
+) {
+ const { videoId } = await context.params;
+
+ const video = await db.video.findUnique({
+ where: {
+ id: videoId,
+ },
+ select: {
+ thumbnailFilename: true,
+ thumbnailMimeType: true,
+ },
+ });
+
+ if (!video?.thumbnailFilename) {
+ return NextResponse.json({ error: "Not found" }, { status: 404 });
+ }
+
+ try {
+ const fileBuffer = await readFile(getThumbnailPath(video.thumbnailFilename));
+ return new NextResponse(fileBuffer, {
+ status: 200,
+ headers: {
+ "Content-Type": video.thumbnailMimeType ?? "image/jpeg",
+ "Cache-Control": "public, max-age=31536000, immutable",
+ },
+ });
+ } catch {
+ return NextResponse.json({ error: "Not found" }, { status: 404 });
+ }
+}
diff --git a/src/app/api/media/videos/[videoId]/route.ts b/src/app/api/media/videos/[videoId]/route.ts
new file mode 100644
index 0000000..7252166
--- /dev/null
+++ b/src/app/api/media/videos/[videoId]/route.ts
@@ -0,0 +1,138 @@
+import { createReadStream } from "node:fs";
+import { stat } from "node:fs/promises";
+import { Readable } from "node:stream";
+
+import { NextResponse } from "next/server";
+
+import { db } from "@/lib/db";
+import { getVideoPath } from "@/lib/storage";
+
+export const runtime = "nodejs";
+export const dynamic = "force-dynamic";
+
+function parseRangeHeader(rangeHeader: string, size: number) {
+ const match = /^bytes=(\d*)-(\d*)$/.exec(rangeHeader.trim());
+ if (!match) {
+ return null;
+ }
+
+ const start = match[1] ? Number(match[1]) : 0;
+ const end = match[2] ? Number(match[2]) : size - 1;
+
+ if (!Number.isInteger(start) || !Number.isInteger(end)) {
+ return null;
+ }
+
+ if (start < 0 || end < start || end >= size) {
+ return null;
+ }
+
+ return { start, end };
+}
+
+function baseHeaders(mimeType: string, size: number): HeadersInit {
+ return {
+ "Content-Type": mimeType,
+ "Accept-Ranges": "bytes",
+ "Content-Length": String(size),
+ "Cache-Control": "public, max-age=0, must-revalidate",
+ };
+}
+
+export async function HEAD(
+ _request: Request,
+ context: RouteContext<"/api/media/videos/[videoId]">,
+) {
+ const { videoId } = await context.params;
+
+ const video = await db.video.findUnique({
+ where: {
+ id: videoId,
+ },
+ select: {
+ videoFilename: true,
+ videoMimeType: true,
+ },
+ });
+
+ if (!video) {
+ return NextResponse.json({ error: "Not found" }, { status: 404 });
+ }
+
+ try {
+ const fileStats = await stat(getVideoPath(video.videoFilename));
+
+ return new NextResponse(null, {
+ status: 200,
+ headers: baseHeaders(video.videoMimeType, fileStats.size),
+ });
+ } catch {
+ return NextResponse.json({ error: "Not found" }, { status: 404 });
+ }
+}
+
+export async function GET(
+ request: Request,
+ context: RouteContext<"/api/media/videos/[videoId]">,
+) {
+ const { videoId } = await context.params;
+
+ const video = await db.video.findUnique({
+ where: {
+ id: videoId,
+ },
+ select: {
+ videoFilename: true,
+ videoMimeType: true,
+ },
+ });
+
+ if (!video) {
+ return NextResponse.json({ error: "Not found" }, { status: 404 });
+ }
+
+ const filePath = getVideoPath(video.videoFilename);
+
+ let fileStats;
+ try {
+ fileStats = await stat(filePath);
+ } catch {
+ return NextResponse.json({ error: "Not found" }, { status: 404 });
+ }
+
+ const rangeHeader = request.headers.get("range");
+ if (!rangeHeader) {
+ const stream = createReadStream(filePath);
+ return new Response(Readable.toWeb(stream) as ReadableStream, {
+ status: 200,
+ headers: baseHeaders(video.videoMimeType, fileStats.size),
+ });
+ }
+
+ const range = parseRangeHeader(rangeHeader, fileStats.size);
+ if (!range) {
+ return new NextResponse(null, {
+ status: 416,
+ headers: {
+ "Content-Range": `bytes */${fileStats.size}`,
+ },
+ });
+ }
+
+ const stream = createReadStream(filePath, {
+ start: range.start,
+ end: range.end,
+ });
+ const chunkSize = range.end - range.start + 1;
+
+ return new Response(Readable.toWeb(stream) as ReadableStream, {
+ status: 206,
+ headers: {
+ "Content-Type": video.videoMimeType,
+ "Accept-Ranges": "bytes",
+ "Content-Length": String(chunkSize),
+ "Content-Range": `bytes ${range.start}-${range.end}/${fileStats.size}`,
+ "Cache-Control": "public, max-age=0, must-revalidate",
+ },
+ });
+}
diff --git a/src/app/api/setup/route.ts b/src/app/api/setup/route.ts
new file mode 100644
index 0000000..ca7572d
--- /dev/null
+++ b/src/app/api/setup/route.ts
@@ -0,0 +1,89 @@
+import { NextResponse } from "next/server";
+import { z } from "zod";
+
+import {
+ createSessionToken,
+ getSessionCookieOptions,
+ SESSION_COOKIE_NAME,
+ hashPassword,
+} from "@/lib/auth";
+import { db } from "@/lib/db";
+import { PRIMARY_CHANNEL_ID, isSetupComplete } from "@/lib/setup";
+import { ensureStorageDirectories } from "@/lib/storage";
+
+export const runtime = "nodejs";
+
+const setupSchema = z.object({
+ channelName: z.string().trim().min(2).max(80),
+ channelDescription: z.string().trim().max(500).optional(),
+ username: z
+ .string()
+ .trim()
+ .min(3)
+ .max(32)
+ .regex(/^[a-zA-Z0-9_-]+$/),
+ password: z.string().min(8).max(128),
+});
+
+function redirectWithError(request: Request, error: string) {
+ const url = new URL("/setup", request.url);
+ url.searchParams.set("error", error);
+ return NextResponse.redirect(url, 303);
+}
+
+export async function POST(request: Request) {
+ if (await isSetupComplete()) {
+ return NextResponse.redirect(new URL("/", request.url), 303);
+ }
+
+ const formData = await request.formData();
+ const parsed = setupSchema.safeParse({
+ channelName: formData.get("channelName"),
+ channelDescription: formData.get("channelDescription") || undefined,
+ username: formData.get("username"),
+ password: formData.get("password"),
+ });
+
+ if (!parsed.success) {
+ return redirectWithError(request, "Please fill all required fields correctly.");
+ }
+
+ const passwordHash = await hashPassword(parsed.data.password);
+
+ try {
+ await ensureStorageDirectories();
+
+ const user = await db.$transaction(async (tx) => {
+ await tx.channel.create({
+ data: {
+ id: PRIMARY_CHANNEL_ID,
+ name: parsed.data.channelName,
+ description: parsed.data.channelDescription || null,
+ },
+ });
+
+ return tx.user.create({
+ data: {
+ username: parsed.data.username,
+ passwordHash,
+ },
+ });
+ });
+
+ const sessionToken = await createSessionToken(user.id, user.username);
+ const response = NextResponse.redirect(new URL("/dashboard", request.url), 303);
+
+ response.cookies.set(
+ SESSION_COOKIE_NAME,
+ sessionToken,
+ getSessionCookieOptions(),
+ );
+
+ return response;
+ } catch {
+ return redirectWithError(
+ request,
+ "Setup failed. If this instance was already initialized, sign in instead.",
+ );
+ }
+}
diff --git a/src/app/api/videos/[videoId]/route.ts b/src/app/api/videos/[videoId]/route.ts
new file mode 100644
index 0000000..d580896
--- /dev/null
+++ b/src/app/api/videos/[videoId]/route.ts
@@ -0,0 +1,227 @@
+import { randomUUID } from "node:crypto";
+import { unlink, writeFile } from "node:fs/promises";
+
+import { NextResponse } from "next/server";
+import { z } from "zod";
+
+import { getAuthenticatedUser } from "@/lib/auth";
+import { db } from "@/lib/db";
+import { getChannel, isSetupComplete } from "@/lib/setup";
+import {
+ ensureStorageDirectories,
+ getThumbnailExtension,
+ getThumbnailPath,
+ getVideoPath,
+} from "@/lib/storage";
+
+export const runtime = "nodejs";
+
+const videoMetadataSchema = z.object({
+ title: z.string().trim().min(1).max(140),
+ description: z.string().trim().max(5000).optional(),
+});
+
+const maxVideoBytes = Number(process.env.MAX_VIDEO_UPLOAD_BYTES ?? 1_073_741_824);
+const maxThumbnailBytes = Number(process.env.MAX_THUMBNAIL_UPLOAD_BYTES ?? 5_242_880);
+
+function redirectWithError(request: Request, error: string) {
+ const url = new URL("/dashboard", request.url);
+ url.searchParams.set("error", error);
+ return NextResponse.redirect(url, 303);
+}
+
+function redirectWithSuccess(request: Request, success: string) {
+ const url = new URL("/dashboard", request.url);
+ url.searchParams.set("success", success);
+ return NextResponse.redirect(url, 303);
+}
+
+type VideoRouteContext = {
+ params: Promise<{ videoId: string }>;
+};
+
+export async function POST(request: Request, { params }: VideoRouteContext) {
+ if (!(await isSetupComplete())) {
+ return NextResponse.redirect(new URL("/setup", request.url), 303);
+ }
+
+ const user = await getAuthenticatedUser();
+ if (!user) {
+ return NextResponse.redirect(new URL("/login", request.url), 303);
+ }
+
+ const channel = await getChannel();
+ if (!channel) {
+ return NextResponse.redirect(new URL("/setup", request.url), 303);
+ }
+
+ const { videoId } = await params;
+ const formData = await request.formData();
+ const shouldDelete = formData.get("intent") === "delete";
+
+ const existingVideo = await db.video.findUnique({
+ where: {
+ id: videoId,
+ },
+ select: {
+ id: true,
+ channelId: true,
+ videoFilename: true,
+ videoMimeType: true,
+ videoSizeBytes: true,
+ thumbnailFilename: true,
+ thumbnailMimeType: true,
+ },
+ });
+
+ if (!existingVideo || existingVideo.channelId !== channel.id) {
+ return redirectWithError(request, "Video not found.");
+ }
+
+ if (shouldDelete) {
+ try {
+ await db.video.delete({
+ where: {
+ id: videoId,
+ },
+ });
+
+ await Promise.all([
+ unlink(getVideoPath(existingVideo.videoFilename)).catch(() => undefined),
+ existingVideo.thumbnailFilename
+ ? unlink(getThumbnailPath(existingVideo.thumbnailFilename)).catch(() => undefined)
+ : undefined,
+ ]);
+
+ return redirectWithSuccess(request, "Video deleted.");
+ } catch {
+ return redirectWithError(request, "Failed to delete video.");
+ }
+ }
+
+ const parsed = videoMetadataSchema.safeParse({
+ title: formData.get("title"),
+ description: formData.get("description") || undefined,
+ });
+
+ if (!parsed.success) {
+ return redirectWithError(request, "Video title is required.");
+ }
+
+ const videoInput = formData.get("video");
+ const replacementVideo =
+ videoInput instanceof File && videoInput.size > 0 ? videoInput : null;
+
+ if (replacementVideo) {
+ if (replacementVideo.size > maxVideoBytes) {
+ return redirectWithError(request, "Video exceeds the configured upload size limit.");
+ }
+
+ const lowerVideoName = replacementVideo.name.toLowerCase();
+ const isMp4 =
+ replacementVideo.type === "video/mp4" || lowerVideoName.endsWith(".mp4");
+ if (!isMp4) {
+ return redirectWithError(request, "Only MP4 uploads are supported right now.");
+ }
+ }
+
+ const thumbnailInput = formData.get("thumbnail");
+ const replacementThumbnail =
+ thumbnailInput instanceof File && thumbnailInput.size > 0 ? thumbnailInput : null;
+
+ let replacementThumbnailExtension: string | null = null;
+
+ if (replacementThumbnail) {
+ if (replacementThumbnail.size > maxThumbnailBytes) {
+ return redirectWithError(
+ request,
+ "Thumbnail exceeds the configured upload size limit.",
+ );
+ }
+
+ replacementThumbnailExtension = getThumbnailExtension(replacementThumbnail);
+ if (!replacementThumbnailExtension) {
+ return redirectWithError(request, "Thumbnail must be JPG, PNG, or WEBP.");
+ }
+ }
+
+ let nextVideoFilename = existingVideo.videoFilename;
+ let nextVideoMimeType = existingVideo.videoMimeType;
+ let nextVideoSizeBytes = existingVideo.videoSizeBytes;
+ let nextThumbnailFilename = existingVideo.thumbnailFilename;
+ let nextThumbnailMimeType = existingVideo.thumbnailMimeType;
+
+ let newVideoPath: string | null = null;
+ let newThumbnailPath: string | null = null;
+
+ if (replacementVideo) {
+ const version = randomUUID().replaceAll("-", "");
+ nextVideoFilename = `${videoId}-${version}.mp4`;
+ nextVideoMimeType = replacementVideo.type || "video/mp4";
+ nextVideoSizeBytes = replacementVideo.size;
+ newVideoPath = getVideoPath(nextVideoFilename);
+ }
+
+ if (replacementThumbnail && replacementThumbnailExtension) {
+ const version = randomUUID().replaceAll("-", "");
+ nextThumbnailFilename = `${videoId}-${version}${replacementThumbnailExtension}`;
+ nextThumbnailMimeType =
+ replacementThumbnail.type ||
+ (replacementThumbnailExtension === ".png"
+ ? "image/png"
+ : replacementThumbnailExtension === ".webp"
+ ? "image/webp"
+ : "image/jpeg");
+ newThumbnailPath = getThumbnailPath(nextThumbnailFilename);
+ }
+
+ try {
+ if (replacementVideo || replacementThumbnail) {
+ await ensureStorageDirectories();
+ }
+
+ if (replacementVideo && newVideoPath) {
+ await writeFile(newVideoPath, Buffer.from(await replacementVideo.arrayBuffer()));
+ }
+
+ if (replacementThumbnail && newThumbnailPath) {
+ await writeFile(
+ newThumbnailPath,
+ Buffer.from(await replacementThumbnail.arrayBuffer()),
+ );
+ }
+
+ await db.video.update({
+ where: {
+ id: videoId,
+ },
+ data: {
+ title: parsed.data.title,
+ description: parsed.data.description || null,
+ videoFilename: nextVideoFilename,
+ videoMimeType: nextVideoMimeType,
+ videoSizeBytes: nextVideoSizeBytes,
+ thumbnailFilename: nextThumbnailFilename,
+ thumbnailMimeType: nextThumbnailMimeType,
+ },
+ });
+
+ await Promise.all([
+ replacementVideo
+ ? unlink(getVideoPath(existingVideo.videoFilename)).catch(() => undefined)
+ : undefined,
+ replacementThumbnail && existingVideo.thumbnailFilename
+ ? unlink(getThumbnailPath(existingVideo.thumbnailFilename)).catch(() => undefined)
+ : undefined,
+ ]);
+
+ return redirectWithSuccess(request, "Video updated.");
+ } catch {
+ await Promise.all([
+ newVideoPath ? unlink(newVideoPath).catch(() => undefined) : undefined,
+ newThumbnailPath ? unlink(newThumbnailPath).catch(() => undefined) : undefined,
+ ]);
+
+ return redirectWithError(request, "Failed to update video.");
+ }
+}
diff --git a/src/app/api/videos/route.ts b/src/app/api/videos/route.ts
new file mode 100644
index 0000000..f12338a
--- /dev/null
+++ b/src/app/api/videos/route.ts
@@ -0,0 +1,159 @@
+import { randomUUID } from "node:crypto";
+import { unlink, writeFile } from "node:fs/promises";
+
+import { NextResponse } from "next/server";
+import { z } from "zod";
+
+import { getAuthenticatedUser } from "@/lib/auth";
+import { db } from "@/lib/db";
+import { getChannel, isSetupComplete } from "@/lib/setup";
+import {
+ ensureStorageDirectories,
+ getThumbnailExtension,
+ getThumbnailPath,
+ getVideoPath,
+} from "@/lib/storage";
+
+export const runtime = "nodejs";
+
+const metadataSchema = z.object({
+ title: z.string().trim().min(1).max(140),
+ description: z.string().trim().max(5000).optional(),
+});
+
+const maxVideoBytes = Number(process.env.MAX_VIDEO_UPLOAD_BYTES ?? 1_073_741_824);
+const maxThumbnailBytes = Number(process.env.MAX_THUMBNAIL_UPLOAD_BYTES ?? 5_242_880);
+
+function redirectWithError(request: Request, error: string) {
+ const url = new URL("/dashboard", request.url);
+ url.searchParams.set("error", error);
+ return NextResponse.redirect(url, 303);
+}
+
+function redirectWithSuccess(request: Request, success: string) {
+ const url = new URL("/dashboard", request.url);
+ url.searchParams.set("success", success);
+ return NextResponse.redirect(url, 303);
+}
+
+export async function POST(request: Request) {
+ if (!(await isSetupComplete())) {
+ return NextResponse.redirect(new URL("/setup", request.url), 303);
+ }
+
+ const user = await getAuthenticatedUser();
+ if (!user) {
+ return NextResponse.redirect(new URL("/login", request.url), 303);
+ }
+
+ const channel = await getChannel();
+ if (!channel) {
+ return NextResponse.redirect(new URL("/setup", request.url), 303);
+ }
+
+ const formData = await request.formData();
+ const metadata = metadataSchema.safeParse({
+ title: formData.get("title"),
+ description: formData.get("description") || undefined,
+ });
+
+ if (!metadata.success) {
+ return redirectWithError(request, "Video title is required.");
+ }
+
+ const videoInput = formData.get("video");
+ if (!(videoInput instanceof File)) {
+ return redirectWithError(request, "Please select a video file.");
+ }
+
+ if (videoInput.size <= 0) {
+ return redirectWithError(request, "Video file cannot be empty.");
+ }
+
+ if (videoInput.size > maxVideoBytes) {
+ return redirectWithError(request, "Video exceeds the configured upload size limit.");
+ }
+
+ const lowerVideoName = videoInput.name.toLowerCase();
+ const isMp4 = videoInput.type === "video/mp4" || lowerVideoName.endsWith(".mp4");
+ if (!isMp4) {
+ return redirectWithError(request, "Only MP4 uploads are supported right now.");
+ }
+
+ const thumbnailInput = formData.get("thumbnail");
+ const thumbnailFile =
+ thumbnailInput instanceof File && thumbnailInput.size > 0
+ ? thumbnailInput
+ : null;
+
+ let thumbnailExtension: string | null = null;
+ if (thumbnailFile) {
+ if (thumbnailFile.size > maxThumbnailBytes) {
+ return redirectWithError(
+ request,
+ "Thumbnail exceeds the configured upload size limit.",
+ );
+ }
+
+ thumbnailExtension = getThumbnailExtension(thumbnailFile);
+ if (!thumbnailExtension) {
+ return redirectWithError(
+ request,
+ "Thumbnail must be JPG, PNG, or WEBP.",
+ );
+ }
+ }
+
+ const videoId = randomUUID().replaceAll("-", "");
+ const videoFilename = `${videoId}.mp4`;
+ const videoPath = getVideoPath(videoFilename);
+
+ let thumbnailFilename: string | null = null;
+ let thumbnailMimeType: string | null = null;
+ let thumbnailPath: string | null = null;
+
+ if (thumbnailFile && thumbnailExtension) {
+ thumbnailFilename = `${videoId}${thumbnailExtension}`;
+ thumbnailMimeType =
+ thumbnailFile.type ||
+ (thumbnailExtension === ".png"
+ ? "image/png"
+ : thumbnailExtension === ".webp"
+ ? "image/webp"
+ : "image/jpeg");
+ thumbnailPath = getThumbnailPath(thumbnailFilename);
+ }
+
+ try {
+ await ensureStorageDirectories();
+
+ await writeFile(videoPath, Buffer.from(await videoInput.arrayBuffer()));
+
+ if (thumbnailFile && thumbnailPath) {
+ await writeFile(thumbnailPath, Buffer.from(await thumbnailFile.arrayBuffer()));
+ }
+
+ await db.video.create({
+ data: {
+ id: videoId,
+ channelId: channel.id,
+ title: metadata.data.title,
+ description: metadata.data.description || null,
+ videoFilename,
+ videoMimeType: videoInput.type || "video/mp4",
+ videoSizeBytes: videoInput.size,
+ thumbnailFilename,
+ thumbnailMimeType,
+ },
+ });
+
+ return redirectWithSuccess(request, "Upload complete.");
+ } catch {
+ await Promise.all([
+ unlink(videoPath).catch(() => undefined),
+ thumbnailPath ? unlink(thumbnailPath).catch(() => undefined) : undefined,
+ ]);
+
+ return redirectWithError(request, "Upload failed. Please try again.");
+ }
+}
diff --git a/src/app/dashboard/layout.tsx b/src/app/dashboard/layout.tsx
new file mode 100644
index 0000000..cce288d
--- /dev/null
+++ b/src/app/dashboard/layout.tsx
@@ -0,0 +1,48 @@
+import Link from "next/link";
+
+import { DashboardTabs } from "@/components/DashboardTabs";
+import { requireAuthenticatedUser } from "@/lib/auth";
+import { getChannel, requireSetup } from "@/lib/setup";
+
+type DashboardLayoutProps = {
+ children: React.ReactNode;
+};
+
+export default async function DashboardLayout({ children }: DashboardLayoutProps) {
+ await requireSetup();
+
+ const [user, channel] = await Promise.all([requireAuthenticatedUser(), getChannel()]);
+
+ return (
+ MeTube
+ @{user.username}
+ ·
+ Manage uploads and channel details
+ {channel?.name ?? "Channel"}
+
+ MP4 uploads appear here. You can edit metadata, replace video files, update + thumbnails, or delete videos anytime. +
++ Publish a video now and see it live on your channel. Make sure to give it a nice + name, thumbnail and description. +
+{video.description}
+ ) : null} ++ {formatDate(video.publishedAt)} + + · + + {formatBytes(video.videoSizeBytes)} +
++ Edits here update your public channel: the home page header, watch pages, and anywhere the + channel name appears. +
++ After logging in, manage your YOUR channel. +
+ + {error ?404
++ The video you requested does not exist on this channel. +
+ + Return home + +
- Looking for a starting point or more instructions? Head over to{" "}
-
+ Age confirmation
+ You must confirm that you are 18 years or older before viewing this channel.
+
- Templates
- {" "}
- or the{" "}
-
- Learning
- {" "}
- center.
-
-
+ This channel has not added a description yet. +
+ )} +No videos yet.
++ {user ? ( + <> + Upload your first one from the{" "} + + dashboard + + . + > + ) : ( + "Sign in to upload from the dashboard." + )} +
++ {formatDate(video.publishedAt)} +
++ FIRST RUN SETUP +
++ This setup runs once per instance. You will create the channel identity and an admin + account used for uploads. +
+ + {error ?Description
+{video.description}
+{description}
+About this video
+ {description ? ( +{description}
+ ) : ( +No description was added for this upload.
+ )} +{displayTitle}
+{dateLabel}
+