Skip to content

feat(cli): persist + show friendly user identity; preserve unknown credential fields#1741

Merged
jrusso1020 merged 2 commits into
mainfrom
feat/cli-user-identity-preserve-fields
Jun 26, 2026
Merged

feat(cli): persist + show friendly user identity; preserve unknown credential fields#1741
jrusso1020 merged 2 commits into
mainfrom
feat/cli-user-identity-preserve-fields

Conversation

@jrusso1020

@jrusso1020 jrusso1020 commented Jun 26, 2026

Copy link
Copy Markdown
Collaborator

What

Mirrors heygen-cli#197 on the hyperframes side. Two things:

  1. Load-bearing: the credentials reader/writer now preserves unknown fields on round-trip. Previously readStore/writeStore (packages/cli/src/auth/store.ts) stripped any key this CLI didn't model, so re-writing the file silently dropped data another CLI wrote.
  2. Adds the optional user block (email/first_name/last_name/username, all optional) — persisted at login from GET /v3/users/me, surfaced as a friendly name in auth login and auth status.

Why

The hyperframes and heygen CLIs share the same on-disk credentials file (~/.heygen/credentials; see packages/cli/src/auth/paths.ts — "Mirrors heygen-cli/internal/paths/paths.go"). heygen-cli#197 just started writing an optional user block to that file. Because hyperframes-cli rebuilt a fresh, typed object on every write and emitted only the keys it knew about, the very next hyperframes auth login / auth logout --keep-api-key / OAuth refresh would have silently destroyed the user block heygen-cli wrote (and any future key either CLI adds). This PR makes the writer forward-compatible and adds the matching user-block feature so the two stay symmetric.

Scope note — preservation is one-way today. This PR delivers the hyperframes half of the cross-CLI guarantee: hyperframes-cli no longer destroys keys heygen-cli (or a future tool) writes to the shared file. It is not full bidirectional round-trip safety yet — heygen-cli's Go side still drops unknown fields on its own writes (internal/auth/file_resolver_test.go still asserts TestFileCredentialResolver_JSON_DropsUnknownFields). The matching [UNKNOWN]-equivalent on the Go side (json.RawMessage / inline-extras map) is the follow-up — tracked in heygen-cli#197 — and closes the loop for two-way safety. Until that lands, a key hyperframes writes survives a hyperframes rewrite but can still be dropped by a subsequent heygen command.

How

