Skip to content

auth: show friendly user name/email on login + auth status#197

Merged
jrusso1020 merged 2 commits into
mainfrom
06-26-show_friendly_user_info_on_auth_status
Jun 26, 2026
Merged

auth: show friendly user name/email on login + auth status#197
jrusso1020 merged 2 commits into
mainfrom
06-26-show_friendly_user_info_on_auth_status

Conversation

@jrusso1020

Copy link
Copy Markdown
Collaborator

Description

Per Somansh's ask on the heygen-cli OAuth thread (James greenlit):
opaque user_ids are unfriendly in auth status output. After login
(both OAuth and API-key paths), fetch /v3/users/me once and persist
email + first_name + last_name + username into
~/.heygen/credentials alongside the credential. auth status
surfaces them in the JSON envelope under credential.user.

Schema

  • New optional user block on jsonCredentials (json tag omitempty).
  • All inner fields (email, first_name, last_name, username)
    are omitempty too so a partially-populated block doesn't litter
    the file with empty strings.
  • The block is additive METADATA, not a credential. Single-credential
    invariant (at-most-one of api_key / oauth) is unchanged; the
    user block is safe to persist alongside either credential type.
  • New public API in internal/auth: UserInfo, SaveUserInfo,
    LoadUserInfo, ClearUserInfo + UserInfo.DisplayName() /
    UserInfo.IsZero() helpers.

Login behavior

  • OAuth path already probed /v3/users/me to surface "Logged in
    as ..." on stderr; that path now also persists the result. Probe
    signature returns UserInfo (was username, email) so the
    persisted block carries first/last name too.
  • API-key path did NOT probe before. Add a best-effort probe using
    x-api-key, persist the result, and emit "Logged in as ..." on
    stderr to match the OAuth path.
  • Both paths handle probe failure gracefully: warn on stderr,
    proceed with the login. The credential is NEVER rolled back on
    a probe failure (tokens / key are still on disk and usable).
  • On probe failure, any stale user block from a prior login is
    cleared so auth status doesn't surface the wrong account.

auth status output

  • New credential.user block in the JSON envelope: email,
    first_name, last_name, username, display_name (the
    resolved priority email > "first last" > username).
  • Only present when the active credential's source is the file
    AND a user block is persisted. Env-source credentials
    (HEYGEN_API_KEY) skip the block — the on-disk one could
    belong to a different key.
  • Strictly additive; the existing data envelope from
    /v3/users/me is unchanged.

Display priority

email > first_name + last_name > username > user_id
(existing fallback). Matches the ask on the thread.

Backwards compatibility

  • Credentials files without the user block (pre-this-change logins)
    parse fine. They show user_id like today; re-login populates the
    friendly fields. No migration needed.
  • The single-credential invariant is preserved end-to-end.

Testing

  • internal/auth: UserInfo.DisplayName priority order, IsZero
    gate, Save/Load round-trip, Save preserves api_key, Save with
    empty UserInfo is a no-op (no file rewrite), Load on absent
    file returns zero, Load on legacy file (no user block) returns
    zero, Clear leaves credential intact, Clear on no-credential
    state removes the file, Clear when no user block is a no-op,
    omitempty schema guard.
  • cmd/heygen: API-key login probe persists friendly fields,
    probe failure is non-fatal (key still on disk), probe failure
    clears stale user block, OAuth login persists full schema
    (first_name, last_name too), auth status surfaces the
    persisted user block, legacy file falls back without the block,
    env-source credential deliberately skips the block.
  • Updated existing OAuth tests to assert the new email-preferred
    display (Logged in as jane@example.com vs the old username
    Logged in as demo) — the friendly-display change is the whole
    point.
  • go test -race ./... clean.
  • go vet ./... clean.
  • golangci-lint run ./... clean (0 issues).

