auth: show friendly user name/email on login + auth status#197
Merged
Conversation
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>
3 tasks
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
approved these changes
Jun 26, 2026
somanshreddy
left a comment
Collaborator
There was a problem hiding this comment.
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:
loadCredentialsFilegates onisJSONObject()before anyjson.Unmarshal, so the new customUnmarshalJSONonly sees JSON-object content; single-line plaintext keys still load asformatLegacy. No risk of logging out existing api-key users.writeCredentialsFileusesMarshalIndent, which re-indents the custom marshaler output, so the file stays pretty-printed. - Cross-CLI round-trip: the
extra map[string]json.RawMessagepassthrough on all three structs (top-level + nested oauth/user) is covered by direct tests (PreservesUnknownFields / PreservesUnknownOAuthSubKey / PreservesUnknownUserSubKey / PreservesUnknownFieldsThroughSave). Nested preservation works because*jsonOAuthTokens/*jsonUserInfosatisfy 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/mewith a 10s timeout. For scripted/offlineauth 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:
marshalExtrasemits 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.
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.
Description
Per Somansh's ask on the heygen-cli OAuth thread (James greenlit):
opaque user_ids are unfriendly in
auth statusoutput. After login(both OAuth and API-key paths), fetch
/v3/users/meonce and persistemail+first_name+last_name+usernameinto~/.heygen/credentialsalongside the credential.auth statussurfaces them in the JSON envelope under
credential.user.Schema
userblock onjsonCredentials(json tagomitempty).email,first_name,last_name,username)are
omitemptytoo so a partially-populated block doesn't litterthe file with empty strings.
invariant (at-most-one of
api_key/oauth) is unchanged; theuser block is safe to persist alongside either credential type.
internal/auth:UserInfo,SaveUserInfo,LoadUserInfo,ClearUserInfo+UserInfo.DisplayName()/UserInfo.IsZero()helpers.Login behavior
/v3/users/meto surface "Logged inas ..." on stderr; that path now also persists the result. Probe
signature returns
UserInfo(wasusername, email) so thepersisted block carries first/last name too.
x-api-key, persist the result, and emit "Logged in as ..." onstderr to match the OAuth path.
proceed with the login. The credential is NEVER rolled back on
a probe failure (tokens / key are still on disk and usable).
cleared so
auth statusdoesn't surface the wrong account.auth statusoutputcredential.userblock in the JSON envelope:email,first_name,last_name,username,display_name(theresolved priority email > "first last" > username).
AND a user block is persisted. Env-source credentials
(
HEYGEN_API_KEY) skip the block — the on-disk one couldbelong to a different key.
dataenvelope from/v3/users/meis unchanged.Display priority
email>first_name+last_name>username>user_id(existing fallback). Matches the ask on the thread.
Backwards compatibility
parse fine. They show
user_idlike today; re-login populates thefriendly fields. No migration needed.
Testing
internal/auth:UserInfo.DisplayNamepriority order,IsZerogate, Save/Load round-trip, Save preserves
api_key, Save withempty
UserInfois a no-op (no file rewrite), Load on absentfile 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,
omitemptyschema 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_nametoo),auth statussurfaces thepersisted user block, legacy file falls back without the block,
env-source credential deliberately skips the block.
display (
Logged in as jane@example.comvs the old usernameLogged in as demo) — the friendly-display change is the wholepoint.
go test -race ./...clean.go vet ./...clean.golangci-lint run ./...clean (0 issues).— Jerrai (https://claude.com/claude-code)