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
+15
View File
@@ -398,3 +398,18 @@ export const rollbackMigration = (projectId: string) =>
* app crash shows up here as phase "interrupted". */
export const getMigrationState = (projectId: string) =>
invoke<MigrationState | null>("get_migration_state", { projectId });
/** Open a URL in the user's own browser.
*
* Replaces `openUrl` from `@tauri-apps/plugin-opener` at every call site. On
* Linux the app ships as an AppImage whose environment leaks into everything
* it spawns, which kills a *cold-launched* browser before it paints while
* `xdg-open` still exits 0 — so the plugin path reported success and did
* nothing (triple-c#34). The Rust side hands the child a repaired environment
* and re-validates the URL, which matters because these URLs originate in an
* untrusted container. macOS and Windows still reach the plugin, just from
* Rust, so there is no platform branch here.
*
* Rejects with a string already phrased for a toast. */
export const openUrlExternal = (url: string) =>
invoke<void>("open_url_external", { url });
+3 -2
View File
@@ -109,7 +109,8 @@ export type UrlCallback = (url: string, source: UrlSource) => void;
* A direct port of `usable_sign_in_link` in
* `commands/auth_token_commands.rs`, and deliberately just as shallow: this is
* a junk filter, not the security decision. `sanitizeRelayUrl` is still the
* only thing standing between any of this and `openUrl`, and duplicating its
* only thing standing between any of this and `openUrlExternal`, and
* duplicating its
* rules here would be a second place for them to go stale.
*
* The one rule from the Rust that is not ported is its `sk-ant-` check: that
@@ -293,7 +294,7 @@ export class UrlDetector {
// include the *whole* C0 range and DEL, not just BEL: an escape or a NUL
// swallowed into the middle of a match becomes a URL that renders as one
// thing in the toast and resolves as another. Everything emitted here is
// still re-validated by `sanitizeRelayUrl` before it can reach `openUrl`;
// still re-validated by `sanitizeRelayUrl` before it can reach the opener;
// stopping the match early only means the legitimate prefix survives
// instead of the whole candidate being thrown away.
// eslint-disable-next-line no-control-regex
+2 -1
View File
@@ -1,6 +1,7 @@
/**
* URL relay — host side of `container/triple-c-open` — and the single URL
* validator every `openUrl` call site in the app is required to go through.
* validator every `openUrlExternal` call site in the app is required to go
* through.
*
* A CLI inside the container has no browser. When it wants to open a URL
* (`gh auth login`, `aws sso login`, `gcloud auth login`, anything honouring