Skip to content

feat(p1.6): mobile pairing via QR code#86

Merged
attson merged 17 commits into
mainfrom
feat/pairing-qr
May 31, 2026
Merged

feat(p1.6): mobile pairing via QR code#86
attson merged 17 commits into
mainfrom
feat/pairing-qr

Conversation

@attson
Copy link
Copy Markdown
Owner

@attson attson commented May 31, 2026

Summary

Implements P1.6 from the roadmap: a logged-in desktop user generates a 5-minute single-use QR code; a fresh mobile install scans it and skips the manual relay-URL + API-token typing.

  • Relay (Go): new pairing_tokens table + api_tokens.source column. POST /api/pair/create (Bearer-auth-gated, 10/min/user) mints a pair_… token and returns {token, expires_at, qr_url}. POST /api/pair/consume (public, 10/min/IP) atomically consumes the token, mints a fresh atk_… for the same user (source='pairing'), and returns {relay_url, api_token, user}. Anti-oracle: invalid/expired/already-consumed all collapse to 404 {code:"pair_invalid"}.
  • Desktop (Wails + Vue): App.CreatePairingToken Wails binding (mirrors FetchRelayMe); PairingPanel.vue embedded in Settings → Relay renders a 240×240 QR via the new qrcode dep with a live 5:00 countdown and regenerate button.
  • Mobile (Capacitor + Vue): new @capacitor-mlkit/barcode-scanning plugin + NSCameraUsageDescription. MobileSetup.vue grows a primary "Scan QR" button; on scan, PairingConsume.vue parses the URL, calls consumePairing, writes the result into localStorage['atterm.relay'], and emits connected.
  • i18n: new settings.relay.pairing.* and mobile.pairing.* keys in both en + zh-CN.

Spec: docs/superpowers/specs/2026-05-31-pairing-qr-design.md
Plan: docs/superpowers/plans/2026-05-31-pairing-qr.md

Test Plan

  • go test ./... — all green
  • npm test — 73 files / 614 tests green
  • npm run build:wails — succeeds
  • npm run build:capacitor — succeeds
  • userstore unit tests cover happy path, double-consume, expired, unknown token, 50-goroutine concurrency
  • relay HTTP tests cover 401 unauth on create, 200 + qr_url shape, 200 consume + /api/me round-trip with new token, 404 on second consume, 404 on unknown
  • desktop test covers Wails binding Bearer header + parsed response, error on no config
  • frontend component tests cover PairingPanel idle→active→expired and PairingConsume parse/consume/save/error branches
  • manual: pair a real iOS Capacitor build against a Wails desktop, confirm mobile reaches home as the paired user

Follow-ups (from final review, not blocking)

  • CSRF gate on /api/pair/create for parity with other state-mutating routes
  • Add an HTTP integration test for the consume rate limit (the wiring exists but isn't asserted)
  • Mobile: prefer the scanned origin over the server-returned relay_url to avoid silent http downgrades through a misconfigured proxy
  • Spec drift: §4.2 example shows user.id as int + a name field — code returns string ULID and no name. Update the spec text.

attson added 17 commits May 31, 2026 16:26
P1.6 roadmap item. Desktop user generates a 5-minute one-time
pairing token via /api/pair/create; mobile scans QR, calls
/api/pair/consume, gets a fresh atk_ token (same user, source=
'pairing') plus the relay URL. Token storage mirrors the existing
invitation pattern in userstore.
Six phases (A-F), ~16 tasks. Phase A corrects spec §5.2/§5.4 to
require a Wails binding (the renderer can't hold the API token).
B-C land the relay backend (userstore pairing + HTTP endpoints).
D-E land desktop PairingPanel and mobile MobileSetup + PairingConsume.
F wraps with i18n and an end-to-end smoke gate.
D4. Adds PairingPanel.vue which calls createPairingToken (D2), renders
the qr_url as a 240px PNG via qrcode (D3), shows a 5-minute countdown,
and flips to an expired state at zero. Embedded at the bottom of the
relay settings pane.

Also adds the minimum settings.relay.pairing.* i18n keys to en + zh-CN
so the MessageKey union covers PairingPanel's t() calls (vue-tsc).
F1 will polish copy and add the remaining keys for E3/E4.

Tests (test-first, all three from the plan): idle button, generated
QR + countdown, expired state after timer advance.
@attson attson merged commit 6fba645 into main May 31, 2026
7 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