import { NextResponse } from "next/server"; import { getSettings, siteSettingsSchema } from "yt-dlp-transcript-common/lib/settings"; import { mergeSettingsPatch, saveSettings } from "../../../settings/saveSettings"; import { OpsInputError, ops, opsFail } from "../_lib"; import { readRoute, redactSecrets } from "../_read"; import { settingsPatchProblem } from "./patch"; export const dynamic = "force-dynamic"; // GET /api/ops/settings[?key=] // // settings.json as the editor reads it — through getSettings, so migrated and // defaulted, exactly the values every action sees (not the raw file: a key the // file leaves out answers with its default). `?key=autoQueue` answers one block. // Secrets are redacted (`_read.ts`). SETTINGS.md is the key table. export async function GET(request: Request) { return readRoute(request, ["key"], async (q) => { const settings = redactSecrets(getSettings()) as unknown as Record; const key = q.get("key"); if (key === null) return { settings }; if (!Object.hasOwn(settings, key)) { return opsFail(`no settings key "${key}" — known: ${Object.keys(settings).sort().join(", ")}`); } return { key, value: settings[key] }; }); } // POST { patch: { : , … } } // // THE EDITOR'S ONE SETTINGS WRITER, over HTTP: `saveSettings(patch)` — the // function every settings form calls — with its merge rule (a block's keys // merge one level; an array, a scalar, or an object nested in a block // replaces). SETTINGS.md is the key table. // // REFUSED, before anything is written (`settings/patch.ts`): // - an unknown top-level key, named; // - a key another writer owns, because writing it raw breaks what that // writer keeps true (channelPriority, autoQueue, workers, storage — each // refusal names the command or page that writes it); // - a value the schema would not keep as sent. The schema never throws: it // coerces (clamps a number, drops an unknown enum, fills a default), so a // `{ ok: true }` would otherwise be the answer to a value that was never // saved. Every leaf the patch names is compared with what the schema makes // of it, and a difference is a 400 naming the path, what was sent and what // would be saved. A leaf the patch leaves out may still be filled with its // default — which is the merge rule, and is what the answer's `value` shows. // // Answers { ok, changed: [keys], value: { : } } — read back // after the write, secrets redacted. export async function POST(request: Request) { return ops(request, ["patch"], async (body) => { const patch = body.patch; if (typeof patch !== "object" || patch === null || Array.isArray(patch)) { throw new OpsInputError('"patch" is required and must be an object of top-level settings keys'); } const keys = Object.keys(patch); if (keys.length === 0) throw new OpsInputError('"patch" is empty — nothing to change'); const current = getSettings(); const problem = settingsPatchProblem( patch as Record, Object.keys(siteSettingsSchema.shape), siteSettingsSchema.parse( mergeSettingsPatch(current, patch as Partial), ) as unknown as Record, ); if (problem) throw new OpsInputError(problem); await saveSettings(patch as Partial); const saved = redactSecrets(getSettings()) as unknown as Record; return NextResponse.json({ ok: true, changed: keys, value: Object.fromEntries(keys.map((k) => [k, saved[k]])), }); }); }