Skip to content

feat(cli): add hyperframes auth login --api-key, status, logout#1081

Open
jrusso1020 wants to merge 1 commit into
mainfrom
05-26-feat_cli_add_hyperframes_auth_login_--api-key_status_logout
Open

feat(cli): add hyperframes auth login --api-key, status, logout#1081
jrusso1020 wants to merge 1 commit into
mainfrom
05-26-feat_cli_add_hyperframes_auth_login_--api-key_status_logout

Conversation

@jrusso1020
Copy link
Copy Markdown
Collaborator

@jrusso1020 jrusso1020 commented May 26, 2026

What

Introduces the hyperframes auth command group + a shared credential
store library that hyperframes-CLI and heygen-cli will both read from.

  • hyperframes auth login --api-key saves a HeyGen API key to
    ~/.heygen/credentials.json (stdin pipe or hidden-input prompt).
  • hyperframes auth status resolves the active credential (env vars
    → file) and verifies it against GET /v3/users/me, printing
    identity + billing.
  • hyperframes auth logout removes the credential (--keep-api-key
    drops only the OAuth block).

Internals (packages/cli/src/auth/):

  • paths.ts~/.heygen layout, HEYGEN_CONFIG_DIR override.
  • store.ts — read/write credentials.json (file 0600, dir 0700)
    with legacy single-line plaintext fallback so existing heygen-cli
    users don't lose their session.
  • resolver.ts — chain: HEYGEN_API_KEYHYPERFRAMES_API_KEY
    file (unexpired OAuth wins over api_key).
  • client.ts — hand-written typed wrapper for GET /v3/users/me
    (intentionally not OpenAPI codegen — single endpoint).
  • errors.ts — typed AuthError with discriminating code.

Why

This is the foundation for hyperframes cloud render. Splitting it
out keeps the cloud-render PR small and lets users sign in today.

The plan originally called for a library-only PR followed by a
commands PR. The fallow dead-code gate flagged the library-only
shape as unused exports, so I bundled them — the library and its
first consumers ship together. PR 3 (OAuth PKCE) and PR 4
(heygen-cli read-side JSON support) follow.

How

  • Credential file format: JSON with optional api_key + oauth
    blocks. Both CLIs read it; the resolver picks the freshest valid
    credential.
  • Auth header selection happens in the HTTP client: OAuth →
    Authorization: Bearer ..., API key → x-api-key: ....
  • HEYGEN_API_URL lets dev testing target api.dev.heygen.com
    without rebuilding.
  • The new auth command lazy-loads its subverbs (same pattern as
    lambda).

Test plan

  • Unit tests added (vitest) for paths, store, resolver,
    client, and errors — 45 tests, all green.
  • bunx tsc --noEmit -p packages/cli/tsconfig.json clean.
  • bunx oxlint + bunx oxfmt --check clean.
  • bunx fallow audit --base origin/main --fail-on-issues
    zero new findings.
  • Smoke test against dev API:
    HEYGEN_API_URL=https://api.dev.heygen.com hyperframes auth login --api-key
    then hyperframes auth status.

Copy link
Copy Markdown
Collaborator Author

jrusso1020 commented May 26, 2026

@jrusso1020 jrusso1020 force-pushed the 05-26-feat_cli_add_hyperframes_auth_login_--api-key_status_logout branch from d75ab76 to a72b6ac Compare May 26, 2026 16:57
@jrusso1020
Copy link
Copy Markdown
Collaborator Author

Self-review pass — addressed 13 of 15 findings. Force-push includes all fixes squashed into the original commit.

Fixed (critical/high):

  1. Filename mismatch~/.heygen/credentials.json~/.heygen/credentials so heygen-cli (which writes the same path, no extension) actually reads what we write. The plan doc consistently used the no-extension form; my task description had a typo.
  2. refreshable doc/code mismatch (resolver.ts) — now refreshable: expired && refresh_token !== undefined (was: true whenever refresh_token existed).
  3. status.ts uncaught INVALID_STORE — wrapped tryResolveCredential in try/catch with friendly hint + JSON-mode handling.
  4. writeStore before verify clobbered good keys — login now: validates hg_… shape pre-write, snapshots existing creds, merges (preserves existing oauth block), and rolls back to the previous credential on 401. Network/5xx errors still leave the new key in place per the transient-blip rationale.
  5. CRLF / header injection — added isHeaderSafe() check on all credential paths (JSON file, env vars, OAuth tokens). Rejects U+0000–U+001F + U+007F. New unit tests for each path.
  6. looksLikeApiKey too loose — tightened from "any printable ASCII 8+" to /^hg_[A-Za-z0-9_-]{5,}$/. Pasting a Stripe key / GitHub PAT now errors clearly.
  7. safeText leaked credentials in error bodies — added scrubCredentials() that redacts hg_… keys, JWTs, and x-api-key: / authorization: substrings before they reach error messages.
  8. res.json() unguarded — wrapped in try/catch, throws ErrApi on non-JSON 2xx.
  9. obj.data array unwrap — added !Array.isArray() guard.
  10. --keep-api-key skipped confirmation — now prompts (with different wording for OAuth-only logout).
  11. stdin hang on non-TTY/no-producer — wrapped readAll in 30s timeout with clear error.
  12. _writeToOutput monkey-patch — replaced with @clack/prompts.password() (already used by sibling commands, bundled at build time).
  13. login validates hg_… shape before disk write — garbage never lands in the store.

Deferred (noted, will address in a follow-up):

  • Temp-file race: tmp = ${path}.${pid}.${ms}.tmp collisions + symlink-attack defense. Fix would use crypto.randomBytes(8) suffix + flag: 'wx'. Single-process serial use is the supported contract; concurrent OAuth-refresh writes don't exist yet.
  • parseDate loose acceptance (e.g. new Date("2099") parses to 2099-01-01Z). Tighten to ISO-8601 regex when PR 3 lands and we care about the wire format.

Tests: 54 unit tests, all green. Lint/format/typecheck/fallow clean.

@jrusso1020 jrusso1020 force-pushed the 05-26-feat_cli_add_hyperframes_auth_login_--api-key_status_logout branch from a72b6ac to 5c81d96 Compare May 26, 2026 20:30
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