Skip to content

[Bug] Race condition in auth.json causes provider credentials to disappear #37

@randomm

Description

@randomm

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

  1. Open two terminal windows
  2. Run bun dev in both (or bun dev + bun dev -- serve)
  3. Add a provider in one terminal
  4. Add a different provider in the other terminal quickly
  5. Check auth.json - one provider may be missing

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions