diff --git a/.env.example b/.env.example index 3335784a6c..26d6ee99e7 100644 --- a/.env.example +++ b/.env.example @@ -598,6 +598,11 @@ PROVIDER_LIMITS_SYNC_SPACING_MS=1500 # Used by: scripts/postinstall.mjs. #OMNIROUTE_SKIP_POSTINSTALL=0 +# Operator-supplied JSON credentials for the offline compression-eval CLI +# (parsed with JSON.parse; leave unset for a dry run). Developer tooling only. +# Used by: scripts/compression-eval/index.ts. Default: {} (empty). +#OMNIROUTE_EVAL_CREDENTIALS={} + # Skip the DB healthcheck entirely on startup (useful for short-lived tasks / tests). # Used by: src/lib/db/core.ts, src/lib/db/healthCheck.ts. Set to 1 to disable. Default: 0. #OMNIROUTE_SKIP_DB_HEALTHCHECK=0 diff --git a/docs/reference/ENVIRONMENT.md b/docs/reference/ENVIRONMENT.md index ffebb7a27d..ad9a789b52 100644 --- a/docs/reference/ENVIRONMENT.md +++ b/docs/reference/ENVIRONMENT.md @@ -1082,3 +1082,12 @@ is developer tooling only. | `OMNIROUTE_URL` | `http://localhost:20128` | `scripts/ad-hoc/regen-opencode-config.ts` | Base URL of the OmniRoute instance to query for `/v1/models`. | | `OMNIROUTE_KEY` | _(unset)_ | `scripts/ad-hoc/regen-opencode-config.ts` | API key to authenticate against the OmniRoute `/v1/models` endpoint. Falls back to `OPENCODE_API_KEY` when unset. | | `OPENCODE_API_KEY` | _(unset)_ | `scripts/ad-hoc/regen-opencode-config.ts` | OpenCode-style API key (`sk-...`) written into the regenerated `opencode.json`. Falls back to `OMNIROUTE_KEY` when unset. | + +### Compression offline-eval harness (ad-hoc tooling) + +Used by `scripts/compression-eval/index.ts`, the offline compression evaluation CLI. +Not required for normal operation — developer tooling only. + +| Variable | Default | Source File | Description | +| --------------------------- | ------------ | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `OMNIROUTE_EVAL_CREDENTIALS` | `{}` (empty) | `scripts/compression-eval/index.ts` | Operator-supplied JSON credentials for the provider exercised by the offline compression-eval CLI (parsed with `JSON.parse`). Leave unset for a dry run. | diff --git a/src/app/api/settings/compression/run-telemetry/route.ts b/src/app/api/settings/compression/run-telemetry/route.ts index 04288d713c..d5e797a75f 100644 --- a/src/app/api/settings/compression/run-telemetry/route.ts +++ b/src/app/api/settings/compression/run-telemetry/route.ts @@ -1,9 +1,14 @@ -import { NextResponse } from "next/server"; +import { NextRequest, NextResponse } from "next/server"; +import { isAuthenticated } from "@/shared/utils/apiAuth"; import { getCompressionRunTelemetrySummary } from "@/lib/db/compressionRunTelemetry"; export const dynamic = "force-dynamic"; -export async function GET() { +export async function GET(request: NextRequest) { + if (!(await isAuthenticated(request))) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + try { const summary = getCompressionRunTelemetrySummary(); return NextResponse.json(summary); diff --git a/tests/unit/compression-run-telemetry-auth.test.ts b/tests/unit/compression-run-telemetry-auth.test.ts new file mode 100644 index 0000000000..cabfb66384 --- /dev/null +++ b/tests/unit/compression-run-telemetry-auth.test.ts @@ -0,0 +1,45 @@ +/** + * Security regression (#4694 sibling-path gate parity): the compression + * run-telemetry route MUST authenticate before disclosing telemetry, mirroring + * its sibling `settings/compression/route.ts`. A behavioral 401 test would need + * full DB/settings bootstrap (isAuthenticated reads settings), so this is a + * source guard that fails if the auth gate is removed or reordered after the + * data call — the same wiring-guard pattern used elsewhere in the suite. + */ +import test from "node:test"; +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { dirname, join } from "node:path"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const ROUTE = join( + __dirname, + "../../src/app/api/settings/compression/run-telemetry/route.ts" +); + +test("run-telemetry GET imports isAuthenticated from the shared auth util", () => { + const src = readFileSync(ROUTE, "utf8"); + assert.match(src, /import\s*\{[^}]*\bisAuthenticated\b[^}]*\}\s*from\s*["']@\/shared\/utils\/apiAuth["']/); +}); + +test("run-telemetry GET gates on auth and returns 401 before reading telemetry", () => { + const src = readFileSync(ROUTE, "utf8"); + + const authIdx = src.indexOf("isAuthenticated(request)"); + const unauthorizedIdx = src.search(/status:\s*401/); + const dataIdx = src.indexOf("getCompressionRunTelemetrySummary("); + + assert.ok(authIdx > 0, "must call isAuthenticated(request)"); + assert.ok(unauthorizedIdx > 0, "must return a 401 on failed auth"); + assert.ok(dataIdx > 0, "must call getCompressionRunTelemetrySummary"); + + // The auth check and its 401 must come BEFORE the telemetry read. + assert.ok(authIdx < dataIdx, "auth check must precede the telemetry read"); + assert.ok(unauthorizedIdx < dataIdx, "401 response must precede the telemetry read"); +}); + +test("run-telemetry GET receives the request object (so auth can read it)", () => { + const src = readFileSync(ROUTE, "utf8"); + assert.match(src, /export\s+async\s+function\s+GET\s*\(\s*request\s*:/); +});