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:
amosblomqvist
2026-09-19 22:52:40 +03:00
parent e6dfa6faa4
commit ec38f91a2b
7 changed files with 318 additions and 50 deletions
+32 -15
View File
@@ -8,38 +8,35 @@
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import { AUTH_KEYS, isKnownAuthKey, maskKey, validateKeyShape } from "../src/core/auth-keys.ts";
import { AUTH_KEYS, genericProviderKeyGuard, isKnownAuthKey, maskKey, validateKeyShape } from "../src/core/auth-keys.ts";
describe("the catalog", () => {
it("is exactly the four curated keys, ids = env-var names", () => {
it("is exactly the three CURATED app-service keys (provider keys live in the provider table)", () => {
assert.deepEqual(
AUTH_KEYS.map((k) => k.id),
["OPENROUTER_API_KEY", "DEEPGRAM_API_KEY", "GOOGLE_SEARCH_API_KEY", "GOOGLE_CSE_ID"],
["DEEPGRAM_API_KEY", "GOOGLE_SEARCH_API_KEY", "GOOGLE_CSE_ID"],
);
for (const k of AUTH_KEYS) {
assert.ok(k.label.length > 0, `${k.id} has a label`);
assert.ok(k.description.length > 0, `${k.id} has a description`);
assert.ok(k.docsUrl.startsWith("https://"), `${k.id} has a docs link`);
}
// OPENROUTER_API_KEY graduated to the provider table (owner decision,
// provider-keys interlude) — not curated anymore.
assert.ok(!isKnownAuthKey("OPENROUTER_API_KEY"));
});
it("isKnownAuthKey: catalog yes, everything else no", () => {
assert.ok(isKnownAuthKey("OPENROUTER_API_KEY"));
assert.ok(isKnownAuthKey("DEEPGRAM_API_KEY"));
assert.ok(isKnownAuthKey("GOOGLE_CSE_ID"));
assert.ok(!isKnownAuthKey("OPENROUTER"));
assert.ok(!isKnownAuthKey("OPENROUTER_API_KEY"));
assert.ok(!isKnownAuthKey("OPENAI_API_KEY"), "provider vars are a separate surface");
assert.ok(!isKnownAuthKey("FUTURE_PLANS_TOKEN"));
assert.ok(!isKnownAuthKey(""));
});
});
describe("validateKeyShape (the 400 gate)", () => {
it("OpenRouter: sk-or- prefix required, length floor past it", () => {
assert.deepEqual(validateKeyShape("OPENROUTER_API_KEY", "sk-or-v1-abc123def456ghi789jkl012"), { ok: true });
assert.ok(!validateKeyShape("OPENROUTER_API_KEY", "sk-proj-abcdefghij").ok, "wrong family (OpenAI paste)");
assert.ok(!validateKeyShape("OPENROUTER_API_KEY", "sk-or-").ok, "prefix alone is a truncation");
assert.ok(!validateKeyShape("OPENROUTER_API_KEY", " ").ok, "empty fails");
});
describe("validateKeyShape (the 400 gate — curated keys)", () => {
it("Google API key: AIza prefix required", () => {
assert.ok(validateKeyShape("GOOGLE_SEARCH_API_KEY", "AIzaSyD-abcdef1234").ok);
assert.ok(!validateKeyShape("GOOGLE_SEARCH_API_KEY", "sk-or-v1-abcdefghij").ok, "wrong family");
@@ -61,13 +58,33 @@ describe("validateKeyShape (the 400 gate)", () => {
});
it("trims surrounding paste whitespace before checking", () => {
assert.ok(validateKeyShape("OPENROUTER_API_KEY", " sk-or-v1-abc123def456 \n").ok);
assert.ok(validateKeyShape("DEEPGRAM_API_KEY", ` ${"d".repeat(40)} `).ok);
});
it("unknown ids are rejected with the supported list", () => {
const r = validateKeyShape("SOMETHING_ELSE", "whatever");
assert.ok(!r.ok);
if (!r.ok) assert.match(r.error, /SOMETHING_ELSE.*OPENROUTER_API_KEY/);
if (!r.ok) assert.match(r.error, /SOMETHING_ELSE.*DEEPGRAM_API_KEY/);
});
});
describe("genericProviderKeyGuard (model-provider keys — paste hygiene only)", () => {
it("accepts any ≥12-char whitespace-free value (families vary)", () => {
assert.deepEqual(genericProviderKeyGuard("OPENROUTER_API_KEY", "sk-or-v1-abc123"), { ok: true });
assert.deepEqual(genericProviderKeyGuard("OPENAI_API_KEY", "whaterver-value-here"), { ok: true });
assert.deepEqual(genericProviderKeyGuard("HF_TOKEN", "hf_xxxxxxxx9999"), { ok: true });
});
it("rejects truncations and whitespace — the only guards", () => {
const short = genericProviderKeyGuard("ZAI_API_KEY", "short");
assert.ok(!short.ok);
if (!short.ok) assert.match(short.error, /at least 12/);
assert.ok(!genericProviderKeyGuard("OPENAI_API_KEY", "has whitespace now").ok);
assert.ok(!genericProviderKeyGuard("OPENAI_API_KEY", " ").ok);
});
it("trims before checking", () => {
assert.ok(genericProviderKeyGuard("OPENROUTER_API_KEY", " sk-or-v1-xyz999 ").ok);
});
});
+92
View File
@@ -0,0 +1,92 @@
/**
* THE NO-DRIFT PIN (provider-keys interlude, 2026-09-19): core/auth-keys.ts
* AUTH_PROVIDER_KEYS claims to be exactly pi-ai's provider→env-var table
* (minus the owner-spec exclusions). This test re-derives the TRUTH from
* pi-ai itself and asserts exact equality — so a pi-ai upgrade that adds,
* renames, or re-labels a provider key FAILS the gate loudly instead of
* the table drifting silently from what pi actually reads.
*
* Derivation (same mechanism that generated the table): pi-ai's exports
* map doesn't expose env-api-keys.js as a subpath, so this test
* deep-imports it by RELATIVE path — tests may; runtime code never does.
* findEnvKeys with a Proxy env that resolves every var name truthy returns
* ALL the env vars a provider reads as API keys.
*
* Owner-spec exclusions (asserted ABSENT): github-copilot (OAuth),
* amazon-bedrock (AWS creds), google-vertex (ADC), azure-openai-responses
* (needs base URL), the cloudflare account-scoped pair; anthropic keeps
* ANTHROPIC_API_KEY only (AUTH_TOKEN/OAUTH_TOKEN stay env-only).
*/
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import { AUTH_PROVIDER_KEYS, isProviderEnvVar } from "../src/core/auth-keys.ts";
import { builtinProviders } from "../node_modules/@earendil-works/pi-ai/dist/providers/all.js";
import { findEnvKeys } from "../node_modules/@earendil-works/pi-ai/dist/env-api-keys.js";
/** pi-ai reads anthropic via AUTH_TOKEN/OAUTH_TOKEN alternates too — those
* stay env-only by owner spec; asserted absent below. */
const ANTHROPIC_ALTERNATES = ["ANTHROPIC_AUTH_TOKEN", "ANTHROPIC_OAUTH_TOKEN"];
const EXCLUDED_PROVIDERS = new Set([
"github-copilot",
"amazon-bedrock",
"google-vertex",
"azure-openai-responses",
"cloudflare-workers-ai",
"cloudflare-ai-gateway",
]);
/** A proxy env where EVERY var name resolves truthy — findEnvKeys then
* returns every env var the provider reads as an API-key source. */
const truthyEnv = new Proxy({} as Record<string, string>, {
get: (_t, key) => (typeof key === "string" ? "x" : undefined),
has: () => true,
});
/** The truth, derived from pi-ai at test time. */
function deriveProviders() {
const rows: Array<{ id: string; envVar: string; label: string }> = [];
for (const p of builtinProviders()) {
const keys = findEnvKeys(p.id, truthyEnv);
if (!keys || EXCLUDED_PROVIDERS.has(p.id)) continue;
const first = keys[0];
if (!first) continue; // unreachable with the truthy proxy — satisfies the checker
const envVar = p.id === "anthropic" ? "ANTHROPIC_API_KEY" : first;
rows.push({ id: p.id, envVar, label: p.name });
}
rows.sort((a, b) => a.id.localeCompare(b.id));
return rows;
}
describe("AUTH_PROVIDER_KEYS — the no-drift pin against pi-ai", () => {
it("is EXACTLY pi-ai's plain-key provider table (id, envVar, label), exclusions applied", () => {
const truth = deriveProviders();
const mine = [...AUTH_PROVIDER_KEYS].sort((a, b) => a.id.localeCompare(b.id));
assert.deepEqual(
mine.map((p) => [p.id, p.envVar, p.label]),
truth.map((p) => [p.id, p.envVar, p.label]),
"pi-ai's provider env-key table changed — regenerate AUTH_PROVIDER_KEYS (src/core/auth-keys.ts) from the derivation in this test",
);
});
it("the owner-spec exclusions are absent", () => {
const ids = new Set(AUTH_PROVIDER_KEYS.map((p) => p.id));
for (const excluded of EXCLUDED_PROVIDERS) assert.ok(!ids.has(excluded), `${excluded} must not be listed`);
for (const p of AUTH_PROVIDER_KEYS) {
assert.ok(!ANTHROPIC_ALTERNATES.includes(p.envVar), `${p.envVar} must not appear`);
}
});
it("openrouter IS in the table (it graduated from the curated catalog)", () => {
const row = AUTH_PROVIDER_KEYS.find((p) => p.id === "openrouter");
assert.ok(row);
assert.equal(row.envVar, "OPENROUTER_API_KEY");
});
it("isProviderEnvVar: exactly the table's distinct env vars", () => {
for (const p of AUTH_PROVIDER_KEYS) assert.ok(isProviderEnvVar(p.envVar), `${p.envVar} is a provider var`);
assert.ok(!isProviderEnvVar("OPENROUTER_API_KEYS"), "typo'd name is not one");
assert.ok(!isProviderEnvVar("DEEPGRAM_API_KEY"), "curated ≠ provider — separate surfaces");
});
});
+46 -1
View File
@@ -26,6 +26,7 @@ for (const varName of [
"DEEPGRAM_API_KEY",
"GOOGLE_SEARCH_API_KEY",
"GOOGLE_CSE_ID",
"OPENAI_API_KEY",
"FUTURE_PLANS_TOKEN",
]) {
delete process.env[varName];
@@ -48,7 +49,7 @@ fs.writeFileSync(
process.env.OPENROUTER_API_KEY = "sk-or-v1-envvalue99";
// Import AFTER env setup — module init runs the boot fill-gap + snapshot.
const { deleteKey, initAuthEnv, keyStatuses, keyValue, resolveKeyStatuses, saveKey } = await import(
const { deleteKey, initAuthEnv, keyStatuses, keyValue, resolveKeyStatuses, saveKey, isWritableAuthKey } = await import(
"../src/server/keys.ts"
);
const { authStore } = await import("../src/store/auth.ts");
@@ -58,6 +59,7 @@ const ENV_OWNED = "sk-or-v1-envvalue99";
after(() => {
delete process.env.OPENROUTER_API_KEY;
delete process.env.DEEPGRAM_API_KEY;
delete process.env.OPENAI_API_KEY;
fs.rmSync(TMP, { recursive: true, force: true });
});
@@ -146,6 +148,49 @@ describe("saveKey / deleteKey (immediate effect, no restart)", () => {
initAuthEnv();
assert.equal(process.env.DEEPGRAM_API_KEY, undefined, "file entry gone → no re-fill");
});
describe("model-provider keys (provider-keys interlude)", () => {
it("absent provider keys do NOT spam status rows — the dropdown is discovery", () => {
const rows = keyStatuses().map((s) => s.id);
assert.ok(!rows.includes("OPENAI_API_KEY"), "never saved → no row");
assert.ok(!rows.includes("ZAI_API_KEY"));
});
it("save via provider env var: generic guard, reflected immediately", () => {
const value = "sk-throwaway-openai-0123456789"; // never a real secret
const r = saveKey("OPENAI_API_KEY", value);
assert.ok(r.ok);
assert.equal(process.env.OPENAI_API_KEY, value, "env updated immediately");
assert.equal(authStore.authVars().OPENAI_API_KEY, value, "file updated");
const row = keyStatuses().find((s) => s.id === "OPENAI_API_KEY")!;
assert.ok(row.present);
assert.equal(row.source, "file", "fill-gap reflection — origin is the store");
});
it("provider truncations/whitespace are 400s (paste hygiene only)", () => {
assert.ok(!saveKey("OPENAI_API_KEY", "short").ok);
assert.ok(!saveKey("OPENAI_API_KEY", "has whitespace now").ok);
// no family check anymore — any ≥12-char token passes
assert.ok(saveKey("OPENROUTER_API_KEY", "not-sk-or-but-12chars").ok);
});
it("isWritableAuthKey: curated ∪ provider env vars; unknown stays unwritable", () => {
assert.ok(isWritableAuthKey("DEEPGRAM_API_KEY"));
assert.ok(isWritableAuthKey("OPENROUTER_API_KEY"));
assert.ok(isWritableAuthKey("OPENAI_API_KEY"));
assert.ok(!isWritableAuthKey("FUTURE_PLANS_TOKEN"));
assert.ok(!isWritableAuthKey("ANTHROPIC_AUTH_TOKEN"), "anthropic alternates stay env-only");
});
it("delete of a provider key: env + file both cleared", () => {
deleteKey("OPENAI_API_KEY");
assert.equal(process.env.OPENAI_API_KEY, undefined);
assert.ok(!("OPENAI_API_KEY" in authStore.authVars()));
// cleanup the openrouter save from the guard test too
deleteKey("OPENROUTER_API_KEY");
assert.ok(!("OPENROUTER_API_KEY" in authStore.authVars()));
});
});
});
describe("resolveKeyStatuses (pure projection)", () => {