import { NextResponse } from "next/server"; import { opsAuth, opsFail } from "./_lib"; // THE READ SIDE'S DOOR (release 19, A3): `GET /api/ops/` routes that // answer what an existing builder already builds for a page — settings, the // storage locations, the sites, the workers, the lanes, the sync scheduler, a // channel's cleanup row — behind the token. A route here is the gate, the query // check and one call; it shapes nothing a page does not already show. // // UNKNOWN QUERY KEYS ARE A 400, as unknown body keys are on the write side: a // misspelled `?key=` would otherwise answer the whole document and look like // an answer to the question asked. export async function readRoute( request: Request, allowedQuery: readonly string[], run: (q: URLSearchParams) => Promise>, ): Promise { const denied = opsAuth(request); if (denied) return denied; const q = new URL(request.url).searchParams; const unknown = [...q.keys()].filter((k) => !allowedQuery.includes(k)); if (unknown.length) { return opsFail( `unknown query key(s): ${unknown.join(", ")} — accepted: ${ allowedQuery.length ? allowedQuery.join(", ") : "none" }`, ); } try { const out = await run(q); return out instanceof NextResponse ? out : NextResponse.json({ ok: true, ...out }); } catch (e) { return opsFail((e as Error).message, 500); } } // A SECRET NEVER LEAVES OVER THIS SURFACE, token or not. A remote worker's // outbound `token` lives in settings.json beside everything else; the ops token // lets a caller run the editor, not read the credentials of the machines it // talks to. Any string under a key that names a secret is replaced by // "" ("" stays "", so "unset" still reads as unset). const SECRET_KEY = /token|secret|password|passwd|api[-_]?key|credential/i; export function redactSecrets(value: T): T { if (Array.isArray(value)) return value.map(redactSecrets) as T; if (typeof value !== "object" || value === null) return value; const out: Record = {}; for (const [k, v] of Object.entries(value as Record)) { out[k] = SECRET_KEY.test(k) && typeof v === "string" && v !== "" ? "" : redactSecrets(v); } return out as T; }