From 4e79c95cc59c6c65f7b10e326aabb9cc9eefe19d Mon Sep 17 00:00:00 2001 From: Matthew Goodwin Date: Fri, 21 Aug 2026 15:05:28 -0500 Subject: [PATCH] =?UTF-8?q?RT:=20secrets=20read-back=20names=20the=20json?= =?UTF-8?q?=20store=20=E2=80=94=20sops=20infers=20binary=20from=20the=20.t?= =?UTF-8?q?mp=20suffix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- lib/__tests__/linear.test.ts | 2 +- lib/secrets/__tests__/store.test.ts | 6 +++--- lib/secrets/store.ts | 8 +++++++- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/lib/__tests__/linear.test.ts b/lib/__tests__/linear.test.ts index 51a8191e..d28a248e 100644 --- a/lib/__tests__/linear.test.ts +++ b/lib/__tests__/linear.test.ts @@ -79,7 +79,7 @@ function fakeSecretsSeams(seedDomains: Record> = removeFile: (p) => { files.delete(p); stats.delete(p); }, async run(cmd): Promise { if (cmd[0] === "sops" && cmd[1] === "-d") { - const domain = domainFromPath(cmd[2]!); + const domain = domainFromPath(cmd[cmd.length - 1]!); return { code: 0, stdout: JSON.stringify(domains.get(domain) ?? {}), stderr: "" }; } if (cmd[0] === "sops" && cmd[1] === "-e") { diff --git a/lib/secrets/__tests__/store.test.ts b/lib/secrets/__tests__/store.test.ts index 255972df..deb97380 100644 --- a/lib/secrets/__tests__/store.test.ts +++ b/lib/secrets/__tests__/store.test.ts @@ -128,7 +128,7 @@ class FakeSecretsExecSeam implements SecretsExecSeam { this.calls.push({ cmd, opts: runOpts }); if (cmd[0] === "sops" && cmd[1] === "-d") { - const target = cmd[2]!; + const target = cmd[cmd.length - 1]!; const staged = this.roundTrippablePlaintext.get(target); if (staged !== undefined) return { code: 0, stdout: staged, stderr: "" }; return this.opts.decrypt ? this.opts.decrypt() : { code: 0, stdout: "{}", stderr: "" }; @@ -303,7 +303,7 @@ describe("writeSecret", () => { expect(execSeam.calls.map((c) => c.cmd)).toEqual([ ["sops", "-d", path], ["sops", "-e", "--filename-override", join("secrets", `${domain}.json`), "--output", outputTmp, staging], - ["sops", "-d", outputTmp], + ["sops", "-d", "--input-type", "json", "--output-type", "json", outputTmp], ]); // The round-trip readback carries the same SOPS_AGE_KEY env as any other sops call — never argv. expect(execSeam.calls[2]?.opts).toEqual({ env: { SOPS_AGE_KEY: "AGE-X" }, sensitive: true }); @@ -332,7 +332,7 @@ describe("writeSecret", () => { const outputTmp = `${path}.${process.pid}.tmp`; expect(execSeam.calls.map((c) => c.cmd)).toEqual([ ["sops", "-e", "--filename-override", join("secrets", `${domain}.json`), "--output", outputTmp, stagingPath(domain)], - ["sops", "-d", outputTmp], + ["sops", "-d", "--input-type", "json", "--output-type", "json", outputTmp], ]); expect(execSeam.files.get(path)).toBe(DEFAULT_CIPHERTEXT); }); diff --git a/lib/secrets/store.ts b/lib/secrets/store.ts index fa37fdca..6262dcd9 100644 --- a/lib/secrets/store.ts +++ b/lib/secrets/store.ts @@ -245,7 +245,13 @@ async function encryptDomain( // sops creates outputTmpPath itself, at umask-derived (not 0600) perms. execSeam.chmod(outputTmpPath, 0o600); - const decryptResult = await execSeam.run(["sops", "-d", outputTmpPath], { env, sensitive: true }); + // sops picks the data store from the file extension; the `.tmp` suffix would + // select the binary store and fail on a JSON tree, so the read-back names + // the store explicitly (the real `.json` targets need no override). + const decryptResult = await execSeam.run( + ["sops", "-d", "--input-type", "json", "--output-type", "json", outputTmpPath], + { env, sensitive: true }, + ); let roundTripped: Record | undefined; if (decryptResult.code === 0) { try {