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
@jrusso1020 jrusso1020 force-pushed the 05-26-feat_cli_add_hyperframes_auth_login_--api-key_status_logout branch from 5c81d96 to da2c2ea Compare May 26, 2026 23:51
Copy link
Copy Markdown
Collaborator

@miguel-heygen miguel-heygen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really clean work — the module split (paths/store/resolver/client/errors) is well-layered and the test coverage is solid. A few things I noticed:

store.ts rename+chmod race — The write path does rename(tmp, path) then chmod(path, FILE_MODE). Between those two calls, the file briefly has whatever perms the renamed inode carried. Since you already chmod the tmp file before renaming, the post-rename chmod is only needed for the overwrite case where the destination had looser perms. You could drop the second chmod entirely and rely on the pre-rename one — rename atomically replaces the inode so the new file's permissions come from the tmp.

SOURCE_LABELS mismatchstatus.ts hardcodes ~/.heygen/credentials.json in the label for file_json, but paths.ts defines CREDENTIAL_FILENAME = "credentials" (no .json extension). Users will see a misleading path in auth status output.

tryResolveCredential error check — Uses (err as { code?: string }).code === "NOT_CONFIGURED" instead of the isAuthError guard that's already imported nearby. Inconsistent and could match a non-AuthError with a .code property.

Rollback on fresh machine — If someone runs hyperframes auth login --api-key=bad_key on a fresh machine and the key fails verification, the rollback says "No previous credential to restore" and leaves the invalid key on disk. Deleting the file entirely seems like the better default here — you don't want a stale bad key sitting in the store.

Missing cli.mdx docs — CLAUDE.md checklist requires new commands to be documented in docs/packages/cli.mdx. Didn't see that in the diff.

None of these are blockers, solid PR overall.

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.

2 participants