diff --git a/scripts/check-miner-package.ts b/scripts/check-miner-package.ts index 5d8b6dbbf..75b9be92b 100644 --- a/scripts/check-miner-package.ts +++ b/scripts/check-miner-package.ts @@ -29,7 +29,21 @@ const REQUIRED = [ "Dockerfile", "schema/miner-goal-spec.schema.json", ]; -const FORBIDDEN_PATH = /(^|\/)(\.dev\.vars|\.env|\.npmrc|.*\.pem|.*private.*key.*|.*secret.*)$/i; +// Exact-name dotfiles that are inherently credential-shaped regardless of extension. +const FORBIDDEN_DOTFILE = /(^|\/)(\.dev\.vars|\.env|\.npmrc)$/i; +// A filename merely SUGGESTING secret-shaped content -- a coarse heuristic that's meaningful for data/config +// files (.json, .txt, .yml, .pem, etc.), where the filename is a fair proxy for what's likely inside, but not +// for .js/.ts source (source files are exempted below): a descriptively-named source file that IMPLEMENTS +// secret/credential-handling logic is normal and expected in this codebase, and a real leaked secret VALUE in +// source is already caught unconditionally by FORBIDDEN_CONTENT below, regardless of the file's name. +const FORBIDDEN_KEYWORD = /(^|\/)(.*\.pem|.*private.*key.*|.*secret.*)$/i; +// .ts$ also matches .d.ts (which always ends in the literal characters ".ts"). +const SOURCE_FILE = /\.(js|ts)$/i; + +function isForbiddenPath(file: string): boolean { + return FORBIDDEN_DOTFILE.test(file) || (!SOURCE_FILE.test(file) && FORBIDDEN_KEYWORD.test(file)); +} + // Stale public-package wording the published README must never ship with (#7013). The sibling // check-mcp-package.ts has always guarded its README against this; the miner-package check did not, so a // pre-release "private beta"/"preview URL" phrasing could ship in the public `@loopover/miner` README unnoticed. @@ -41,7 +55,7 @@ type ReadContentFn = (file: string) => string; export function validateMinerPackFileList(files: readonly PackedFile[], readContent: ReadContentFn): string[] { const paths = files.map((file) => (typeof file === "string" ? file : file.path)).sort(); for (const file of paths) { - if (FORBIDDEN_PATH.test(file)) throw new Error(`Forbidden file in miner package: ${file}`); + if (isForbiddenPath(file)) throw new Error(`Forbidden file in miner package: ${file}`); if (!ALLOWED.some((pattern) => pattern.test(file))) throw new Error(`Unexpected file in miner package: ${file}`); const content = readContent(file); if (FORBIDDEN_CONTENT.test(content)) throw new Error(`Secret-like content found in miner package file: ${file}`); diff --git a/test/unit/check-miner-package.test.ts b/test/unit/check-miner-package.test.ts index 35b97527c..30f68d1ba 100644 --- a/test/unit/check-miner-package.test.ts +++ b/test/unit/check-miner-package.test.ts @@ -34,6 +34,47 @@ describe("check-miner-package script", () => { expect(result.out).toContain("Forbidden file in miner package: .env"); }); + describe("the secret-filename heuristic only applies to non-source files", () => { + it("accepts a source file whose name merely mentions 'secret' or 'private key' (no leaked value inside)", () => { + const result = runChecker({ + CHECK_MINER_PACK_TEST_FILES: JSON.stringify([ + "package.json", + "bin/loopover-miner.js", + "lib/secret-helper.js", + "lib/secret-helper.d.ts", + "lib/private-key-rotation.js", + "DEPLOYMENT.md", + "Dockerfile", + "docs/coding-agent-driver.md", + "schema/miner-goal-spec.schema.json", + ]), + CHECK_MINER_PACK_TEST_CONTENT: "console.log('this file discusses secrets but holds no secret value');", + }); + expect(result.status).toBe(0); + expect(result.out).toMatch(/^Miner package dry-run ok:/); + expect(result.out).toContain("lib/secret-helper.js"); + expect(result.out).toContain("lib/private-key-rotation.js"); + }); + + it("still rejects a non-code file whose name suggests it IS a secret (.pem, secrets.json)", () => { + const pem = runChecker({ CHECK_MINER_PACK_TEST_FILES: JSON.stringify(["my-private-key.pem"]) }); + expect(pem.status).toBe(1); + expect(pem.out).toContain("Forbidden file in miner package: my-private-key.pem"); + + const json = runChecker({ CHECK_MINER_PACK_TEST_FILES: JSON.stringify(["config/secrets.json"]) }); + expect(json.status).toBe(1); + expect(json.out).toContain("Forbidden file in miner package: config/secrets.json"); + }); + + it("still rejects the exact-name dotfiles (.env/.npmrc/.dev.vars) regardless of this change", () => { + for (const dotfile of [".env", ".npmrc", ".dev.vars"]) { + const result = runChecker({ CHECK_MINER_PACK_TEST_FILES: JSON.stringify([dotfile]) }); + expect(result.status).toBe(1); + expect(result.out).toContain(`Forbidden file in miner package: ${dotfile}`); + } + }); + }); + it("rejects a README carrying stale public-package wording (#7013)", () => { const result = runChecker({ CHECK_MINER_PACK_TEST_FILES: JSON.stringify([