commit 4ab1f87c1a65075b22b1d91d1c9aa7dd1e474bb7 Author: JosLe Date: Mon May 4 16:09:15 2026 +0200 init: video-share — minimal no-signup file/video share with TTL diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..5d31d38 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,10 @@ +node_modules +npm-debug.log +.git +.gitignore +.env +.env.* +*.md +data +uploads +.DS_Store diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b061d7c --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +node_modules +.env +.env.* +data/ +uploads/ +*.log +.DS_Store diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f372f29 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,24 @@ +FROM node:20-alpine + +WORKDIR /app + +COPY package.json ./ +RUN npm install --omit=dev --no-audit --no-fund + +COPY src ./src + +ENV NODE_ENV=production \ + PORT=3000 \ + HOST=0.0.0.0 \ + DATA_DIR=/data/uploads \ + MAX_FILE_SIZE=524288000 + +RUN mkdir -p /data/uploads && chown -R node:node /data +USER node + +EXPOSE 3000 + +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ + CMD wget -qO- http://127.0.0.1:3000/healthz >/dev/null || exit 1 + +CMD ["node", "src/server.js"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..1183708 --- /dev/null +++ b/README.md @@ -0,0 +1,79 @@ +# share + +Minimalist no-signup file/video drop. Upload a file, get a private UUID link, share it. Files self-destruct after a configurable TTL. + +- No accounts, no DB. Metadata is a tiny JSON sidecar per upload. +- Streamed uploads (no in-memory buffering). +- HTTP `Range` requests so HTML5 video can seek. +- Background sweeper deletes expired files every 5 min. +- Per-upload `deleteToken` lets the uploader revoke early. + +## Local dev + +```bash +npm install +npm run dev +# http://127.0.0.1:3000 +``` + +Files land in `./data/uploads`. + +## Configuration + +| Env | Default | Notes | +|---|---|---| +| `PORT` | `3000` | HTTP port | +| `HOST` | `0.0.0.0` | Bind host | +| `DATA_DIR` | `/data/uploads` | Persisted directory (mount a volume here) | +| `MAX_FILE_SIZE` | `524288000` | 500 MB. Bytes. | +| `LOG_LEVEL` | `info` | Fastify logger level | + +TTL options exposed in the UI: 30m / 1h / 6h / 24h. Edit `TTL_OPTIONS` in `src/server.js` to change. + +## Deploy on Coolify (montlab.dev) + +Two ways. Pick one. + +### Option A — Dockerfile build pack (matches existing apps) + +1. Push this repo to Gitea: `git.montlab.dev/JosLe/video-share`. +2. In Coolify, create a new app: + - Build pack: **Dockerfile** + - Source: Public Repository → `https://git.montlab.dev/JosLe/video-share.git` + - Branch: `main` + - Port: `3000` + - Domain: `share.montlab.dev` (Caddy + Let's Encrypt is automatic) +3. **Persistent storage** → add a volume: + - Source: `share_data` (named volume, or a host path you prefer) + - Destination: `/data` +4. **Environment variables** (optional override): + - `MAX_FILE_SIZE=524288000` +5. Set up the Gitea webhook exactly like the other apps in `INFRASTRUCTURE.md`. +6. Deploy. First time may need "Force redeploy without cache". + +### Option B — Docker Compose build pack + +Use the bundled `docker-compose.yaml`. Pick build pack **Docker Compose** in Coolify, point at this repo, set the domain on the `app` service to `share.montlab.dev`, port `3000`. The named volume `share_data` is declared in the compose file. + +## Caddy / proxy notes + +Coolify's Caddy fronts the app, so HTTPS, HTTP/2 and request body limits are handled there. caddy-docker-proxy v2.9 does **not** cap upload size by default for HTTP/2, but if a future config sets `request_body { max_size ... }`, bump it above `MAX_FILE_SIZE`. + +## Endpoints + +| Method | Path | Purpose | +|---|---|---| +| `GET` | `/` | Upload page | +| `POST` | `/upload` | multipart form: `file` + `ttl` (`30m`/`1h`/`6h`/`24h`) | +| `GET` | `/v/:id` | Viewer (HTML, video player) | +| `GET` | `/f/:id` | Raw file stream, supports `Range` | +| `GET` | `/f/:id?dl=1` | Force download | +| `GET` | `/api/info/:id` | JSON metadata | +| `DELETE` | `/api/:id?token=...` | Revoke with the delete token | +| `GET` | `/healthz` | Liveness | + +Links use UUID v4. They are unguessable but **not authenticated** — anyone with the link can view. Send the link over a private channel. + +## Disk usage + +A 500 MB upload at 1 h TTL uses ~500 MB until the sweep cycle picks it up (≤5 min after expiry). For peak sizing assume `users × max_concurrent_uploads × MAX_FILE_SIZE × longest_TTL/sweep_interval`. Coolify's CX22 has 40 GB SSD — keep an eye on it or lower `MAX_FILE_SIZE`. diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..9b67d04 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,16 @@ +services: + app: + build: . + restart: unless-stopped + environment: + PORT: 3000 + DATA_DIR: /data/uploads + MAX_FILE_SIZE: ${MAX_FILE_SIZE:-524288000} + LOG_LEVEL: info + volumes: + - share_data:/data + expose: + - "3000" + +volumes: + share_data: diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..f66fe07 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1214 @@ +{ + "name": "video-share", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "video-share", + "version": "0.1.0", + "dependencies": { + "@fastify/multipart": "^8.3.0", + "@fastify/static": "^7.0.4", + "fastify": "^4.28.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@fastify/accept-negotiator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@fastify/accept-negotiator/-/accept-negotiator-1.1.0.tgz", + "integrity": "sha512-OIHZrb2ImZ7XG85HXOONLcJWGosv7sIvM2ifAPQVhg9Lv7qdmMBNVaai4QTdyuaqbKM5eO6sLSQOYI7wEQeCJQ==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@fastify/ajv-compiler": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-3.6.0.tgz", + "integrity": "sha512-LwdXQJjmMD+GwLOkP7TVC68qa+pSSogeWWmznRJ/coyTcfe9qA05AHFSe1eZFwK6q+xVRpChnvFUkf1iYaSZsQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.11.0", + "ajv-formats": "^2.1.1", + "fast-uri": "^2.0.0" + } + }, + "node_modules/@fastify/busboy": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-3.2.0.tgz", + "integrity": "sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA==", + "license": "MIT" + }, + "node_modules/@fastify/deepmerge": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@fastify/deepmerge/-/deepmerge-2.0.2.tgz", + "integrity": "sha512-3wuLdX5iiiYeZWP6bQrjqhrcvBIf0NHbQH1Ur1WbHvoiuTYUEItgygea3zs8aHpiitn0lOB8gX20u1qO+FDm7Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/error": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@fastify/error/-/error-4.2.0.tgz", + "integrity": "sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/fast-json-stringify-compiler": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-4.3.0.tgz", + "integrity": "sha512-aZAXGYo6m22Fk1zZzEUKBvut/CIIQe/BapEORnxiD5Qr0kPHqqI69NtEMCme74h+at72sPhbkb4ZrLd1W3KRLA==", + "license": "MIT", + "dependencies": { + "fast-json-stringify": "^5.7.0" + } + }, + "node_modules/@fastify/merge-json-schemas": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.1.1.tgz", + "integrity": "sha512-fERDVz7topgNjtXsJTTW1JKLy0rhuLRcquYqNR9rF7OcVpCa2OVW49ZPDIhaRRCaUuvVxI+N416xUoF76HNSXA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + } + }, + "node_modules/@fastify/multipart": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/@fastify/multipart/-/multipart-8.3.1.tgz", + "integrity": "sha512-pncbnG28S6MIskFSVRtzTKE9dK+GrKAJl0NbaQ/CG8ded80okWFsYKzSlP9haaLNQhNRDOoHqmGQNvgbiPVpWQ==", + "license": "MIT", + "dependencies": { + "@fastify/busboy": "^3.0.0", + "@fastify/deepmerge": "^2.0.0", + "@fastify/error": "^4.0.0", + "fastify-plugin": "^4.0.0", + "secure-json-parse": "^2.4.0", + "stream-wormhole": "^1.1.0" + } + }, + "node_modules/@fastify/send": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@fastify/send/-/send-2.1.0.tgz", + "integrity": "sha512-yNYiY6sDkexoJR0D8IDy3aRP3+L4wdqCpvx5WP+VtEU58sn7USmKynBzDQex5X42Zzvw2gNzzYgP90UfWShLFA==", + "license": "MIT", + "dependencies": { + "@lukeed/ms": "^2.0.1", + "escape-html": "~1.0.3", + "fast-decode-uri-component": "^1.0.1", + "http-errors": "2.0.0", + "mime": "^3.0.0" + } + }, + "node_modules/@fastify/static": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/@fastify/static/-/static-7.0.4.tgz", + "integrity": "sha512-p2uKtaf8BMOZWLs6wu+Ihg7bWNBdjNgCwDza4MJtTqg+5ovKmcbgbR9Xs5/smZ1YISfzKOCNYmZV8LaCj+eJ1Q==", + "license": "MIT", + "dependencies": { + "@fastify/accept-negotiator": "^1.0.0", + "@fastify/send": "^2.0.0", + "content-disposition": "^0.5.3", + "fastify-plugin": "^4.0.0", + "fastq": "^1.17.0", + "glob": "^10.3.4" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@lukeed/ms": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@lukeed/ms/-/ms-2.0.2.tgz", + "integrity": "sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", + "license": "MIT" + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/abstract-logging": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", + "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", + "license": "MIT" + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "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/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv/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==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/avvio": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/avvio/-/avvio-8.4.0.tgz", + "integrity": "sha512-CDSwaxINFy59iNwhYnkvALBwZiTydGkOecZyPkqBpABYR1KqGEsET0VOOYDwtleZSUIdeY36DC2bSZ24CO1igA==", + "license": "MIT", + "dependencies": { + "@fastify/error": "^3.3.0", + "fastq": "^1.17.1" + } + }, + "node_modules/avvio/node_modules/@fastify/error": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@fastify/error/-/error-3.4.1.tgz", + "integrity": "sha512-wWSvph+29GR783IhmvdwWnN4bUxTD01Vm5Xad4i7i1VuAOItLvbPAb69sb0IQ2N57yprvhNIwAP5B6xfKTmjmQ==", + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/fast-content-type-parse": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-1.1.0.tgz", + "integrity": "sha512-fBHHqSTFLVnR61C+gltJuE5GkVQMV0S2nqUO8TJ+5Z3qAKG8vAx4FKai1s5jq/inV1+sREynIWSuQ6HgoSXpDQ==", + "license": "MIT" + }, + "node_modules/fast-decode-uri-component": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", + "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "node_modules/fast-json-stringify": { + "version": "5.16.1", + "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-5.16.1.tgz", + "integrity": "sha512-KAdnLvy1yu/XrRtP+LJnxbBGrhN+xXu+gt3EUvZhYGKCr3lFHq/7UFJHHFgmJKoqlh6B40bZLEv7w46B0mqn1g==", + "license": "MIT", + "dependencies": { + "@fastify/merge-json-schemas": "^0.1.0", + "ajv": "^8.10.0", + "ajv-formats": "^3.0.1", + "fast-deep-equal": "^3.1.3", + "fast-uri": "^2.1.0", + "json-schema-ref-resolver": "^1.0.1", + "rfdc": "^1.2.0" + } + }, + "node_modules/fast-json-stringify/node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/fast-querystring": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", + "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", + "license": "MIT", + "dependencies": { + "fast-decode-uri-component": "^1.0.1" + } + }, + "node_modules/fast-uri": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-2.4.0.tgz", + "integrity": "sha512-ypuAmmMKInk5q7XcepxlnUWDLWv4GFtaJqAzWKqn62IpQ3pejtr5dTVbt3vwqVaMKmkNR55sTT+CqUKIaT21BA==", + "license": "MIT" + }, + "node_modules/fastify": { + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/fastify/-/fastify-4.29.1.tgz", + "integrity": "sha512-m2kMNHIG92tSNWv+Z3UeTR9AWLLuo7KctC7mlFPtMEVrfjIhmQhkQnT9v15qA/BfVq3vvj134Y0jl9SBje3jXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/ajv-compiler": "^3.5.0", + "@fastify/error": "^3.4.0", + "@fastify/fast-json-stringify-compiler": "^4.3.0", + "abstract-logging": "^2.0.1", + "avvio": "^8.3.0", + "fast-content-type-parse": "^1.1.0", + "fast-json-stringify": "^5.8.0", + "find-my-way": "^8.0.0", + "light-my-request": "^5.11.0", + "pino": "^9.0.0", + "process-warning": "^3.0.0", + "proxy-addr": "^2.0.7", + "rfdc": "^1.3.0", + "secure-json-parse": "^2.7.0", + "semver": "^7.5.4", + "toad-cache": "^3.3.0" + } + }, + "node_modules/fastify-plugin": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-4.5.1.tgz", + "integrity": "sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ==", + "license": "MIT" + }, + "node_modules/fastify/node_modules/@fastify/error": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@fastify/error/-/error-3.4.1.tgz", + "integrity": "sha512-wWSvph+29GR783IhmvdwWnN4bUxTD01Vm5Xad4i7i1VuAOItLvbPAb69sb0IQ2N57yprvhNIwAP5B6xfKTmjmQ==", + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/find-my-way": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-8.2.2.tgz", + "integrity": "sha512-Dobi7gcTEq8yszimcfp/R7+owiT4WncAJ7VTTgFH1jYJ5GaG1FbhjwDG820hptN0QDFvzVY3RfCzdInvGPGzjA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-querystring": "^1.0.0", + "safe-regex2": "^3.1.0" + }, + "engines": { + "node": ">=14" + } + }, + "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==", + "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/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/json-schema-ref-resolver": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-1.0.1.tgz", + "integrity": "sha512-EJAj1pgHc1hxF6vo2Z3s69fMjO1INq6eGHXZ8Z6wCQeldCuwxGK9Sxf4/cScGn3FZubCVUehfWtcDM/PLteCQw==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/light-my-request": { + "version": "5.14.0", + "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-5.14.0.tgz", + "integrity": "sha512-aORPWntbpH5esaYpGOOmri0OHDOe3wC5M2MQxZ9dvMLZm6DnaAn0kJlcbU9hwsQgLzmZyReKwFwwPkR+nHu5kA==", + "license": "BSD-3-Clause", + "dependencies": { + "cookie": "^0.7.0", + "process-warning": "^3.0.0", + "set-cookie-parser": "^2.4.1" + } + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/pino": { + "version": "9.14.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-9.14.0.tgz", + "integrity": "sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==", + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", + "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", + "license": "MIT" + }, + "node_modules/pino/node_modules/process-warning": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", + "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/process-warning": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-3.0.0.tgz", + "integrity": "sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ret": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.4.3.tgz", + "integrity": "sha512-0f4Memo5QP7WQyUEAYUO3esD/XjOc3Zjjg5CPsAq1p8sIu0XPeMbHJemKA0BO7tV0X7+A0FoEpbmHXWxPyD3wQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-regex2": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-3.1.0.tgz", + "integrity": "sha512-RAAZAGbap2kBfbVhvmnTFv73NWLMvDGOITFYTZBAaY8eR+Ir4ef7Up/e7amo+y1+AH+3PtLkrt9mvcTsG9LXug==", + "license": "MIT", + "dependencies": { + "ret": "~0.4.0" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/secure-json-parse": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", + "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==", + "license": "BSD-3-Clause" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sonic-boom": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.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/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/stream-wormhole": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stream-wormhole/-/stream-wormhole-1.1.0.tgz", + "integrity": "sha512-gHFfL3px0Kctd6Po0M8TzEvt3De/xu6cnRrjlfYNhwbhLPLwigI2t1nc6jrzNuaYg5C4YF78PPFuQPzRiqn9ew==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/thread-stream": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", + "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + } + }, + "node_modules/toad-cache": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.0.tgz", + "integrity": "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..b693ad3 --- /dev/null +++ b/package.json @@ -0,0 +1,19 @@ +{ + "name": "video-share", + "version": "0.1.0", + "private": true, + "type": "module", + "description": "Minimalist no-signup video/file share with private UUID links and TTL", + "scripts": { + "start": "node src/server.js", + "dev": "node --watch src/server.js" + }, + "engines": { + "node": ">=20" + }, + "dependencies": { + "@fastify/multipart": "^8.3.0", + "@fastify/static": "^7.0.4", + "fastify": "^4.28.1" + } +} diff --git a/src/public/index.html b/src/public/index.html new file mode 100644 index 0000000..fb4e769 --- /dev/null +++ b/src/public/index.html @@ -0,0 +1,77 @@ + + + + + + +share — quick file drop + + + +
+
+

