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.
This commit is contained in:
@@ -11,10 +11,11 @@
|
||||
*
|
||||
* 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.
|
||||
* (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 { isKnownAuthKey } from "@/src/core";
|
||||
import { isWritableAuthKey } from "@/src/server";
|
||||
import { deleteKey, saveKey } from "@/src/server";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
@@ -23,7 +24,7 @@ type Params = { params: Promise<{ id: string }> };
|
||||
|
||||
export async function PUT(req: Request, { params }: Params) {
|
||||
const { id } = await params;
|
||||
if (!isKnownAuthKey(id)) {
|
||||
if (!isWritableAuthKey(id)) {
|
||||
return Response.json({ error: `unknown key "${id}"` }, { status: 404 });
|
||||
}
|
||||
let body: unknown;
|
||||
@@ -42,7 +43,7 @@ export async function PUT(req: Request, { params }: Params) {
|
||||
|
||||
export async function DELETE(_req: Request, { params }: Params) {
|
||||
const { id } = await params;
|
||||
if (!isKnownAuthKey(id)) {
|
||||
if (!isWritableAuthKey(id)) {
|
||||
return Response.json({ error: `unknown key "${id}"` }, { status: 404 });
|
||||
}
|
||||
return Response.json({ status: deleteKey(id) });
|
||||
|
||||
+16
-8
@@ -1,17 +1,25 @@
|
||||
/**
|
||||
* GET /api/keys — the auth page's key list (auth-keys wave, 2026-09-19).
|
||||
* GET /api/keys — the auth page's key list (auth-keys wave, 2026-09-19;
|
||||
* providers added by the provider-keys interlude same wave).
|
||||
*
|
||||
* Every curated key (present or not) plus unknown file vars surfaced as
|
||||
* generic rows. Masked only — last 4 + length; PLAINTEXT NEVER LEAVES THE
|
||||
* STORE THROUGH THIS API (owner decision). `source` tells the page whether
|
||||
* the live value comes from the environment (dev .env.local / launch env)
|
||||
* or the file, and `shadowedByEnv` explains why a saved key is not the
|
||||
* live one (env always wins).
|
||||
* generic rows — ABSENT provider keys deliberately do NOT spam status
|
||||
* rows; the provider dropdown is the discovery surface. `providers`
|
||||
* carries it: {id, envVar, label} for every pi-ai plain-key provider,
|
||||
* derived from pi-ai's own provider→env-var table (no-drift pin in
|
||||
* tests/auth-provider-keys.test.ts) — the settings UI reads it optionally
|
||||
* with a graceful fallback.
|
||||
*
|
||||
* Masked only — last 4 + length; PLAINTEXT NEVER LEAVES THE STORE THROUGH
|
||||
* THIS API (owner decision). `source` tells the page whether the live
|
||||
* value comes from the environment (dev .env.local / launch env) or the
|
||||
* file, and `shadowedByEnv` explains why a saved key is not the live one
|
||||
* (env always wins).
|
||||
*/
|
||||
import { keyStatuses } from "@/src/server";
|
||||
import { keyStatuses, providerKeyList } from "@/src/server";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function GET() {
|
||||
return Response.json({ keys: keyStatuses() });
|
||||
return Response.json({ keys: keyStatuses(), providers: providerKeyList() });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user