Skip to content

fix(auth): keep credentials the current build cannot decode on mutation - #53864

Open
SeashoreShi wants to merge 2 commits into
anomalyco:devfrom
SeashoreShi:fix/auth-preserve-undecodable
Open

SeashoreShi wants to merge 2 commits into
anomalyco:devfrom
SeashoreShi:fix/auth-preserve-undecodable

Conversation

@SeashoreShi

@SeashoreShi SeashoreShi commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Issue for this PR

Closes #42387

Type of change

  • Bug fix

What does this PR do?

Auth.all() (packages/opencode/src/auth/index.ts) decodes auth.json and drops entries it cannot decode (Record.filterMap(..., decode, () => undefined)). Both set() and remove() read through all() and then writeJson the result, so a single mutation rewrote auth.json from the filtered map and permanently deleted every undecodable credential — including entries written by a newer/older build or by the Desktop app, which also mutates the file in the background (provider-metadata refresh). This is silent, unrecoverable data loss.

The fix separates reading from mutating: a new internal read() returns the raw undecoded map, all() = read() + filter (read behaviour unchanged), and set/remove write back from the raw map, so entries the current build cannot decode are preserved verbatim.

How did you verify your code works?

Added a test in test/auth/auth.test.ts: an undecodable entry sits next to a valid one; after auth.set(...) and auth.remove(...) the undecodable entry is still on disk untouched, the removed key is gone, and all() still hides the undecodable entry. It fails against the previous code. The reviewer also reproduced the auth logout path from the issue and confirmed the fix (and that the new test fails on base / passes here, with typecheck passing).

Local note: my checkout could not run this package's tests/typecheck (effect subpath/version resolution fails on base dev too), so CI is doing the red/green confirmation for my side.

Screenshots / recordings

n/a — data-layer change.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

`Auth.all()` filters out entries whose JSON shape the running build does not
recognize. `set()` and `remove()` both wrote back from `all()`, so a single
mutation rewrote auth.json from the filtered map and permanently deleted every
undecodable credential — including entries written by another build or by the
Desktop app, which also mutates auth.json in the background.

Read the raw (undecoded) map for mutations and keep the filtering in `all()`
for reads, so undecodable entries survive `set`/`remove` untouched.
@github-actions github-actions Bot added contributor needs:compliance This means the issue will auto-close after 2 hours. labels Oct 8, 2026
@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

The following comment was made by an LLM, it may be inaccurate:

@opencode-agent opencode-agent Bot 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.

I reproduced the bug from #42387 through the CLI. With an entry this build can't read in auth.json, opencode auth logout openai on dev deletes that entry too. With this PR it is kept and only openai is removed. The new test fails on the base and passes here, and typecheck passes.

The fix is small and in the right place: set and remove now write back from the unfiltered file contents, and all()/get() still hide entries they can't read. One side effect: when OPENCODE_AUTH_CONTENT is set, all() now filters those entries too, where before it returned them unchecked. That seems fine and more consistent.

Two small notes on the test, inline.

// `all()` filters those out for reads, and the old `set`/`remove` wrote back
// the filtered map, so a single `set` permanently deleted every entry whose
// JSON shape this build did not recognize (see #42387).
it.instance("set preserves entries the current build cannot decode", () =>

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.

This test sits outside the describe("Auth") block (the closing }) comes just before it), so it shows up at the top level with odd indentation. Could you move it inside the block?

Also, the linked issue's reproduction goes through remove() (auth logout). Adding one auth.remove("anthropic") call, then checking that future-provider is still on disk, would cover both write paths without making the test any bigger.

…nd cover remove

Review feedback: the test sat outside `describe("Auth")`, and the issue's
reproduction goes through `remove()` (`auth logout`). Move it inside the block
and exercise both write paths (set + remove).
@SeashoreShi

Copy link
Copy Markdown
Contributor Author

Thanks for the review and for reproducing the auth logout path against dev — that was the exact mechanism in the report, so good to have it confirmed end-to-end.

Both notes are addressed in 47051df: the test now lives inside the describe("Auth") block, and it exercises auth.remove(...) as well — asserting that the undecodable entry survives both the set and the remove path while the removed key is gone. PR description checklist is fixed too.

@github-actions github-actions Bot removed the needs:compliance This means the issue will auto-close after 2 hours. label Oct 8, 2026
@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

Thanks for updating your PR! It now meets our contributing guidelines. 👍

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

1 participant