History

fix(settings): persist only the billing delta so resolved secrets never hit disk (#60)

The Spend toggle PUT round-tripped Config.getGlobal() into Config.updateGlobal(). getGlobal() returns the resolved config, with {env:VAR} and {file:...} references substituted, so flipping a toggle rewrote openscience.json with plaintext secrets and destroyed the env indirection. updateGlobal already deep-merges into the raw file, so the route only needs to pass the billing delta.

Aayam Bansal committed 4 months ago

Commit 4a0bd751 parent fb842b42 files · +40 −3Browse files

Unified · Split

2 files
  1. backend/cli/src/server/routes/settings/billing.ts +4 −3
  2. backend/cli/test/server/settings-billing.test.ts +36 −0
backend/cli/src/server/routes/settings/billing.ts+4−3
@@ -69,9 +69,10 @@ export const BillingSettingsRoutes = lazy(() => · 68 unmodified lines
6969 validator("json", BillingPatch),
7070 async (c) => {
7171 const patch = c.req.valid("json")
72− const cfg = await Config.getGlobal()
73− const next = { ...cfg, billing: { ...(cfg.billing ?? {}), ...patch } }
74− await Config.updateGlobal(next)
72+ // Persist only the delta. updateGlobal deep-merges into the raw file;
73+ // writing back Config.getGlobal() would bake resolved {env:}/{file:}
74+ // secrets into openscience.json in plaintext.
75+ await Config.updateGlobal({ billing: patch })
7576 log.info("update", { keys: Object.keys(patch) })
7677
7778 // Mirror the LLM toggle to the account-scoped server billing mode, then force
backend/cli/test/server/settings-billing.test.ts+36−0
@@ -0,0 +1,36 @@
1+import { test, expect, afterEach } from "bun:test"
2+import path from "path"
3+import fs from "fs/promises"
4+import { Global } from "../../src/global"
5+import { BillingSettingsRoutes } from "../../src/server/routes/settings/billing"
6+
7+const file = path.join(Global.Path.config, "openscience.json")
8+
9+afterEach(async () => {
10+ await fs.rm(file, { force: true }).catch(() => {})
11+})
12+
13+test("PUT persists the toggle without baking resolved secrets into the config file", async () => {
14+ process.env["SPEND_TOGGLE_TEST_KEY"] = "sk-live-super-secret-123"
15+ await fs.mkdir(Global.Path.config, { recursive: true })
16+ await Bun.write(
17+ file,
18+ JSON.stringify({ provider: { openrouter: { options: { apiKey: "{env:SPEND_TOGGLE_TEST_KEY}" } } } }, null, 2),
19+ )
20+
21+ const res = await BillingSettingsRoutes().request("/", {
22+ method: "PUT",
23+ headers: { "content-type": "application/json" },
24+ body: JSON.stringify({ llm: "byok" }),
25+ })
26+ expect(res.status).toBe(200)
27+ const state = await res.json()
28+ expect(state.llm).toBe("byok")
29+
30+ const text = await Bun.file(file).text()
31+ expect(text).toContain("{env:SPEND_TOGGLE_TEST_KEY}")
32+ expect(text).not.toContain("sk-live-super-secret-123")
33+
34+ const written = JSON.parse(text)
35+ expect(written.billing).toEqual({ llm: "byok" })
36+})

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t