mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 12:54:29 +00:00
## What Adds OAuth 2.0 + PKCE login as the default for `hyperframes auth login`, plus refresh-token + 401 auto-retry + `auth refresh`. Stacks on top of PR #1081 (the API-key + shared store work). - `hyperframes auth login` (no flags) — opens the user's browser to `/v1/oauth/authorize`, captures the code on an ephemeral `127.0.0.1:<port>/oauth/callback`, exchanges it for tokens with PKCE S256, and persists. `--api-key` opts back into the legacy long-lived-key path from PR #1081. - `hyperframes auth refresh` — force-refresh the OAuth access token using the stored refresh_token. Mostly useful for testing the path. - `hyperframes auth logout` — best-effort revokes via `POST /v1/oauth/revoke` (RFC 7009) before wiping local state. - `AuthClient` now refreshes-and-retries once on a 401 when the caller wires `onUnauthenticatedRefresh`. `auth status` wires it. Internals added in `packages/cli/src/auth/`: - `pkce.ts` — RFC 7636 code_verifier + S256 code_challenge. - `loopback.ts` — ephemeral 127.0.0.1 HTTP server; state validation, 120s timeout, styled success/error page. - `browser.ts` — wraps `open` with a `BROWSER=none` / `HF_NO_BROWSER=1` fallback that prints the URL. - `oauth.ts` — `startAuthorizationCodeFlow`, `refreshTokens`, `revokeTokens`, `requireOAuthConfigured`, `parseTokenResponse`. ## Why This is the foundation OAuth flow that lets free-tier users authenticate without managing a long-lived key. Refresh + auto-retry means CLI commands keep working past the access_token lifetime without bugging the user. The OAuth client_id (`q2A2QRSke2LrFTPJhoDbHtXh`) is the one James created in the `oauth2_client` table. Baked in as a build-time default; override via `HYPERFRAMES_OAUTH_CLIENT_ID` for dev/test. ## How - Public client: PKCE only, no `client_secret`. Backend already requires PKCE (`movio/logic/oauth2.py:638`). - Loopback port is ephemeral (`server.listen(0)`) — the backend wildcards localhost ports for public clients (`movio/model/oauth2.py:check_redirect_uri`), so the registered redirect URI's port is a placeholder. - State parameter is generated per-flow + validated on callback to prevent CSRF. - Token-response parsing is permissive on `expires_in` type (some servers return it as a string) but strict on `access_token` presence. - 401 retry happens at the `AuthClient.fetchUser` layer, not the command layer — so future endpoints inherit it for free. - `persistOAuth` merges into the existing store (preserves co-located `api_key`). `auth login` (API-key path) does the symmetric thing. ## Test plan - [x] 80 unit tests, all green. `vitest run src/auth/`. - [x] PKCE: verifier within 43-128 chars, challenge = SHA-256, S256 method, distinct outputs each call. - [x] Loopback: state mismatch / IdP error / missing-code / timeout / 404 non-callback paths all rejected; success path captures `code`. - [x] OAuth: `refreshTokens` posts correct body, persists, throws `REFRESH_FAILED` on 400/401 and `API_ERROR` on 5xx. Existing api_key preserved on refresh. - [x] AuthClient: 401 retries with refreshed bearer on OAuth, does NOT retry for api_key, returns 401 if refresh hook fails. - [x] `bunx oxlint` / `bunx oxfmt --check` / `bunx tsc` clean. - [x] `bunx fallow audit --base origin/main --fail-on-issues` — only inherited `help.ts:showUsage` finding (from main, not this PR). - [ ] Smoke test against dev API: `HEYGEN_API_URL=https://api.dev.heygen.com hyperframes auth login` then `hyperframes auth status` then `hyperframes auth refresh`. ## Out of scope - Cloud render commands — separate plan. - PR 4 (heygen-cli read-side JSON support) — independent, ships after.