Summary
The callback path in src/server/auth.ts exchanges the authorization code with the broker before proving that the resulting credentials can be persisted locally. If the local write fails, the flow rejects after the code has already been consumed.
This does not silently corrupt local state, but it produces a bad recovery path: auth succeeded upstream, the single-use code is burned, and the user has to start over.
Evidence
src/server/auth.ts
155-164: exchangeCodeThroughBroker(...) consumes the code and returns tokens
166-171: computes expiry and calls writeSavedAuth(input, { ... })
173: resolves the pending auth only after the write succeeds
184-198: any failure in this block rejects the auth flow and returns an error page
So the ordering is:
- exchange succeeds
- local persistence happens
- success is reported
If step 2 fails, the code from step 1 is already spent.
Reproduction
- Start Supabase OAuth.
- Before completing the callback, make the target auth store unwritable.
- example: permissions problem on
.opencode/
- read-only filesystem
- disk full / write failure
- Complete the browser callback.
Expected
Either:
- persistence feasibility is checked before the browser flow begins, or
- the failure path gives a clear recovery message with minimal wasted work
Actual
The callback fails after the code is exchanged, so the user must repeat the full authorization flow.
Impact
- Bad recovery UX when local persistence fails.
- Repeated browser auth flows for what is effectively a local filesystem problem.
- Harder debugging because the visible failure appears during OAuth completion, but the root cause is local persistence.
Suggested Fix
Recommended layered fix:
- Preflight the auth store path before starting the browser flow.
- verify parent directory can be created
- verify target path is writable
- Use an atomic write strategy for the auth file.
- Improve the failure message to explicitly say:
- authorization succeeded
- credentials could not be saved locally
- user should fix permissions/storage and retry
This issue may not be fully eliminable because the OAuth code is inherently single-use, but it can be made much less painful and much easier to understand.
Acceptance Criteria
- Browser auth does not start if the store path is clearly unwritable up front.
- Auth-file writes are atomic.
- Persistence failure after exchange produces a targeted recovery message instead of a generic auth failure.
- Regression coverage exists for write failure during callback persistence.
Notes
This is lower severity than the callback-context bug or refresh race, but it is still worth tracking because it creates an avoidable failure mode at the end of a successful OAuth flow.
Summary
The callback path in
src/server/auth.tsexchanges the authorization code with the broker before proving that the resulting credentials can be persisted locally. If the local write fails, the flow rejects after the code has already been consumed.This does not silently corrupt local state, but it produces a bad recovery path: auth succeeded upstream, the single-use code is burned, and the user has to start over.
Evidence
src/server/auth.ts155-164:exchangeCodeThroughBroker(...)consumes the code and returns tokens166-171: computes expiry and callswriteSavedAuth(input, { ... })173: resolves the pending auth only after the write succeeds184-198: any failure in this block rejects the auth flow and returns an error pageSo the ordering is:
If step 2 fails, the code from step 1 is already spent.
Reproduction
.opencode/Expected
Either:
Actual
The callback fails after the code is exchanged, so the user must repeat the full authorization flow.
Impact
Suggested Fix
Recommended layered fix:
This issue may not be fully eliminable because the OAuth code is inherently single-use, but it can be made much less painful and much easier to understand.
Acceptance Criteria
Notes
This is lower severity than the callback-context bug or refresh race, but it is still worth tracking because it creates an avoidable failure mode at the end of a successful OAuth flow.