Files
learnharness/app/api/keys/[id]/route.ts
T
amosblomqvist ec38f91a2b feat(keys): free-form model-provider keys — pi-ai provider table on /api/keys
Provider-keys interlude (owner spec, 2026-09-19): any pi-compatible
provider's key is manageable, not just the curated set.

GET /api/keys now also returns providers: {id, envVar, label}[] — every
pi-ai provider that takes a plain api-key env var (33 today), derived
from pi-ai's own provider→env-var table. No-drift mechanism: the table
is generated from pi-ai (builtinProviders + findEnvKeys over a
truthy-for-every-var proxy env) and pinned by
tests/auth-provider-keys.test.ts, which re-derives the truth at test
time and asserts EXACT equality — a pi-ai upgrade fails the gate loudly.

Owner-spec exclusions: github-copilot (OAuth), amazon-bedrock (AWS),
google-vertex (ADC), azure-openai-responses (base URL), the cloudflare
pair; anthropic keeps ANTHROPIC_API_KEY only. OPENROUTER_API_KEY
graduates from the curated catalog to the provider table (dedupe — the
UI pins it always-visible); curated stays Deepgram + the Google search
pair. Statuses: curated + present vars + unknown file vars — absent
provider keys never spam rows; the dropdown is discovery.

Write side: PUT/DELETE accept provider env vars (generic truncation
guard — trim, no whitespace, ≥12; family checks stay on the curated
three). Present-in-env provider vars surface as status rows so
env-wins/shadow semantics stay honest.
2026-09-19 22:52:40 +03:00

51 lines
2.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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). Writable ids are the CURATED CATALOG ∪ THE
* MODEL-PROVIDER env vars (provider-keys interlude); anything else is 404
* — unknown file vars are preserved, not editable.
*/
import { isWritableAuthKey } from "@/src/server";
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 (!isWritableAuthKey(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 (!isWritableAuthKey(id)) {
return Response.json({ error: `unknown key "${id}"` }, { status: 404 });
}
return Response.json({ status: deleteKey(id) });
}