commit b69a6629c94ea5523aba3540e559a16b29f271c1
parent 129de94dcad57c0c2c16d5a84cbcf294a58e4453
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Tue, 22 Sep 2026 16:39:41 -0400
editor: /api/test/worker-token does not exist unless the harness started the server
It is an unauthenticated GET that SETS A CREDENTIAL. `?set=x` hands any caller a
token of its choosing for every `/api/ops/*` and `/api/worker/*` route; `?unset=1`
is a remote kill switch for both. Being a plain GET, both are reachable by CSRF
from any page the operator's browser loads — so binding Caddy to loopback is no
defence, because the browser is inside the loopback, and there is no path rule
for `/api/test` anyway.
The surface is opt-in now, the way the worker endpoint it manipulates is:
`EDITOR_TEST_ROUTES=1` is set by `dev:test` and `start:test`, the two scripts
Playwright's webServer, the sharded runner and Dockerfile.test all boot through,
and by nothing else. 404 rather than 403, so the answer is indistinguishable
from a route that was never built.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Diffstat:
3 files changed, 37 insertions(+), 4 deletions(-)
diff --git a/editor/app/api/test/_guard.ts b/editor/app/api/test/_guard.ts
@@ -0,0 +1,26 @@
+import { NextResponse } from "next/server";
+
+// THE /api/test ROUTES EXIST ONLY WHEN THE E2E HARNESS STARTED THIS SERVER.
+//
+// Every route under this directory is UNAUTHENTICATED and every one of them
+// mutates live process state: it drops the job registry and the runner
+// singletons, fabricates a stuck job, restarts a lane — and `worker-token`
+// SETS A CREDENTIAL. An unauthenticated `GET /api/test/worker-token?set=x`
+// hands the caller a token of its own choosing for `/api/ops/*` and
+// `/api/worker/*`; `?unset=1` is a remote kill switch for both. As plain GETs,
+// all of that is reachable by CSRF from any page the operator's browser loads,
+// which is why binding to loopback is not an answer — the browser is inside the
+// loopback. The Caddyfile has no path rule for `/api/test` either.
+//
+// So the surface is opt-in, the way the worker endpoint is: `EDITOR_TEST_ROUTES=1`
+// is set by `dev:test` and `start:test` (editor/package.json) — the two scripts
+// Playwright's webServer, the sharded runner and Dockerfile.test all boot
+// through — and by nothing else. A real editor never sets it.
+//
+// 404 AND NOT 403, deliberately: the answer must be indistinguishable from a
+// route that was never built. A 403 advertises that the harness exists and that
+// there is an env var worth guessing.
+export function testRouteDenied(): NextResponse | null {
+ if (process.env.EDITOR_TEST_ROUTES === "1") return null;
+ return NextResponse.json({ error: "Not Found" }, { status: 404 });
+}
diff --git a/editor/app/api/test/worker-token/route.ts b/editor/app/api/test/worker-token/route.ts
@@ -1,5 +1,6 @@
import { NextResponse } from "next/server";
import { workerEndpointEnabled } from "yt-dlp-transcript-common/lib/workerToken";
+import { testRouteDenied } from "../_guard";
export const dynamic = "force-dynamic";
@@ -24,9 +25,15 @@ export const dynamic = "force-dynamic";
// later /api/ops and /api/worker spec answering 503 — a whole suite red from
// one failure. `?set=` with no value is the restore, and it is idempotent.
//
-// Mounted unconditionally, like the other /api/test routes: the editor is a
-// localhost admin tool, not a deployed service.
+// GUARDED BY `EDITOR_TEST_ROUTES`, and it is the route that made the guard
+// necessary: an unauthenticated GET that SETS a credential is a CSRF-able
+// token grant, and "the editor is a localhost admin tool" does not help — the
+// operator's browser is inside the loopback. See _guard.ts.
export async function GET(req: Request) {
+ // FIRST LINE, BEFORE THE QUERY IS EVEN PARSED. This route sets a credential;
+ // off a harness-started server it does not exist. See _guard.ts.
+ const denied = testRouteDenied();
+ if (denied) return denied;
const url = new URL(req.url);
// `?set=<token>` restores (or changes) it; `?unset=1` removes it entirely.
// Exactly one of the two, so a typo cannot silently do nothing.
diff --git a/editor/package.json b/editor/package.json
@@ -5,8 +5,8 @@
"type": "module",
"scripts": {
"dev": "next dev --port ${EDITOR_PORT:-3001}",
- "dev:test": "WORKER_TOKEN=test-worker-token TRANSCRIPTS_DIR=$(pwd)/test-transcripts EXPORT_PUBLIC_DIR=$(pwd)/test-transcripts/.export-public SETTINGS_FILE=$(pwd)/test-settings.json EDITOR_CHANGELOG_FILE=$(pwd)/test-changelog.md YTDLP_BIN=$(pwd)/e2e/fixtures/bin/fake-ytdlp.mjs GALLERY_DL_BIN=$(pwd)/e2e/fixtures/bin/fake-gallery-dl.mjs WHISPER_BIN=$(pwd)/e2e/fixtures/bin/fake-whisper.mjs WHISPER_MODEL=/dev/null CHOUGH_BIN=$(pwd)/e2e/fixtures/bin/fake-chough.mjs CHOUGH_MODEL=/dev/null PARAKEET_STITCH_BIN=$(pwd)/e2e/fixtures/bin/fake-parakeet-stitch.mjs PARAKEET_CLI=/dev/null PARAKEET_MODEL=/dev/null DIARIZE_BIN=$(pwd)/e2e/fixtures/bin/fake-diarize.mjs FFMPEG_BIN=$(pwd)/e2e/fixtures/bin/fake-ffmpeg.mjs FFPROBE_BIN=$(pwd)/e2e/fixtures/bin/fake-ffprobe.mjs OLLAMA_URL=http://127.0.0.1:${OLLAMA_STUB_PORT:-11435} CLAUDE_BIN=$(pwd)/e2e/fixtures/bin/fake-claude.mjs FINDMNT_BIN=$(pwd)/e2e/fixtures/bin/fake-findmnt.mjs UDISKSCTL_BIN=$(pwd)/e2e/fixtures/bin/fake-udisksctl.mjs AUDIO_CHECK_INTERVAL_MS_OVERRIDE=300 AUDIO_CHECK_SIZE_GATE_OVERRIDE=4096 AUDIO_CHECK_INTERVAL_FLOOR_MS_OVERRIDE=50 AUDIO_CHECK_RECOVER_STEP_MS_OVERRIDE=100 AUDIO_CHECK_RECOVER_AFTER_OVERRIDE=2 next dev --port ${PORT:-3011}",
- "start:test": "WORKER_TOKEN=test-worker-token TRANSCRIPTS_DIR=$(pwd)/test-transcripts EXPORT_PUBLIC_DIR=$(pwd)/test-transcripts/.export-public SETTINGS_FILE=$(pwd)/test-settings.json EDITOR_CHANGELOG_FILE=$(pwd)/test-changelog.md YTDLP_BIN=$(pwd)/e2e/fixtures/bin/fake-ytdlp.mjs GALLERY_DL_BIN=$(pwd)/e2e/fixtures/bin/fake-gallery-dl.mjs WHISPER_BIN=$(pwd)/e2e/fixtures/bin/fake-whisper.mjs WHISPER_MODEL=/dev/null CHOUGH_BIN=$(pwd)/e2e/fixtures/bin/fake-chough.mjs CHOUGH_MODEL=/dev/null PARAKEET_STITCH_BIN=$(pwd)/e2e/fixtures/bin/fake-parakeet-stitch.mjs PARAKEET_CLI=/dev/null PARAKEET_MODEL=/dev/null DIARIZE_BIN=$(pwd)/e2e/fixtures/bin/fake-diarize.mjs FFMPEG_BIN=$(pwd)/e2e/fixtures/bin/fake-ffmpeg.mjs FFPROBE_BIN=$(pwd)/e2e/fixtures/bin/fake-ffprobe.mjs OLLAMA_URL=http://127.0.0.1:${OLLAMA_STUB_PORT:-11435} CLAUDE_BIN=$(pwd)/e2e/fixtures/bin/fake-claude.mjs FINDMNT_BIN=$(pwd)/e2e/fixtures/bin/fake-findmnt.mjs UDISKSCTL_BIN=$(pwd)/e2e/fixtures/bin/fake-udisksctl.mjs AUDIO_CHECK_INTERVAL_MS_OVERRIDE=300 AUDIO_CHECK_SIZE_GATE_OVERRIDE=4096 AUDIO_CHECK_INTERVAL_FLOOR_MS_OVERRIDE=50 AUDIO_CHECK_RECOVER_STEP_MS_OVERRIDE=100 AUDIO_CHECK_RECOVER_AFTER_OVERRIDE=2 next start --port ${PORT:-3011}",
+ "dev:test": "EDITOR_TEST_ROUTES=1 WORKER_TOKEN=test-worker-token TRANSCRIPTS_DIR=$(pwd)/test-transcripts EXPORT_PUBLIC_DIR=$(pwd)/test-transcripts/.export-public SETTINGS_FILE=$(pwd)/test-settings.json EDITOR_CHANGELOG_FILE=$(pwd)/test-changelog.md YTDLP_BIN=$(pwd)/e2e/fixtures/bin/fake-ytdlp.mjs GALLERY_DL_BIN=$(pwd)/e2e/fixtures/bin/fake-gallery-dl.mjs WHISPER_BIN=$(pwd)/e2e/fixtures/bin/fake-whisper.mjs WHISPER_MODEL=/dev/null CHOUGH_BIN=$(pwd)/e2e/fixtures/bin/fake-chough.mjs CHOUGH_MODEL=/dev/null PARAKEET_STITCH_BIN=$(pwd)/e2e/fixtures/bin/fake-parakeet-stitch.mjs PARAKEET_CLI=/dev/null PARAKEET_MODEL=/dev/null DIARIZE_BIN=$(pwd)/e2e/fixtures/bin/fake-diarize.mjs FFMPEG_BIN=$(pwd)/e2e/fixtures/bin/fake-ffmpeg.mjs FFPROBE_BIN=$(pwd)/e2e/fixtures/bin/fake-ffprobe.mjs OLLAMA_URL=http://127.0.0.1:${OLLAMA_STUB_PORT:-11435} CLAUDE_BIN=$(pwd)/e2e/fixtures/bin/fake-claude.mjs FINDMNT_BIN=$(pwd)/e2e/fixtures/bin/fake-findmnt.mjs UDISKSCTL_BIN=$(pwd)/e2e/fixtures/bin/fake-udisksctl.mjs AUDIO_CHECK_INTERVAL_MS_OVERRIDE=300 AUDIO_CHECK_SIZE_GATE_OVERRIDE=4096 AUDIO_CHECK_INTERVAL_FLOOR_MS_OVERRIDE=50 AUDIO_CHECK_RECOVER_STEP_MS_OVERRIDE=100 AUDIO_CHECK_RECOVER_AFTER_OVERRIDE=2 next dev --port ${PORT:-3011}",
+ "start:test": "EDITOR_TEST_ROUTES=1 WORKER_TOKEN=test-worker-token TRANSCRIPTS_DIR=$(pwd)/test-transcripts EXPORT_PUBLIC_DIR=$(pwd)/test-transcripts/.export-public SETTINGS_FILE=$(pwd)/test-settings.json EDITOR_CHANGELOG_FILE=$(pwd)/test-changelog.md YTDLP_BIN=$(pwd)/e2e/fixtures/bin/fake-ytdlp.mjs GALLERY_DL_BIN=$(pwd)/e2e/fixtures/bin/fake-gallery-dl.mjs WHISPER_BIN=$(pwd)/e2e/fixtures/bin/fake-whisper.mjs WHISPER_MODEL=/dev/null CHOUGH_BIN=$(pwd)/e2e/fixtures/bin/fake-chough.mjs CHOUGH_MODEL=/dev/null PARAKEET_STITCH_BIN=$(pwd)/e2e/fixtures/bin/fake-parakeet-stitch.mjs PARAKEET_CLI=/dev/null PARAKEET_MODEL=/dev/null DIARIZE_BIN=$(pwd)/e2e/fixtures/bin/fake-diarize.mjs FFMPEG_BIN=$(pwd)/e2e/fixtures/bin/fake-ffmpeg.mjs FFPROBE_BIN=$(pwd)/e2e/fixtures/bin/fake-ffprobe.mjs OLLAMA_URL=http://127.0.0.1:${OLLAMA_STUB_PORT:-11435} CLAUDE_BIN=$(pwd)/e2e/fixtures/bin/fake-claude.mjs FINDMNT_BIN=$(pwd)/e2e/fixtures/bin/fake-findmnt.mjs UDISKSCTL_BIN=$(pwd)/e2e/fixtures/bin/fake-udisksctl.mjs AUDIO_CHECK_INTERVAL_MS_OVERRIDE=300 AUDIO_CHECK_SIZE_GATE_OVERRIDE=4096 AUDIO_CHECK_INTERVAL_FLOOR_MS_OVERRIDE=50 AUDIO_CHECK_RECOVER_STEP_MS_OVERRIDE=100 AUDIO_CHECK_RECOVER_AFTER_OVERRIDE=2 next start --port ${PORT:-3011}",
"build": "next build",
"start": "next start --port ${EDITOR_PORT:-3001}",
"lint": "eslint",