Archilyzer · Source

archilyzer

Archilyzer
git clone https://archilyzer.pages.dev/source/archilyzer.git
Log | Files | Refs | README | LICENSE

commit 98ccceb1d57770c44af3b2ea0d0b1c903f180cdd
parent b69a6629c94ea5523aba3540e559a16b29f271c1
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Tue, 22 Sep 2026 16:40:34 -0400

editor: the other four /api/test routes are behind the same door

worker-token was the sharpest case, not the only one. All four siblings were
mounted unconditionally on the same reasoning — "the editor is a localhost admin
tool" — and all four are unauthenticated GETs that mutate live process state:
invalidate-cache cancels every running job and drops four singletons, stuck-job
writes a fabricated record into the registry, resume-lane stops and restarts a
lane runner, uncaught-count resets a counter a spec asserts on. The operator's
browser is inside the loopback, so each is CSRF-able from any page they have
open.

One guard, `EDITOR_TEST_ROUTES`, set by the two scripts every harness boots
through, and the comments that argued the old position now say why it moved.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Diffstat:
Meditor/app/api/test/invalidate-cache/route.ts | 12++++++++++--
Meditor/app/api/test/resume-lane/route.ts | 10++++++++--
Meditor/app/api/test/stuck-job/route.ts | 9+++++++--
Meditor/app/api/test/uncaught-count/route.ts | 5+++++
4 files changed, 30 insertions(+), 6 deletions(-)

diff --git a/editor/app/api/test/invalidate-cache/route.ts b/editor/app/api/test/invalidate-cache/route.ts @@ -3,6 +3,7 @@ import { revalidatePath } from "next/cache"; import { resetSnapshotScheduler } from "yt-dlp-transcript-common/jobs/snapshotScheduler"; import { resetChannelSnapshotMemo } from "yt-dlp-transcript-common/controller/channels"; import { resetStorageProbeMemo } from "yt-dlp-transcript-common/controller/storageLocations"; +import { testRouteDenied } from "../_guard"; export const dynamic = "force-dynamic"; @@ -39,13 +40,20 @@ function cancelLiveJobs() { } } -// E2E test harness only. Mounted unconditionally so it's reachable from the -// dev:test script; the editor is intended for localhost use, not deployment. +// E2E test harness only, and GUARDED BY `EDITOR_TEST_ROUTES` — which dev:test +// and start:test set, so it is still reachable from exactly the servers that +// need it. It cancels every live job and drops four singletons; an +// unauthenticated GET doing that is CSRF-able from any page the operator has +// open, loopback or not. See _guard.ts. export async function POST() { + const denied = testRouteDenied(); + if (denied) return denied; return invalidate(); } export async function GET() { + const denied = testRouteDenied(); + if (denied) return denied; return invalidate(); } diff --git a/editor/app/api/test/resume-lane/route.ts b/editor/app/api/test/resume-lane/route.ts @@ -6,6 +6,7 @@ import { } from "yt-dlp-transcript-common/controller/autoRunner"; import { LANES } from "yt-dlp-transcript-common/lib/autoQueueTypes"; import type { AutoQueueKind } from "yt-dlp-transcript-common/jobs/autoQueueState"; +import { testRouteDenied } from "../_guard"; export const dynamic = "force-dynamic"; @@ -19,9 +20,14 @@ export const dynamic = "force-dynamic"; // It replaced /api/test/resume-backfill-sweep, which did this for the sweep; // that route retired with the sweep in slice 1.3. // -// Mounted unconditionally, like the other /api/test routes: the editor is a -// localhost admin tool, not a deployed service. +// GUARDED BY `EDITOR_TEST_ROUTES`, like every other /api/test route. It was +// mounted unconditionally on the reasoning that the editor is a localhost admin +// tool — but the operator's browser is inside the loopback, so an +// unauthenticated GET that stops and restarts a lane runner is CSRF-able. +// See _guard.ts. export async function GET(req: Request) { + const denied = testRouteDenied(); + if (denied) return denied; const lane = new URL(req.url).searchParams.get("lane") ?? ""; if (!LANES.includes(lane as AutoQueueKind)) { return NextResponse.json( diff --git a/editor/app/api/test/stuck-job/route.ts b/editor/app/api/test/stuck-job/route.ts @@ -7,6 +7,7 @@ import { type JobRecord, } from "yt-dlp-transcript-common/jobs/registry"; import { getPaths } from "yt-dlp-transcript-common/lib/paths"; +import { testRouteDenied } from "../_guard"; export const dynamic = "force-dynamic"; @@ -17,9 +18,13 @@ export const dynamic = "force-dynamic"; // NOT auto-healed, so it persists across polls and the test can prove // FORCE-RELEASE (not auto-heal) clears it. We reproduce it directly // (backdating startedAt) since a genuinely wedged child would be racy. -// Mounted unconditionally, like the other /api/test routes — the editor is a -// localhost admin tool, not deployed. +// GUARDED BY `EDITOR_TEST_ROUTES`, like every other /api/test route. It was +// mounted unconditionally on the reasoning that the editor is a localhost admin +// tool — but the operator's browser is inside the loopback, so an +// unauthenticated GET that writes into the registry is CSRF-able. See _guard.ts. export async function GET(request: Request) { + const denied = testRouteDenied(); + if (denied) return denied; const url = new URL(request.url); const queueKey = url.searchParams.get("queue") || "stuck-queue"; diff --git a/editor/app/api/test/uncaught-count/route.ts b/editor/app/api/test/uncaught-count/route.ts @@ -1,4 +1,5 @@ import { NextResponse } from "next/server"; +import { testRouteDenied } from "../_guard"; export const dynamic = "force-dynamic"; @@ -55,6 +56,8 @@ function getState(): CountState { } export async function GET() { + const denied = testRouteDenied(); + if (denied) return denied; const state = getState(); return NextResponse.json({ uncaught: state.uncaught, @@ -64,6 +67,8 @@ export async function GET() { } export async function DELETE() { + const denied = testRouteDenied(); + if (denied) return denied; const state = getState(); state.uncaught = 0; state.unhandled = 0;