Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions src/shared/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
31 changes: 19 additions & 12 deletions src/shared/manifestVersionResolve.ts
Original file line number Diff line number Diff line change
@@ -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;
}
22 changes: 22 additions & 0 deletions test/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}!" },
Expand Down
29 changes: 29 additions & 0 deletions test/manifestVersionResolve.test.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});