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:
parent
e59ea1da69
commit
c58f84c6e9
@ -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!({
|
||||
|
||||
@ -38,7 +38,7 @@ Query parameters:
|
||||
| `fip` | no | Node's `fips0` ULA (IPv6). Once the phone is meshed, the node's UI stays reachable at `http://[<fip>]` from anywhere — this is the remote-access address (replaces the old WireGuard 10.44.0.1 flow). |
|
||||
| `fhost` | no | Host the phone's embedded FIPS dials (same host `url` resolved to). |
|
||||
| `fudp` / `ftcp` | no | Mesh transport ports on `fhost` (currently 2121/udp and 8443/tcp). |
|
||||
| `fanchors` | no | Comma-joined `npub@host:port/transport` rendezvous anchors (the node's seed-anchor list, capped at 4). The phone peers with these too so it can route to the node via the public mesh when the direct endpoint is unreachable (away from home / NAT). |
|
||||
| `fanchors` | no | Comma-joined `npub@host:port/transport` rendezvous anchors, capped at 4. **The FIRST entry is the paired node itself** (`fnpub` at its current LAN host — the addr is a dial *hint*, the npub is the identity). The remaining entries are the node's seed anchors, and the node guarantees the Archipelago public anchor (vps2, `146.59.87.168:8444/tcp`) is present even if the operator trimmed their own list — so the phone can always rendezvous through the public mesh when the LAN endpoint is unreachable (away from home / NAT). |
|
||||
|
||||
Examples the web UI actually emits:
|
||||
|
||||
@ -87,6 +87,31 @@ Examples the web UI actually emits:
|
||||
remote access = the node's fips0 ULA (`fip`), which the WebView falls back to
|
||||
automatically when the LAN address stops answering.
|
||||
|
||||
## npub-first connectivity (2026-07-23 — REQUIRED app-side changes)
|
||||
|
||||
The QR used to be effectively IP-first: the app connected to `url`/`fhost` and
|
||||
broke as soon as the LAN renumbered or the phone left home. FIPS peers on
|
||||
**npubs**; IPs are only dial hints. The app must treat them that way:
|
||||
|
||||
1. **Identity = `fnpub`.** The saved server entry is keyed by the node's npub
|
||||
(fall back to origin only when the QR has no FIPS params). Re-scanning the
|
||||
same npub updates the entry even if every address changed.
|
||||
2. **Peer with the node itself as the first anchor** (`fanchors[0]`): on LAN
|
||||
this is direct p2p over FIPS (mDNS/known-endpoint dial via archy-fips-core
|
||||
v0.4+, which discovers LAN peers without any IP pinning), so it keeps
|
||||
working after DHCP renumbering.
|
||||
3. **Peer with the public anchors too** (remaining `fanchors` entries — the
|
||||
Archipelago vps2 anchor is always included by the node): away from LAN the
|
||||
phone routes to the node's npub via the public mesh and reaches the UI at
|
||||
`http://[<fip>]` (the fips0 ULA).
|
||||
4. **Address selection order** for the WebView: LAN `url` when it answers →
|
||||
ULA `fip` over the mesh otherwise. Never hard-fail because the scanned LAN
|
||||
IP stopped existing.
|
||||
|
||||
(Node-side counterpart shipped 2026-07-23: `fips.pair-info` always includes
|
||||
the vps2 public anchor, and the web UI prepends the node's self-anchor to
|
||||
`fanchors`.)
|
||||
|
||||
## Testing checklist (app side)
|
||||
|
||||
- [ ] Scan demo QR from https://demo.archipelago-foundation.org → auto-connected demo session.
|
||||
|
||||
@ -348,9 +348,19 @@ async function buildPairingUrl(): Promise<string> {
|
||||
if (info.udp_port) params.set('fudp', String(info.udp_port))
|
||||
if (info.tcp_port) params.set('ftcp', String(info.tcp_port))
|
||||
// Rendezvous anchors (compact: npub@addr/transport, comma-joined).
|
||||
// Cap keeps the QR at a camera-friendly density; the node lists its
|
||||
// most reachable anchors first.
|
||||
const anchors = (info.anchors || []).slice(0, 4)
|
||||
// FIRST entry is the paired node ITSELF: the phone peers with it by
|
||||
// npub (the fhost addr is only a dial hint), so LAN contact is direct
|
||||
// p2p over FIPS and survives DHCP renumbering; the node's public
|
||||
// anchors follow for reaching it away from home. Cap keeps the QR at a
|
||||
// camera-friendly density; the node lists its most reachable anchors
|
||||
// first.
|
||||
const selfAnchor = info.tcp_port
|
||||
? [{ npub: info.npub, addr: `${params.get('fhost')}:${info.tcp_port}`, transport: 'tcp' }]
|
||||
: []
|
||||
const anchors = [
|
||||
...selfAnchor,
|
||||
...(info.anchors || []).filter((a) => a.npub !== info.npub),
|
||||
].slice(0, 4)
|
||||
if (anchors.length) {
|
||||
params.set(
|
||||
'fanchors',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user