Skip to content

fix(mobile): dedupe host cards by pinned public key on re-pair (STA-1840)#8833

Merged
brennanb2025 merged 3 commits into
mainfrom
brennanb2025/mobile-host-dedup-publickey
Jul 15, 2026
Merged

fix(mobile): dedupe host cards by pinned public key on re-pair (STA-1840)#8833
brennanb2025 merged 3 commits into
mainfrom
brennanb2025/mobile-host-dedup-publickey

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Summary

Re-pairing a desktop that was already paired created a duplicate host card on mobile (the STA-1840 "multiple hosts" symptom). The pairing coordinator always minted a fresh host-${now} id, and persistHost dedups only on that id, so a second row was appended for the same physical desktop. The "connect from anywhere" CTA on an unreachable direct-only host nudges the user to re-scan, which made this reachable in normal use.

Pairing now resolves the durable host identity by the desktop's pinned publicKeyB64 — the desktop's stable, cryptographically-unique identity key — and reuses the existing host's id and name so the pairing merges into the existing card. Every downstream pairing artifact (relay journal, credential bundle, saveHost) then keys to the same id. A genuinely new desktop key falls through to a fresh id/name, so distinct desktops still get distinct cards.

What this fixes (addresses all findings from review)

  • Prevents new duplicates — reuse the existing id/name by pinned public key.
  • Heals existing duplicates — an authoritative save collapses any already-stored rows sharing that public key and cleans up the dropped rows' credentials/overlays (users already bitten by the shipped bug are healed on their next re-pair).
  • Refreshes the live client on re-pair — because re-pair now reuses the id, a client cached under that id would otherwise keep the stale endpoint/relay, so a re-pair meant to switch a host onto relay wouldn't take effect until restart. Pairing now closes the host's client on success (closeHost) so the destination screen opens a fresh client with the newly-paired profile — mirroring the existing removeHost() refresh path. No-op for a brand-new host.
  • Fails closed on unreadable storage — identity resolution uses one strict durable read; if host storage is unreadable it aborts the pairing instead of falling back to minting another identity (which would duplicate).
  • Clears stale relay routing — re-pairing a previously-relay host direct-only removes the old relay overlay so the merged card doesn't keep advertising dead relay state.
  • One durable read for identity + naming, instead of two serial reads before connecting.

Changes

  • host-store.ts: resolvePairingHostIdentity() (strict single read: reuse existing id/name by public key, else name a new host); persistHost collapses same-key duplicate rows and cleans their credentials/overlays; clears the relay overlay on a direct-only save that reuses an id.
  • mobile-relay-host-overlay-store.ts: batched removeMobileRelayHostOverlays; skip the storage write when nothing changed.
  • pre-profile-pairing-coordinator.ts: resolve the host identity up front and thread it through the whole flow.
  • pair-confirm.tsx / pair-scan.tsx: closeHost(hostId) on pairing success so a reused id reconnects on the new profile.

Screenshots

No visual change — pairing-logic only. Behavior: re-pairing the same desktop updates the one existing row (and reconnects it) instead of adding a second row.

Testing

  • tsc --noEmit (mobile) — clean
  • oxlint / oxfmt --check (changed files) — clean
  • vitest runfull mobile suite: 242 files, 1,719 passed, 2 skipped. New/updated cases cover: resolve-by-key with one durable read, name-from-same-read, fail-closed on unreadable storage, collapse already-persisted duplicates, clear stale relay on direct-only re-pair, no-touch on a new direct host, batched/skip-no-op overlay writes.
  • Not device-verified. The definitive end-to-end check (pair a desktop → re-scan its QR → confirm one card and that it reconnects on the new endpoint) needs an emulator/physical run. The logic is unit-proven; the live pair→re-pair path is not exercised here.

Notes

Mobile only — does not touch the desktop build. This is the P1-1 item from the v1.4.142 release audit. It does not address the separate relay-pairing P2s (offer-TTL, recovery-resurrect, clear-after-commit).

…840)

Re-pairing an already-paired desktop minted a fresh host-${now} id and
appended a second host row, because persistHost dedups only on id and the
pairing coordinator always mints a new id. The desktop CTA to re-scan for
relay made this reachable in normal use, so one desktop showed as multiple
hosts.

Reuse the existing host id and name when the desktop pinned publicKeyB64 is
already stored, so the pairing merges into the existing card. Every downstream
write (journal, credential bundle, saveHost) then keys to the same id and
persistHost updates in place. A new desktop key falls through to a fresh id.
@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: e671cbf6-3d7a-4abf-9e33-2db81ce3d1d5

📥 Commits

Reviewing files that changed from the base of the PR and between 9a88386 and 3b8a15b.

📒 Files selected for processing (2)
  • mobile/app/pair-confirm.tsx
  • mobile/app/pair-scan.tsx

📝 Walkthrough

Walkthrough

Adds durable host identity resolution by pinned public key, allowing pre-profile pairing to reuse existing host IDs and names or generate new identities. Host persistence now deduplicates matching records and cleans stale overlays, token-cache entries, and credentials. Relay overlay removal supports batches and avoids unchanged storage writes. Successful pairing closes cached host clients before navigation. Tests cover lookup, re-pairing, deduplication, cleanup, and storage behavior.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ⚠️ Warning Summary and testing are solid, but the required AI Review Report and Security Audit sections are missing. Add the AI Review Report and Security Audit sections, and confirm the review covered macOS/Linux/Windows cross-platform behavior per the template.
✅ Passed checks (3 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 states the mobile host dedupe fix on re-pair and matches the main change.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

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.

@brennanb2025 brennanb2025 force-pushed the brennanb2025/mobile-host-dedup-publickey branch from 9a88386 to c7c0dca Compare July 15, 2026 08:21
…ects

Re-pairing the same desktop now reuses its existing host id (STA-1840 dedup),
so a client cached under that id keeps the stale endpoint/relay. A re-pair meant
to switch a host onto relay (the exact 'connect from anywhere' case) would not
take effect until an app restart. Close the host's client on pairing success so
the destination screen opens a fresh client with the newly-paired profile —
mirroring the removeHost() path that already refreshes the client on re-pair. A
brand-new host has no cached entry, so this is a no-op there.
@brennanb2025 brennanb2025 merged commit c12ade5 into main Jul 15, 2026
2 checks passed
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