Add device-code enrollment and multi-token bearer auth#24
Merged
andrew-jon-p7a merged 6 commits intomainfrom Apr 29, 2026
Merged
Add device-code enrollment and multi-token bearer auth#24andrew-jon-p7a merged 6 commits intomainfrom
andrew-jon-p7a merged 6 commits intomainfrom
Conversation
Signed-off-by: andrew-jon-p7a <andrewprzybilla@gmail.com>
# Conflicts: # pnpm-lock.yaml
Replaces the "create a member, copy the token, paste it into your VM config" onboarding flow with a gh-auth-style device-code handshake (RFC 8628). The bearer plaintext now travels directly from the broker to the operator's CLI on its next poll — never through a copy-paste path that could leak via terminal scrollback, clipboard history, or chat threads. Server: - New SQLite-backed `tokens` table — multi-token-per-member, with `(label, origin, created_at, last_used_at, expires_at, created_by)`. Boot-time bootstrap migration copies each member's `tokenHash` from `ac7.json` into the store as `origin = 'bootstrap'`, idempotent across reboots. Auth resolver in `auth.ts` reads only from the token store after migration. - New SQLite-backed `pending_enrollments` table + `EnrollmentStore` for the device-code flow. KEK-wraps the issued plaintext at rest. - Five new endpoints: `POST /enroll` (anonymous mint, per-IP rate limited 10/hr), `POST /enroll/poll` (RFC 8628 wire shape with authorization_pending / slow_down / expired_token / access_denied as 400 + JSON), `GET /enroll/pending`, `POST /enroll/approve` (bind-or-create modes), `POST /enroll/reject`. All gated on `members.manage` for director actions. - Two new endpoints under members admin: `GET /members/:name/tokens`, `DELETE /members/:name/tokens/:id` — `members.manage` or self. `rotate-token` semantics now revoke every peer token before minting a fresh one. CLI: - New `ac7 connect` command with the box-banner UI, RFC 8628 poll backoff, SIGINT-safe cancellation, `--no-write` debug mode. - Saved bearer at `~/.config/ac7/auth.json` (mode 0o600 in 0o700 dir; XDG / macOS / Windows paths). `makeClient()` resolves tokens in order: `--token` flag → `AC7_TOKEN` → saved entry, so existing CI / scripted setups keep working unchanged. Web UI: - New `/enroll` page (top-level, gated by session): code entry, source IP / UA / labelHint visibility, bind-or-create toggle. Deep links via `?code=…` are cleared from history with replaceState. - New `MemberTokenList` under each member's Manage tab: per-row label / origin / created / last-used / expires + revoke. - New `PendingEnrollments` panel under MembersPanel — auto- refreshing, hidden when empty, inline reject + deep-link approve. Tests: 38 new server tests (TokenStore, EnrollmentStore, full endpoint flow including RFC 8628 outcomes and multi-token semantics after approval/rotation/delete) + 3 new CLI tests covering happy path, access_denied, and expired_token. Docs: README onboarding now points at `ac7 connect` with the env-var fallback documented for CI. New `docs/enrollment.mdx` covers the flow, RFC 8628 alignment, security posture, multi-token semantics, and the legacy `ac7 rotate` break-glass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Signed-off-by: andrew-jon-p7a <andrewprzybilla@gmail.com>
Resolves textual conflicts in the import sort order of three files where my new enrollment-related imports landed adjacent to imports added by the SaaS pairing PR (#18) and dusk styling PR (#23): - apps/server/src/app.ts (3 conflict blocks, all kept-mine) - packages/sdk/src/client.ts (4 conflict blocks, all kept-mine) - apps/server/test/channels-endpoints.test.ts (3 blocks, kept-mine) All resolutions are pure "keep my additions" — origin/main has nothing in those positions, the conflicts were textual artifacts of git's auto-merge giving up on adjacent alphabetized sorts. Also pulled in the dependabot-merged @hono/node-server 2.0.0 bump (#22). Verified no breaking-change impact: the serve() factory pattern we use is stable across the major version. All 717 tests pass and biome is clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Signed-off-by: andrew-jon-p7a <andrewprzybilla@gmail.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.
Summary
Replaces the "create a member, copy the token, paste it into your VM config" onboarding flow with a gh-auth-style device-code handshake (RFC 8628). The bearer plaintext now travels directly from the broker to the operator's CLI on its next poll — never through copy-paste, terminal scrollback, or chat threads.
ac7 connecton the device prints a short user code + verification URL; the director (signed in via TOTP) approves in the browser; the CLI's next poll resolves with the bearer and writes it to~/.config/ac7/auth.json(mode 0o600).tokensSQLite table replaces single-token-per-member. Each device gets its own labeled row; rotating no longer kicks every peer offline.{error: <code>}(authorization_pending/slow_down/expired_token/access_denied); success returns 200 with the bearer.--tokenflag andAC7_TOKENenv var still work — CLI'smakeClient()resolves in order: flag → env → saved entry. Existing CI scripts keep running.What's in here
Server
apps/server/src/tokens.ts—TokenStore(SQLitetokenstable, KEK-aware, debouncedlast_used_atupdates).apps/server/src/enrollments.ts—EnrollmentStore(SQLitepending_enrollments, KEK-wraps issued plaintext at rest, RFC 8628 outcomes).apps/server/src/app.ts— five new routes (/enroll,/enroll/poll,/enroll/pending,/enroll/approve,/enroll/reject) + two token-mgmt routes (GET/DELETE /members/:name/tokens(/:id)). Per-IP rate limit (10/hr) on/enroll.apps/server/src/auth.ts— bearer resolver now reads fromTokenStore; bindstokenIdto the request context.apps/server/src/run.ts— boot-time idempotent migration ofac7.jsontokenHash→tokenstable.SDK — types, schemas, paths, client methods for the eight new endpoints.
CLI — new
ac7 connectcommand with box-banner UI, RFC 8628 poll backoff, SIGINT-safe cancellation. Newauth-config.tshelper for~/.config/ac7/auth.json(XDG / macOS / Windows aware).Web — new
/enrollSPA route (handles its own anonymous → login bounce),MemberTokenListunder member admin,PendingEnrollmentspanel auto-refreshing under MembersPanel.Docs — README onboarding now points at
ac7 connect; newdocs/enrollment.mdxcovers the flow, RFC 8628 alignment, security posture, multi-token semantics, and the legacyac7 rotatebreak-glass.Security posture
/enrollrate limit + 5-min single-use TTL.expired_token.members.managerequired to approve/reject. The operator runningac7 connectcannot approve themselves.Test plan
tokens.test.ts,enrollments.test.ts,enroll-endpoints.test.ts)connect.test.tscovering happy path,access_denied,expired_token)pnpm run lint: 0 errors, 0 warningsac7 serve, runac7 connecton a second machine, approve via web UI, verify token works forac7 claude-code?code=…is cleared from history bar after readDecisions worth flagging in review
ac7 connect, notac7 enroll— the latter already exists for TOTP enrollment for web UI sign-in. The two are semantically different (machine onboarding vs human web-login enrollment); keeping them distinct avoids overloading.ac7 rotatesemantics changed. Previously: single-token replacement. Now: revokes every active token for the member and mints one fresh — the break-glass posture for "I think a token leaked." Adding a new device should useac7 connect, which adds a peer token without touching the others.ac7.jsontokenHashis no longer load-bearing after first boot. SQLite is the resolver's source of truth; the JSON hash is preserved for human-readability and hand-edit support but the resolver doesn't read it after migration.logger.info()calls on every state mutation (enrollment minted/approved/rejected,token revoked/rotated, etc). A dedicatedaudit_logSQL table is a future enhancement; the structured log is sufficient for v1.🤖 Generated with Claude Code