Skip to content

fix(relay): name the PC in the pairing link without a code - #2868

Merged
devswha merged 5 commits into
mainfrom
lane/issue-2599
Nov 5, 2026
Merged

devswha merged 5 commits into
mainfrom
lane/issue-2599

Conversation

@devswha

@devswha devswha commented Nov 5, 2026

Copy link
Copy Markdown
Owner

Part of #2599 (item 2, for the pairing link added after #2759).

Problem

#2759 made RelayService.offer() (POST /api/relay/offer, the QR code) carry the PC's name, so the phone app lists a relay PC under its name instead of its scope URL. The code-less pairing link added later in #2696 (RelayService.pairingLink(), POST /api/relay/link, Copy a link without a code) still builds its offer without name. A phone that pairs from that link saves the PC as https://app.ovis.sh/s/<32 hex>/ again, which is the gap #2599 item 2 describes.

Fix

Both offers take the name from one helper, offerName() in server/relay/service.ts: os.hostname() (the name Ovis lists the PC under, as in server/machines.ts), cut to the 64 characters RelayOffer allows, left out when empty. The name rides in the #offer= fragment, so it never reaches app.ovis.sh or the relay. The link holds no code either way. Its name is no more secret than the one the QR offer already carries.

The code-less link has only shipped in betas (v0.4.23-beta.6), so this PR extends its unreleased Added fragment (changelog.d/relay-pair-by-comparing-digits.md) instead of adding a Fixed line for a feature the next release introduces.

