add POST /api/vpn/enroll for OffTail

The in-app Tailscale needs one thing from the platform: a way to turn an authenticated
Officer session into a Headscale pre-auth key, so the phone registers itself instead of
someone pasting a key by hand. The VPN's control and data planes talk directly to
Headscale — never through /api — so this is not a proxy and should not become one.

Headscale itself runs on a separate host, managed manually. The platform consumes
HEADSCALE_URL (also returned as controlUrl) and HEADSCALE_API_KEY, both from the host
env. Neither is set yet, which is why an unconfigured instance answers 503 rather than
crashing — Headscale is being stood up in parallel.

The response is `{ controlUrl, authKey }` exactly, because enrollVpn() in
@officer/core/officer-net.ts reads those two fields; changing the shape means changing the
app.

Two things the spec's sketch does not do:

- The `user` field changed meaning across Headscale versions — a name on <=v0.22, a
  numeric id on v0.23+ — and we cannot see which one is running from here. So it resolves
  the id via /api/v1/user and tries that first, falling back to the name. Whichever the
  live server accepts wins, and neither version needs a config flag.
- Upstream calls carry a 10s timeout, and upstream error bodies are logged but never
  returned to the client: that is an admin API and its errors are descriptive.

The standalone app's origin follows the platform's own convention rather than the spec's
literal: every other app origin is an env var with a scope rule, so this one is
OFFICER_TAIL_ORIGIN (set in .env on this host), restricted to /api/auth + /api/vpn the way
OffVault is restricted to /api/auth + /api/vault. The OffTail tile embedded in the main
Officer app needs nothing — it reuses OFFICER_APP_ORIGIN.

Not implemented: the optional GET/DELETE /api/vpn/devices. They are not needed for a first
connection and are better written against a running Headscale.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-30 08:43:12 +00:00
co-authored by Claude Opus 5
parent 3da97ffe2e
commit 44a9124ef5
3 changed files with 120 additions and 1 deletions
+2
View File
@@ -22,6 +22,7 @@ import { router as fileBrowserRouter } from './api/file-browser/router';
import { musicRouter } from './api/music/router';
import { vaultRouter } from './api/vault/router';
import { slskdRouter } from './api/slskd/router';
import { vpnRouter } from './api/vpn/router';
import { systemMonitorRouter } from './api/system-monitor/system-monitor';
import { activityRouter } from './api/activity/router';
import './api/music/sidecar-server'; // side-effect: capture the officer-music audio server port
@@ -105,6 +106,7 @@ protectedRouter.route('/task-logs', taskLogsRouter);
protectedRouter.route('/file-browser', fileBrowserRouter);
protectedRouter.route('/music', musicRouter);
protectedRouter.route('/slskd', slskdRouter);
protectedRouter.route('/vpn', vpnRouter);
protectedRouter.route('/system-monitor', systemMonitorRouter);
protectedRouter.route('/activity', activityRouter);
protectedRouter.route('/dev-server', devServerRouter);