Skip to content

feat(electron): one-click xnet://connect desktop deep link (0214 D2) - #249

Merged
crs48 merged 1 commit into
mainfrom
claude/laughing-vaughan-dc3e68
Jun 24, 2026
Merged

feat(electron): one-click xnet://connect desktop deep link (0214 D2)#249
crs48 merged 1 commit into
mainfrom
claude/laughing-vaughan-dc3e68

Conversation

@crs48

@crs48 crs48 commented Jun 24, 2026

Copy link
Copy Markdown
Owner

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://connect link is an open redirect into a native app, so it's gated twice:

  1. Hard validation in the main process — new pure, unit-tested apps/electron/src/main/deep-link.ts: parseConnectDeepLink + isAllowedHubUrl require wss://, an xNet-host allowlist (xnet.fyi/xnet.app + subdomains; env-overridable via XNET_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)://.
  2. Explicit user confirmation in the renderer — new ConnectHubDialog shows the exact hub and never auto-connects; Cancel leaves the current hub untouched.

What's in it

  • Main (index.ts): handleDeepLink routes a validated payload to the renderer over xnet:cloud-connect, with cold-launch pending delivery on did-finish-load. Preload exposes onCloudConnect.
  • Renderer: on confirm, persist the hub via the new hub-url.ts (mirrors the web setPersistedHubUrl; also read on boot in ipc-sync-manager.ts and in Settings → Network), then apply it live via configureShareSession (re-points sync, no restart).
  • Dashboard (dashboard.ts): replaces the "coming soon" note with the xnet://connect button (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.
  • Changelog fragment added.

Remaining manual check (can't be done headless): launch a packaged desktop build and click the button end-to-end.

🤖 Generated with Claude Code

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>
@crs48
crs48 temporarily deployed to pr-249 June 24, 2026 16:29 — with GitHub Actions Inactive
@github-actions

github-actions Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Preview removed for PR #249.

github-actions Bot added a commit that referenced this pull request Jun 24, 2026
@crs48
crs48 merged commit 37f0bf1 into main Jun 24, 2026
10 of 11 checks passed
@crs48
crs48 deleted the claude/laughing-vaughan-dc3e68 branch June 24, 2026 16:35
github-actions Bot added a commit that referenced this pull request Jun 24, 2026
* 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[] {
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants