feat(auth): browser-based Stytch OAuth login + environment switching#6
Merged
Conversation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The PyPI name "assemblyai-cli" is already taken by a third party, which blocks the clean `uv tool install` / `pipx install` / Homebrew path. Rename the distribution to the available "aai-cli". The `aai` command and the `assemblyai_cli` import package are unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Match the new distribution name (aai-cli). Renames the package directory,
updates all imports, the `aai` console-script entry point, hatchling wheel
packages, mypy files, scripts, and the uv.lock root package name. No behavior
change; the keyring service name ("assemblyai-cli") is intentionally preserved
to keep already-stored credentials readable.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fixes found during end-to-end sandbox testing: - Start B2B OAuth discovery on test.stytch.com (Stytch's API domain), not the vanity *.customers.stytch.dev CNAME, so the CSRF state cookie is set on the same domain as the OAuth callback — fixes `oauth_invalid_state`. - Point at the Stytch project that ams.sandbox000 is bound to (public-token-test-a161155e…) so AMS authenticates the discovery token. - Add env-overridable SIGNUP_URL (sandbox default) and surface it in the friendlier 0-org "no AssemblyAI account yet" message. Verified end-to-end against sandbox: browser login → AMS discover/exchange → find-or-create "AssemblyAI CLI" key → stored in keychain; second login reuses the same key (idempotent). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a named-environment registry (production, sandbox000) mapping the full backend host set: api_base (/v2 upload+transcript), streaming_host (/v3/ws), llm_gateway_base (/v1), AMS, and Stytch domain/public-token/signup URL. Select with the global --env <name> / --sandbox flags or AAI_ENV; `aai login` records the resolved env on the profile so a credential and its hosts always stay matched — this structurally prevents the sandbox-key-against-prod-host rejection (HTTP 1008 / 401). Precedence: --env > AAI_ENV > profile > default. client.py (SDK base_url + streaming api_host), llm.py (gateway base), and the auth modules now resolve hosts from the active environment instead of hardcoded prod literals; whoami reports the active env. Default is sandbox000 for now; flip DEFAULT_ENV to production once prod AMS/Stytch values are stood up. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The gateway returns 401/403 both for an invalid key and for a plan-entitlement
block ("Your account does not have access to LLM Gateway"). Mapping that to
auth_failure() told unpaid accounts to re-run `aai login`, which never helps.
Surface the gateway's own message as APIError so the cause is actionable.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address code-review findings in the new browser-OAuth login + environment switching. All changes are defensive; the happy path is unchanged. - loopback: map a port-bind OSError (port 8585 already in use) to a clean APIError instead of letting a raw traceback escape run_login_flow. - flow: tolerate AMS responses that key the token name as `token_name` (not `name`) and skip tokens whose `api_key` the list omits, so an existing CLI token is reused rather than crashing or duplicated. - flow: note the chosen organization when an identity has more than one, instead of silently picking the first. - flow: wrap required response fields in `_require()` so an unexpected 200 shape surfaces as "Run 'aai login' again" rather than a KeyError. - flow: read the account id from the exchange() response instead of a second GET /v1/auth round-trip. - config: map a malformed config.toml (TOMLDecodeError) to a clean CLIError so every command no longer aborts with a traceback. - context/main: warn when an explicit --env contradicts the profile's stored env (the stored key won't authenticate against the other hosts). - ams: extract a shared _client() helper for the five httpx.Client blocks. Adds regression tests for each (TDD). 545 pass, 93% branch coverage. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`uv lock` relocates the editable root-package entry under its new `aai-cli` name; no dependency versions change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CI runs `ruff format --check` with the locked ruff 0.15.16; reformat the files whose style differs under that version. No behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.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 paste-your-API-key login with a browser-based Stytch B2B OAuth
flow, and adds
--env/--sandboxenvironment switching bound to each profile.aai loginnow opens the browser, captures the OAuth callback on a fixedloopback port, discovers the account via AMS, and mints/reuses an
AssemblyAI CLIAPI key — with--api-keyretained as a non-interactiveescape hatch for CI.
Also renames the package/distribution
assemblyai_cli → aai_cli/aai-cli(the PyPI name
assemblyai-cliis squatted).What's new
aai_cli/auth/— OAuth login flow:discovery.pybuilds the Stytch B2B OAuth discovery start URLloopback.pybinds a fixed-port loopback server to capture one callbackams.pyis the Accounts Management Service client (discover / exchange / auth / projects / tokens)flow.pyorchestrates browser → AMS → API keyaai_cli/environments.py— per-environment host bundles (production / sandbox000), selected via--env,--sandbox,AAI_ENV, or the profile's stored env; default issandbox000until prod AMS/Stytch is stood up.aai_cli/context.py—AppState+ environment/profile resolution; warns when an explicit--envcontradicts the profile's bound env.Hardening (from code review)
APIError, not a raw traceback.token_namevsname, skip tokens without an exposedapi_key, and map unexpected 200 shapes to a clean "run login again" error instead ofKeyError.config.toml→ cleanCLIErrorinstead of aTOMLDecodeErrortraceback on every command.Note
Loopback OAuth CSRF (a
statenonce) was intentionally deferred: the only robust fix appends a query param todiscovery_redirect_url, which depends on Stytch preserving extra query params rather than strict-exact-matching the registered redirect. To confirm before implementing.Test plan
scripts/check.sh— 545 unit tests pass, 93% branch coverage (gate 90%), ruff + mypy clean.-m e2e) require a live key + kokoro and are not run here.🤖 Generated with Claude Code