feat(mobile): per-device pairing tokens - #27
Merged
Conversation
…manage
`hyperpanes pair` no longer hands the phone the master token. It POSTs the new
master-only `/devices` endpoint to mint a distinct, labelled, full-authority
(unscoped) token and embeds THAT in the QR, so the master credential never leaves
the host.
A scope is a whitelist of the panes that exist now, so it can't serve a full remote
head that must reach panes opened later — hence device tokens are unscoped, but:
* individually revocable — `hyperpanes revoke <label>` / DELETE /devices?label=
* listable — `hyperpanes devices` / GET /devices (never echoes tokens)
* optionally TTL'd — `hyperpanes pair --ttl 30d`
* persisted — device-tokens.json (0600, beside control.json),
reloaded on start, so pairing survives a host restart
(the guarantee the master token gets on remote binds).
All three /devices verbs are master-only, so a sandboxed agent's scoped token can
never mint or enumerate device credentials.
Core: TokenStore device methods + resolve/TTL/clear; persistence/device_tokens.rs;
server loads the table on start; routes for POST/GET/DELETE /devices.
App CLI: control_cli.rs (connect/base-url/ttl helpers), pair.rs mints via the API,
new devices.rs (`devices`/`revoke`), main.rs dispatch + usage.
Tests: 7 TokenStore device tests, 3 persistence tests, control_cli/base-url/ttl
tests, and a /devices HTTP round-trip (mint → full-authority → list → 403 for scoped
→ revoke → 401). Docs: mobile-client-plan.md security model + mobile README.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
For a public "pair your own host" mobile app,
hyperpanes pairwas handing every phone the master token. This wires pairing to mint a per-device credential instead, so the master token never leaves the host and each device can be listed and revoked independently.The existing scoped-token path (
POST /tokens) is the wrong tool for this: a scope is a whitelist of the panes that exist now, so a scoped token would go blind the moment a new pane opens — and scoped tokens are in-memory only (die on restart). A phone is a full remote head that must survive restarts, so device tokens are a new class: unscoped (full authority) but named, revocable, TTL-able, and persisted.What
hyperpanes pair [--device <label>] [--ttl <30d|12h|90m|<ms>>]— POSTs the new master-only/devicesendpoint, mints a device token, embeds that in the QR. TTL omitted = never expires.hyperpanes devices— list paired clients by label (tokens never echoed).hyperpanes revoke <label>— drop one without disturbing the others.device-tokens.json(0600, besidecontrol.json), reloaded on start → pairing survives a host restart, same as the master token on remote binds.POST/GET/DELETE /devicesare all master-only — a sandboxed agent's scoped token can't mint or enumerate device creds.The Flutter app needs no change (a device token is a bearer token like any other).
Files
control/tokens.rs(device methods + resolve/TTL/clear),persistence/device_tokens.rs(new),persistence/paths.rs,control/server.rs(load on start),control/routes.rs(endpoints).control_cli.rs(new — connect/base-url/ttl helpers),pair.rs(mint via API),devices.rs(new),main.rs(dispatch + usage).docs/mobile-client-plan.mdsecurity model, mobileREADME.md.Tests
TokenStoredevice tests (mint/resolve/TTL-prune/revoke-by-label/list/clear), 3 persistence round-trip tests,control_clibase-url + TTL-parse tests, and a/devicesHTTP round-trip (mint → full-authority on/state→ list-omits-token → 403 for scoped token → revoke → 401).🤖 Generated with Claude Code