Feature/batch allow big (#3128)

Podman deployment options + larger upload body-size limits (+CONTAINER_HOST docs). Integrated into release/v3.8.10.
This commit is contained in:
Markus Hartung
2026-06-04 19:49:18 +02:00
committed by GitHub
parent 67addc63ad
commit f0e3a9bec9
14 changed files with 305 additions and 41 deletions

View File

@@ -6,7 +6,6 @@
# │ Reference: docs/ENVIRONMENT.md for full details and usage scenarios. │
# └─────────────────────────────────────────────────────────────────────────────┘
# ═══════════════════════════════════════════════════════════════════════════════
# 1. REQUIRED SECRETS — Must be set before first run!
# ═══════════════════════════════════════════════════════════════════════════════
@@ -62,7 +61,6 @@ DISABLE_SQLITE_AUTO_BACKUP=false
# Default: redis://localhost:6379 (or redis://redis:6379 in Docker)
REDIS_URL=redis://localhost:6379
# ═══════════════════════════════════════════════════════════════════════════════
# 3. NETWORK & PORTS
# ═══════════════════════════════════════════════════════════════════════════════
@@ -159,6 +157,11 @@ OMNIROUTE_USE_TURBOPACK=1
# Values: production | development | Default: production
NODE_ENV=production
# Container runtime — controls startup script behavior (permissions, advice).
# Values: docker | podman | Default: docker
# Set to "podman" when running under rootless Podman so the entrypoint
# gives the correct fix instructions (podman unshare chown vs sudo chown).
CONTAINER_HOST=docker
# ═══════════════════════════════════════════════════════════════════════════════
# 4. SECURITY & AUTHENTICATION
@@ -238,7 +241,6 @@ ALLOW_API_KEY_REVEAL=false
# When unset, OmniRoute uses the per-feature defaults. Set to "false"/"0" to disable.
# OUTBOUND_SSRF_GUARD_ENABLED=true
# ═══════════════════════════════════════════════════════════════════════════════
# 5. INPUT SANITIZATION & PII PROTECTION (FASE-01)
# ═══════════════════════════════════════════════════════════════════════════════
@@ -271,7 +273,6 @@ ALLOW_API_KEY_REVEAL=false
# PII_RESPONSE_SANITIZATION=false
# PII_RESPONSE_SANITIZATION_MODE=redact # redact = mask PII | warn = log only | block = drop response
# ═══════════════════════════════════════════════════════════════════════════════
# 6. TOOL & ROUTING POLICIES
# ═══════════════════════════════════════════════════════════════════════════════
@@ -291,7 +292,6 @@ ALLOW_API_KEY_REVEAL=false
# Default: 5000 | Minimum: 1000
# OMNIROUTE_PAYLOAD_RULES_RELOAD_MS=5000
# ═══════════════════════════════════════════════════════════════════════════════
# 7. URLS & CLOUD SYNC
# ═══════════════════════════════════════════════════════════════════════════════
@@ -365,7 +365,6 @@ NEXT_PUBLIC_CLOUD_URL=
#OMNIROUTE_OPENCODE_QUOTA_URL=https://opencode.ai/zen/go/v1/quota
#OMNIROUTE_OPENCODE_GO_QUOTA_URL=https://api.z.ai/api/monitor/usage/quota/limit
# ═══════════════════════════════════════════════════════════════════════════════
# 8. OUTBOUND PROXY (Upstream Provider Calls)
# ═══════════════════════════════════════════════════════════════════════════════
@@ -393,7 +392,6 @@ NEXT_PUBLIC_ENABLE_SOCKS5_PROXY=true
# Used by: open-sse/services/claudeTurnstileSolver.ts
# OMNIROUTE_TURNSTILE_IGNORE_TLS_ERRORS=false
# ═══════════════════════════════════════════════════════════════════════════════
# 9. CLI TOOL INTEGRATION
# ═══════════════════════════════════════════════════════════════════════════════
@@ -423,7 +421,6 @@ NEXT_PUBLIC_ENABLE_SOCKS5_PROXY=true
# CLI_QODER_BIN=qoder
# CLI_QWEN_BIN=qwen
# ═══════════════════════════════════════════════════════════════════════════════
# 10. INTERNAL AGENT & MCP INTEGRATIONS
# ═══════════════════════════════════════════════════════════════════════════════
@@ -554,7 +551,6 @@ PROVIDER_LIMITS_SYNC_INTERVAL_MINUTES=70
# Default: ~/.gemini/antigravity-cli/antigravity-oauth-token
#AGY_TOKEN_FILE=
# ═══════════════════════════════════════════════════════════════════════════════
# 11. OAUTH PROVIDER CREDENTIALS
# ═══════════════════════════════════════════════════════════════════════════════
@@ -701,7 +697,6 @@ GITHUB_OAUTH_CLIENT_ID=Iv1.b507a08c87ecfe98
# CLI_USER_ID= # legacy alias for OMNIROUTE_USER_ID
# SERVER_URL= # legacy alias for OMNIROUTE_SERVER
# ═══════════════════════════════════════════════════════════════════════════════
# 12. PROVIDER USER-AGENT OVERRIDES
# ═══════════════════════════════════════════════════════════════════════════════
@@ -737,7 +732,6 @@ GEMINI_CLI_USER_AGENT="google-api-nodejs-client/10.3.0"
# CODEX_USER_AGENT string. Used by: open-sse/config/codexClient.ts.
# CODEX_CLIENT_VERSION=0.132.0
# ═══════════════════════════════════════════════════════════════════════════════
# 13. CLI FINGERPRINT COMPATIBILITY (Anti-Detection)
# ═══════════════════════════════════════════════════════════════════════════════
@@ -766,7 +760,6 @@ GEMINI_CLI_USER_AGENT="google-api-nodejs-client/10.3.0"
#KIMI_CLI_VERSION=1.36.0
#KIMI_CODING_DEVICE_ID=
# ═══════════════════════════════════════════════════════════════════════════════
# 14. API KEY PROVIDERS
# ═══════════════════════════════════════════════════════════════════════════════
@@ -789,7 +782,6 @@ GEMINI_CLI_USER_AGENT="google-api-nodejs-client/10.3.0"
# OpenAI/Mistral/Together/Fireworks/NVIDIA configured via Dashboard → Providers
# also work for embeddings.
# ═══════════════════════════════════════════════════════════════════════════════
# 15. TIMEOUT SETTINGS
# ═══════════════════════════════════════════════════════════════════════════════
@@ -868,7 +860,6 @@ GEMINI_CLI_USER_AGENT="google-api-nodejs-client/10.3.0"
# Default: 30000 (30 seconds)
# SHUTDOWN_TIMEOUT_MS=30000
# ═══════════════════════════════════════════════════════════════════════════════
# 16. LOGGING
# ═══════════════════════════════════════════════════════════════════════════════
@@ -942,7 +933,6 @@ APP_LOG_TO_FILE=true
# Default: 100000
# PROXY_LOGS_TABLE_MAX_ROWS=100000
# ═══════════════════════════════════════════════════════════════════════════════
# 17. MEMORY OPTIMIZATION (Low-RAM / Docker)
# ═══════════════════════════════════════════════════════════════════════════════
@@ -1022,7 +1012,6 @@ APP_LOG_TO_FILE=true
# Used by: open-sse/utils/usageTracking.ts
# USAGE_TOKEN_BUFFER=100
# ═══════════════════════════════════════════════════════════════════════════════
# 18. PRICING SYNC
# ═══════════════════════════════════════════════════════════════════════════════
@@ -1038,7 +1027,6 @@ APP_LOG_TO_FILE=true
# Comma-separated data sources. Default: litellm
# PRICING_SYNC_SOURCES=litellm
# ═══════════════════════════════════════════════════════════════════════════════
# 19. MODEL SYNC (Dev)
# ═══════════════════════════════════════════════════════════════════════════════
@@ -1047,7 +1035,6 @@ APP_LOG_TO_FILE=true
# Default: 86400 (24 hours)
# MODELS_DEV_SYNC_INTERVAL=86400
# ═══════════════════════════════════════════════════════════════════════════════
# 20. PROVIDER-SPECIFIC SETTINGS
# ═══════════════════════════════════════════════════════════════════════════════
@@ -1128,7 +1115,6 @@ APP_LOG_TO_FILE=true
# Used by: open-sse/config/providerRegistry.ts — allows Docker service names.
# LOCAL_HOSTNAMES=omlx,mlx-audio
# ═══════════════════════════════════════════════════════════════════════════════
# 21. PROXY HEALTH
# ═══════════════════════════════════════════════════════════════════════════════
@@ -1154,7 +1140,6 @@ APP_LOG_TO_FILE=true
# Used by: src/lib/tokenHealthCheck.ts. Default: 3000.
# HEALTHCHECK_STAGGER_MS=3000
# ═══════════════════════════════════════════════════════════════════════════════
# 22. DEBUGGING
# ═══════════════════════════════════════════════════════════════════════════════
@@ -1214,7 +1199,6 @@ APP_LOG_TO_FILE=true
# Enable E2E test mode — relaxes auth and enables test harness hooks.
# NEXT_PUBLIC_OMNIROUTE_E2E_MODE=true
# ═══════════════════════════════════════════════════════════════════════════════
# 23. GITHUB INTEGRATION (Issue Reporting)
# ═══════════════════════════════════════════════════════════════════════════════
@@ -1232,7 +1216,6 @@ APP_LOG_TO_FILE=true
# GITHUB_ISSUES_TOKEN when unset.
# GITHUB_TOKEN=
# ═══════════════════════════════════════════════════════════════════════════════
# 24. PROVIDER QUOTAS, TUNNELS & SANDBOXED SKILLS
# ═══════════════════════════════════════════════════════════════════════════════
@@ -1337,7 +1320,6 @@ APP_LOG_TO_FILE=true
# SKILLS_SANDBOX_NETWORK_ENABLED=0
# SKILLS_ALLOWED_SANDBOX_IMAGES=
# ═══════════════════════════════════════════════════════════════════════════════
# 25. TEST & E2E
# ═══════════════════════════════════════════════════════════════════════════════
@@ -1448,8 +1430,8 @@ INSPECTOR_MASK_SECRETS=true
INSPECTOR_LLM_HOSTS_EXTRA=
INSPECTOR_INTERNAL_INGEST_TOKEN=
# Quota Sharing (Group B — planos 16+22)
QUOTA_STORE_DRIVER=sqlite # sqlite | redis
# QUOTA_STORE_REDIS_URL= # ex.: redis://localhost:6379 (apenas quando driver=redis)
# QUOTA_SATURATION_THRESHOLD=0.5 # 0..1; >= threshold ativa modo strict (sem empréstimo)
# QUOTA_SOFT_DEPRIORITIZE_FACTOR=0.7 # 0..1; multiplicador do score quando soft policy ativa
# QUOTA_CONSUMPTION_RETENTION_DAYS=14 # GC de buckets quota_consumption.updated_at antigos
QUOTA_STORE_DRIVER=sqlite # sqlite | redis
# QUOTA_STORE_REDIS_URL= # ex.: redis://localhost:6379 (apenas quando driver=redis)
# QUOTA_SATURATION_THRESHOLD=0.5 # 0..1; >= threshold ativa modo strict (sem empréstimo)
# QUOTA_SOFT_DEPRIORITIZE_FACTOR=0.7 # 0..1; multiplicador do score quando soft policy ativa
# QUOTA_CONSUMPTION_RETENTION_DAYS=14 # GC de buckets quota_consumption.updated_at antigos

View File

@@ -540,6 +540,22 @@ devbox run npm run dev
📖 [Docker Guide](docs/guides/DOCKER_GUIDE.md) — Compose profiles, Caddy HTTPS, Cloudflare tunnels.
**🦭 Podman**
```bash
# 1. Build the image
podman build --target runner-base -t omniroute:base .
# 2. Fix data directory permissions for rootless Podman
mkdir -p data && podman unshare chown 1000:1000 ./data
# 3. Set runtime in .env, then run (see contrib/podman/ for Quadlet)
echo "CONTAINER_HOST=podman" >> .env
podman compose --profile base up -d
```
📖 [Podman Guide](contrib/podman/README.md) — Quadlet setup, podman-compose, Quadlet.
</details>
<br/>
@@ -779,6 +795,7 @@ Compression: aggressive (~50%) → double your free quota · Cost: $0/mo
| Document | Description |
| ---------------------------------------------------- | -------------------------------------------------------------- |
| [Docker Guide](docs/guides/DOCKER_GUIDE.md) | Docker run, Compose profiles, Caddy HTTPS, tunnels, image tags |
| [Podman Guide](contrib/podman/README.md) | Quadlet systemd integration, podman-compose, SELinux |
| [VM Deployment](docs/ops/VM_DEPLOYMENT_GUIDE.md) | Complete guide: VM + nginx + Cloudflare setup |
| [Fly.io Deployment](docs/ops/FLY_IO_DEPLOYMENT_GUIDE.md) | Deploy to Fly.io with persistent storage |
| [Termux Guide](docs/guides/TERMUX_GUIDE.md) | Run OmniRoute on Android via Termux |

128
contrib/podman/README.md Normal file
View File

@@ -0,0 +1,128 @@
# Podman Deployment
Run OmniRoute with Podman via **Quadlet** (systemd integration) or **podman compose**.
---
## Option A: Quadlet (recommended)
### 1. Build the image
```bash
cd /path/to/omniroute
podman build --target runner-base -t omniroute:base .
# For web-cookie providers (gemini-web, claude-web, claude-turnstile):
podman build --target runner-web -t omniroute:web .
# For CLI tool support:
podman build --target runner-cli -t omniroute:cli .
```
### 2. Copy Quadlet files to the systemd directory
```bash
mkdir -p ~/.config/containers/systemd/omniroute
cp contrib/podman/*.container ~/.config/containers/systemd/omniroute/
cp contrib/podman/*.network ~/.config/containers/systemd/omniroute/
cp contrib/podman/*.volume ~/.config/containers/systemd/omniroute/
```
### 3. Mount the project .env for secrets
Edit `~/.config/containers/systemd/omniroute/omniroute.container` and
uncomment/replace the `EnvironmentFile` line with the absolute path to
your project `.env`:
```
EnvironmentFile=/home/USER/code/docker/OmniRoute/.env
```
Make sure `CONTAINER_HOST=podman` is set in that `.env`.
Alternatively, edit the env vars directly in the `.container` file.
### 4. Reload systemd and start
```bash
systemctl --user daemon-reload
systemctl --user start omniroute-redis
systemctl --user start omniroute
```
### 5. Verify
```bash
systemctl --user status omniroute
curl http://localhost:20128/v1/models
```
To follow logs:
```bash
journalctl --user -u omniroute -f
```
### 6. Enable on boot
```bash
systemctl --user enable omniroute-redis
systemctl --user enable omniroute
```
---
## Option B: podman compose
The project's `docker-compose.yml` now works with both Docker and Podman.
Just set `CONTAINER_HOST=podman` in `.env` before starting.
### 1. Prepare the data directory
Rootless Podman maps container UIDs into a subordinate range. The
`node` user (UID 1000) inside the container maps to a different UID
on the host, so it cannot write to `./data` owned by your host user.
Fix the ownership **before** starting:
```bash
mkdir -p data
podman unshare chown 1000:1000 ./data
```
### 2. Set the runtime in `.env`
Make sure `.env` contains:
```env
CONTAINER_HOST=podman
```
### 3. Start
```bash
podman compose --profile base up -d
```
### Profiles
Same profiles as `docker compose`:
| Profile | Command |
| ------------------------------ | ---------------------------------------------------- |
| `base` (no CLIs) | `podman compose --profile base up -d` |
| `web` (+Chromium/Playwright) | `podman compose --profile web up -d` |
| `cli` (+CLI tools) | `podman compose --profile cli up -d` |
| `host` (host-mounted binaries) | `podman compose --profile host up -d` |
| `cliproxyapi` (sidecar) | `podman compose --profile cliproxyapi up -d` |
---
## How it works
The `docker-compose.yml` uses fully-qualified image names
(`docker.io/library/redis:7-alpine`) and flat variable expansions so it
works with both Docker and Podman without a separate compose file.
The entrypoint script (`check-permissions.sh`) reads `CONTAINER_HOST`
from `.env` to give the correct fix instructions:
- **docker**: `sudo chown -R ... ./data`
- **podman**: `podman unshare chown 1000:1000 ./data`

View File

@@ -0,0 +1,26 @@
[Unit]
Description=OmniRoute Redis (rate-limiter backend)
After=network-online.target
Wants=network-online.target
[Container]
Image=docker.io/redis:7-alpine
ContainerName=omniroute-redis
Network=omniroute.network
NetworkAlias=redis
Volume=omniroute-redis.volume:/data
Exec=redis-server --save 60 1 --loglevel warning
HealthCmd=/bin/sh -c "redis-cli ping | grep -q PONG"
HealthInterval=10s
HealthRetries=3
HealthTimeout=5s
[Service]
Restart=always
[Install]
WantedBy=default.target

View File

@@ -0,0 +1,3 @@
# Persistent Redis data
[Volume]
Label=omniroute

View File

@@ -0,0 +1,50 @@
[Unit]
Description=OmniRoute — Unified AI Proxy
After=network-online.target
Wants=network-online.target
Requires=omniroute-redis.service
After=omniroute-redis.service
[Container]
# Use this image if your building locally
Image=localhost/omniroute:base
# Use this image if you are pulling from Docker Hub
# Image=diegosouzapw/omniroute:base
ContainerName=omniroute
Network=omniroute.network
PublishPort=20128:20128
PublishPort=20129:20129
Volume=omniroute.volume:/app/data:Z
# Use a bind mount if you need direct host access to the data:
# Volume=/home/USER/code/docker/OmniRoute/data:/app/data:Z
Environment=DATA_DIR=/app/data
Environment=PORT=20128
Environment=DASHBOARD_PORT=20128
Environment=API_PORT=20129
Environment=API_HOST=0.0.0.0
Environment=REDIS_URL=redis://redis:6379
Environment=JWT_SECRET=change-me-to-a-random-base64-string
Environment=API_KEY_SECRET=change-me-to-a-random-base64-string
Environment=INITIAL_PASSWORD=change-me-to-a-random-hex-string
Environment=NODE_ENV=production
Environment=REQUIRE_API_KEY=true
# Load additional secrets (API keys, OAuth creds) from the project .env:
# EnvironmentFile=%h/code/docker/OmniRoute/.env
HealthCmd=node /app/healthcheck.mjs
HealthInterval=30s
HealthRetries=3
HealthTimeout=5s
HealthStartPeriod=15s
[Service]
Restart=always
[Install]
WantedBy=default.target

View File

@@ -0,0 +1,2 @@
[Network]
Label=omniroute

View File

@@ -0,0 +1,3 @@
# OmniRoute persistent data (SQLite DB, logs, backups)
[Volume]
Label=omniroute

View File

@@ -27,7 +27,7 @@ x-common: &common
environment:
- DATA_DIR=/app/data # Must match the volume mount below
- PORT=${PORT:-20128}
- DASHBOARD_PORT=${DASHBOARD_PORT:-${PORT:-20128}}
- DASHBOARD_PORT=${DASHBOARD_PORT:-20128}
- API_PORT=${API_PORT:-20129}
- API_HOST=${API_HOST:-0.0.0.0}
- REDIS_URL=${REDIS_URL:-redis://redis:6379}
@@ -43,7 +43,7 @@ x-common: &common
services:
# ── Redis (Rate Limiter Backend) ──────────────────────────────────
redis:
image: redis:7-alpine
image: docker.io/library/redis:7-alpine
container_name: omniroute-redis
restart: unless-stopped
ports:
@@ -66,7 +66,7 @@ services:
target: runner-base
image: omniroute:base
ports:
- "${DASHBOARD_PORT:-${PORT:-20128}}:${DASHBOARD_PORT:-${PORT:-20128}}"
- "${DASHBOARD_PORT:-20128}:${DASHBOARD_PORT:-20128}"
- "${API_PORT:-20129}:${API_PORT:-20129}"
profiles:
- base
@@ -83,7 +83,7 @@ services:
target: runner-web
image: omniroute:web
ports:
- "${DASHBOARD_PORT:-${PORT:-20128}}:${DASHBOARD_PORT:-${PORT:-20128}}"
- "${DASHBOARD_PORT:-20128}:${DASHBOARD_PORT:-20128}"
- "${API_PORT:-20129}:${API_PORT:-20129}"
profiles:
- web
@@ -97,7 +97,7 @@ services:
target: runner-cli
image: omniroute:cli
ports:
- "${DASHBOARD_PORT:-${PORT:-20128}}:${DASHBOARD_PORT:-${PORT:-20128}}"
- "${DASHBOARD_PORT:-20128}:${DASHBOARD_PORT:-20128}"
- "${API_PORT:-20129}:${API_PORT:-20129}"
volumes:
- ./data:/app/data
@@ -116,12 +116,12 @@ services:
target: runner-base
image: omniroute:base
ports:
- "${DASHBOARD_PORT:-${PORT:-20128}}:${DASHBOARD_PORT:-${PORT:-20128}}"
- "${DASHBOARD_PORT:-20128}:${DASHBOARD_PORT:-20128}"
- "${API_PORT:-20129}:${API_PORT:-20129}"
environment:
- DATA_DIR=/app/data
- PORT=${PORT:-20128}
- DASHBOARD_PORT=${DASHBOARD_PORT:-${PORT:-20128}}
- DASHBOARD_PORT=${DASHBOARD_PORT:-20128}
- API_PORT=${API_PORT:-20129}
- API_HOST=${API_HOST:-0.0.0.0}
- CLI_MODE=host

View File

@@ -877,6 +877,7 @@ Provider quota endpoints, network tunnels (Tailscale, Ngrok, MITM debug proxy),
| `DB_BACKUP_MAX_FILES` | `20` | `src/lib/db/backup.ts` | Maximum SQLite backup files retained on disk. |
| `DB_BACKUP_RETENTION_DAYS` | `0` | `src/lib/db/backup.ts` | Maximum age (days) of retained backups. `0` disables age-based pruning. |
| `OMNIROUTE_TLS_PROXY_URL` | _(unset)_ | `open-sse/services/chatgptTlsClient.ts` | Override the TLS sidecar URL for tests. Production should leave unset. |
| `CONTAINER_HOST` | `docker` | `scripts/check-permissions.sh` | Container runtime hint for the entrypoint permission check. Set to `podman` under rootless Podman so the fix instructions use `podman unshare chown` instead of `sudo chown`. |
| `QUOTA_STORE_DRIVER` | `sqlite` | `src/lib/quota/storeFactory.ts` | Quota-share consumption store backend: `sqlite` (default) or `redis`. |
| `QUOTA_STORE_REDIS_URL` | _(unset)_ | `src/lib/quota/storeFactory.ts` | Redis connection string used when `QUOTA_STORE_DRIVER=redis` (e.g. `redis://localhost:6379`). |
| `QUOTA_SATURATION_THRESHOLD` | `0.5` | `src/lib/quota/enforce.ts` | Pool saturation ratio (0..1); at/above it the pool enters strict mode (no borrowing). |

View File

@@ -104,6 +104,10 @@ const nextConfig = {
serverActions: {
bodySizeLimit: process.env.OMNIROUTE_SERVER_ACTIONS_BODY_LIMIT || "50mb",
},
// Next.js proxy (middleware) has a default 10MB body clone limit. File
// uploads (OpenAI-compatible /v1/files) routinely exceed this. Match the
// 512 MB server-side cap; tune via env if needed.
proxyClientMaxBodySize: process.env.NEXT_PROXY_BODY_LIMIT || "512mb",
},
outputFileTracingRoot: projectRoot,
outputFileTracingIncludes: {

View File

@@ -8,11 +8,18 @@ if [ -n "$OMNIROUTE_MEMORY_MB" ]; then
export NODE_OPTIONS="${NODE_OPTIONS:-} --max-old-space-size=${OMNIROUTE_MEMORY_MB}"
fi
if [ -d "/app/data" ] && [ ! -w "/app/data" ]; then
echo "WARNING: /app/data is not writable by the current user (UID $(id -u))."
echo "Run this on the Docker host to fix:"
echo " sudo chown -R $(id -u):$(id -g) /app/data"
echo " chmod -R u+rwX ./data"
DATA_PATH="${DATA_DIR:-/app/data}"
if [ -d "$DATA_PATH" ] && [ ! -w "$DATA_PATH" ]; then
echo "WARNING: $DATA_PATH is not writable by the current user (UID $(id -u))."
if [ "${CONTAINER_HOST:-}" = "podman" ]; then
echo "Rootless Podman maps container UIDs into a subordinate range."
echo "Run this on the host to fix (using the host-side bind-mount path):"
echo " podman unshare chown -R $(id -u):$(id -g) <host-data-dir>"
else
echo "Run this on the Docker host to fix (using the host-side bind-mount path):"
echo " sudo chown -R $(id -u):$(id -g) <host-data-dir>"
echo " chmod -R u+rwX <host-data-dir>"
fi
fi
exec "$@"

View File

@@ -26,6 +26,9 @@ export const MAX_BODY_BYTES_IMPORT = 100 * 1024 * 1024;
/** Larger limit for audio transcription uploads: 100 MB */
export const MAX_BODY_BYTES_AUDIO = 100 * 1024 * 1024;
/** Larger limit for file uploads: 500 MB */
export const MAX_BODY_BYTES_FILE = 500 * 1024 * 1024;
/** Configured limit — reads from env or falls back to 10 MB */
export const MAX_BODY_BYTES = parseRequestBodyLimitBytes(process.env.MAX_BODY_SIZE_BYTES);
@@ -34,6 +37,7 @@ type BodySizeRule = { prefix: string; limit: number };
const ROUTE_LIMITS: BodySizeRule[] = [
{ prefix: "/api/db-backups/import", limit: MAX_BODY_BYTES_IMPORT },
{ prefix: "/api/v1/audio/transcriptions", limit: MAX_BODY_BYTES_AUDIO },
{ prefix: "/api/v1/files", limit: MAX_BODY_BYTES_FILE },
];
export function getDefaultRequestBodyLimitMb(): number {
@@ -62,7 +66,7 @@ export function checkBodySize(request: Request, limit: number = MAX_BODY_BYTES):
const contentLength = request.headers.get("content-length");
if (contentLength) {
const bytes = parseInt(contentLength, 10);
const bytes = Number.parseInt(contentLength, 10);
if (!Number.isNaN(bytes) && bytes > limit) {
return new Response(
JSON.stringify({

View File

@@ -2,6 +2,7 @@ import test from "node:test";
import assert from "node:assert/strict";
import {
MAX_BODY_BYTES_AUDIO,
MAX_BODY_BYTES_FILE,
getBodySizeLimit,
checkBodySize,
} from "../../src/shared/middleware/bodySizeGuard.ts";
@@ -40,3 +41,39 @@ test("checkBodySize reports the configured request limit in 413 responses", asyn
assert.equal(body.error.code, "PAYLOAD_TOO_LARGE");
assert.match(body.error.message, /100 MB/);
});
test("/api/v1/files route has 512 MB dedicated limit floor", () => {
const limit = getBodySizeLimit("/api/v1/files", { maxBodySizeMb: 1 });
assert.equal(limit, MAX_BODY_BYTES_FILE);
});
test("/api/v1/files route guard allows 500 MB file upload", () => {
const thirtyMb = 500 * 1024 * 1024;
const request = new Request("http://localhost/api/v1/files", {
method: "POST",
headers: { "content-length": String(thirtyMb) },
});
assert.equal(checkBodySize(request, getBodySizeLimit("/api/v1/files", { maxBodySizeMb: 10 })), null);
});
test("/api/v1/files route guard rejects >512 MB file upload", async () => {
const tooBig = 600 * 1024 * 1024;
const request = new Request("http://localhost/api/v1/files", {
method: "POST",
headers: { "content-length": String(tooBig) },
});
const response = checkBodySize(request, getBodySizeLimit("/api/v1/files", { maxBodySizeMb: 10 }));
assert.ok(response);
assert.equal(response.status, 413);
const body = await response.json();
assert.equal(body.error.code, "PAYLOAD_TOO_LARGE");
});
test("/api/v1/files route guard allows 15 MB (10 MB+ real-world scenario)", () => {
const fifteenMb = 15 * 1024 * 1024;
const request = new Request("http://localhost/api/v1/files", {
method: "POST",
headers: { "content-length": String(fifteenMb) },
});
assert.equal(checkBodySize(request, getBodySizeLimit("/api/v1/files")), null);
});