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
69 validator("json", BillingPatch),69 validator("json", BillingPatch),
70 async (c) => {70 async (c) => {
71 const patch = c.req.valid("json")71 const patch = c.req.valid("json")
72 const cfg = await Config.getGlobal()72 // Persist only the delta. updateGlobal deep-merges into the raw file;
73 const next = { ...cfg, billing: { ...(cfg.billing ?? {}), ...patch } }73 // writing back Config.getGlobal() would bake resolved {env:}/{file:}
74 await Config.updateGlobal(next)74 // secrets into openscience.json in plaintext.
75 await Config.updateGlobal({ billing: patch })
75 log.info("update", { keys: Object.keys(patch) })76 log.info("update", { keys: Object.keys(patch) })
76 77
77 // Mirror the LLM toggle to the account-scoped server billing mode, then force78 // 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 @@
1import { test, expect, afterEach } from "bun:test"
2import path from "path"
3import fs from "fs/promises"
4import { Global } from "../../src/global"
5import { BillingSettingsRoutes } from "../../src/server/routes/settings/billing"
6
7const file = path.join(Global.Path.config, "openscience.json")
8
9afterEach(async () => {
10 await fs.rm(file, { force: true }).catch(() => {})
11})
12
13test("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