diff --git a/src/shared/config.ts b/src/shared/config.ts index 7073f58..fde4fb0 100644 --- a/src/shared/config.ts +++ b/src/shared/config.ts @@ -29,9 +29,11 @@ export function replaceVariables( // Check if this pattern actually exists in the string if (result.match(pattern)) { if (Array.isArray(replacement)) { + // Log the key only — never the value. user_config options can be + // sensitive (API keys, passwords), so the substituted value and the + // replacement array must not be written to the console. console.warn( - `Cannot replace ${key} with array value in string context: "${value}"`, - { key, replacement }, + `Cannot replace "${key}" with an array value in string context`, ); } else { result = result.replace(pattern, replacement); diff --git a/src/shared/manifestVersionResolve.ts b/src/shared/manifestVersionResolve.ts index 5bc6381..55f05a2 100644 --- a/src/shared/manifestVersionResolve.ts +++ b/src/shared/manifestVersionResolve.ts @@ -1,24 +1,31 @@ import { MANIFEST_SCHEMAS } from "./constants.js"; export function getManifestVersionFromRawData(manifestData: unknown) { - let manifestVersion: keyof typeof MANIFEST_SCHEMAS | null = null; + if (typeof manifestData !== "object" || !manifestData) { + return null; + } + + // manifest_version is authoritative when present. If it is present but + // unsupported, do NOT silently fall back to the deprecated dxt_version — the + // manifest has declared its version, so an unsupported value is a resolution + // failure (null), not a reason to resolve via the deprecated field. if ( - typeof manifestData === "object" && - manifestData && "manifest_version" in manifestData && - typeof manifestData.manifest_version === "string" && - Object.keys(MANIFEST_SCHEMAS).includes(manifestData.manifest_version) + typeof manifestData.manifest_version === "string" ) { - manifestVersion = - manifestData.manifest_version as keyof typeof MANIFEST_SCHEMAS; - } else if ( - typeof manifestData === "object" && - manifestData && + return Object.keys(MANIFEST_SCHEMAS).includes(manifestData.manifest_version) + ? (manifestData.manifest_version as keyof typeof MANIFEST_SCHEMAS) + : null; + } + + // Fall back to the deprecated dxt_version only when manifest_version is absent. + if ( "dxt_version" in manifestData && typeof manifestData.dxt_version === "string" && Object.keys(MANIFEST_SCHEMAS).includes(manifestData.dxt_version) ) { - manifestVersion = manifestData.dxt_version as keyof typeof MANIFEST_SCHEMAS; + return manifestData.dxt_version as keyof typeof MANIFEST_SCHEMAS; } - return manifestVersion; + + return null; } diff --git a/test/config.test.ts b/test/config.test.ts index 7e29f69..1f856fc 100644 --- a/test/config.test.ts +++ b/test/config.test.ts @@ -29,6 +29,28 @@ describe("replaceVariables", () => { consoleWarnSpy.mockRestore(); }); + it("should not log the array value when warning (no secret leakage)", () => { + const consoleWarnSpy = jest.spyOn(console, "warn").mockImplementation(); + replaceVariables("token=${user_config.api_keys}", { + "user_config.api_keys": ["super-secret-1", "super-secret-2"], + }); + expect(consoleWarnSpy).toHaveBeenCalled(); + // The warning must reference the key but never the sensitive values. + // Serialize each argument (including objects) so a value logged inside an + // object — e.g. console.warn(msg, { replacement }) — is also inspected. + const logged = consoleWarnSpy.mock.calls + .map((call) => + call + .map((arg) => (typeof arg === "string" ? arg : JSON.stringify(arg))) + .join(" "), + ) + .join(" "); + expect(logged).toContain("user_config.api_keys"); + expect(logged).not.toContain("super-secret-1"); + expect(logged).not.toContain("super-secret-2"); + consoleWarnSpy.mockRestore(); + }); + it("should replace variables in objects", () => { const result = replaceVariables( { message: "Hello ${name}!" }, diff --git a/test/manifestVersionResolve.test.ts b/test/manifestVersionResolve.test.ts new file mode 100644 index 0000000..6a2a896 --- /dev/null +++ b/test/manifestVersionResolve.test.ts @@ -0,0 +1,29 @@ +import { getManifestVersionFromRawData } from "../src/shared/manifestVersionResolve"; + +describe("getManifestVersionFromRawData", () => { + it("resolves a supported manifest_version", () => { + expect(getManifestVersionFromRawData({ manifest_version: "0.3" })).toBe( + "0.3", + ); + }); + + it("falls back to dxt_version when manifest_version is absent", () => { + expect(getManifestVersionFromRawData({ dxt_version: "0.3" })).toBe("0.3"); + }); + + it("does not fall back to dxt_version when manifest_version is present but unsupported", () => { + // manifest_version is authoritative; an unsupported value must not silently + // resolve via the deprecated dxt_version field. + expect( + getManifestVersionFromRawData({ + manifest_version: "99.0", + dxt_version: "0.3", + }), + ).toBeNull(); + }); + + it("returns null for non-object input", () => { + expect(getManifestVersionFromRawData(null)).toBeNull(); + expect(getManifestVersionFromRawData("0.3")).toBeNull(); + }); +});