State of the other #2599 items on main (cbcb961)

  • Item 1 (open app.ovis.sh/s/ links in the native apps) is still open. It needs an Android App Link (autoVerify intent-filter plus /.well-known/assetlinks.json with the Play signing key's SHA-256) and an iOS Universal Link (Associated Domains plus apple-app-site-association with the Team ID). That takes signing details and an Apple capability change from the maintainer, so this PR does not close the issue.
  • Item 3 (file path in the app.ovis.sh page address) was fixed by fix(relay): keep a PC's file paths and panes out of app.ovis.sh requests #2605: readerHref() goes through pageAddress(), which puts the query in the fragment in relay mode.

Validation

  • New test names the PC in the pairing link without a code as Ovis lists it (server/relay/service.contract.test.ts) failed on main (Expected: "<hostname>", Received: undefined) and passes with the fix.
  • bun test server/relay/service.contract.test.ts server/relay/pair-sas.contract.test.ts server/dispatch.test.ts: 67 pass, 0 fail.
  • bun run check fast ran on the code and test change: all steps ok (unit: 11427 pass, 0 fail, 45 skip). The changelog fragment was edited afterwards and checked with bun test scripts/changelog-fold.test.ts (12 pass).
  • Skipped: bun run check full, browser and native-app runs. The change is server-side only, in an offer the existing client paths (relayLink() in app/web/shell.js, src/lib/relay/shell.tsx) already read name from.

🤖 Generated with Claude Code

The code-less pairing link (POST /api/relay/link) built its offer without
`name`, so a phone that paired from it listed the PC by its raw scope URL
(https://app.ovis.sh/s/<id>/), as #2599 item 2 described for the coded offer
before #2759. Both offers now take the name from one helper.

Part of #2599.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Nov 5, 2026 •

Copy link
Copy Markdown

Explain Change →

Caution

Review failed

The pull request is closed.

  • 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

Both relay pairing links now hold the PC's name (the QR offer since #2759,
the code-less link with this PR); docs/relay.md listed their contents
without it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@devswha

devswha commented Nov 5, 2026

Copy link
Copy Markdown
Owner Author

Review: Codex (gpt-6.1-terra, xhigh, read-only) at 110e0ae

MEDIUM: the code-less link now carries the host name, while docs/relay.md said it holds the PC's server_id, public key and relay only. The URL fragment keeps the name from app.ovis.sh and the relay, but not from whatever channel the link is sent through (a messenger, a support ticket).

LOW (not changed): a phone that paired from a code-less link before this change keeps its unnamed entry. Opening a newer, named link reuses the saved device record (shared/relay-device.ts), and shared/relay-channel.ts keeps its name. This already applies to PCs paired from a QR offer before #2759. Renaming saved PCs from a later offer is a separate change.

Test note (not changed): on a host whose hostname() is empty, both name tests (the existing QR one and the new one) would expect "" where the offer leaves name out. CI and real hosts have a host name.

CodeRabbit skipped this PR, so this review stands in for it. The review ran at 110e0ae; the only commit since is the docs change f583576.

@devswha

devswha commented Nov 5, 2026

Copy link
Copy Markdown
Owner Author

Merge note

Merged origin/main (1dd4f3fe) into the branch. #2882 (which advertises the regional relay in the code-less pairing link) changed the same line of RelayService.pairingLink(); the conflict was resolved by keeping both sides, so the link now carries relay: this.advertised from main and the PC's name (...offerName()) from this PR, the same fields the QR offer carries. The changelog fragment, docs/relay.md and server/relay/service.contract.test.ts merged without conflicts and read consistently with #2882.

Validation at 27c6407a: bun test server/relay/service.contract.test.ts server/relay/pair-sas.contract.test.ts server/relay/session-ws.contract.test.ts (82 pass, 0 fail) and bun run check fast passed (workflow syntax, generated types and themes, typecheck, build; unit 11660 pass, 0 fail, 46 skip).

CodeRabbit did not review this PR; the Codex review above stands in for it. Its open question for the maintainer is unchanged: whether the code-less link should carry the host name at all (#2599 item 2 asks for it). No review threads are open.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Nov 5, 2026 •

Copy link
Copy Markdown

Deploying ovis with  Cloudflare Pages  Cloudflare Pages

Latest commit: 5e54321
Status: ✅  Deploy successful!
Preview URL: https://5dfc1948.ovis.pages.dev
Branch Preview URL: https://lane-issue-2599.ovis.pages.dev

View logs

@devswha

devswha commented Nov 5, 2026

Copy link
Copy Markdown
Owner Author

Local merge gate (owner-approved): origin/main merged, bun run check fast passed at this head, review CLEAR at this head (Codex Terra xhigh: no findings), threads resolved, merges cleanly. Merged without waiting for GitHub CI.

@devswha
devswha merged commit 6fb88b8 into main Nov 5, 2026
16 of 18 checks passed
@devswha
devswha deleted the lane/issue-2599 branch November 5, 2026 12:46
devswha added a commit that referenced this pull request Nov 6, 2026
Part of #2599 (item 1, the Android half). Items 2 and 3 already landed:
#2759 and #2868 put the PC's name in both pairing offers, and #2605
moved the file path out of app.ovis.sh requests.

## Problem
A relay pairing link (`https://app.ovis.sh/s/<server_id>/#offer=...`)
tapped outside the app, in a message or read from the PC's QR code by
the phone's camera, opens in the browser as the hosted web app. The
Android app never sees it, because its manifest declares no `VIEW`
filter for `https://app.ovis.sh/s/` and app.ovis.sh serves no
`/.well-known/assetlinks.json`.

## Change
- **Hosted shell** (`scripts/shell-build.ts`): `dist-shell/` now carries
`/.well-known/assetlinks.json` and
`/.well-known/apple-app-site-association`, sent as `application/json`
(`_headers`). The asset links name `sh.ovis.app` with two certificates:
Google Play's app signing key (copied from Play Console, App integrity,
which signs every copy Play installs) and the upload key (read from the
certificate in the uploaded 1.0 (4) bundle, which signs a release build
installed by hand). The Apple file names `5987KT43TJ.sh.ovis.app` for
`/s/*` only. The front page `/` stays the web app's.
- **Android manifest**: an `autoVerify` intent-filter for
`https://app.ovis.sh` with path prefix `/s/`.
- **`MainActivity`**: only a link to a PC's scope (`/s/<32 hex>/`, port
443 or none) is taken (`PCStore.appLink`); any other page of app.ovis.sh
leaves the open page alone. A link to pair from (the scope alone or with
its `#offer=`) is taken out of its intent, kept in memory
(`PCStore.link`) and shows the connect screen, both at a cold start and
while a PC is open (`onNewIntent` starts the activity again, as Change
PC does). A view of a PC (a file's `#read=`, a pane's `#pane=`, an older
`?pane=`) opens that PC at that view when the app has it saved, as the
browser did, and otherwise leaves the open page alone. A link the system
hands back when the activity is restored or started from Recents is not
opened again.
- **`HerdrShellPlugin.takeLink`**: hands the pending link out once, and
never in an activity that shows a PC: a PC's page has the bridge too and
must not take another PC's pairing link.
- **Connect screen** (`app/web/connect.js`): on Android it takes the
link once (`HerdrShell.takeLink`), fills the address field with it and
opens it through the same `open()` as a scanned QR code, so
`relayLink()` checks the offer the same way and shows the same errors.

The link is opened without another tap, as a scanned QR code is: tapping
it is the user's action, and the hosted web app already pairs from the
same link in the browser.

