feat(electron): one-click xnet://connect desktop deep link (0214 D2) - #249
Merged
Conversation
Fast-follow to the guided-connect cloud dashboard (#247): the dashboard's Desktop tab now offers an "Open in desktop app" button that hands the hub to the Electron app over `xnet://connect?hub=<wss>`, replacing the copy-paste-only flow (the copy-paste fallback stays). Security is the crux — an xnet://connect link is an open redirect into a native app, so it's gated twice: - Hard validation in the main process (new pure, unit-tested apps/electron/src/main/deep-link.ts): wss-only, xNet-host allowlist (env-overridable, mirrors the share-endpoint policy), reject embedded credentials, length-bounded. Wired into handleDeepLink + pending delivery. - Explicit user confirmation in the renderer (new ConnectHubDialog) — never auto-connects; Cancel leaves the current hub untouched. On confirm it persists the hub via a new renderer hub-url lib (mirrors the web setPersistedHubUrl; also read on boot and in Settings -> Network) and applies it live via configureShareSession. The dashboard only emits the button for an allowlisted xNet hub (https->wss normalized), so staging (*.run.app) cleanly falls back to copy-paste. Full in-app device-claim parity stays as D3. Tests: deep-link (11), hub-url (4), dashboard (+3). Changelog fragment added. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Contributor
|
Preview removed for PR #249. |
| * Returns the payload to confirm in the renderer, or null if the URL is not a | ||
| * well-formed, allowlisted connect link (the caller treats null as "ignore"). | ||
| */ | ||
| export function parseConnectDeepLink(rawUrl: string): CloudConnectPayload | null { |
| * parse failure, wrong scheme, or off-allowlist host returns false — we never | ||
| * "best effort" a hub the user didn't vet. | ||
| */ | ||
| export function isAllowedHubUrl(raw: string): boolean { |
| } | ||
| } | ||
|
|
||
| export function ConnectHubDialog({ request, onCancel, onConfirm }: ConnectHubDialogProps) { |
| * staging/self-host via `XNET_ALLOWED_HUB_HOSTS` (comma-separated bare hosts), | ||
| * mirroring the share-endpoint policy (`XNET_ALLOWED_SHARE_ENDPOINTS`). | ||
| */ | ||
| function allowedHubHosts(): string[] { |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fast-follow to the guided-connect cloud dashboard (#247) — item D2 from exploration 0214. Turns the dashboard's Desktop "paste this URL into Settings" flow into one click: "Open in desktop app" →
xnet://connect?hub=<wss>→ the Electron app pre-fills + connects after you confirm.Security (the crux)
An
xnet://connectlink is an open redirect into a native app, so it's gated twice:apps/electron/src/main/deep-link.ts:parseConnectDeepLink+isAllowedHubUrlrequirewss://, an xNet-host allowlist (xnet.fyi/xnet.app+ subdomains; env-overridable viaXNET_ALLOWED_HUB_HOSTS, mirroring the existing share-endpoint policy), reject embedded credentials, and bound the length. Rejects lookalikes (evilxnet.fyi,xnet.fyi.evil.com),ws://,http(s)://.ConnectHubDialogshows the exact hub and never auto-connects; Cancel leaves the current hub untouched.What's in it
handleDeepLinkroutes a validated payload to the renderer overxnet:cloud-connect, with cold-launchpendingdelivery ondid-finish-load. Preload exposesonCloudConnect.hub-url.ts(mirrors the websetPersistedHubUrl; also read on boot inipc-sync-manager.tsand in Settings → Network), then apply it live viaconfigureShareSession(re-points sync, no restart).xnet://connectbutton (https→wss normalized, URL-encoded), keeping the copy-paste fallback. The button is emitted only for an allowlisted xNet hub, so staging (*.run.app) cleanly shows copy-paste instead of a dead button.Full in-app device-claim parity (the dual-proof binding) stays as the D3 follow-up — passkey + code approval still finish in the dashboard.
Tests
deep-link.test.ts(11),hub-url.test.ts(4),dashboard.test.ts(+3 → 26). Full cloud unit suite (205) green; eslint clean; prettier clean.Remaining manual check (can't be done headless): launch a packaged desktop build and click the button end-to-end.
🤖 Generated with Claude Code