fix: route every host-browser open through open_url_external

The Rust command existed but nothing called it. All four frontend call
sites still used `openUrl` from `@tauri-apps/plugin-opener`, so the
environment fix was inert and the three dialogs carried the same Linux bug
as the terminal: DockerInstallDialog's docs link, ClaudeAuthModal's sign-in
link and UpdateDialog's release link would all have reported success while
launching nothing.

`openUrlExternal` in tauri-commands.ts is now the single sink. There is no
platform branch: Linux gets the sanitized spawn, macOS and Windows reach
the same plugin as before but from Rust, and every platform picks up the
Rust-side re-validation, which matters because these URLs originate in an
untrusted container.

Comments in urlRelay.ts and urlDetector.ts that named `openUrl` as the sink
they guard are updated to match, and the two test files that mocked
`@tauri-apps/plugin-opener` now mock the command instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-17 10:09:57 -07:00
co-authored by Claude Opus 5
parent 9297020688
commit 5a09254538
9 changed files with 46 additions and 33 deletions
@@ -11,14 +11,12 @@ vi.mock("../../lib/tauri-commands", () => ({
hasClaudeToken: vi.fn(),
clearClaudeToken: vi.fn(),
cancelClaudeToken: (...args: unknown[]) => cancelClaudeToken(...args),
openUrlExternal: (...args: unknown[]) => openUrlExternal(...args),
}));
const cancelClaudeToken = vi.fn(() => Promise.resolve());
const openUrl = vi.fn();
vi.mock("@tauri-apps/plugin-opener", () => ({
openUrl: (...args: unknown[]) => openUrl(...args),
}));
const openUrlExternal = vi.fn();
/** Captured event handlers, keyed by event name, so tests can emit. */
const handlers = new Map<string, (event: { payload: unknown }) => void>();
@@ -174,7 +172,7 @@ describe("ClaudeAuthModal", () => {
const link = await screen.findByRole("link", { name: url });
fireEvent.click(link);
await waitFor(() => expect(openUrl).toHaveBeenCalledWith(url));
await waitFor(() => expect(openUrlExternal).toHaveBeenCalledWith(url));
});
it("ignores output belonging to a different project", async () => {
@@ -259,8 +257,8 @@ describe("ClaudeAuthModal", () => {
const link = await screen.findByRole("link", { name: FULL_URL });
fireEvent.click(link);
await waitFor(() => expect(openUrl).toHaveBeenCalledWith(FULL_URL));
expect(openUrl).not.toHaveBeenCalledWith(TRUNCATED_URL);
await waitFor(() => expect(openUrlExternal).toHaveBeenCalledWith(FULL_URL));
expect(openUrlExternal).not.toHaveBeenCalledWith(TRUNCATED_URL);
});
it("refuses a hyperlink target that is not an Anthropic sign-in address", async () => {
@@ -270,7 +268,7 @@ describe("ClaudeAuthModal", () => {
emitLink("https://evil.tld/cai/oauth/authorize?code=true");
expect(screen.queryByRole("link")).not.toBeInTheDocument();
expect(openUrl).not.toHaveBeenCalled();
expect(openUrlExternal).not.toHaveBeenCalled();
});
it("ignores a hyperlink belonging to a different project", async () => {