The auth/keys wave (2026-09-19): the server now owns the flat auth.json map directly (LH_AUTH_FILE from main.cjs; repo-root auth.json in dev, gitignored). The store reflects itself into the child's env at boot with fill-gap precedence — real env (incl. .env.local in dev) always wins — so every consumer keeps its plain process.env reads and saved keys take effect immediately, no restart; main.cjs's spawn-time injection retired (an env copy would shadow every settings-page change). Curated 4-key catalog (OpenRouter, Deepgram, Google search pair) with shape-only validation (no live pings — owner decision), 400s on bad pastes, last-4+length masking (plaintext never leaves the store), unknown file vars preserved + surfaced. Routes: GET /api/keys, PUT/DELETE /api/keys/:id. Tests: catalog/store/composition, with the env-sanitizing prologue (a dev shell may export real keys; tests use throwaway values only).
50 lines
2.0 KiB
TypeScript
50 lines
2.0 KiB
TypeScript
/**
|
|
* PUT /api/keys/:id — save one key; DELETE — remove it (auth-keys wave,
|
|
* 2026-09-19). `id` is the env-var/file key name (OPENROUTER_API_KEY, …).
|
|
*
|
|
* PUT body {value}: shape-gated (owner decision — bad pastes are 400s
|
|
* naming the expected format, never stored; validation is SHAPE-ONLY, no
|
|
* live pings). Saves take effect immediately (file + env reflected; no
|
|
* restart). A var owned by the REAL env is persisted for later but NOT
|
|
* reflected — the response's status row carries shadowedByEnv so the page
|
|
* can explain why the saved key is not the live one.
|
|
*
|
|
* DELETE: gone from the file always; the env copy goes only when it was
|
|
* the store's own reflection (a real env var stays — env wins). Idempotent
|
|
* (absent key = success). Unknown ids are 404 — the curated catalog is
|
|
* the writable surface; unknown file vars are preserved, not editable.
|
|
*/
|
|
import { isKnownAuthKey } from "@/src/core";
|
|
import { deleteKey, saveKey } from "@/src/server";
|
|
|
|
export const dynamic = "force-dynamic";
|
|
|
|
type Params = { params: Promise<{ id: string }> };
|
|
|
|
export async function PUT(req: Request, { params }: Params) {
|
|
const { id } = await params;
|
|
if (!isKnownAuthKey(id)) {
|
|
return Response.json({ error: `unknown key "${id}"` }, { status: 404 });
|
|
}
|
|
let body: unknown;
|
|
try {
|
|
body = await req.json();
|
|
} catch {
|
|
return Response.json({ error: "body must be JSON" }, { status: 400 });
|
|
}
|
|
if (!body || typeof body !== "object" || Array.isArray(body) || typeof (body as { value?: unknown }).value !== "string") {
|
|
return Response.json({ error: "body must be {value: string}" }, { status: 400 });
|
|
}
|
|
const result = saveKey(id, (body as { value: string }).value);
|
|
if (!result.ok) return Response.json({ error: result.error }, { status: 400 });
|
|
return Response.json({ status: result.status });
|
|
}
|
|
|
|
export async function DELETE(_req: Request, { params }: Params) {
|
|
const { id } = await params;
|
|
if (!isKnownAuthKey(id)) {
|
|
return Response.json({ error: `unknown key "${id}"` }, { status: 404 });
|
|
}
|
|
return Response.json({ status: deleteKey(id) });
|
|
}
|