feat(companion): npub-first pairing — node self-anchor + guaranteed public anchor in the QR

The pairing QR was effectively IP-first: the phone dialed the scanned LAN
host and went dark when the LAN renumbered or it left home. FIPS peers on
npubs; IPs are only dial hints. fanchors now leads with the paired node
ITSELF (fnpub @ current host, npub-keyed so LAN contact is direct p2p over
FIPS and survives DHCP), and fips.pair-info always includes the Archipelago
vps2 public anchor (pairing hint only — the node's own anchor file is not
modified) so the phone can rendezvous through the public mesh when away.
Contract doc updated with the REQUIRED app-side changes (built on the Mac).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-07-23 15:30:40 -04:00
co-authored by Claude Fable 5
parent e59ea1da69
commit c58f84c6e9
3 changed files with 53 additions and 6 deletions
+14 -2
View File
@@ -30,9 +30,21 @@ impl RpcHandler {
// The node's seed anchors ride along so the phone can rendezvous
// through the same public mesh points when the node's LAN endpoint
// isn't directly dialable (phone away from home, node behind NAT).
let anchors = fips::anchors::load(&self.config.data_dir)
let mut anchor_list = fips::anchors::load(&self.config.data_dir)
.await
.unwrap_or_default()
.unwrap_or_default();
// Pairing must always carry a public rendezvous point: without one the
// phone is IP-bound to the LAN host it scanned and goes dark the
// moment it leaves that network. This is a pairing hint only — the
// node's own anchor file is not modified, so an operator's removal of
// the default anchors still sticks for the node itself.
if !anchor_list
.iter()
.any(|a| a.npub == fips::anchors::ARCHY_ANCHOR_NPUB)
{
anchor_list.push(fips::anchors::archy_anchor());
}
let anchors = anchor_list
.into_iter()
.map(|a| {
serde_json::json!({