Skip to content

feat(mobile): verified https pairing links; custom scheme carries no secret - #27475

Merged
OrcaWin merged 9 commits into
mainfrom
np/d2-verified-links
Oct 11, 2026
Merged

OrcaWin merged 9 commits into
mainfrom
np/d2-verified-links

Conversation

@OrcaWin

@OrcaWin OrcaWin commented Oct 11, 2026 •

Copy link
Copy Markdown
Collaborator
Files Added Deleted Net
Test 10 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​456 $\color{#cf222e}{\Huge{\mathbf{−}}}$​3 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​453
Prod 21 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​526 $\color{#cf222e}{\Huge{\mathbf{−}}}$​131 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​395

ELI5

Today the phone pairing secret travels in an orca://pair?code=… link. Any app on a phone can register itself for orca://, so a link someone taps could hand the secret to the wrong app. Now the desktop gives out an ordinary web link, https://www.onorca.dev/pair#<code>. iOS and Android only open that link in the app that onorca.dev vouches for. The phone app also stops accepting a secret from an orca:// link at all.

What Changed

Before: "Copy pairing code" in Settings → Mobile (and the mobile page) copied orca://pair?code=<secret>. Tapping any orca://pair?code=… or orca://pair-confirm?code=… link on a phone opened Orca Mobile's "Pair with this desktop?" screen, whichever app had actually claimed the scheme.

After:

  • Desktop: the copy button gives https://www.onorca.dev/pair#<code>. The code is only in the part after #, which browsers never send to the server. The QR code is unchanged (see compatibility below). orca serve --mobile-pairing also prints a Phone pairing link: line. The Pairing URL: line and the JSON readiness output are unchanged.
  • Phone, links opened by the OS: a new app/+native-intent.ts rewrites every incoming link before the router sees it.
    • The verified https link goes to the confirm screen.
    • Any orca:// link (or unverified https link) that targets a pairing screen with a code goes to the scanner, with a note: "That orca:// link can't pair your phone, because any app can open orca:// links. Scan the QR code on your computer instead."
    • A bare orca://pair just opens the scanner.
    • The old root-layout listener and app/pair.tsx, which pulled codes out of orca:// links, are gone. app/pair.tsx is now a plain redirect to the scanner.
  • Phone, in-app scan and paste: still accept orca://pair?code=, the new https link, and a bare code. These never pass through the OS link dispatcher, so another app can't intercept them.
  • Confirm screen: before you tap Pair, it shows the desktop's address (the same string printed under the QR on the desktop) and a short host-key fingerprint. A verified link only proves which app opened it, not which desktop it points to.
  • App config: Android gets an autoVerify intent filter for exactly https://www.onorca.dev/pair. iOS gets applinks:www.onorca.dev, but only when the release sets ORCA_IOS_ASSOCIATED_DOMAINS=1 (see Notes).
  • Site files: config/verified-links/ holds apple-app-site-association, assetlinks.json, and a pair.html fallback page, plus a README that says exactly what to deploy and how. A test keeps these files, mobile/app.config.js, and the link builder (src/shared/verified-pairing-link.ts) in agreement.

Mixed versions

Desktop Phone What happens
old new The QR works in the in-app scanner, and a pasted orca:// code works. Tapping the old orca://pair?code= link is refused, and the scanner opens with the note telling the user to scan the QR.
new old The QR still encodes orca://pair?code=, so the old in-app scanner works. Tapping the https link opens the browser on /pair, where the fallback page explains what to do and can copy the bare code. Old apps accept a bare code in Paste. Pasting the https link itself into an old app fails with "Not a valid pairing code".
new new All paths work. A tapped https link opens the app once the site files are deployed (iOS also needs the opt-in variable).

Why

  • Fragment, not query. A query string reaches the server, its logs, and any proxy along the way. A fragment stays on the device.
  • www.onorca.dev, not the apex. onorca.dev answers with a 308 redirect to www, and neither OS follows a redirect while verifying a domain.
  • Rewrite in +native-intent, not in a screen. This runs before routing, so no screen can be reached from an orca:// link with a code. That covers orca://pair-confirm?code=… too, which routed straight to the confirm screen before.
  • QR stays orca:// for now. Every shipped phone's in-app scanner only understands that form. An in-app scan never goes through the OS, so it can't be intercepted. The QR can flip together with the ticket work in the follow-up.
  • iOS entitlement is opt-in. The App Store archive is signed manually with a profile fetched from Apple. If the entitlement is added before the App ID has the Associated Domains capability, the iOS release fails to sign. With the variable, the release keeps working until the owner flips it.

Linked Issue

Part of #17397. That issue closes once single-use tickets with owner approval have also shipped.

Visual Proof

N/A for screenshots. Mobile UI changes are text-only: a note on the scanner screen, and an address plus host-key box on the confirm screen. Verified with unit tests. No device or simulator run was done (no simulator on this machine).

Testing

  • pnpm tc, pnpm run check:code-quality:changed, and every pnpm lint step. The native-plugins step reports import/no-cycle warnings in mobile/src/transport/client-context.tsx and mobile/src/terminal/document/*; those files are untouched here and the warnings are already on main.
  • Root: src/shared/verified-pairing-link.test.ts, src/shared/verified-pairing-link-files.test.ts, src/shared/pairing.test.ts, src/main/ipc/mobile.test.ts, src/main/ipc/mobile-pairing-copy-link.test.ts, src/main/server/serve-readiness.test.ts, plus the renderer mobile/settings suites.
  • Mobile: pnpm typecheck, check:tests-typecheck, oxlint, and the full vitest run. One failure, mobile-e2ee-v2-client-session "bounded binary string conversions", is environmental: local Node 26 has a native base64 encoder, so btoa is never called. It is unrelated to this change.
  • I manually tested these changes locally
  • Automated tests added/updated, or explained why not below

Review

Agent skill upstream boundary

  • Not applicable, or this change copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

Owner deploy steps (outside this repo). These must happen before a desktop release that ships this; until then the copied https link opens a 404:

  1. Serve config/verified-links/apple-app-site-association at https://www.onorca.dev/.well-known/apple-app-site-association and config/verified-links/assetlinks.json at https://www.onorca.dev/.well-known/assetlinks.json. Both need Content-Type: application/json, a direct 200 with no redirect, and no auth or bot challenge.
  2. Serve config/verified-links/pair.html at https://www.onorca.dev/pair, with no third-party scripts.
  3. Fill in the placeholders: the Apple Team ID in the AASA file, and in assetlinks.json the SHA-256 of the key that actually signs the shipped APK (the Play app signing key, if the app goes to Play).
  4. iOS: enable Associated Domains on the App ID com.stably.orca.mobile, then set the repository variable ORCA_IOS_ASSOCIATED_DOMAINS=1.

Finding: the direct-download APK from mobile-android-release.yml is signed with the Expo template's public debug keystore (FA:C6:17:…), because no release signing is configured. That fingerprint must not go in assetlinks.json, and the test refuses it. Android link verification needs a real private release key.

Security: no secret ever travels in a query string or through the custom scheme into the app. Cross-platform: desktop changes are string-only. SSH/remote: unaffected; the runtime (desktop and CLI) pairing codes and their parsers still accept orca://, and now also accept the https form.

Checklist

  • This PR is small and focused
  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred)

@coderabbitai

coderabbitai Bot commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Important

Review skipped

Review was skipped as selected files did not have any reviewable changes.

⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: cdf8a486-fecf-497c-9919-b0b33971069b

📥 Commits

Reviewing files that changed from the base of the PR and between 1b1f0eeedb4103d551735c11f67307572f5fa64c and 82c3703.


You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough
📝 Walkthrough
📝 Walkthrough
📝 Walkthrough

Walkthrough

The change adds verified HTTPS pairing links with pairing codes in URL fragments. It adds deployment files and platform configuration for Android and opt-in iOS link handling. Desktop pairing output now provides verified phone links. Mobile routing parses incoming links, routes accepted links to confirmation, and shows refusal notices for certain links. The confirmation screen displays the pairing endpoint and, when available, its host-key fingerprint.








Priority: ⬇️ Low

Merge Risk: 🟡 Moderate · up to 1b1f0

Resolve the Android link-verification and code-bearing link concerns before merging. Also correct the scanner notice so it accurately describes refused HTTPS links.

Pre-merge checks | Passed 4 | Failed 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 28 functions across 26 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Title check Passed The title clearly and concisely describes the main change: verified HTTPS pairing links with secrets removed from the custom scheme.
Description check Passed The description is complete and follows the repository template. It explains the user-visible changes, rationale, compatibility behavior, testing, deployment prerequisites, security considerations, vi…



✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR







  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 52e15619-d70b-4354-8009-525e6e0e79cb
📥 Commits

Reviewing files that changed from the base of the PR and between d3417b2 and a564892.

📒 Files selected for processing (30)
  • .github/workflows/mobile-ios-release.yml
  • config/verified-links/README.md
  • config/verified-links/apple-app-site-association
  • config/verified-links/assetlinks.json
  • config/verified-links/pair.html
  • mobile/app.config.js
  • mobile/app/+native-intent.ts
  • mobile/app/_layout.tsx
  • mobile/app/pair-confirm.tsx
  • mobile/app/pair-scan.tsx
  • mobile/app/pair.tsx
  • mobile/src/pair-scan-styles.ts
  • mobile/src/transport/incoming-pairing-link.test.ts
  • mobile/src/transport/incoming-pairing-link.ts
  • mobile/src/transport/pair-confirm-state.test.ts
  • mobile/src/transport/pair-confirm-state.ts
  • mobile/src/transport/pairing.test.ts
  • mobile/src/transport/pairing.ts
  • src/main/ipc/mobile-pairing-copy-link.test.ts
  • src/main/ipc/mobile.test.ts
  • src/main/ipc/mobile.ts
  • src/main/server/serve-readiness.test.ts
  • src/main/server/serve-readiness.ts
  • src/preload/api/mobile-api.ts
  • src/preload/api/mobile-bridge.ts
  • src/shared/pairing.test.ts
  • src/shared/pairing.ts
  • src/shared/verified-pairing-link-files.test.ts
  • src/shared/verified-pairing-link.test.ts
  • src/shared/verified-pairing-link.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 3 remain after this review.

Comment on lines +43 to +45
The direct-download APK built by `.github/workflows/mobile-android-release.yml` is currently signed
with the build template's public debug keystore. Do not list that fingerprint (`FA:C6:17:45:…`): it
is public, so any app signed with it could claim the links.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Use a private signing key for direct-download APKs before relying on App Links.

The documented direct-download APK uses the public debug key, but assetlinks.json must not authorize that key. Android therefore cannot verify pairing links for that APK; a user who taps the new phone link reaches the browser fallback instead of the app. Sign this distribution with a private release key and publish its fingerprint before enabling the link flow for it. (developer.android.com)

🧰 Tools
🪛 LanguageTool

[uncategorized] ~43-~43: The official name of this software platform is spelled with a capital “H”.
Context: ...oads. The direct-download APK built by .github/workflows/mobile-android-release.yml i...

(GITHUB)

Comment thread mobile/app/pair-scan.tsx
Comment on lines +44 to +45
That orca:// link can’t pair your phone, because any app can open orca:// links. Scan the QR
code on your computer instead.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Explain both types of refused link.

If a user opens https://evil.example/pair#abc123, the router shows this notice. The notice calls that HTTPS link an orca:// link. Use wording that covers unverified HTTPS links and custom-scheme links, so the refusal gives the correct reason.

@@ -119,6 +120,12 @@ function renderHumanReadiness(readiness: ServeReadiness): string {
lines.push(`Mobile pairing QR:\n${readiness.pairing.qr}`)
}
lines.push(`Pairing URL: ${readiness.pairing.url}`)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Do not print the custom-scheme URL as a mobile pairing link.

When readiness.pairing.scope is mobile, this line still prints orca://pair with the pairing code before the new HTTPS phone link. A user can tap or share the first link, which still exposes the code to an app that claimed the custom scheme. Print only the verified phone link for mobile scope; retain the existing URL where another scope requires it.

@OrcaWin
OrcaWin force-pushed the np/d2-verified-links branch from f188cd2 to bc897e8 Compare October 11, 2026 15:04

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 0ce561f7-64c2-4179-8534-41c5d4ed9dd3
📥 Commits

Reviewing files that changed from the base of the PR and between 2d3a7b98288b057f3f56285a90b28419a51c6c6a and 1b1f0eeedb4103d551735c11f67307572f5fa64c.

📒 Files selected for processing (3)
  • mobile/src/transport/incoming-pairing-link-router-agreement.test.ts
  • mobile/src/transport/incoming-pairing-link.test.ts
  • mobile/src/transport/incoming-pairing-link.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 3 remain after this review.

[...target.searchParams.keys()].length > 0 ||
target.hash.length > 1 ||
(outer?.hash.length ?? 0) > 1
return carriesPayload ? REFUSED_PATH : '/pair-scan'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use an accurate notice for refused HTTPS links.

When a user opens https://evil.example/pair#abc123, this branch sends them to the scanner with PAIR_SCAN_LINK_REFUSED_NOTICE. The scanner then says “That orca:// link can’t pair your phone,” although the refused link was HTTPS. Use a separate HTTPS notice or wording that applies to both link types. (raw.githubusercontent.com)

m4air and others added 9 commits October 11, 2026 09:15
…secret

The desktop's copy-link now emits https://www.onorca.dev/pair#<code>, with the
code only in the fragment. Orca Mobile claims that link as a verified app link
and refuses any pairing code delivered through orca://, which any app can claim;
orca://pair alone opens the scanner with an explanation. In-app QR scan and
paste still accept the custom scheme, so older desktops and phones keep pairing.

The confirm screen shows the desktop address and host key fingerprint before
pairing. config/verified-links/ holds the apple-app-site-association,
assetlinks.json and fallback page to deploy on www.onorca.dev, with a test that
keeps them, mobile/app.config.js and the link builder in agreement.

Part of #17397.

Co-Authored-By: Claude <noreply@anthropic.com>
…rified links

The router unwraps expo-development-client ?url= links and normalizes dot
segments after +native-intent runs, so an orca:// wrapper could still deliver a
code to the confirm screen. Refuse any unverified link carrying code= at any
encoding depth, and match pairing routes on decoded, dot-free segments.

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
…ng codes

An invalid UTF-8 escape made the whole escape run fail to decode, hiding an
adjacent encoded &code= that the router still decodes leniently.

Co-Authored-By: Claude <noreply@anthropic.com>
… codes

Co-Authored-By: Claude <noreply@anthropic.com>
…ing codes

Co-Authored-By: Claude <noreply@anthropic.com>
Stop re-implementing URL decoding. The guard now runs expo-router's own
extractExpoPathFromURL and parseUrlUsingCustomBase, the functions its linking
applies after +native-intent, and refuses any link from which the router would
read a `code` param or reach a pairing route with a payload. A link the router
cannot parse is refused. Release builds refuse expo-development-client
wrappers outright. A property-style test walks wrapping depths, encoding
layers, stripped control characters, case and unicode, and asserts that the
guard never passes on a link from which the router would read a code.

Co-Authored-By: Claude <noreply@anthropic.com>
A host link with a segment named pair was sent to the scanner.

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
@OrcaWin
OrcaWin force-pushed the np/d2-verified-links branch from 1b1f0ee to 82c3703 Compare October 11, 2026 16:15
@OrcaWin
OrcaWin merged commit 20f8cb0 into main Oct 11, 2026
39 checks passed
OrcaWin added a commit that referenced this pull request Oct 12, 2026
…ries no secret (#27475)" (#27693)

This reverts commit 20f8cb0.

Co-authored-by: m4air <m4air@Mac.localdomain>
brennanb2025 pushed a commit that referenced this pull request Oct 12, 2026
…ries no secret (#27475)" (#27693)

This reverts commit 20f8cb0.

Co-authored-by: m4air <m4air@Mac.localdomain>
(cherry picked from commit cf58a13)
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.

1 participant