share

+

no signup · ephemeral · private link

+
+ +
+ + +
+ + +
+ + +
+ + + + + +
+ files self-destruct after expiry +
+
+ + + + + diff --git a/src/public/style.css b/src/public/style.css new file mode 100644 index 0000000..d478057 --- /dev/null +++ b/src/public/style.css @@ -0,0 +1,215 @@ +:root { + --bg: #0e0f12; + --fg: #e8e8e8; + --muted: #8a8f98; + --accent: #7cf2c4; + --accent-fg: #0e0f12; + --line: #23262d; + --card: #15171c; + --danger: #ff6b6b; +} + +* { box-sizing: border-box; } +html, body { margin: 0; padding: 0; } +body { + background: var(--bg); + color: var(--fg); + font: 15px/1.5 ui-monospace, "JetBrains Mono", Menlo, Consolas, monospace; + min-height: 100vh; + display: flex; + align-items: flex-start; + justify-content: center; + padding: 6vh 20px 40px; +} +body.center { align-items: center; } + +a { color: var(--accent); text-decoration: none; } +a:hover { text-decoration: underline; } + +.muted { color: var(--muted); } + +.brand { + font-weight: 600; + font-size: 22px; + letter-spacing: 0.5px; + color: var(--fg); +} +.brand:hover { text-decoration: none; color: var(--accent); } + +main.upload { + width: 100%; + max-width: 560px; +} +main.upload header { + margin-bottom: 24px; +} +main.upload h1 { + margin: 0 0 4px; +} + +.drop { + display: block; + border: 1px dashed var(--line); + border-radius: 12px; + background: var(--card); + padding: 48px 24px; + text-align: center; + cursor: pointer; + transition: border-color .15s, background .15s; +} +.drop:hover, .drop.over { + border-color: var(--accent); + background: #181b22; +} +.drop-title { font-size: 17px; margin-bottom: 4px; } + +.row { + display: flex; + gap: 12px; + align-items: end; + margin-top: 16px; +} +.ttl { flex: 1; display: flex; flex-direction: column; gap: 4px; } +select, input[type="text"] { + background: var(--card); + color: var(--fg); + border: 1px solid var(--line); + border-radius: 8px; + padding: 10px 12px; + font: inherit; + width: 100%; +} +select:focus, input[type="text"]:focus { outline: none; border-color: var(--accent); } + +.btn { + display: inline-block; + background: var(--card); + color: var(--fg); + border: 1px solid var(--line); + border-radius: 8px; + padding: 10px 16px; + font: inherit; + cursor: pointer; + text-align: center; +} +.btn:hover { border-color: var(--accent); color: var(--accent); text-decoration: none; } +.btn.primary { + background: var(--accent); + color: var(--accent-fg); + border-color: var(--accent); + font-weight: 600; +} +.btn.primary:hover { color: var(--accent-fg); filter: brightness(1.05); } +.btn.primary:disabled { opacity: .4; cursor: not-allowed; filter: none; } +.btn.ghost { background: transparent; } + +.progress { margin-top: 16px; } +.bar { + height: 6px; + background: var(--line); + border-radius: 3px; + overflow: hidden; +} +.fill { + height: 100%; + width: 0; + background: var(--accent); + transition: width .15s; +} +#progressText { margin-top: 6px; font-size: 13px; } + +.result { + margin-top: 24px; + padding: 20px; + background: var(--card); + border: 1px solid var(--line); + border-radius: 12px; +} +.result-title { font-weight: 600; margin-bottom: 12px; } +.link-row { display: flex; gap: 8px; } +.link-row input { + font-size: 13px; +} +.actions { display: flex; gap: 8px; margin-top: 12px; } +.extra { margin-top: 14px; } +.extra summary { cursor: pointer; user-select: none; } +.extra .link-row { margin-top: 8px; } + +.error { + margin-top: 16px; + padding: 12px 14px; + background: #2a1416; + border: 1px solid var(--danger); + border-radius: 8px; + color: var(--danger); + font-size: 14px; +} + +.foot { + margin-top: 28px; + font-size: 12px; + text-align: center; +} + +/* viewer */ +main.viewer { + width: 100%; + max-width: 1000px; +} +main.viewer header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; +} +.player { + background: #000; + border-radius: 12px; + overflow: hidden; + display: flex; + justify-content: center; + align-items: center; + min-height: 240px; +} +.player video, .player audio, .player img { + display: block; + max-width: 100%; + max-height: 80vh; + width: 100%; + height: auto; + background: #000; +} +.player audio { width: 100%; } +.filebox { + padding: 40px; + color: var(--fg); + text-align: center; +} +.filebox .filename { font-size: 18px; margin-bottom: 6px; word-break: break-all; } + +footer.meta { + display: flex; + justify-content: space-between; + align-items: center; + gap: 12px; + margin-top: 14px; + flex-wrap: wrap; +} +footer.meta .filename { word-break: break-all; } + +main.card { + background: var(--card); + border: 1px solid var(--line); + border-radius: 12px; + padding: 32px 40px; + text-align: center; + max-width: 420px; +} +main.card h1 { margin: 0 0 8px; font-size: 48px; } + +@media (max-width: 480px) { + body { padding: 24px 12px; } + .drop { padding: 32px 16px; } + .row { flex-direction: column; align-items: stretch; } + .btn.primary { width: 100%; } +} diff --git a/src/public/upload.js b/src/public/upload.js new file mode 100644 index 0000000..964e1b9 --- /dev/null +++ b/src/public/upload.js @@ -0,0 +1,147 @@ +(() => { + const $ = (id) => document.getElementById(id); + const form = $('form'); + const drop = $('drop'); + const fileInput = $('file'); + const dropTitle = $('dropTitle'); + const dropHint = $('dropHint'); + const submitBtn = $('submit'); + const ttlSel = $('ttl'); + const progress = $('progress'); + const bar = $('bar'); + const progressText = $('progressText'); + const result = $('result'); + const link = $('link'); + const open = $('open'); + const copyBtn = $('copy'); + const resetBtn = $('reset'); + const errorBox = $('error'); + const resultMeta = $('resultMeta'); + const delLink = $('delLink'); + const copyDel = $('copyDel'); + + const MAX = window.MAX_FILE_SIZE || (500 * 1024 * 1024); + + function human(n) { + if (!Number.isFinite(n)) return '?'; + const u = ['B', 'KB', 'MB', 'GB', 'TB']; + let i = 0; + while (n >= 1024 && i < u.length - 1) { n /= 1024; i++; } + return `${n.toFixed(n >= 10 || i === 0 ? 0 : 1)} ${u[i]}`; + } + + function setFile(f) { + if (!f) return; + if (f.size > MAX) { + showError(`file too large (${human(f.size)} > ${human(MAX)})`); + fileInput.value = ''; + submitBtn.disabled = true; + dropTitle.textContent = 'drop a file here'; + return; + } + hideError(); + dropTitle.textContent = f.name; + dropHint.textContent = human(f.size); + submitBtn.disabled = false; + } + + fileInput.addEventListener('change', () => setFile(fileInput.files[0])); + + ['dragenter', 'dragover'].forEach(ev => + drop.addEventListener(ev, e => { e.preventDefault(); drop.classList.add('over'); })); + ['dragleave', 'drop'].forEach(ev => + drop.addEventListener(ev, e => { e.preventDefault(); drop.classList.remove('over'); })); + drop.addEventListener('drop', e => { + const f = e.dataTransfer?.files?.[0]; + if (f) { + const dt = new DataTransfer(); + dt.items.add(f); + fileInput.files = dt.files; + setFile(f); + } + }); + + function showError(msg) { + errorBox.textContent = msg; + errorBox.hidden = false; + } + function hideError() { + errorBox.hidden = true; + errorBox.textContent = ''; + } + + form.addEventListener('submit', (e) => { + e.preventDefault(); + const f = fileInput.files[0]; + if (!f) return; + submitBtn.disabled = true; + hideError(); + progress.hidden = false; + bar.style.width = '0%'; + progressText.textContent = 'uploading 0%'; + + const fd = new FormData(); + fd.append('ttl', ttlSel.value); + fd.append('file', f); + + const xhr = new XMLHttpRequest(); + xhr.open('POST', '/upload'); + xhr.upload.addEventListener('progress', (ev) => { + if (!ev.lengthComputable) return; + const pct = Math.round((ev.loaded / ev.total) * 100); + bar.style.width = pct + '%'; + progressText.textContent = `uploading ${pct}% (${human(ev.loaded)} / ${human(ev.total)})`; + }); + xhr.onload = () => { + progress.hidden = true; + submitBtn.disabled = false; + let body = {}; + try { body = JSON.parse(xhr.responseText); } catch {} + if (xhr.status >= 200 && xhr.status < 300) { + showResult(body, f); + } else { + showError(body.error || `upload failed (${xhr.status})`); + } + }; + xhr.onerror = () => { + progress.hidden = true; + submitBtn.disabled = false; + showError('network error'); + }; + xhr.send(fd); + }); + + function showResult(body, f) { + form.hidden = true; + result.hidden = false; + link.value = body.url; + open.href = body.url; + delLink.value = body.deleteUrl || ''; + const exp = new Date(body.expiresAt); + resultMeta.textContent = `${f.name} · ${human(f.size)} · expires ${exp.toLocaleString()}`; + } + + async function copyValue(input, btn) { + try { + await navigator.clipboard.writeText(input.value); + } catch { + input.select(); + document.execCommand('copy'); + } + const orig = btn.textContent; + btn.textContent = 'copied'; + setTimeout(() => { btn.textContent = orig; }, 1200); + } + copyBtn.addEventListener('click', () => copyValue(link, copyBtn)); + copyDel.addEventListener('click', () => copyValue(delLink, copyDel)); + + resetBtn.addEventListener('click', () => { + fileInput.value = ''; + form.reset(); + form.hidden = false; + result.hidden = true; + submitBtn.disabled = true; + dropTitle.textContent = 'drop a file here'; + dropHint.textContent = `or click to choose · max ${human(MAX)}`; + }); +})(); diff --git a/src/public/viewer.js b/src/public/viewer.js new file mode 100644 index 0000000..8d9370e --- /dev/null +++ b/src/public/viewer.js @@ -0,0 +1,28 @@ +(() => { + const el = document.getElementById('expiry'); + if (!el) return; + const expiresAt = parseInt(el.dataset.expires, 10); + if (!expiresAt) return; + + function fmt(ms) { + if (ms <= 0) return 'expired'; + const s = Math.floor(ms / 1000); + const h = Math.floor(s / 3600); + const m = Math.floor((s % 3600) / 60); + const sec = s % 60; + if (h > 0) return `expires in ${h}h ${m}m`; + if (m > 0) return `expires in ${m}m ${sec}s`; + return `expires in ${sec}s`; + } + + function tick() { + const left = expiresAt - Date.now(); + el.textContent = fmt(left); + if (left <= 0) { + setTimeout(() => location.reload(), 1500); + return; + } + } + tick(); + setInterval(tick, 1000); +})(); diff --git a/src/server.js b/src/server.js new file mode 100644 index 0000000..7855259 --- /dev/null +++ b/src/server.js @@ -0,0 +1,329 @@ +import Fastify from 'fastify'; +import multipart from '@fastify/multipart'; +import staticPlugin from '@fastify/static'; +import fs from 'node:fs'; +import fsp from 'node:fs/promises'; +import path from 'node:path'; +import crypto from 'node:crypto'; +import { fileURLToPath } from 'node:url'; +import { pipeline } from 'node:stream/promises'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +const PORT = parseInt(process.env.PORT || '3000', 10); +const HOST = process.env.HOST || '0.0.0.0'; +const DATA_DIR = process.env.DATA_DIR || path.join(__dirname, '..', 'data', 'uploads'); +const MAX_FILE_SIZE = parseInt(process.env.MAX_FILE_SIZE || String(500 * 1024 * 1024), 10); +const SWEEP_INTERVAL_MS = 5 * 60 * 1000; + +const TTL_OPTIONS = { + '30m': 30 * 60 * 1000, + '1h': 60 * 60 * 1000, + '6h': 6 * 60 * 60 * 1000, + '24h': 24 * 60 * 60 * 1000, +}; +const DEFAULT_TTL = '1h'; + +await fsp.mkdir(DATA_DIR, { recursive: true }); + +const app = Fastify({ + logger: { level: process.env.LOG_LEVEL || 'info' }, + bodyLimit: MAX_FILE_SIZE + 5 * 1024 * 1024, + trustProxy: true, +}); + +await app.register(multipart, { + limits: { + fileSize: MAX_FILE_SIZE, + files: 1, + fields: 5, + }, +}); + +await app.register(staticPlugin, { + root: path.join(__dirname, 'public'), + prefix: '/static/', + decorateReply: false, +}); + +const metaPath = (id) => path.join(DATA_DIR, `${id}.json`); + +async function readMeta(id) { + if (!/^[a-f0-9-]{36}$/i.test(id)) return null; + try { + const buf = await fsp.readFile(metaPath(id), 'utf8'); + return JSON.parse(buf); + } catch { + return null; + } +} + +async function deleteUpload(id, meta) { + const m = meta || (await readMeta(id)); + if (!m) return; + await Promise.allSettled([ + fsp.rm(path.join(DATA_DIR, m.storedName), { force: true }), + fsp.rm(metaPath(id), { force: true }), + ]); +} + +async function sweep() { + try { + const entries = await fsp.readdir(DATA_DIR); + const now = Date.now(); + for (const entry of entries) { + if (!entry.endsWith('.json')) continue; + const id = entry.slice(0, -5); + const meta = await readMeta(id); + if (!meta) continue; + if (now > meta.expiresAt) { + app.log.info({ id }, 'sweep: deleting expired'); + await deleteUpload(id, meta); + } + } + } catch (err) { + app.log.warn({ err }, 'sweep error'); + } +} + +setInterval(() => { sweep().catch(() => {}); }, SWEEP_INTERVAL_MS).unref(); +sweep().catch(() => {}); + +function publicBase(req) { + const proto = (req.headers['x-forwarded-proto'] || req.protocol || 'https') + .toString().split(',')[0].trim(); + const host = (req.headers['x-forwarded-host'] || req.headers.host || '') + .toString().split(',')[0].trim(); + return `${proto}://${host}`; +} + +function escapeHtml(s) { + return String(s).replace(/[&<>"']/g, (c) => ({ + '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', + }[c])); +} + +function humanSize(bytes) { + if (!Number.isFinite(bytes)) return '?'; + const u = ['B', 'KB', 'MB', 'GB', 'TB']; + let i = 0; let n = bytes; + while (n >= 1024 && i < u.length - 1) { n /= 1024; i++; } + return `${n.toFixed(n >= 10 || i === 0 ? 0 : 1)} ${u[i]}`; +} + +app.get('/healthz', async () => 'ok'); + +app.get('/robots.txt', async (req, reply) => { + reply.type('text/plain').send('User-agent: *\nDisallow: /\n'); +}); + +app.get('/', async (req, reply) => { + const html = await fsp.readFile(path.join(__dirname, 'public', 'index.html'), 'utf8'); + reply.type('text/html; charset=utf-8').send( + html.replace('{{MAX_FILE_SIZE_HUMAN}}', humanSize(MAX_FILE_SIZE)) + .replace('{{MAX_FILE_SIZE}}', String(MAX_FILE_SIZE)) + ); +}); + +app.post('/upload', async (req, reply) => { + const data = await req.file(); + if (!data) return reply.code(400).send({ error: 'no file' }); + + const ttlField = data.fields?.ttl; + const ttlKey = (ttlField?.value || DEFAULT_TTL).toString(); + const ttlMs = TTL_OPTIONS[ttlKey] ?? TTL_OPTIONS[DEFAULT_TTL]; + + const id = crypto.randomUUID(); + const deleteToken = crypto.randomBytes(16).toString('hex'); + const rawExt = path.extname(data.filename || '').slice(0, 16); + const safeExt = /^\.[A-Za-z0-9]{1,15}$/.test(rawExt) ? rawExt.toLowerCase() : ''; + const storedName = `${id}${safeExt}`; + const storedPath = path.join(DATA_DIR, storedName); + + try { + await pipeline(data.file, fs.createWriteStream(storedPath)); + } catch (err) { + await fsp.rm(storedPath, { force: true }); + if (data.file.truncated) { + return reply.code(413).send({ error: 'file too large' }); + } + app.log.error({ err }, 'upload failed'); + return reply.code(500).send({ error: 'upload failed' }); + } + if (data.file.truncated) { + await fsp.rm(storedPath, { force: true }); + return reply.code(413).send({ error: 'file too large' }); + } + + const stat = await fsp.stat(storedPath); + const meta = { + id, + originalName: data.filename || `file${safeExt}`, + storedName, + mime: data.mimetype || 'application/octet-stream', + size: stat.size, + createdAt: Date.now(), + expiresAt: Date.now() + ttlMs, + ttl: ttlKey, + deleteToken, + }; + await fsp.writeFile(metaPath(id), JSON.stringify(meta)); + + const base = publicBase(req); + return reply.send({ + id, + url: `${base}/v/${id}`, + expiresAt: meta.expiresAt, + deleteToken, + deleteUrl: `${base}/api/${id}?token=${deleteToken}`, + }); +}); + +app.get('/api/info/:id', async (req, reply) => { + const meta = await readMeta(req.params.id); + if (!meta || Date.now() > meta.expiresAt) { + return reply.code(404).send({ error: 'not found' }); + } + return { + id: meta.id, + originalName: meta.originalName, + mime: meta.mime, + size: meta.size, + expiresAt: meta.expiresAt, + }; +}); + +app.delete('/api/:id', async (req, reply) => { + const meta = await readMeta(req.params.id); + if (!meta) return reply.code(404).send({ error: 'not found' }); + const token = req.query?.token; + if (!token || token !== meta.deleteToken) { + return reply.code(403).send({ error: 'forbidden' }); + } + await deleteUpload(meta.id, meta); + return { ok: true }; +}); + +app.get('/v/:id', async (req, reply) => { + const meta = await readMeta(req.params.id); + if (!meta || Date.now() > meta.expiresAt) { + if (meta) await deleteUpload(meta.id, meta); + reply.code(404).type('text/html; charset=utf-8').send(notFoundHtml()); + return; + } + reply.type('text/html; charset=utf-8').send(viewerHtml(meta)); +}); + +app.get('/f/:id', async (req, reply) => { + const meta = await readMeta(req.params.id); + if (!meta || Date.now() > meta.expiresAt) { + if (meta) await deleteUpload(meta.id, meta); + return reply.code(404).send('not found'); + } + const filePath = path.join(DATA_DIR, meta.storedName); + let stat; + try { stat = await fsp.stat(filePath); } + catch { return reply.code(404).send('not found'); } + + const fileSize = stat.size; + const range = req.headers.range; + reply.header('Accept-Ranges', 'bytes'); + reply.header('Content-Type', meta.mime || 'application/octet-stream'); + reply.header('Cache-Control', 'private, no-store'); + + const dispoMode = req.query?.dl === '1' ? 'attachment' : 'inline'; + const safeName = meta.originalName.replace(/[\r\n"]/g, ''); + reply.header( + 'Content-Disposition', + `${dispoMode}; filename*=UTF-8''${encodeURIComponent(safeName)}` + ); + + if (range) { + const m = /^bytes=(\d*)-(\d*)$/.exec(range); + if (!m) { + return reply.code(416).header('Content-Range', `bytes */${fileSize}`).send(); + } + let start = m[1] === '' ? undefined : parseInt(m[1], 10); + let end = m[2] === '' ? undefined : parseInt(m[2], 10); + if (start === undefined && end === undefined) { + return reply.code(416).header('Content-Range', `bytes */${fileSize}`).send(); + } + if (start === undefined) { + start = Math.max(0, fileSize - end); + end = fileSize - 1; + } + if (end === undefined || end >= fileSize) end = fileSize - 1; + if (start > end || start >= fileSize) { + return reply.code(416).header('Content-Range', `bytes */${fileSize}`).send(); + } + reply.code(206); + reply.header('Content-Range', `bytes ${start}-${end}/${fileSize}`); + reply.header('Content-Length', end - start + 1); + return reply.send(fs.createReadStream(filePath, { start, end })); + } + reply.header('Content-Length', fileSize); + return reply.send(fs.createReadStream(filePath)); +}); + +function notFoundHtml() { + return ` +gone + + +
+

404

+

This link has expired or never existed.

+

← new upload

+
`; +} + +function viewerHtml(meta) { + const mime = meta.mime || ''; + const name = escapeHtml(meta.originalName); + const size = humanSize(meta.size); + const fileUrl = `/f/${meta.id}`; + const dlUrl = `/f/${meta.id}?dl=1`; + + let player; + if (mime.startsWith('video/')) { + player = ``; + } else if (mime.startsWith('audio/')) { + player = ``; + } else if (mime.startsWith('image/')) { + player = `${name}`; + } else { + player = `
${name}
+
${escapeHtml(mime || 'unknown')} · ${size}
`; + } + + return ` + + + +${name} + + +
+
+ share + expires … +
+
${player}
+ +
+ +`; +} + +try { + await app.listen({ port: PORT, host: HOST }); + app.log.info({ DATA_DIR, MAX_FILE_SIZE, MAX_HUMAN: humanSize(MAX_FILE_SIZE) }, 'ready'); +} catch (err) { + app.log.error(err); + process.exit(1); +}