forked from anomalyco/opencode
-
Notifications
You must be signed in to change notification settings - Fork 0
Closed
Description
Problem
Users report that providers keep disappearing from ~/.local/share/opencode/auth.json. Investigation reveals race conditions and error handling flaws in the authentication storage system.
Root Cause Analysis
1. Non-Atomic Read-Modify-Write Pattern
Auth.set() in packages/opencode/src/auth/index.ts:
export async function set(key: string, info: Info) {
const file = Bun.file(filepath)
const data = await all() // READ
await Bun.write(file, JSON.stringify({ ...data, [key]: info }, null, 2)) // WRITE
await fs.chmod(file.name!, 0o600)
}Problem: Between read and write, another process can write, causing data loss.
2. Silent Failure Returns Empty Object
Auth.all() silently returns {} on any read failure:
const data = await file.json().catch(() => ({}) as Record<string, unknown>)Problem: If file is corrupted/unreadable, next Auth.set() writes empty object + new provider, erasing all existing providers.
3. No File Locking
A Lock utility exists at util/lock.ts but is not used by the auth module. Multiple processes compete for the same file:
- CLI TUI
- Dev server (
bun dev -- serve) - Web app (via SDK)
- Desktop app
- Multiple terminal windows
4. Race Condition Timeline
| Time | Process A | Process B | File State |
|---|---|---|---|
| T1 | Reads: { p1, p2 } |
- | { p1, p2 } |
| T2 | Adding p3... | Reads: { p1, p2 } |
{ p1, p2 } |
| T3 | Writes: { p1, p2, p3 } |
- | { p1, p2, p3 } |
| T4 | Done | Writes: { p1, p2 } |
{ p1, p2 } ❌ p3 lost |
Key Files
| File | Purpose |
|---|---|
packages/opencode/src/auth/index.ts |
Core auth.json read/write operations |
packages/opencode/src/provider/auth.ts |
Provider auth flow (OAuth/API key) |
packages/opencode/src/util/lock.ts |
Lock utility (NOT used by auth) |
Suggested Fix
Option 1: Use Existing Lock Utility
import { Lock } from "../util/lock"
export async function set(key: string, info: Info) {
const release = await Lock.write("auth")
try {
const file = Bun.file(filepath)
const data = await all()
await Bun.write(file, JSON.stringify({ ...data, [key]: info }, null, 2))
await fs.chmod(file.name!, 0o600)
} finally {
release()
}
}Option 2: Atomic Write with Rename
export async function set(key: string, info: Info) {
const file = Bun.file(filepath)
const tempFile = filepath + ".tmp." + crypto.randomUUID()
const data = await all()
await Bun.write(tempFile, JSON.stringify({ ...data, [key]: info }, null, 2))
await fs.chmod(tempFile, 0o600)
await fs.rename(tempFile, filepath) // Atomic on most filesystems
}Option 3: Better Error Handling
export async function all(): Promise<Record<string, Info>> {
const file = Bun.file(filepath)
if (!await file.exists()) return {}
try {
const data = await file.json()
// ... validation
} catch (e) {
log.error("Failed to read auth.json, preserving existing file", { error: e })
throw e // Don't silently return empty object!
}
}Priority
High - Data loss affecting user credentials
Reproduction
- Open two terminal windows
- Run
bun devin both (orbun dev+bun dev -- serve) - Add a provider in one terminal
- Add a different provider in the other terminal quickly
- Check auth.json - one provider may be missing
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels