Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<sender>`: the sender's icon. `~/.config/nearpush/icons/<sender>.svg`, then
`.png`, then a built-in icon (`kiro`: the official logo from <https://kiro.dev/icon.svg>),
`.png`, then a built-in icon (`kiro`: the official logo from <https://kiro.dev/icon.svg>;
`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
Expand Down
6 changes: 6 additions & 0 deletions crates/nearpushd/assets/icons/auth-proxy.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 4 additions & 1 deletion crates/nearpushd/src/assets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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</text>"));
Expand Down
97 changes: 97 additions & 0 deletions docs/auth-proxy.md
Original file line number Diff line number Diff line change
@@ -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<br/>local tool]
N -->|nearpush send<br/>loopback + token| D[nearpushd<br/>127.0.0.1:8787]
P -->|outcome| N
end
D -->|tailscale serve| T[HTTPS on tailnet]
T -->|wss /ws| I[iPhone SE<br/>Safari page]
I -->|tap card| A[approval plane<br/>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 "<one-line summary>" --url "<approval-plane link for this request>"` |
| approved and executed | dismiss the `allow.` card, then `nearpush send --sender auth-proxy --state all_set --title "<summary> — 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\": <id printed by nearpush send>}"
```

## 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.