Skip to content

feat: sign in with GitHub instead of pasting a token - #13

Merged
ProJedi1234 merged 2 commits into
mainfrom
device-flow-login
Aug 26, 2026
Merged

feat: sign in with GitHub instead of pasting a token#13
ProJedi1234 merged 2 commits into
mainfrom
device-flow-login

Conversation

@ProJedi1234

@ProJedi1234 ProJedi1234 commented Aug 26, 2026

Copy link
Copy Markdown
Owner

stele auth login used to have the user obtain a GitHub access token and paste it in for the server's exchange route to inspect. The server now proxies GitHub's device flow itself (ProJedi1234/stele-pages#47–#49), so login becomes: call POST /auth/github/device, print the verification URL and user code on stderr (never open a browser — this tool is usually run over SSH), and poll POST /auth/github/exchange with the device code until the server answers. On 201 the existing path runs unchanged — verify via /admin/whoami, write the credential to disk. On 401 the sign-in was refused or cancelled, and the remedy printed is to sign in again, since a lost token is recovered by rotation rather than by an operator. The CLI never holds a client ID or a GitHub access token at any point.

The poll loop (DeviceSignIn) sleeps before the first poll, only ever raises the interval (slow_down is a bigger number, not a new case; a smaller or unreadable interval never speeds it up), clamps every wait to the time remaining, and gives up after the server's expiresIn. No new flags and no new exit codes: --admin still skips the sign-in (a sign-in can only mint publish), and the pasted-token prompt survives as the fallback for a deployment that has not configured a client ID — which is also how the bootstrap token is spent on a fresh one.

Version goes 0.4.0 → 0.5.0 per this repo's precedent for feature commits; the server's minimumCLIVersion is untouched, since the version gate fires on writes and writes did not change.

Tests: swift build and just build (release) clean; just test: 188 tests in 21 suites, all passing — new DeviceFlowTests cover the request shapes, all four poll outcomes, interval growth and bounds, the expiry arithmetic, and that the bundle's description withholds the device code.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LBzRbsSw9JzfEg1p3a3FVo

Summary by CodeRabbit

  • New Features

    • Added GitHub device-flow sign-in for auth login, showing a verification URL and one-time code.
    • GitHub sign-in now provides publish-only credentials while keeping GitHub tokens off the client machine.
    • Added clear handling for pending, refused, expired, and failed sign-in attempts.
    • Retained token-based login for administrative use and deployments without device-flow support.
    • Updated credential metadata and messaging to distinguish login methods.
  • Documentation

    • Expanded authentication guidance, including verification steps, fallback options, and terminal visibility considerations.
  • Chores

    • Updated the client version to 0.5.0.

`stele auth login` now asks the server to start a GitHub device flow,
prints the code and the URL for a person to open, and waits. What it
stores is the publish-only credential the server minted at the end of
it.

The flow is proxied end to end: the OAuth app's client ID lives on the
server and the GitHub access token is consumed inside one server
request, so this machine holds neither. A login that pasted a GitHub
token would have taken a stele secret out of the agent's reach by
putting a GitHub one on the same disk.

The pasted-token path stays, and is not a legacy branch. `--admin`
takes it, because a sign-in mints `publish` and only `publish` and can
never produce the credential that flag asks to keep; and a deployment
that has not configured GitHub sign-in refuses the start route, which
is also how the first credential on a fresh deployment is made. Without
the fallback `auth login` would have no answer at all on those
deployments.

No new flags and no new exit codes. A terminal refusal is the server's
one byte-identical 401 — cancelled, not an owner, no allowlist — so it
becomes `.refused` rather than an error with nothing to say, and a 500
still throws: an outage must not read as a person saying no.

The poll loop lives in SteleKit with an injected clock, because "does
slow_down actually slow the polling down" and "does this give up when
the code expires" are the two questions the feature has and both are
about seconds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LBzRbsSw9JzfEg1p3a3FVo
@coderabbitai

coderabbitai Bot commented Aug 26, 2026

Copy link
Copy Markdown

Review Change Stack

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: 984e6052-5fb8-471b-b07e-01d0c2e07b5b

📥 Commits

Reviewing files that changed from the base of the PR and between 5fa542d and 8f50a9d.

📒 Files selected for processing (6)
  • README.md
  • Sources/SteleKit/DeviceFlow.swift
  • Sources/SteleKit/SteleClient.swift
  • Sources/SteleKit/Version.swift
  • Sources/stele/AuthCommands.swift
  • Tests/SteleKitTests/DeviceFlowTests.swift

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The CLI now defaults to GitHub device-flow authentication. The SteleKit client provides device-code endpoints and bounded polling. Credential storage records authentication origin. Admin and unsupported deployments retain token fallback. Documentation and version metadata were updated.

Changes

GitHub device sign-in

Layer / File(s) Summary
Device-flow contracts and client endpoints
Sources/SteleKit/DeviceFlow.swift, Sources/SteleKit/SteleClient.swift
Adds device-flow models, endpoint paths, request handling, response decoding, and pending, refused, or minted outcomes.
Polling coordinator and deterministic validation
Sources/SteleKit/DeviceFlow.swift, Tests/SteleKitTests/DeviceFlowTests.swift
Adds bounded polling with injectable timing. Tests cover responses, deadlines, intervals, device-code propagation, and redaction.
CLI sign-in and credential origins
Sources/stele/AuthCommands.swift, Sources/SteleKit/Version.swift
Defaults auth login to GitHub sign-in, preserves fallback paths, and stores origin-specific credential metadata and errors. Version 0.5.0 is reported.
Authentication documentation
README.md
Documents device-flow instructions, publish-only credentials, server-side token handling, fallback conditions, and TTY exposure.

Estimated code review effort: 4 (Complex) | ~45 minutes

Merge Risk: ⚪ Minimal · up to 8f50a

The CLI changes sign-in from pasted GitHub tokens to the server-mediated device flow while preserving the existing credential path and fallback behavior; no actionable merge-blocking risk remains.

Sequence Diagram(s)

sequenceDiagram
  participant Operator
  participant LoginCommand
  participant DeviceSignIn
  participant SteleClient
  participant AuthService
  participant CredentialStorage

  Operator->>LoginCommand: run auth login
  LoginCommand->>DeviceSignIn: start device sign-in
  DeviceSignIn->>SteleClient: request device code
  SteleClient->>AuthService: POST /auth/github/device
  AuthService-->>SteleClient: device code bundle
  SteleClient-->>DeviceSignIn: verification URL and user code
  DeviceSignIn->>AuthService: poll device-code exchange
  AuthService-->>DeviceSignIn: pending or minted/refused
  DeviceSignIn-->>LoginCommand: sign-in outcome
  LoginCommand->>CredentialStorage: store minted credential and origin
Loading

Poem

I’m a rabbit who watched the codes hop,

GitHub approval made polling stop.
Tokens stayed safely away,
While credentials found their way.
The CLI now knows which path to adopt.


Note

🎁 Summarized by CodeRabbit Free

Your organization is on the Free plan. CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please upgrade your subscription to CodeRabbit Pro by visiting https://app.coderabbit.ai/settings/billing.

Comment @coderabbitai help to get the list of available commands.

A stele server that predates the device flow answers the start route
with its uniform 404 — deliberately, so the route's absence is not a
probe result — and for as long as such servers exist, "this CLI is
newer than that server" is the likeliest meaning of the status. The
generic notFound error read as "wrong host" and sent the person
auditing a URL they had typed correctly.

The 404 now takes the same fallback the 401 does, with its own
sentence: the 401 is a deployment that answered and declined, the 404
is one that may simply be older, and the hedge is kept because all
this side has seen is a status an arbitrary non-stele host would also
produce.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LBzRbsSw9JzfEg1p3a3FVo
@ProJedi1234

Copy link
Copy Markdown
Owner Author

Added 8f50a9d: a 404 from the start route now falls back to the token prompt the way the 401 does, with its own stderr line ("the server may predate the device flow"). Found live: with no --host the CLI resolved the default host — a production server still on the old code — whose uniform 404 surfaced as the generic "the server has nothing there", which reads as a mistyped host rather than an older server. Verified against that same old server: the fallback line prints, then the non-TTY guard stops the prompt as designed. swift build clean; swift test: 188 tests in 21 suites passing.

@ProJedi1234
ProJedi1234 merged commit f87b21f into main Aug 26, 2026
1 check passed
@ProJedi1234
ProJedi1234 deleted the device-flow-login branch August 26, 2026 15:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant