Skip to content

feat(home-route): inline passkey sign-in on the dashboard - #450

Merged
frahlg merged 7 commits into
masterfrom
feat/inline-signin
Jun 5, 2026
Merged

feat(home-route): inline passkey sign-in on the dashboard#450
frahlg merged 7 commits into
masterfrom
feat/inline-signin

Conversation

@frahlg

@frahlg frahlg commented Jun 5, 2026

Copy link
Copy Markdown
Member

Summary

Closes the discoverability gap in the P2P-only home route: the dashboard now is the door. When you open home.fortytwowatts.com and aren't signed in, a discreet "Sign in with your passkey" banner (+ a header key) appears, and the passkey ceremony runs in place over the same strict P2P channel — no redirect to /owner-access/login.html (which would spawn a fresh channel with no session).

  • whoami-driven: shown only when not signed in on a remote origin; hidden on the LAN bypass and once signed in. Re-checks when the P2P channel (re)connects (whoami needs the channel up to answer).
  • Ceremony rides ownerFetch (strict / FIX-B) — login/start, login/finish, whoami, logout never traverse the relay in cleartext. The public-route fetch guard still passes.
  • Styling uses DESIGN.md tokens (hairline, single amber accent, near-black on-accent text); flips cleanly in light theme.

Part of the #438 seamless-UX layer (the inline-login slice). Device-key silent re-auth + the discreet transport indicator are the remaining/already-shipped pieces.

Verification

  • Dev-deployed to the field Pi; confirmed in a browser on home.fortytwowatts.com: the banner + header key appear when unauthenticated, over a live ● DIRECT channel (screenshot in chat).
  • node --test web/** green incl. the public-route fetch guard; next-app.js parses.

Test plan

  • (owner) click Sign in on home.* off-WiFi → Face ID → dashboard populates with live data

frahlg and others added 7 commits June 5, 2026 13:43
The dashboard now reveals a discreet sign-in banner + header key when whoami
reports the viewer isn't signed in on a remote origin, and runs the passkey
login ceremony in place over the same strict P2P channel (ownerFetch) — no
redirect to /owner-access/login.html, which would spawn a fresh channel with
no session. Re-checks on P2P (re)connect. LAN bypass + signed-in views never
show it. Closes the discoverability gap (part of #438 seamless UX).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Evolve the inline-signin banner into a real gate: a logged-out remote visitor
now sees ONLY a clean sign-in card, never the empty dashboard chrome (which
rendered 'No devices configured' and read as an unconfigured instance). The
gate covers the dashboard until whoami confirms a signed-in owner; the passkey
ceremony runs in place over the strict P2P channel. Suppresses the no-devices
prompt while logged out. The transport indicator is now informational (direct/
relayed/connecting), not a confusing toggle that broke the P2P-only channel.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A reload briefly painted the dashboard before next-app.js could cover it with
the gate. Add a pre-paint inline <head> script (mirroring the theme-flash
guard) that marks <html class=ftw-gated> on a remote origin from the first
frame; CSS shows the opaque full-screen gate immediately, and setupAuth()
clears ftw-gated once whoami confirms a signed-in owner. The gate is now a
class toggle (not the hidden attribute) so the pre-paint CSS can win.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… Pi)

Spec for the relay-hardening Fredrik approved: relay serves the sign-in shell
+ /api/identity from its own embedded copy (Pi not touched), and forwards
signaling to the Pi ONLY when it carries a device-key proof the relay can
verify. The device-key (LAN-born, wallet-bound) becomes the sole credential
that can reach a Pi from the internet, and also mints the session silently.
Surface-by-surface threat model + build slices + one open trade-off for review.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…mous path to a Pi)

Relay serves the sign-in shell (-home-web) + /api/identity (from -home-pubkey)
itself, so anonymous visitors never reach the Pi. Signaling is gated by a
device-key proof (relay-issued nonce, signed by a non-extractable WebCrypto
ECDSA P-256 key minted at LAN enrollment, verified against the device-pubkeys
the Pi publishes on /me/register) — else 403, Pi never woken. The device-key
also mints the session silently over the channel (device-PoP); step-up keeps a
passkey; revocation drops the key on Pi + relay. Built in parallel across relay/
Pi/browser against locked wire contracts (C1-C4), then integrated. All Go unit
tests + 97 web tests pass. Pending Codex security GO before live.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Codex returned NO-GO on the parallel build; these close the findings relevant
to the shippable relay-hardening (slices 1+2):
- #1 CRITICAL: require -home-web when a home host is set (else the relay fell
  back to forwarding anonymous GETs to the Pi — the exact surface we forbid).
  requireHomePin now enforces it.
- #2 HIGH: device_pubkeys are now covered by the registration signature
  (MeRegisterSigningStringV2); a captured /me/register can no longer be replayed
  with a swapped/added device key. + regression test.
- #4 MED: symlink-escape in the -home-web static server closed (EvalSymlinks +
  re-check the resolved path stays under the web root).
Deferred to the C2-enforcement phase (inert until the device-key gate is ON),
with TODOs: #3 (bind device_pubkey to the enroll ceremony) and #5 (separate
per-IP limiter on challenge issuance). All Go unit + 97 web tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ault)

Slices 1+2 (relay serves the shell + /api/identity itself, closing the
anonymous-FETCH surface) ship independently of the device-key signaling gate:
-require-device-key defaults OFF, so signalBrowserOffer parks the raw offer
with no proof (pre-C2 behaviour) and a home Pi that doesn't yet publish
device-keys keeps working. Flip it on once device-keys are enrolled to close
the anonymous-SIGNALING surface. C2 tests now assert the enforced path; a new
test asserts the dormant default. All Go unit + web tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@frahlg
frahlg merged commit dff16b5 into master Jun 5, 2026
2 checks passed
@frahlg
frahlg deleted the feat/inline-signin branch August 7, 2026 08:16
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