import path from "node:path"; import { NextResponse } from "next/server"; import { getPaths } from "yt-dlp-transcript-common/lib/paths"; import { readLogChunk } from "yt-dlp-transcript-common/jobs/listJobs"; import { getRegistry } from "yt-dlp-transcript-common/jobs/registry"; export const dynamic = "force-dynamic"; // A JOB'S LOG, AND IT IS DELIBERATELY UNGATED. // // WHAT IT LEAKS, stated plainly: the text of any job's log by id, plus its // status and queue position. That is yt-dlp output, channel slugs, video ids, // file paths inside the corpus, and whatever a controller chose to log. It is // not credentials (a step's env is never logged) and it is not media. // // WHY IT IS NOT BEHIND `WORKER_TOKEN` like /api/worker/* and /api/ops/*. // `authorizeWorkerRequest` answers 503 when the variable is UNSET — "this // surface is off until you opt in" — which is exactly right for a surface that // accepts instructions, and exactly wrong here: the browser polls this endpoint // same-origin, with no token, for every run panel in the app. Gating it would // switch every job log off on a default install, and the fix an operator would // reach for is to put a shared secret into the page that starts the job. // // THE BOUNDARY IS THE NETWORK, NOT THIS ROUTE. Nothing in the editor publishes // a port; Caddy is the single front door and binds 127.0.0.1, and // `docker/guard-exposure.sh` REFUSES TO START if a private app is bound // off-loopback with no auth in front of it (AGENTS.md, "The runtime // container"). An editor reachable by a stranger is a misconfiguration that // guard exists to make impossible, and one this route could not fix anyway. // // So: `scripts/archilyzer-ops.mjs`'s `followJob` sends no authorization header, // because there was never anything to send it to. export async function GET( request: Request, { params }: { params: Promise<{ id: string }> }, ) { const { id } = await params; if (!/^[A-Za-z0-9_-]+$/.test(id)) { return NextResponse.json({ error: "Invalid id" }, { status: 400 }); } const url = new URL(request.url); const fromRaw = url.searchParams.get("from"); const from = fromRaw ? Math.max(0, Number.parseInt(fromRaw, 10) || 0) : 0; const paths = getPaths(); const logPath = path.join(paths.jobsDir, `${id}.log`); const { content, nextOffset } = await readLogChunk(logPath, from); const registry = getRegistry(); const record = registry.get(id); const status = record ? record.status : "archived"; const queueKey = record?.queueKey; const queuePosition = record ? registry.positionInQueue(id) : -1; return NextResponse.json({ content, nextOffset, status, queueKey, queuePosition, }); }