From cfa82c24cc46fe7f5469f7a8cf750bcf3893d6b8 Mon Sep 17 00:00:00 2001 From: Fredrik Ahlgren Date: Sat, 17 Oct 2026 07:59:25 +0200 Subject: [PATCH 1/3] feat(pairing): name the Cloud home in the pairing QR Signed-off-by: Fredrik Ahlgren --- .changeset/cloud-qr-v3.md | 5 +++++ go/internal/appenroll/payload.go | 9 +++------ go/internal/appenroll/payload_home_test.go | 14 ++++---------- go/internal/appuplink/livebox_test.go | 7 +++---- 4 files changed, 15 insertions(+), 20 deletions(-) create mode 100644 .changeset/cloud-qr-v3.md diff --git a/.changeset/cloud-qr-v3.md b/.changeset/cloud-qr-v3.md new file mode 100644 index 000000000..bb7eddde3 --- /dev/null +++ b/.changeset/cloud-qr-v3.md @@ -0,0 +1,5 @@ +--- +"ftw": patch +--- + +Include the Cloud home id in a linked box's pairing QR so the web app can pair through FTW Cloud. diff --git a/go/internal/appenroll/payload.go b/go/internal/appenroll/payload.go index e44247eb3..32e6ade1a 100644 --- a/go/internal/appenroll/payload.go +++ b/go/internal/appenroll/payload.go @@ -47,12 +47,9 @@ const ( MaxLANHintChars = 64 ) -// QRNamesHome is the one switch that puts the home id in the QR, as v3. It -// stays false until the apps parse v3 and those builds are out: ftw-app's -// Enrollment.swift and Enrollment.kt, and ftw-webapp's enrollment.ts. A v3 -// QR before then stops pairing on every shipped app. Until it flips, a new -// phone learns the home id from hello_ok.cloud on its first session. -const QRNamesHome = false +// QRNamesHome puts the Cloud home id in the QR, as v3. The web client needs +// it to pair through Cloud; iPhone also reads v3. An unlinked box keeps v2. +const QRNamesHome = true // SetHome tells enrolment where to find the home id at FTW Cloud, for a QR // that names it. fn returns "" while the box is not linked. diff --git a/go/internal/appenroll/payload_home_test.go b/go/internal/appenroll/payload_home_test.go index 19b5d3e8f..b40fd46e5 100644 --- a/go/internal/appenroll/payload_home_test.go +++ b/go/internal/appenroll/payload_home_test.go @@ -63,14 +63,8 @@ func TestAnUnlinkedBoxShowsV2(t *testing.T) { } } -// The QR stays v2 until every shipped app parses v3: ftw-app's -// Enrollment.swift and Enrollment.kt and ftw-webapp's enrollment.ts each -// take exactly five segments today. Flipping QRNamesHome is step 2's, and -// this test is there to be changed with it. -func TestTheQRStaysV2UntilTheAppsParseV3(t *testing.T) { - if QRNamesHome { - t.Fatal("QRNamesHome is on; check that ftw-app (iOS and Android) and ftw-webapp parse v3 in builds people have, then change this test") - } +// A linked box gives a new client enough to pair with the relay blocked. +func TestTheLinkedQRNamesItsCloudHome(t *testing.T) { id, _ := newIdentity(t) id.SetHome(func() string { return testHome }) code, _, _ := id.MintPairingCode(apiauth.RoleOwner, PairingTTL) @@ -78,7 +72,7 @@ func TestTheQRStaysV2UntilTheAppsParseV3(t *testing.T) { if err != nil { t.Fatal(err) } - if strings.Contains(url, base64.RawURLEncoding.EncodeToString([]byte(testHome))) || !strings.Contains(url, "#"+PayloadVersion+".") { - t.Fatalf("the QR names the home before the apps can read it: %q", url) + if !strings.Contains(url, "."+base64.RawURLEncoding.EncodeToString([]byte(testHome))) || !strings.Contains(url, "#"+PayloadVersionHome+".") { + t.Fatalf("the linked QR omits its Cloud home: %q", url) } } diff --git a/go/internal/appuplink/livebox_test.go b/go/internal/appuplink/livebox_test.go index 483b0b585..636f5d252 100644 --- a/go/internal/appuplink/livebox_test.go +++ b/go/internal/appuplink/livebox_test.go @@ -124,6 +124,7 @@ func TestLiveBoxForTheiPhone(t *testing.T) { defer closeTun() uplink.Tunnel().Attach(timedTun{inner: link, log: tunLog}) bound.Store(link) + enroll.SetHome(link.Home) enroll.OnChange(link.RosterChanged) enroll.OnPairingCode(link.RosterChanged) ctx, cancel := context.WithCancel(context.Background()) @@ -170,13 +171,11 @@ func TestLiveBoxForTheiPhone(t *testing.T) { } link.RosterChanged() time.Sleep(300 * time.Millisecond) - v2, err := enroll.EnrollmentURL(code, "") + pairURL, err := enroll.EnrollmentURL(code, "") if err != nil { t.Fatal(err) } - // v3 by hand while appenroll.QRNamesHome stays off. - v3 := strings.Replace(v2, "#v2.", "#v3.", 1) + "." + base64.RawURLEncoding.EncodeToString([]byte(link.Status().Home)) - if err := os.WriteFile(filepath.Join(dir, "pair.url"), []byte(v3), 0o600); err != nil { + if err := os.WriteFile(filepath.Join(dir, "pair.url"), []byte(pairURL), 0o600); err != nil { t.Fatal(err) } t.Logf("box linked as %s; pairing link written", link.Status().Home) From e862ca3a93e068dd273b73d999025ce2316c346e Mon Sep 17 00:00:00 2001 From: Fredrik Ahlgren Date: Sat, 17 Oct 2026 08:00:20 +0200 Subject: [PATCH 2/3] chore: mark Cloud pairing as a minor change Signed-off-by: Fredrik Ahlgren --- .changeset/cloud-qr-v3.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/cloud-qr-v3.md b/.changeset/cloud-qr-v3.md index bb7eddde3..e5c0d9c69 100644 --- a/.changeset/cloud-qr-v3.md +++ b/.changeset/cloud-qr-v3.md @@ -1,5 +1,5 @@ --- -"ftw": patch +"ftw": minor --- Include the Cloud home id in a linked box's pairing QR so the web app can pair through FTW Cloud. From e91caa40c150fe67fcb5208b98c0b195f71857be Mon Sep 17 00:00:00 2001 From: Fredrik Ahlgren Date: Sat, 17 Oct 2026 08:03:38 +0200 Subject: [PATCH 3/3] docs(pairing): correct the shipped QR reader comments Signed-off-by: Fredrik Ahlgren --- go/internal/appenroll/payload.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/go/internal/appenroll/payload.go b/go/internal/appenroll/payload.go index 32e6ade1a..13f95f589 100644 --- a/go/internal/appenroll/payload.go +++ b/go/internal/appenroll/payload.go @@ -33,8 +33,8 @@ const ( // PayloadVersionHome is v2 with a sixth segment: the home id at FTW // Cloud, base64url of its 26 ASCII characters, so a new phone knows - // where to dial before any session with the box. No shipped app reads - // it; each parser takes exactly five segments of v2. + // where to dial before any session with the box. Both the web client + // and iPhone read this form. PayloadVersionHome = "v3" // PayloadOrigin is where the app is served. It is also the passkey RP @@ -63,7 +63,7 @@ func (i *Identity) SetHome(fn func() string) { // // lanHint is where the box believes it can be reached on the local network, // and is allowed to be empty: a box that does not know its own address should -// say nothing rather than guess, and the app falls back to the relay. +// say nothing rather than guess. The Cloud home id does not depend on it. func (i *Identity) EnrollmentURL(code []byte, lanHint string) (string, error) { return i.enrollmentURL(code, lanHint, QRNamesHome) }