Skip to content

feat(wallet): oauth login flow via dashboard.zerion.io#95

Merged
zerts merged 1 commit into
mainfrom
cli-support-oauth-flow-in-dashboard-wlt-1728
Jul 14, 2026
Merged

feat(wallet): oauth login flow via dashboard.zerion.io#95
zerts merged 1 commit into
mainfrom
cli-support-oauth-flow-in-dashboard-wlt-1728

Conversation

@zerts

@zerts zerts commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

Add a modern browser login flow (like claude / gh auth login): the CLI starts a loopback server, opens dashboard.zerion.io/oauth/authorize, and captures the API key from the redirect. Exposed as a standalone zerion login command and as an interactive picker in zerion init (browser login first, then paste-a-key, then pay-per-call info).

  • cli/utils/api/oauth.js: loopback authorize flow (client_id=zerion-cli, 127.0.0.1 bind, state CSRF guard, 302 to /oauth/success, 5m timeout)
  • cli/utils/api/interactive-auth.js: shared auth-method picker
  • cli/utils/common/select.js: arrow-key single-select extracted from policy-picker.js (now reused there)
  • cli/utils/common/browser.js: shared openBrowser helper
  • ZERION_DASHBOARD_URL override for staging/local QA
  • README + --help + router usage updated

Add a modern browser login flow (like `claude` / `gh auth login`): the CLI
starts a loopback server, opens dashboard.zerion.io/oauth/authorize, and
captures the API key from the redirect. Exposed as a standalone `zerion login`
command and as an interactive picker in `zerion init` (browser login first,
then paste-a-key, then pay-per-call info).

- cli/utils/api/oauth.js: loopback authorize flow (client_id=zerion-cli,
  127.0.0.1 bind, state CSRF guard, 302 to /oauth/success, 5m timeout)
- cli/utils/api/interactive-auth.js: shared auth-method picker
- cli/utils/common/select.js: arrow-key single-select extracted from
  policy-picker.js (now reused there)
- cli/utils/common/browser.js: shared openBrowser helper
- ZERION_DASHBOARD_URL override for staging/local QA
- README + --help + router usage updated

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@linear-code

linear-code Bot commented Jul 14, 2026

Copy link
Copy Markdown

WLT-1728

@zerts

zerts commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator Author

✅ Loop review — iteration 1/5

Verdict: approved

Severity Area Finding
minor tests New browser-login flow (oauth.js / login.js / interactive-auth.js / select.js) has no automated test coverage (cli/utils/api/oauth.js:1)
minor code login.js HELP object is unreachable — router intercepts --help/-h before command dispatch (mirrors pre-existing init.js quirk) (cli/commands/login.js:141)

Summary

Reviewed PR #95 at head 7107f1fbd6d3338e353e2f3d0d6a7a51a24dbb96 (branch
cli-support-oauth-flow-in-dashboard-wlt-1728) against WLT-1728 and the PRD
comment on the issue (authored by the assignee — the authoritative spec; there
are no human PR review threads to reconcile).

The change adds a modern browser (OAuth-style) login to the CLI: a zerion login command and an interactive auth-method picker wired into zerion init.
The CLI starts a loopback HTTP server on 127.0.0.1 (ephemeral port), opens
dashboard.zerion.io/oauth/authorize?client_id=zerion-cli&redirect_uri=<loopback>&state=<256-bit random>,
and captures the API key from the redirect, verifying state as a CSRF guard
before trusting the credential. A shared arrow-key selector was extracted from
policy-picker.js into cli/utils/common/select.js and re-consumed there.

Everything in the spec is implemented and verified working end-to-end. All 299
unit tests pass; the auth integration suite skips cleanly (needs paid keys). I
drove every changed flow live (see verification log): the happy path, all three
OAuth error branches, the non-TTY guard, and all four interactive-picker paths.
Only two minor findings, neither blocking — hence approved.

Findings

[minor/tests] New browser-login flow has no automated test coverage

cli/utils/api/oauth.js, cli/commands/login.js, cli/utils/api/interactive-auth.js,
and cli/utils/common/select.js are all new and none are referenced by any test
(grep over cli/tests for oauth|login|selectOne|interactive-auth|authenticateWithBrowser
returns nothing). The repo otherwise has strong test conventions (299 tests,
including a dedicated auth.test.mjs and config.test.mjs). The
security-relevant branches — state mismatch (CSRF), access_denied, and
no-code — are pure functions of the callback query string and would be cheap
to cover with a unit test that drives authenticateWithBrowser against a fake
loopback callback (exactly what I did manually). buildAuthorizeUrl is even
exported, suggesting it was intended to be testable. The ticket asked the
implementer not to run the (paid) integration tests and to provide manual QA
steps instead, so this is a reasonable omission — noting it as a follow-up, not
a blocker. All branches were verified working manually.