Unknown-field preservation (the important part): the reader stashes every unrecognized top-level key — and every unrecognized key inside the oauth / user sub-objects — onto a hidden, module-private Symbol-keyed slot on the parsed object; the serializer spreads that slot back out first, then layers the known (validated) fields on top. A symbol keeps the passthrough off the typed surface (callers can't read/write it, Object.keys / JSON.stringify skip it) and it never feeds an HTTP header. Known fields stay strictly validated (header-safety, required oauth.access_token, etc.) exactly as before.

The same contract now also binds the destructive paths. clearOAuth, clearUserInfo, and the failed auth login --api-key rollback previously deleted the whole credentials file when no known api_key/oauth survived — even when the hidden unknown-field bag held a future/foreign key. A new hasPreservedUnknownData(record) helper (checks the top-level bag + the oauth/user sub-object bags) gates those paths: when no known credential survives BUT unknown/foreign data exists, they write the credential-less remnant (carrying the unknown bag) instead of deleting the file. They still delete only when nothing worth preserving remains.

Friendly-display, mirroring #197:

  • New StoredUserInfo schema + omitempty-style serialization (empty fields and an all-empty block are omitted). Legacy files with no user block parse fine — user is just undefined.
  • New packages/cli/src/auth/user.ts: saveUserInfo / loadUserInfo / clearUserInfo (all go through readStore/writeStore, so they preserve co-located credentials + unknown keys) and userDisplayName (precedence email > "first last" > first-only > last-only > username).
  • auth login (both OAuth and --api-key paths) probes /v3/users/me, persists the block, and prints Signed in as <display>. Probe failure is non-fatal — the credential stays on disk; a stale block from a prior login is cleared so a wrong account can't surface.
  • auth status adds persisted_user to the JSON envelope (with a resolved display_name) for file-sourced credentials, and a cached Account: row in human output when the live probe fails. Env-sourced credentials (HEYGEN_API_KEY / HYPERFRAMES_API_KEY) deliberately skip it — the on-disk block could belong to a different key. The existing live user field is unchanged (strictly additive).
  • Fixed persistOAuth in auth/oauth.ts, which rebuilt a minimal { api_key?, oauth } record and so would have dropped the user block + unknown keys on every OAuth login/refresh — it now starts from the existing record and overrides only oauth.

Schema matches #197 exactly (email, first_name, last_name, username; display_name is the resolved precedence value, computed not stored).

Test plan

How was this tested?

  • Unit tests added/updated
  • Manual testing performed
  • Documentation updated (if applicable)

New / updated tests (all in packages/cli/src/auth + .../commands/auth):

  • Preserve-unknown-fields round-trip (the critical one): unknown top-level key, unknown key inside oauth, unknown key inside user, and the exact cross-CLI scenario (heygen-cli's user block survives a hyperframes-cli credential rewrite). Plus clearOAuth and the OAuth fresh-login path both preserve the user block + unknown keys. End-to-end through auth login too.
  • Destructive-path preservation (new, addresses the review): clearOAuth, clearUserInfo, and the --api-key rollback each keep the file (writing the unknown bag) when no known credential survives but a foreign top-level/sub-object key does; hasPreservedUnknownData unit tests at all three levels.
  • Refresh-path round-trip (new): an unknown key inside the oauth sub-object survives a no-rotation refreshTokens write (the most-frequent write path).
  • Schema round-trip + omitempty / optional: populated fields round-trip; empty fields and an all-empty block are not written.
  • Backwards-compat: legacy files with no user block parse cleanly (user undefined; persisted_user: null); a malformed user sub-field is ignored rather than rejecting the whole file.
  • Login persistence + graceful probe failure + stale-clear: success persists the block; probe failure keeps the credential and clears a stale block; rejected-key rollback restores the prior block.
  • auth status surface: persisted block with resolved display_name; env-source skips it; cached fallback when the live probe fails.

Local verification: full CLI suite 1020 tests green; tsc --noEmit, repo-wide oxlint . and oxfmt --check . all clean; fallow audit passes (no above-threshold findings).

— Jerrai

…edential fields

The `~/.heygen/credentials` file is SHARED with the Go `heygen` CLI. This
is the hyperframes-side mirror of heygen-cli#197, which adds an optional
`user` block to that file. Two CLIs writing one file must round-trip each
other's data without loss.

Load-bearing change: the credentials reader/writer now PRESERVES unknown
fields on round-trip. Previously readStore/writeStore stripped any key
this CLI didn't model, so writing the file back would silently drop the
`user` block heygen-cli wrote (and any future key). Unrecognized top-level
keys, and unknown keys inside `oauth` / `user`, are captured on a hidden
symbol slot and re-emitted verbatim. Known fields stay strictly validated.

Also mirrors heygen-cli#197's friendly-display feature:
- New optional `user` block schema (email/first_name/last_name/username),
  all omitempty; legacy files without it parse fine.
- After login (OAuth + api-key paths) probe /v3/users/me, persist the
  block, and show a friendly name (email > "first last" > username).
  Probe failure is non-fatal (login still succeeds); a stale block is
  cleared on probe failure so a wrong account can't surface.
- `auth status` surfaces the persisted block (persisted_user in JSON,
  a cached Account row in human output) for file-sourced credentials;
  env-sourced credentials skip it (the on-disk block may belong to a
  different key).
- Fixed the OAuth write path to carry the user block + unknown keys
  across a fresh login / refresh (it previously rebuilt a minimal record).

Tests: preserve-unknown-fields round-trip (top-level, oauth, user), the
exact cross-CLI `user`-block scenario, schema round-trip + omitempty,
backwards-compat with legacy files, login persistence + graceful probe
failure + stale-clear, and the `auth status` surface. Full CLI suite
(1009 tests) green; oxlint + oxfmt + tsc clean.

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

@miga-heygen miga-heygen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review — feat(cli): persist + show friendly user identity; preserve unknown credential fields

SHA: 48b4beb

Really solid PR. The cross-CLI forward-compatibility problem is real and the Symbol-keyed passthrough is a clean solution — invisible to typed consumers, survives spread, skipped by JSON.stringify and Object.keys, and never feeds an HTTP header. The design reads like you genuinely thought through what a shared credential file means for two independently-versioned CLIs.

Architecture / Design

The layering is right:

  • store.ts owns read/write/serialize/parse and the unknown-field machinery
  • user.ts owns the persistence convenience helpers (save / load / clear) and display logic (userDisplayName)
  • login.ts and status.ts consume via the barrel export and treat user-block persistence as best-effort

persistOAuth fix is the most load-bearing one-liner in the PR — going from { ...(existing.api_key ? { api_key: existing.api_key } : {}), oauth } to { ...existing, oauth } correctly preserves the user block, any unknown top-level keys on the symbol slot, AND the api_key, while still fully replacing the oauth block. The old version was a silent data-loss footgun on every OAuth login/refresh.

Findings

All minor — nothing blocking.

(1) assignOptionalStrings uses an as Record<string, unknown> index cast on out.
The spec field names are documented as the contract, and the callers are all internal + well-typed, so this is fine in practice. Just noting that a stray field name in the spec would bypass the typed interface silently. Since the KNOWN_*_KEYS sets already mirror the spec tuples, they're effectively the same list maintained in two places — not a real risk today but worth knowing if either grows.

(2) clearUserInfo checks !credentials.api_key && !credentials.oauth to decide whether to delete the file. This is correct, but it means an unknown top-level key (from the other CLI) would be orphaned on disk with no credential. That's the same behavior as clearOAuth — consistent, and probably the right call since unknown keys without a credential are truly orphaned metadata. Just confirming this is intentional, not an oversight.

(3) saveUserInfo does credentials.user = { ...info } — the shallow copy drops the [UNKNOWN] symbol from info's prototype chain. This is actually correct: info comes from toStoredUserInfo(user) which builds a fresh plain object, so there's no symbol to lose. If saveUserInfo were ever called with the existing parsed credentials.user (which does carry unknown user-sub-keys on the symbol), the shallow copy would still propagate the symbol because object spread copies own symbol-keyed properties. So this works either way — just making explicit that I verified both paths.

(4) Minor: combineName returns "" (empty string) when both first and last are absent. The caller (userDisplayName) treats this as falsy and falls through to username, so the empty-string return is effectively undefined. It works, but undefined would be slightly more self-documenting. Truly a nit.

Test coverage

Thorough. The tests cover:

  • Unknown-field preservation at all three levels (top-level, inside oauth, inside user)
  • The exact cross-CLI scenario (heygen-cli's user block survives a hyperframes-cli rewrite)
  • clearOAuth preserving user block + unknown keys
  • OAuth fresh-login path preserving user block + unknown keys
  • Schema round-trip + omitempty semantics
  • Backwards-compat with legacy files
  • Malformed user sub-field leniency
  • Login persistence + probe failure + stale-clear + rollback
  • auth status JSON surface with persisted_user + display_name
  • Env-sourced credentials correctly skip the persisted block
  • Cached identity fallback when the live probe fails

The one gap I'd flag: there's no explicit test for unknown keys inside the oauth sub-object surviving a refresh (the preserveMissing: true path where { ...existing.oauth, ...tokens } should carry the symbol). The fresh-login test covers top-level + user preservation, but the refresh path's oauth-internal unknown-key preservation is only implicitly covered by the collectUnknown / serializeOAuth unit tests. Not blocking — the code is correct — but a targeted round-trip test through refreshTokens would close the gap.

CI

All required checks pass.

Verdict

LGTM. The forward-compatibility invariant is correctly enforced, the user-display feature is symmetric with heygen-cli#197, and the test suite is comprehensive. Ship it.

— Miga

@james-russo-rames-d-jusso james-russo-rames-d-jusso left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Reviewed at 48b4beb6c65e072cf44e579e7038f20e059ebdae. COMMENT only — James authored, so the stamp call is his.

The load-bearing change (Symbol-keyed unknown-field passthrough + writer-starts-from-existing on every co-located write) is the right shape and well-tested on the hyperframes side — the cross-CLI scenario test in store.test.ts ("preserves the heygen-cli user block when this CLI rewrites only the credential") is exactly the canary I'd want. persistOAuth in oauth.ts:421 correctly switched from { api_key?, oauth } to { ...existing, oauth } so the OAuth login/refresh paths stop being the silent-clobber site. clearOAuth and clearUserInfo both spread-then-delete, so [UNKNOWN] rides through. The auth status persisted_user skip-for-env-credential at status.ts (isFileSource gate) is the right call — an env key could belong to a different account than the file block, and surfacing the file block there would mislabel the active identity. Strictly additive persisted_user next to live user keeps the JSON envelope backwards-compatible.

A few notes, ordered by load-bearingness:

🟡 Cross-CLI symmetry is one-way until heygen-cli#197 matches. The PR body says "The two CLIs have to round-trip each other's data without loss" — this PR delivers the hyperframes half (preserves Go-written extras), but heygen-cli#197 is still open and internal/auth/file_resolver_test.go:217 still asserts TestFileCredentialResolver_JSON_DropsUnknownFields. Concrete failure: hyperframes-cli writes a future top-level key → user runs any heygen auth ... command → Go's FileCredentialStore.Save rebuilds from jsonCredentials and drops the unknown key. The cross-CLI guarantee only holds once both sides preserve. Worth either landing the matching [UNKNOWN]-equivalent on heygen-cli (json.RawMessage extras / inline-extras map) before declaring victory, or trimming the PR body's claim to "hyperframes-cli no longer destroys keys heygen-cli writes" (the one-direction half it does deliver today).

🟡 Refresh-grant unknown-field preservation is correct-but-untested. persistOAuth(preserveMissing: true) at oauth.ts:414-416 does { ...existing.oauth, ...tokens } — spread carries the [UNKNOWN] Symbol slot from existing.oauth, so a heygen-cli-written oauth.id_token does survive a no-rotation refresh. ✅ behavior. ❌ test coverage — the new oauth.test.ts "preserves … across fresh login" only exercises preserveMissing: false, and the existing refresh tests (preserves the prior refresh_token when the server omits it, preserves an existing api_key when persisting refreshed oauth) don't assert sub-block unknown keys. Refresh is the most-frequent write path, so a silent regression here would be the worst-case path. Suggest a single test analogous to the OAuth fresh-login one but going through refreshTokens to lock in the invariant.

💭 Newly-server-emitted oauth sub-keys are still dropped. parseTokenResponse at oauth.ts:332-345 builds OAuthTokens from only the modelled fields — if the IdP starts emitting e.g. id_token or a new claim, it never makes it onto the [UNKNOWN] slot and is lost before write. Different cross-CLI invariant than what this PR is solving (the OTHER CLI didn't write the key here — the server did), so probably fine to defer, but a collectUnknown(obj, KNOWN_OAUTH_KEYS) pass at parse time would close the loop cheaply.

💭 userDisplayName priority doc vs. behavior. PR body says email > "first last" > username, but combineName returns first || last || "", so the actual order is email > "first last" > first-only > last-only > username (and the new unit tests confirm). The behavior is fine — first || last is the right fallback — just worth a line in the PR body / docstring on userDisplayName so a future reader doesn't read the precedence comment at user.ts:24 and miss the first-only / last-only intermediates. Mirror this to heygen-cli's DisplayName() while you're there (its combineName does the same thing).

💭 Header docstring date. store.ts:14 shows "expires_at": "2026-06-25T12:00:00Z" in the example — that's literally yesterday. Bump it forward or strip to "<ISO-8601 UTC>" so a reader six months from now isn't double-taking on a stale literal.

✅ The hidden-Symbol slot is the right primitive (off the typed surface, skipped by Object.keys / JSON.stringify, never feeds an HTTP header).
✅ The "no credential survives → delete file" branch in both clearOAuth and clearUserInfo correctly frees orphan-metadata.
✅ Lenient parse for user (malformed sub-field ignored vs. file rejected) — right call given user is additive metadata not a credential, and the test that pins this contract is well-placed.
✅ Stale-block clear on probe-success-but-empty-identity (status-user.test.ts "clears a stale user block when the new key's identity probe returns nothing") — exactly the right "wrong account can't leak through" defense.
✅ Env-credential skip on persisted_user is correctly carried through both JSON and human surfaces.

— Rames D Jusso

@miguel-heygen miguel-heygen 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 current HEAD 48b4beb6.

Strengths:

  • packages/cli/src/auth/store.ts:385 / :393 re-emit hidden unknown bags before known fields, so normal read → write round-trips preserve foreign top-level, OAuth, and user keys.
  • packages/cli/src/auth/oauth.ts:414 starts OAuth persistence from the existing record and overrides only oauth, which fixes the prior user/unknown-key clobber on OAuth writes.
  • packages/cli/src/commands/auth/status.ts:153 correctly refuses to surface persisted file identity for env-sourced credentials, avoiding account mislabeling.

Blocker:

  • packages/cli/src/auth/store.ts:169, packages/cli/src/auth/user.ts:91, and packages/cli/src/commands/auth/login.ts:204 still treat only known api_key/oauth as data worth preserving. If the file contains only a future/foreign top-level key, these paths delete the whole credentials file: clearOAuth() after removing the known OAuth block, clearUserInfo() after removing user, and failed auth login --api-key rollback when the previous file had no known credential. That violates the PR’s forward-compatibility contract because an unknown top-level key can be a future credential/metadata key owned by another CLI. The normal serializer preserves it, but these destructive paths still clobber it. Please add a small helper for “has preserved unknown/foreign data” and use it to write the remaining record instead of deleting when unknown data survives, plus regression tests for at least rollback and one cleanup path.

Notes:

  • Rames’s cross-CLI symmetry point is verified: heygen-cli#197 is still open and heygen-cli main still has TestFileCredentialResolver_JSON_DropsUnknownFields, so the PR body should avoid claiming full bidirectional round-trip safety until the Go side lands.
  • Focused local test/typecheck execution in this fresh worktree was blocked by missing installed dependencies (vitest, AWS SDK, etc.); required GitHub CI is green.

Verdict: REQUEST CHANGES
Reasoning: The main read/write path is solid, but three same-contract cleanup/rollback paths can still delete preserved unknown data. That is a data-loss bug in the forward-compatibility surface this PR is adding.

— Magi

Addresses Magi's REQUEST_CHANGES on #1741. The credentials reader/writer
already round-trips unknown/foreign keys (the cross-CLI forward-compat
contract), but three destructive paths still deleted the whole file when
no known api_key/oauth survived — even when the hidden Symbol-keyed
unknown-field bag held a future credential another CLI owns. That clobbers
exactly the data this PR preserves.

- Add `hasPreservedUnknownData(record)` to store.ts (checks the top-level
  unknown bag + the oauth/user sub-object bags) and export it via the
  barrel.
- `clearOAuth`, `clearUserInfo`, and the failed `auth login --api-key`
  rollback now write the credential-less remnant (carrying the unknown
  bag) instead of deleting the file when unknown/foreign data survives.
  They still delete when nothing worth preserving remains.
- Regression tests: rollback path + both cleanup paths (clearOAuth,
  clearUserInfo) preserve a foreign top-level key; `hasPreservedUnknownData`
  unit tests at all three levels.

Also addresses the review's minor items:
- Add a refresh-path round-trip test (`refreshTokens`) proving an unknown
  key inside the oauth sub-object survives a no-rotation refresh — the
  most-frequent write path, previously only implicitly covered.
- Clarify the `userDisplayName` / `combineName` docstrings: precedence is
  `email > "first last" > first-only > last-only > username`.
- Replace the stale `expires_at` example date in store.ts with
  `<ISO-8601 UTC>`.

Full CLI suite green (1020 tests); tsc, oxlint, oxfmt --check all clean.

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

@miguel-heygen miguel-heygen 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.

R2 at e96aca26. My prior blocker is resolved.

Specific checks:

  • packages/cli/src/auth/store.ts:183 adds hasPreservedUnknownData, and clearOAuth now gates deletion on it at packages/cli/src/auth/store.ts:203, so clearing OAuth no longer deletes a credential-less file that still carries preserved foreign data.
  • packages/cli/src/auth/user.ts:110 applies the same guard after removing the user block, so clearUserInfo preserves a foreign top-level remnant instead of deleting it.
  • packages/cli/src/commands/auth/login.ts:205 uses the same helper during rejected --api-key rollback, so an unknown-only previous credentials file is restored instead of being removed.
  • Regression coverage now pins all three destructive paths: packages/cli/src/auth/store.test.ts:373, packages/cli/src/auth/user.test.ts:138, and packages/cli/src/commands/auth/login.test.ts:142. The refresh-path unknown-key gap is also pinned at packages/cli/src/auth/oauth.test.ts:186.
  • PR body now scopes the cross-CLI guarantee correctly as one-way until the Go CLI side lands.

Required CI is green, including Windows required checks. Optional regression shards are still in flight at review time; they are not part of the required approval gate.

Verdict: APPROVE
Reasoning: The cleanup/rollback data-loss paths now preserve unknown foreign data instead of deleting it, and the added regression tests cover the exact failure modes raised in the CR.

— Magi

@james-russo-rames-d-jusso james-russo-rames-d-jusso left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

R2 — feat(cli): persist + show friendly user identity; preserve unknown credential fields

SHA reviewed: e96aca260b57e75921b7b62508af406ab4bc5a9b
Prior R1 SHA: 48b4beb6
Verdict: COMMENT — James authored, so the stamp call stays with him.

R1 findings → R2 verification

🟡 R1 #1 — Cross-CLI symmetry one-way → ✅ resolved (via PR-body alignment, option b). The PR body now scopes the claim correctly under a "Scope note — preservation is one-way today" callout: explicit "It is not full bidirectional round-trip safety yet — heygen-cli's Go side still drops unknown fields on its own writes (internal/auth/file_resolver_test.go still asserts TestFileCredentialResolver_JSON_DropsUnknownFields)", and names heygen-cli#197 as the Go-side follow-up that closes the loop. Reads accurately; no longer overclaims. (Resolved by intent-documentation alignment, not by changing code — legitimate close since the code matches the now-trimmed claim.)

🟡 R1 #2 — Refresh-grant unknown-field carry untested → ✅ resolved. New test at packages/cli/src/auth/oauth.test.ts:186-216 ("preserves an unknown key INSIDE the oauth sub-object across a refresh"). Pins ACTUAL preservation, not reflexivity: writes oauth.id_token: "future_id_token_value" to disk, runs refreshTokens (the preserveMissing: true no-rotation path), reads back the raw JSON file via fs.readFile, asserts onDisk.oauth.id_token === "future_id_token_value". Exactly the canary I asked for on the most-frequent write path.

💭 R1 #3 — Server-emitted new oauth sub-keys still dropped at parseTokenResponse → 📝 acknowledged-deferred (no diff, no commitment given; was tagged defer-OK at R1 since it's a different invariant — server is the source, not another CLI). Still holds, still defer-OK.

💭 R1 #4userDisplayName priority docstring imprecision → ✅ resolved. packages/cli/src/auth/user.ts:35-42 now reads "email > 'first last' > first-only > last-only > username > undefined" with an explicit note that the intermediates come from combineName returning first || last || "". combineName itself at :51-60 now documents the one-sided fall-through. Doc matches behavior at all three levels.

💭 R1 #5 — Stale docstring date → ✅ resolved. packages/cli/src/auth/store.ts:11 swapped the literal "2026-06-25T12:00:00Z" example for "<ISO-8601 UTC>". Reader-proof.

Magi's CR (miguel-heygen review id 4581042489) — observed-as-addressed

Magi's REQUEST_CHANGES blocker called out three same-contract destructive paths still gated on known-credential-only: clearOAuth, clearUserInfo, auth login --api-key rollback. Each verified at HEAD:

  • packages/cli/src/auth/store.ts:195-214 (clearOAuth) — now gates the delete branch on !next.api_key && !hasPreservedUnknownData(next). When unknown/foreign data survives, writes the credential-less remnant instead of deleting. Comment at :204-209 explicitly notes the cross-CLI rationale.
  • packages/cli/src/auth/user.ts:106-115 (clearUserInfo) — same shape: !credentials.api_key && !credentials.oauth && !hasPreservedUnknownData(credentials) gates the delete. Note this checks AFTER delete credentials.user, so the user-sub-bag is intentionally dropped with the explicit user-block clear (semantically coupled); only top-level + oauth sub-bags keep the file alive. Defensible.
  • packages/cli/src/commands/auth/login.ts:203-223 (rollback)if (previous.api_key || previous.oauth || hasPreservedUnknownData(previous)) keeps the prior file alive on rejected-key. Comment at :206-210 documents the foreign-credential preservation case.
  • packages/cli/src/auth/store.ts:183-188 (hasPreservedUnknownData helper) — checks credentials[UNKNOWN], credentials.oauth?.[UNKNOWN], credentials.user?.[UNKNOWN] via hasUnknownBag (:190-192), which guards bag !== undefined && Object.keys(bag).length > 0 — no false-positive on an empty bag. Exported via the auth barrel (packages/cli/src/auth/index.ts:11).
  • Regression tests at all three levels (real on-disk data, not reflexivity):
    • packages/cli/src/auth/store.test.ts:415-457hasPreservedUnknownData unit tests covering empty record, all-known fields, top-level unknown, oauth sub-object unknown, user sub-object unknown.
    • packages/cli/src/auth/store.test.ts:373-387 + :389-405clearOAuth keeps file when only a foreign top-level key OR a foreign user-sub key survives; :407-413 still deletes when nothing worth preserving remains.
    • packages/cli/src/auth/user.test.ts:138-155clearUserInfo preserves a foreign top-level key when no credential survives.
    • packages/cli/src/commands/auth/login.test.ts:142-159 — rollback preserves a prior foreign top-level key when no known credential was present pre-attempt.

All three call sites use a single, correctly-implemented helper; the tests pin actual foreign-data round-trip, not a tautology. From my read this fully addresses the blocker — but this is for Magi to clear when ready, not for me.

Sibling delete-path scan (no R1 finding; surfaced for completeness)

Per my usual "find sibling call sites" pass, the remaining deleteStore call sites are:

  • packages/cli/src/commands/auth/login.ts:217 — rollback's no-prior-credential branch. Correctly gated above by hasPreservedUnknownData(previous). ✅
  • packages/cli/src/commands/auth/logout.ts:53 — explicit auth logout (NO --keep-api-key). Full sign-out path. Not gated by hasPreservedUnknownData.

That last one is a defensible scope choice — auth logout (no flags) is "I'm done with this account," so dropping other-CLI's user block or a foreign top-level key with it is reasonable user intent. Just flagging that it's the one remaining delete-the-whole-file path that DOESN'T check the unknown bag; if the cross-CLI contract is meant to be "no destructive path ever clobbers what we don't recognize," logout would be the holdout. Mirror with whatever heygen-cli does on its parallel auth logout. Not a blocker — just a sub-question.

NEW findings

None. Read the new code, the new tests, and the docstring/PR-body diffs as if fresh; nothing surfaced. The fix shape is exactly what Magi asked for and exactly what closed my R1 follow-ups.

CI

Required checks green per gh pr view. Author reports 1020 tests green + tsc/oxlint/oxfmt clean locally.

— Rames D Jusso

@jrusso1020 jrusso1020 merged commit b9b5780 into main Jun 26, 2026
59 of 75 checks passed
@jrusso1020 jrusso1020 deleted the feat/cli-user-identity-preserve-fields branch June 26, 2026 17:16
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.

4 participants