— Jerrai (https://claude.com/claude-code)

Per Somansh's ask on the heygen-cli OAuth thread (James greenlit):
opaque user_ids are unfriendly in `auth status` output. After login
(both OAuth and API-key paths), fetch /v3/users/me once and persist
email + first_name + last_name + username into ~/.heygen/credentials
alongside the credential. `auth status` surfaces them in the JSON
envelope under credential.user.

Schema
- New optional `user` block on jsonCredentials (json tag omitempty).
- All inner fields (email, first_name, last_name, username) are
  omitempty too — a partially-populated block doesn't litter the
  file with empty strings.
- The block is additive METADATA, not a credential. Single-credential
  invariant (at-most-one of api_key / oauth) is unchanged; the user
  block is safe to persist alongside either credential type.
- New public API in internal/auth: UserInfo, SaveUserInfo,
  LoadUserInfo, ClearUserInfo + UserInfo.DisplayName() /
  UserInfo.IsZero() helpers.

Login behavior
- OAuth path already probed /v3/users/me to surface "Logged in as
  ..." on stderr; that path now also persists the result. Probe
  signature returns UserInfo (was username, email) so the persisted
  block carries first/last name too.
- API-key path did NOT probe before. Add a best-effort probe using
  x-api-key, persist the result, and emit "Logged in as ..." on
  stderr to match the OAuth path.
- Both paths handle probe failure gracefully: warn on stderr,
  proceed with the login. The credential is NEVER rolled back on a
  probe failure (tokens / key are still on disk and usable).
- On probe failure, any stale user block from a prior login is
  cleared so `auth status` doesn't surface the wrong account.

`auth status` output
- New credential.user block in the JSON envelope: email, first_name,
  last_name, username, display_name (email > "first last" > username).
- Only present when the active credential's source is the file AND
  a user block is persisted. Env-source credentials (HEYGEN_API_KEY)
  skip the block — the on-disk one could belong to a different key.
- Strictly additive; the existing `data` envelope from /v3/users/me
  is unchanged.

Display priority
- email > first + last name > username > user_id (existing fallback)
- Matches Somansh's ask and James's greenlight on the thread.

Backwards compatibility
- Credentials files without the user block (pre-this-change logins)
  parse fine. They show user_id like today; re-login populates the
  friendly fields. No migration needed.
- The single-credential invariant is preserved end-to-end.

Tests
- internal/auth: UserInfo.DisplayName priority order, IsZero gate,
  Save/Load round-trip, Save preserves api_key, Save with empty
  UserInfo is a no-op (no file rewrite), Load on absent file
  returns zero, Load on legacy file (no user block) returns zero,
  Clear leaves credential intact, Clear on no-credential state
  removes the file, Clear when no user block is a no-op, omitempty
  schema guard.
- cmd/heygen: API-key login probe persists friendly fields, probe
  failure is non-fatal, probe failure clears stale user block,
  OAuth login persists full schema (first_name, last_name too),
  auth status surfaces persisted user block, legacy file falls
  back without the block, env-source credential deliberately skips
  the block.
- Updated existing OAuth tests to assert the new email-preferred
  display ("Logged in as jane@example.com" vs the old username
  "Logged in as demo") — the friendly-display change is the whole
  point.
- go test -race clean. go vet clean. golangci-lint clean.

— Jerrai (https://claude.com/claude-code)

Co-Authored-By: Jerrai <noreply@anthropic.com>
The ~/.heygen/credentials file is shared with the Node hyperframes CLI
(and any future tool). hyperframes#1741 hardened the Node writer to
preserve unknown/foreign top-level and nested fields so it never strips
what heygen-cli writes. This makes the Go side do the same, so the shared
file round-trips safely in BOTH directions.

The Go credentials structs (jsonCredentials, jsonOAuthTokens,
jsonUserInfo) previously dropped any JSON key they didn't model on
read->write, silently clobbering data another CLI wrote. Now each struct
captures unrecognized keys into an unexported `extra`
map[string]json.RawMessage at unmarshal time and re-emits them verbatim
(sorted, deterministic) at marshal time via custom Marshal/UnmarshalJSON.
Known fields stay strictly typed and validated; the passthrough is purely
additive and never feeds an HTTP header. Mirrors the known-key sets in
hyperframes-oss packages/cli/src/auth/store.ts.

Reversed TestFileCredentialResolver_JSON_DropsUnknownFields to assert
preservation, and added round-trip coverage for unknown top-level keys,
unknown oauth sub-keys, unknown user sub-keys, and the cross-CLI scenario
through the public FileCredentialStore.Save path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@somanshreddy somanshreddy left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed the friendly-user-info work plus the Go-side unknown-field preservation. LGTM, approving.

Verified the load-bearing pieces directly rather than trusting the diff:

  • Legacy plaintext is safe: loadCredentialsFile gates on isJSONObject() before any json.Unmarshal, so the new custom UnmarshalJSON only sees JSON-object content; single-line plaintext keys still load as formatLegacy. No risk of logging out existing api-key users. writeCredentialsFile uses MarshalIndent, which re-indents the custom marshaler output, so the file stays pretty-printed.
  • Cross-CLI round-trip: the extra map[string]json.RawMessage passthrough on all three structs (top-level + nested oauth/user) is covered by direct tests (PreservesUnknownFields / PreservesUnknownOAuthSubKey / PreservesUnknownUserSubKey / PreservesUnknownFieldsThroughSave). Nested preservation works because *jsonOAuthTokens / *jsonUserInfo satisfy json.Marshaler, so the alias marshal recurses into their custom marshalers.
  • Schema parity with the merged hyperframes#1741: identical snake_case keys (email/first_name/last_name/username) and matching KNOWN_USER_KEYS on both sides.
  • DisplayName preference email > first+last > username is the right call (email is the dependable fallback since most accounts have no first_name).
  • auth status correctly omits the user block for env-source credentials (the on-disk block could belong to a different key). Good detail.
  • Stale-user-clear on probe failure prevents surfacing the wrong account after a credential switch.

Non-blocking notes for a possible follow-up:

  • api-key login now always probes /v3/users/me with a 10s timeout. For scripted/offline auth login --api-key (e.g. air-gapped CI piping a key) that can add up to 10s before the login returns. It is best-effort and non-fatal, but a shorter probe timeout would keep the scripted/agent path snappy since the friendly name is cosmetic.
  • Cosmetic: marshalExtras emits sorted keys when extras are present but struct-order keys when not, so the on-disk key order shifts depending on whether a foreign block exists. Harmless for a machine-managed file.
  • Minor: email + name are now persisted at rest in ~/.heygen/credentials (0600). Low sensitivity and user-only readable, just flagging the new data-at-rest field.

This resolves the hex-username greeting (the friendly-display ask). The other auth-UX items we discussed (duplicate stdout+stderr result, column-less --human table for auth, and --oauth not failing fast under CI/HEYGEN_NONINTERACTIVE) are independent and still open separately.

@jrusso1020 jrusso1020 merged commit 20ad9c9 into main Jun 26, 2026
9 checks passed
@jrusso1020 jrusso1020 deleted the 06-26-show_friendly_user_info_on_auth_status branch June 26, 2026 17:59
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