diff --git a/README.md b/README.md index 03be4c5..ad330d0 100644 --- a/README.md +++ b/README.md @@ -521,7 +521,8 @@ otherwise (see above). Use `nearpush send --state allow` by hand or from another "missed":N}`. `{"type":"session_update","session":…,"label":…}` arrives when the summarizer labels a session. A browser `Origin` must match the `Host` header. - `GET /icons/`: the sender's icon. `~/.config/nearpush/icons/.svg`, then - `.png`, then a built-in icon (`kiro`: the official logo from ), + `.png`, then a built-in icon (`kiro`: the official logo from ; + `auth-proxy`: a shield mark drawn for this repo), then a generated tile with the sender's first letter. Only `[a-z0-9-]{1,32}` names resolve; anything else is 404. - `GET /`, `/manifest.webmanifest`, `/icon-*.png`, `/icons/*`: need an allowlisted diff --git a/crates/nearpushd/assets/icons/auth-proxy.svg b/crates/nearpushd/assets/icons/auth-proxy.svg new file mode 100644 index 0000000..25dca4b --- /dev/null +++ b/crates/nearpushd/assets/icons/auth-proxy.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/crates/nearpushd/src/assets.rs b/crates/nearpushd/src/assets.rs index e8ef4eb..a7f63ae 100644 --- a/crates/nearpushd/src/assets.rs +++ b/crates/nearpushd/src/assets.rs @@ -13,9 +13,11 @@ pub const ICON_192: &[u8] = include_bytes!("../assets/icon-192.png"); pub const ICON_512: &[u8] = include_bytes!("../assets/icon-512.png"); /// Official Kiro logo, from https://kiro.dev/icon.svg. pub const KIRO_SVG: &[u8] = include_bytes!("../assets/icons/kiro.svg"); +/// auth-proxy sender mark (a shield with a keyhole), drawn for this repo. +pub const AUTH_PROXY_SVG: &[u8] = include_bytes!("../assets/icons/auth-proxy.svg"); /// Built-in sender icons, used when no user override exists. -const BUILTIN_ICONS: &[(&str, &[u8])] = &[("kiro", KIRO_SVG)]; +const BUILTIN_ICONS: &[(&str, &[u8])] = &[("kiro", KIRO_SVG), ("auth-proxy", AUTH_PROXY_SVG)]; pub const SVG: &str = "image/svg+xml"; pub const PNG: &str = "image/png"; @@ -107,6 +109,7 @@ mod tests { let d = Some(dir.path()); // Built-in, then generic. assert_eq!(resolve_icon(d, "kiro").unwrap().source, IconSource::Builtin); + assert_eq!(resolve_icon(d, "auth-proxy").unwrap().source, IconSource::Builtin); let g = resolve_icon(d, "cli").unwrap(); assert_eq!((g.source, g.content_type), (IconSource::Generic, SVG)); assert!(std::str::from_utf8(&g.bytes).unwrap().contains(">C")); diff --git a/docs/auth-proxy.md b/docs/auth-proxy.md new file mode 100644 index 0000000..b428667 --- /dev/null +++ b/docs/auth-proxy.md @@ -0,0 +1,97 @@ +# auth-proxy → nearpushd → iPhone + +How [auth-proxy](https://github.com/openabdev/auth-proxy) approval requests are +shown on the iPhone desk display. This is the integration contract from +nearpush#2; the approval mechanism itself is auth-proxy#5 (`require_approval`) +with Muse as the first caller (auth-proxy#6). + +nearpush is the **display layer only**. A card can catch the owner's eye and +link to the approval plane; it can never approve anything. Approval happens +only on the approval plane, signed by the owner's key (WebAuthn / device key), +and only auth-proxy executes the request afterwards. + +## Architecture + +```mermaid +flowchart LR + subgraph OwnerHost["Owner-controlled host"] + P[auth-proxy] -->|request parked| N[notifier
local tool] + N -->|nearpush send
loopback + token| D[nearpushd
127.0.0.1:8787] + P -->|outcome| N + end + D -->|tailscale serve| T[HTTPS on tailnet] + T -->|wss /ws| I[iPhone SE
Safari page] + I -->|tap card| A[approval plane
owner signs] + A -->|approve / deny| P +``` + +- **auth-proxy** parks a request whose grant says `require_approval` and hands + a summary to a small local notifier running on the same host. +- **The notifier** is trusted local code, like the hooks: it alone may push + `allow.` cards, over loopback with the push token. Tailnet viewers can view + and dismiss cards but can never push one, so a card on the phone always + originates from the owner's host. +- **The card** carries a one-line summary and the approval-plane link as its + `--url`. It never carries the request body, a credential, or an approve + action. +- **The approval plane** is a separate surface (PWA or native app) holding the + owner's signing key. Tapping the card opens it; nothing about the signature + or the decision passes through nearpush. + +## The contract + +One parked request is one card, sender `auth-proxy` (which has a built-in +icon, a shield mark). The notifier keeps the event id that `nearpush send` +prints and follows the request to its outcome: + +| auth-proxy event | nearpush action | +|---|---| +| request parked | `nearpush send --sender auth-proxy --app auth-proxy --state allow --title "" --url ""` | +| approved and executed | dismiss the `allow.` card, then `nearpush send --sender auth-proxy --state all_set --title " — done"` | +| denied | dismiss the `allow.` card (no replacement card; a denial needs no attention) | +| expired with no decision | dismiss the `allow.` card, so a dead request never lingers as if it were actionable | + +Dismissal from the notifier uses the loopback path with the push token: + +```sh +curl -s http://127.0.0.1:8787/v1/dismiss \ + -H "Authorization: Bearer $(cat ~/.config/nearpush/token)" \ + -H 'Content-Type: application/json' \ + -d "{\"event_id\": }" +``` + +## One approval, end to end + +```mermaid +sequenceDiagram + participant C as Caller (Muse) + participant P as auth-proxy + participant N as notifier + participant D as nearpushd + participant I as iPhone + participant A as approval plane + C->>P: write request (surrogate) + P->>P: grant says require_approval: park + P-->>C: 202 + request id + P->>N: parked (summary + approval link) + N->>D: allow. card + D-->>I: big card + I->>A: tap card, open approval link + A->>A: owner reviews full request, signs + A->>P: approve (bound to request hash) + P->>P: execute exactly once, audit + P-->>C: outcome: executed + P->>N: outcome + N->>D: dismiss allow. card, push all set. +``` + +## Non-goals + +- No approve or deny control anywhere in nearpush — not on the card, not in + the drawer, not in any viewer-reachable endpoint. A compromised viewer must + gain nothing. +- No request payloads on cards beyond a one-line summary. The full request + lives on the approval plane only. +- No background notification promise: as the README states, nearpush is a + display, not a notifier. A parked request that nobody sees simply expires + in auth-proxy, which is the safe outcome.