[minor/code] login.js HELP object is never displayed

cli/commands/login.js:141 defines a rich HELP object and login() opens with
if (flags.help || flags.h) { print(HELP); return; } — but router.dispatch
short-circuits on flags.help || flags.h and prints the global usage before
dispatching to any command, so zerion login --help (and -h) never reach that
branch. Confirmed live: zerion login --help prints the global usage JSON, not
the command HELP. This exactly mirrors the pre-existing behavior of
zerion init --help, so the PR is following an existing convention rather than
introducing a new defect — the command-level HELP in init.js is unreachable
the same way. Left as a minor note; not a regression this PR introduced.

Verification log

  • Head SHA reviewed: 7107f1fbd6d3338e353e2f3d0d6a7a51a24dbb96
  • Tests:
    • npm install (package-lock.json, npm 11.14.1, node v22.14.0) — clean.
    • npm test (unit) — 299 pass / 0 fail (69 suites), incl. init.test.mjs
      and router.test.mjs which still pass after the significant init.js rewrite
      (they exercise the non-TTY / --yes / --no-auth paths, which are unchanged).
    • npm run test:integration:auth — 4 tests skipped (require paid
      TEST_ZERION_* keys); expected, not a failure. No pre-existing failures
      observed in the unit suite.
  • Live (CLI flow driving — no web app, so no browser/Playwright; drove the
    actual zerion login loopback server directly):
    scratch dir
    /tmp/loop-review-eGu2mP (driver.mjs = OAuth branch matrix via a real
    loopback callback; pty_driver.py / init_pty.py = interactive picker over a
    real pty). Each scenario used a throwaway $HOME and
    ZERION_DASHBOARD_URL=http://127.0.0.1:... (never touched real config or the
    production dashboard).
    • zerion login --browser --no-open happy path → authorize URL carries
      client_id=zerion-cli, loopback redirect_uri, 43-char base64url state;
      callback with code+state302 to <dashboard>/oauth/success,
      apiKey persisted to config, stdout {ok:true,action:"login",method:"oauth"},
      exit 0. Process exited ~26 ms after the callback (no server-close hang).
    • Deny (error=access_denied) → 200 friendly page, oauth_denied, exit 1, no
      config written.
    • Tampered state400, body "state mismatch…", oauth_state_mismatch,
      exit 1, no config (CSRF guard works).
    • No code/error → 400, oauth_no_code, exit 1, no config.
    • Non-TTY without --browsernot_interactive error, exit 1.
    • ZERION_DASHBOARD_URL override honored for both the authorize and success URLs.
    • Interactive picker (real pty): default = browser login (option 0, Enter) →
      OAuth flow + key saved; ↓+Enter = paste (masked readSecret, key saved,
      method paste); ↓↓+Enter = pay-per-call (prints env guidance, persists
      nothing, method payg); Esc = "Cancelled — no changes made" (user_cancelled).
    • zerion init --no-install --no-skills (real pty) → auth step opens the same
      picker; Esc records {step:"auth",skipped:true,reason:"user_cancelled"} and
      onboarding continues (exit 0). Onboarding extended with auth as the first
      interactive step, per the ticket.
    • Already-configured key → prints "! An API key is already configured —
      continuing will replace it." before proceeding.
    • policy-picker.js refactor: no leftover pickOne refs; all ANSI/BACK
      symbols imported from select.js; both modules import-load cleanly;
      selectOne logic is byte-for-byte the old pickOne plus an allowBack
      option defaulting to true (so policy-picker behavior is unchanged).
  • Console/network: clean — no stray output; stdout stays a pure JSON channel
    (all human text goes to stderr), verified across every scenario above.
  • Animation: no animation/motion code in the diff (ANSI terminal menu redraw
    only; not CSS/JS animation).

Posted by ai-runner.

@zerts zerts changed the title WLT-1728: add browser (OAuth) login to CLI feat (wallet): oauth login flow via dashboard.zerion.io Jul 14, 2026
@zerts zerts changed the title feat (wallet): oauth login flow via dashboard.zerion.io feat(wallet): oauth login flow via dashboard.zerion.io Jul 14, 2026
@zerts
zerts merged commit c1a1cf4 into main Jul 14, 2026
1 check passed
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