Add a local web interface - #8
Merged
Merged
Conversation
A new 'ui' command serves a single-page interface over the Management API, covering everything the CLI does: listing keys with usage and limits, adding, renaming, deleting, per-key and server-wide caps, and QR codes. It follows the system light or dark theme and adds no dependencies — the page is inlined, and the QR reuses qrcode-terminal's block output, which renders as a scannable code in the browser. The Management API URL is full admin control of the server, so the interface is deliberately narrow. It never reaches the browser: the local server holds it and proxies. Three guards back that up — it binds loopback only; every API call must carry a token minted at startup and handed over in the printed URL, which also forces a CORS preflight on any cross-origin attempt; and requests whose Host header is not the loopback address are refused, which is what stops DNS rebinding. Two bugs found while testing rather than after shipping: - The page's own fetch calls were blocked by its Content-Security-Policy. connect-src falls back to default-src, which was 'none'. Caught by driving the real page in a browser; a test now asserts the directive. - start() built the request handler before listen() assigned a port, so the Host check compared against the requested port. With --port 0 the kernel picks one and every request would have been refused. The handler is now built from the bound port. Adds getServerInfo() to the client to read the server-wide limit, and 23 tests driving the handler over a real socket with a stand-in client, so they need no Outline server or certificate. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QJR2DDBimsijgYgZS3bUS8
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.
A new
uicommand serves a single-page interface covering everything the CLI does. This was the "Web App" item on outline-br's old roadmap.The page lists keys with usage, limit and access URL, and handles add, rename, delete, per-key and server-wide caps, and QR codes. It follows the system light/dark theme.
No new dependencies. The page is inlined — no build step, no bundler, no CDN. The QR reuses
qrcode-terminal's block output, which renders as a genuinely scannable code in the browser, so nothing new was needed for it either.Security
The Management API URL is full administrative control of the server, so the interface is deliberately narrow. It never reaches the browser — the local server holds the credential and proxies each call. Three guards back that up:
127.0.0.1, so nothing else on the network can reach it.Hostis not the loopback address are refused — this is what stops DNS rebinding from turning an attacker's domain into a route to127.0.0.1.Two bugs caught while testing
Both found before shipping rather than after, and both now covered by tests:
The page's own
fetchcalls were blocked by its own CSP.connect-srcfalls back todefault-src, which was'none', so the table stayed on "Loading…" forever. Only visible by driving the real page in a browser — every curl test passed. A test now asserts the directive is present.start()built the request handler beforelisten()assigned a port. The Host check therefore compared against the requested port. With--port 0, where the kernel picks the port, every request would have been refused with 403. The handler is now built from the bound port.Testing
62 tests, all passing (23 new). The server tests drive the handler over a real socket with a stand-in client, so they need no Outline server and no certificate — fast, and no
opensslgate.Beyond the unit tests I drove the actual page in headless Chromium: loading, adding a key through the dialog, opening the QR modal, setting a limit, and both colour schemes, with zero console errors. Contrast was measured rather than eyeballed — every text/background pair passes WCAG AA in both themes (link 5.17 light / 6.49 dark, muted URL 4.83 / 6.40). Also verified
--port 0, port-in-use, an invalid port, and clean SIGTERM shutdown.Also
getServerInfo()added to the client, so the interface can show the server-wide default cap.Generated by Claude Code