feat(cli): persist + show friendly user identity; preserve unknown credential fields#1741
Conversation
…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
left a comment
There was a problem hiding this comment.
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.tsowns read/write/serialize/parse and the unknown-field machineryuser.tsowns the persistence convenience helpers (save/load/clear) and display logic (userDisplayName)login.tsandstatus.tsconsume 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, insideuser) - The exact cross-CLI scenario (heygen-cli's user block survives a hyperframes-cli rewrite)
clearOAuthpreserving 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 statusJSON surface withpersisted_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
left a comment
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
Reviewed current HEAD 48b4beb6.
Strengths:
packages/cli/src/auth/store.ts:385/:393re-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:414starts OAuth persistence from the existing record and overrides onlyoauth, which fixes the prioruser/unknown-key clobber on OAuth writes.packages/cli/src/commands/auth/status.ts:153correctly 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, andpackages/cli/src/commands/auth/login.ts:204still treat only knownapi_key/oauthas 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 removinguser, and failedauth login --api-keyrollback 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
mainstill hasTestFileCredentialResolver_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
left a comment
There was a problem hiding this comment.
R2 at e96aca26. My prior blocker is resolved.
Specific checks:
packages/cli/src/auth/store.ts:183addshasPreservedUnknownData, andclearOAuthnow gates deletion on it atpackages/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:110applies the same guard after removing theuserblock, soclearUserInfopreserves a foreign top-level remnant instead of deleting it.packages/cli/src/commands/auth/login.ts:205uses the same helper during rejected--api-keyrollback, 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, andpackages/cli/src/commands/auth/login.test.ts:142. The refresh-path unknown-key gap is also pinned atpackages/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
left a comment
There was a problem hiding this comment.
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 #4 — userDisplayName 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-209explicitly 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 AFTERdelete 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-210documents the foreign-credential preservation case.packages/cli/src/auth/store.ts:183-188(hasPreservedUnknownDatahelper) — checkscredentials[UNKNOWN],credentials.oauth?.[UNKNOWN],credentials.user?.[UNKNOWN]viahasUnknownBag(:190-192), which guardsbag !== 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-457—hasPreservedUnknownDataunit 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-405—clearOAuthkeeps file when only a foreign top-level key OR a foreign user-sub key survives;:407-413still deletes when nothing worth preserving remains.packages/cli/src/auth/user.test.ts:138-155—clearUserInfopreserves 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 byhasPreservedUnknownData(previous). ✅packages/cli/src/commands/auth/logout.ts:53— explicitauth logout(NO--keep-api-key). Full sign-out path. Not gated byhasPreservedUnknownData.
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
What
Mirrors heygen-cli#197 on the hyperframes side. Two things:
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.userblock (email/first_name/last_name/username, all optional) — persisted at login fromGET /v3/users/me, surfaced as a friendly name inauth loginandauth status.Why
The
hyperframesandheygenCLIs share the same on-disk credentials file (~/.heygen/credentials; seepackages/cli/src/auth/paths.ts— "Mirrorsheygen-cli/internal/paths/paths.go"). heygen-cli#197 just started writing an optionaluserblock to that file. Because hyperframes-cli rebuilt a fresh, typed object on every write and emitted only the keys it knew about, the very nexthyperframes auth login/auth logout --keep-api-key/ OAuth refresh would have silently destroyed theuserblock heygen-cli wrote (and any future key either CLI adds). This PR makes the writer forward-compatible and adds the matchinguser-block feature so the two stay symmetric.How
Unknown-field preservation (the important part): the reader stashes every unrecognized top-level key — and every unrecognized key inside the
oauth/usersub-objects — onto a hidden, module-privateSymbol-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.stringifyskip it) and it never feeds an HTTP header. Known fields stay strictly validated (header-safety, requiredoauth.access_token, etc.) exactly as before.The same contract now also binds the destructive paths.
clearOAuth,clearUserInfo, and the failedauth login --api-keyrollback previously deleted the whole credentials file when no knownapi_key/oauthsurvived — even when the hidden unknown-field bag held a future/foreign key. A newhasPreservedUnknownData(record)helper (checks the top-level bag + theoauth/usersub-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:
StoredUserInfoschema +omitempty-style serialization (empty fields and an all-empty block are omitted). Legacy files with nouserblock parse fine —useris justundefined.packages/cli/src/auth/user.ts:saveUserInfo/loadUserInfo/clearUserInfo(all go throughreadStore/writeStore, so they preserve co-located credentials + unknown keys) anduserDisplayName(precedenceemail > "first last" > first-only > last-only > username).auth login(both OAuth and--api-keypaths) probes/v3/users/me, persists the block, and printsSigned 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 statusaddspersisted_userto the JSON envelope (with a resolveddisplay_name) for file-sourced credentials, and a cachedAccount: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 liveuserfield is unchanged (strictly additive).persistOAuthinauth/oauth.ts, which rebuilt a minimal{ api_key?, oauth }record and so would have dropped theuserblock + unknown keys on every OAuth login/refresh — it now starts from the existing record and overrides onlyoauth.Schema matches #197 exactly (
email,first_name,last_name,username;display_nameis the resolved precedence value, computed not stored).Test plan
How was this tested?
New / updated tests (all in
packages/cli/src/auth+.../commands/auth):oauth, unknown key insideuser, and the exact cross-CLI scenario (heygen-cli'suserblock survives a hyperframes-cli credential rewrite). PlusclearOAuthand the OAuth fresh-login path both preserve the user block + unknown keys. End-to-end throughauth logintoo.clearOAuth,clearUserInfo, and the--api-keyrollback each keep the file (writing the unknown bag) when no known credential survives but a foreign top-level/sub-object key does;hasPreservedUnknownDataunit tests at all three levels.oauthsub-object survives a no-rotationrefreshTokenswrite (the most-frequent write path).userblock parse cleanly (userundefined;persisted_user: null); a malformedusersub-field is ignored rather than rejecting the whole file.auth statussurface: persisted block with resolveddisplay_name; env-source skips it; cached fallback when the live probe fails.Local verification: full CLI suite 1020 tests green;
tsc --noEmit, repo-wideoxlint .andoxfmt --check .all clean; fallow audit passes (no above-threshold findings).— Jerrai