## What needs a new native build, and what does not
- No native build: the two `.well-known` files. They go live when a
release deploys the hosted shell (`release.yml` deploys `dist-shell` to
Pages; Wrangler uploads `.well-known/`, its ignore list covers only
`_worker.js`, `_redirects`, `_headers`, `_routes.json`, `functions` and
VCS folders).
- Android build: the intent-filter, `MainActivity`,
`HerdrShellPlugin.takeLink` and `connect.js` ship in the app's bundle,
so they reach users with the next Play build. Android verifies the link
at install, so the order of the two does not matter: until both are out,
such links keep opening in the browser.
- iOS (not in this PR): the Associated Domains capability
(`applinks:app.ovis.sh`) has to be turned on for the App ID in the Apple
developer account first; adding the entitlement before that would break
the next iOS build's signing. After that, the entitlement and a handler
in `SceneDelegate`/`HerdrBridgeViewController` follow this same path.
The `apple-app-site-association` file served here is what that step
needs, so #2599 stays open for it.

## Validation
- `bun test scripts/shell-build.test.ts`: the new tests failed on
`origin/main` (`Export named 'ANDROID_APP_FINGERPRINTS' not found`) and
pass now (17 pass). They check both files against the Android manifest's
filter and `app/capacitor.config.json`'s id.
- `./gradlew :app:testDebugUnitTest` (JDK 21): `PCStoreTest` 19 pass, 0
fail. The first version of the new link test failed (1 of 18) with the
host check removed.
- `./gradlew :app:assembleDebug`, then the APK on an Android 15 (API 35)
emulator. `pm get-app-links sh.ovis.app` lists `app.ovis.sh` (state
`none`: the live app.ovis.sh has no asset links yet, and a debug key is
not in them). The domain was then allowed with `pm
set-app-links-user-selection`, which stands in for verification, and
links were sent with `am start -a VIEW -c BROWSABLE -d <link>` and no
package:
- A. App not running, link with an offer named "Lane QA PC": the app
started cold on `https://app.ovis.sh/s/0123…cdef/` (read from the
WebView's DevTools), and the connect screen later listed it as "Lane QA
PC (app.ovis.sh)".
- B. That PC open, link whose offer names another PC: the app went to
the connect screen with the link in the address field and "This pairing
link is damaged or names another PC" (screenshot below).
- C. Same link sent with `FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY` (as
Recents does): not opened, the app stayed on the PC it had open.
- D. Control for C, the link without the flag: opened that second PC
with its offer.
- After the review round, on the rebuilt APK: G. a file link
(`#read=%2Fhome%2Fa.md&machine=local`) of the saved PC, sent from the
connect screen, opened that PC at
`…/#read=%2Fhome%2Fa.md&machine=local`. H. a pane link of a PC the app
does not have, sent while G was open, left G's page as it was. A and B
behaved as before.
- `bun run check fast`: all steps ok on 8ee5bae, the review fixes with
origin/main merged in (unit: 12621 pass, 0 fail, 50 skip).
- Skipped: browser regressions (no client component changed), a real
pairing through a live relay (the offers above name no running PC;
pairing itself is unchanged and already covered for scanned links), iOS
(not changed).

| B: running app, bad link (the PC from A listed under its offer's name)
| A: cold start, the app opened the PC's page on app.ovis.sh |
|---|---|
|
![](https://raw.githubusercontent.com/devswha/ovis/3076e1e26a4799eae4356db3cafb527661acf690/b-running-bad-link.png)
|
![](https://raw.githubusercontent.com/devswha/ovis/3076e1e26a4799eae4356db3cafb527661acf690/a-cold-link.png)
|

## Review round (omo + Codex Terra xhigh at 73d8e8a)
- Fixed, HIGH (Codex): a PC's page could call `takeLink` and race the
connect screen for another PC's pairing link. `takeLink` now answers
only in an activity that shows no PC.
- Fixed, MEDIUM (omo, Codex): a file or pane link of the hosted shell
was taken and failed on the connect screen as "not a PC", closing the
open PC. Such a view now opens a saved PC at that view, and is otherwise
left alone.
- Fixed, MEDIUM (Codex): a malformed `/s/...` link closed the open page
before the connect screen rejected it. Only `/s/<32 hex>/` links are
taken now. A pairing link with a damaged offer still goes to the connect
screen and shows its error, as a scanned one does.
- Fixed, LOW (omo): `:443` is accepted like no port.
- Not changed, LOW (omo): the Caddy mirror sends no JSON type for
`/.well-known/*`; only app.ovis.sh is in the app's link, the mirror is
never asked.
- Not changed, LOW (omo): another app on the phone can send the app a
pairing link, as it can open one in the browser today; the pairing then
asks for what the offer would ask for in the browser.
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