Secret Scan / scan (push) Successful in 6s
Build App (Preview) / compute-version (pull_request) Successful in 5s
Secret Scan / scan (pull_request) Successful in 5s
Build App (Preview) / create-release (pull_request) Successful in 2s
Build App (Preview) / build-macos (pull_request) Successful in 2m41s
Build App (Preview) / build-windows (pull_request) Successful in 4m53s
Build App (Preview) / build-linux (pull_request) Successful in 7m5s
Build App (Preview) / prune-previews (pull_request) Successful in 1s
Round 4 review findings: - Disclose and warn on a custom Docker image the import would set (HIGH): it's the image every project container is created from, so an undisclosed change here was a sharper version of the redirected-base-URL problem round 3 already flagged for the model backends. - Recreate a running gateway container when an import restores a new secret with the shape unchanged (MEDIUM): reconcile_gateway's shape comparison can't see a secret-only change, so the container would otherwise keep serving old key material indefinitely. - Report keychain write failures back to the caller instead of only logging them (MEDIUM): apply_settings_import now returns SettingsImportOutcome with secret_restore_warnings so a partial restore can't read as unqualified success. - Pin a hash of the previewed file's ciphertext and refuse to apply if it changed on disk (MEDIUM): closes a TOCTOU between preview and apply. - Sanitize and cap every free-form string a preview surfaces, and move the warning boxes above the replace list in the UI (MEDIUM): an unbounded base URL or image name could otherwise push the security warnings below the scroll fold. - Validate the Docker socket path on import the same as the SSH key and CA cert paths (LOW): it was the one mounted host path validate_settings_update didn't cover. - Fix ExportedSecrets::is_empty() to treat whitespace-only as blank, like every other secret-presence check in this feature (LOW). - Authenticate the file header as AEAD associated data (LOW, defense in depth) and correct two doc comments that overstated the password not being cached.
126 lines
5.0 KiB
TypeScript
126 lines
5.0 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import { describeImport, describeImportWarnings } from "./settingsImportPreview";
|
|
import type { SettingsImportPreview } from "./types";
|
|
|
|
function preview(overrides: Partial<SettingsImportPreview> = {}): SettingsImportPreview {
|
|
return {
|
|
exported_at: "2026-08-27T00:00:00Z",
|
|
app_version: "0.4.14",
|
|
custom_env_var_count: 0,
|
|
gateway_model_count: 0,
|
|
has_claude_code_settings: false,
|
|
has_claude_oauth_token: false,
|
|
has_gateway_api_key: false,
|
|
has_gateway_master_key: false,
|
|
has_web_terminal_access_token: false,
|
|
enables_web_terminal: false,
|
|
ollama_base_url: null,
|
|
llamacpp_base_url: null,
|
|
openai_compatible_base_url: null,
|
|
gateway_api_base: null,
|
|
image_source: "registry",
|
|
custom_image_name: null,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe("describeImport", () => {
|
|
it("always names the settings replacement, even with nothing else set", () => {
|
|
expect(describeImport(preview())).toEqual([
|
|
"Your global settings (all of them — this replaces what's here now)",
|
|
]);
|
|
});
|
|
|
|
it("singularizes a count of exactly one", () => {
|
|
const items = describeImport(preview({ custom_env_var_count: 1, gateway_model_count: 1 }));
|
|
expect(items).toContain("1 global custom env var");
|
|
expect(items).toContain("1 gateway model");
|
|
});
|
|
|
|
it("pluralizes counts greater than one", () => {
|
|
const items = describeImport(preview({ custom_env_var_count: 3, gateway_model_count: 2 }));
|
|
expect(items).toContain("3 global custom env vars");
|
|
expect(items).toContain("2 gateway models");
|
|
});
|
|
|
|
it("names every present secret and setting without naming absent ones", () => {
|
|
const items = describeImport(
|
|
preview({
|
|
has_claude_code_settings: true,
|
|
has_claude_oauth_token: true,
|
|
has_gateway_api_key: true,
|
|
has_gateway_master_key: true,
|
|
}),
|
|
);
|
|
expect(items).toContain("Global Claude Code settings");
|
|
expect(items).toContain("Your shared Claude login");
|
|
expect(items).toContain("The gateway provider API key");
|
|
expect(items).toContain("The gateway master key");
|
|
// None of the count-based items, since both counts are 0.
|
|
expect(items.some((i) => i.includes("env var"))).toBe(false);
|
|
expect(items.some((i) => i.includes("gateway model"))).toBe(false);
|
|
});
|
|
|
|
it("names the web terminal access token like any other present secret", () => {
|
|
const items = describeImport(preview({ has_web_terminal_access_token: true }));
|
|
expect(items).toContain("The web terminal access token");
|
|
});
|
|
|
|
it("names custom base URLs verbatim, since they're endpoints rather than secrets", () => {
|
|
const items = describeImport(
|
|
preview({
|
|
ollama_base_url: "http://10.0.0.5:11434",
|
|
gateway_api_base: "https://gateway.example/v1",
|
|
}),
|
|
);
|
|
expect(items).toContain("Ollama server: http://10.0.0.5:11434");
|
|
expect(items).toContain("Gateway upstream: https://gateway.example/v1");
|
|
expect(items.some((i) => i.includes("llama.cpp"))).toBe(false);
|
|
expect(items.some((i) => i.includes("OpenAI-compatible"))).toBe(false);
|
|
});
|
|
|
|
it("names a custom Docker image when set, falling back to a placeholder if unnamed", () => {
|
|
expect(
|
|
describeImport(preview({ image_source: "custom", custom_image_name: "ghcr.io/me/triple-c" })),
|
|
).toContain("Docker image: ghcr.io/me/triple-c");
|
|
expect(describeImport(preview({ image_source: "custom", custom_image_name: null }))).toContain(
|
|
"Docker image: (no image name set)",
|
|
);
|
|
expect(describeImport(preview({ image_source: "registry" })).some((i) => i.includes("Docker image"))).toBe(
|
|
false,
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("describeImportWarnings", () => {
|
|
it("is empty when nothing about the import needs extra attention", () => {
|
|
expect(describeImportWarnings(preview())).toEqual([]);
|
|
});
|
|
|
|
it("warns when the import enables the web terminal, regardless of the token", () => {
|
|
// `enabled` and the token are independent — the warning is about the
|
|
// service turning on, whether or not a token came with it.
|
|
expect(describeImportWarnings(preview({ enables_web_terminal: true }))).toEqual([
|
|
"Enables the remote web terminal, which listens on your network.",
|
|
]);
|
|
expect(
|
|
describeImportWarnings(
|
|
preview({ enables_web_terminal: true, has_web_terminal_access_token: true }),
|
|
),
|
|
).toHaveLength(1);
|
|
});
|
|
|
|
it("warns about a dormant web terminal token even while the terminal stays off", () => {
|
|
expect(describeImportWarnings(preview({ has_web_terminal_access_token: true }))).toEqual([
|
|
"Includes a web terminal access token that will activate the next time the web terminal is turned on.",
|
|
]);
|
|
});
|
|
|
|
it("warns about a custom Docker image every time, not only when it changes", () => {
|
|
expect(
|
|
describeImportWarnings(preview({ image_source: "custom", custom_image_name: "evil:latest" })),
|
|
).toEqual(["Runs every project container from a custom Docker image: evil:latest."]);
|
|
expect(describeImportWarnings(preview({ image_source: "registry" }))).toEqual([]);
|
|
});
|
|
});
|