Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/facett

facett-auth-ui/.nornir/facett-auth-ui.md

facett-auth-ui — the shared device-code sign-in pane

One-line pitch

One robot-testable egui Facet that renders an RFC 8628 device-code login, so every suite app's Sign in surface (dwarves' Profile · korp · holger-ui) is ONE pane — not a per-app twin.

Why it exists (LAW #5 — DRY)

dwarves-demo/src/profile.rs grew an interim LoginSeam whose real backend (NornirLogin::sign_in) could only leave a "(see terminal for the device code)" note — the RFC 8628 prompt never rendered in the GUI. korp and holger-ui would each grow their own copy of the same Sign-in-button → device-prompt → poll → signed-in flow. That is a data-parameterised pane masquerading as N copies. This leaf owns the one pane body; the HOST supplies the data through a seam.

Layering — AUTH-LIBRARY-AGNOSTIC (the important constraint)

facett carries no nornir-auth-client / edda auth dependency. The pane is presentational + host-driven: the HOST implements the LoginSeam trait (wiring the real device-code grant + token store), and LoginPane only ever renders the states the seam surfaces. This mirrors how the Release tab takes a ReleaseConvergence in and the About card takes an Identity in — the pane never reaches for a backend.

Deps: facett-core (the Facet contract + the shared clipboard::put the Copy buttons reuse), egui, serde_json. Optional nornir-testmatrix behind the testmatrix feature only. A leaf beside facett-about / facett-confirm.

Design — the chosen shape

The seam surfaces four states; the pane paints whichever one it holds:

LoginState                       pane renders
  ├─ SignedOut                    "Sign in" button → seam.begin_device_code()
  ├─ AwaitingUser{user_code,uri}  URL (hyperlink, clickable) + Copy URL
  │                               user_code (monospace) + Copy code
  │                               "Waiting for approval…"  → seam.poll() each frame
  ├─ SignedIn{subject}            "Signed in as {subject}"
  │                               credential NAMES (seam.credential_names — never values)
  │                               "Sign out" button → seam.sign_out()
  └─ Error(msg)                   red message + "Try again" → begin()

The seam (HOST implements)

pub trait LoginSeam {
    fn source(&self) -> &str { "device-code" }          // honest banner
    fn begin_device_code(&mut self) -> LoginState;      // → AwaitingUser | Error | SignedIn
    fn poll(&mut self) -> LoginState;                   // → AwaitingUser | SignedIn | Error
    fn current_identity(&self) -> Option<String> { None }  // cached token (no network)
    fn sign_out(&mut self) -> LoginState { LoginState::SignedOut }
    fn credential_names(&self) -> Vec<String> { Vec::new() }  // NAMES only — never values
}

Generalises dwarves' interim single-blocking sign_in into the begin/poll shape a real in-GUI device-code flow needs. No method exposes a token/secret value — by construction a credential value can never reach the pane or its state_json.

API

Robot test (L4) — matrix cell facett-auth-ui/login-pane

tests/robot.rs renders the REAL LoginPane under egui_kittest over a StubLogin and drives SignedOut → AwaitingUser → SignedIn, asserting off the live AccessKit tree: the Sign in button paints; the user-code + the verification URL paint (the prompt is shown IN-app, not left as a terminal note); the subject + a Sign out button paint; and NO secret value string appears anywhere in the pane's state_json. Inline src/lib.rs tests cover the headless update_json/state_json drive, a cached-identity open, and the Error state. RED-when-broken: dropping the user-code/URL from the awaiting surface, or leaking a token into state_json, fails an assertion. Emits gated #[cfg(feature="testmatrix")].

Security invariant (must-NOT-change)

The seam has no value getter and state_json carries no value/token/secret key — only the subject (identity) + credential names. The robot's no-secret assertion is the contract; any future field that could carry a value must not land here.

Status / gap

Pane + seam + robot test shipped. The per-app hosts implementing LoginSeam over nornir-auth-client and mounting this pane — dwarves-demo's Profile (replacing its interim profile.rs LoginSeam + "see terminal" note), korp, holger-ui — are DEFERRED to the host-side agents (separate work). The QR-of-URL nicety is a deferred follow-up (no encoder dep pulled in here).