- Add GET/PUT /api/settings for encrypted LLM configuration - SettingsResponse with masked key display - SettingsUpdate validation with optional fields - Defaults: llmBaseUrl "https://openrouter.ai/api/v1", llmModel "anthropic/claude-sonnet-4.5" - Encrypted storage with AES-256-GCM for llm_key - getSetting() export for future route groups - Comprehensive test coverage: defaults, updates, masking, encryption at rest, validation Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
59 lines
2.2 KiB
TypeScript
59 lines
2.2 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
|
import { mkdtempSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { eq } from "drizzle-orm";
|
|
import { createApp } from "../src/app";
|
|
import { openDb } from "../src/db";
|
|
import { settings } from "../src/db/schema";
|
|
import { loadOrCreateKey } from "../src/lib/crypto";
|
|
|
|
async function authedApp() {
|
|
const dir = mkdtempSync(join(tmpdir(), "helios-"));
|
|
const db = openDb(dir);
|
|
const app = createApp({ db, key: loadOrCreateKey(dir) });
|
|
const j = (b: unknown) => ({ method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(b) });
|
|
await app.request("/api/setup", j({ password: "hunter2hunter2" }));
|
|
const cookie = (await app.request("/api/login", j({ password: "hunter2hunter2" }))).headers.get("set-cookie")!;
|
|
return { app, db, cookie };
|
|
}
|
|
|
|
describe("settings", () => {
|
|
test("defaults, update, masked key, encrypted at rest", async () => {
|
|
const { app, db, cookie } = await authedApp();
|
|
const h = { cookie };
|
|
|
|
let s = await (await app.request("/api/settings", { headers: h })).json();
|
|
expect(s).toEqual({
|
|
llmBaseUrl: "https://openrouter.ai/api/v1",
|
|
llmModel: "anthropic/claude-sonnet-4.5",
|
|
llmKeyMasked: null,
|
|
});
|
|
|
|
const put = await app.request("/api/settings", {
|
|
method: "PUT",
|
|
headers: { ...h, "content-type": "application/json" },
|
|
body: JSON.stringify({ llmKey: "sk-or-v1-supersecret-abcd", llmModel: "meta-llama/llama-4" }),
|
|
});
|
|
expect(put.status).toBe(204);
|
|
|
|
s = await (await app.request("/api/settings", { headers: h })).json();
|
|
expect(s.llmKeyMasked).toBe("…abcd");
|
|
expect(s.llmModel).toBe("meta-llama/llama-4");
|
|
|
|
const raw = (await db.select().from(settings).where(eq(settings.key, "llm_key"))).at(0)!.value;
|
|
expect(raw.startsWith("enc:")).toBe(true);
|
|
expect(raw).not.toContain("supersecret");
|
|
});
|
|
|
|
test("rejects bad body", async () => {
|
|
const { app, cookie } = await authedApp();
|
|
const r = await app.request("/api/settings", {
|
|
method: "PUT",
|
|
headers: { cookie, "content-type": "application/json" },
|
|
body: JSON.stringify({ llmBaseUrl: "not a url" }),
|
|
});
|
|
expect(r.status).toBe(400);
|
|
});
|
|
});
|