fix(core): import legacy auth.json only into the user's database file (#63716)

This commit is contained in:
Aiden Cline
2026-11-13 02:56:05 -06:00
committed by GitHub
Unverified
parent de63679df0
commit f2687cc30a
3 changed files with 26 additions and 7 deletions
+8 -5
View File
@@ -30,7 +30,9 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/st
// releasing a shared semaphore resumes the waiting object's fiber inside the
// releasing object's I/O context, where its first storage call is rejected as
// cross-object I/O.
const databaseLayer = (lock: Effect.Effect<Semaphore.Semaphore>) =>
// Only a database file the user opens imports their legacy auth.json; in-memory and injected databases belong to
// embedders and tests, which must not pick up the user's credentials.
const databaseLayer = (lock: Effect.Effect<Semaphore.Semaphore>, legacyCredentials = false) =>
Layer.effect(
Service,
Effect.gen(function* () {
@@ -48,7 +50,7 @@ const databaseLayer = (lock: Effect.Effect<Semaphore.Semaphore>) =>
// Durable Object SQLite always enforces foreign keys and rejects the pragma.
if (supportsForeignKeyToggle && !durableObject) yield* db.run("PRAGMA foreign_keys = ON")
const semaphore = yield* lock
yield* semaphore.withPermit(DatabaseMigration.apply(db))
yield* semaphore.withPermit(DatabaseMigration.apply(db, { legacyCredentials }))
return { db }
}).pipe(Effect.orDie),
@@ -70,9 +72,10 @@ export function layer(options: Options = { path: ":memory:" }) {
return Layer.unwrap(
Effect.gen(function* () {
const provide = (filename: string) =>
databaseLayer(filename === ":memory:" ? Semaphore.make(1) : Effect.succeed(lockFor(filename))).pipe(
Layer.provide(sqliteLayer({ filename })),
)
(filename === ":memory:"
? databaseLayer(Semaphore.make(1))
: databaseLayer(Effect.succeed(lockFor(filename)), true)
).pipe(Layer.provide(sqliteLayer({ filename })))
const filename = options.path ?? ":memory:"
if (filename === ":memory:") return provide(filename)
const file = isAbsolute(filename) ? filename : join((yield* Global.Service).data, filename)
+2 -2
View File
@@ -21,7 +21,7 @@ export type Migration = {
// Not serialized here: the Database layer holds a lock scoped to the database
// it is bootstrapping, since two instances over one file must not race.
export function apply(db: Database) {
export function apply(db: Database, options = { legacyCredentials: true }) {
return Effect.gen(function* () {
// OpenCode owns the unprefixed table namespace. Embedders sharing this
// database may own underscore-prefixed tables, which bootstrap ignores.
@@ -36,7 +36,7 @@ export function apply(db: Database) {
yield* db.transaction((tx) =>
Effect.gen(function* () {
yield* schema.up(tx)
yield* legacyCredentials.up(tx)
if (options.legacyCredentials) yield* legacyCredentials.up(tx)
yield* tx.run(
sql`CREATE TABLE ${sql.identifier("migration")} (id TEXT PRIMARY KEY, time_completed INTEGER NOT NULL)`,
)
@@ -553,6 +553,22 @@ describe("DatabaseMigration", () => {
)
})
test("leaves legacy credentials out of in-memory databases", async () => {
await using tmp = await tmpdir()
await Bun.write(
path.join(tmp.path, "auth.json"),
JSON.stringify({ openai: { type: "oauth", refresh: "refresh", access: "access", expires: 123 } }),
)
const rows = await Effect.runPromise(
Database.Service.use((database) => database.db.all(sql`SELECT id FROM credential`)).pipe(
Effect.provide(Database.layer({ path: ":memory:" })),
Effect.provideService(Global.Service, Global.make({ data: tmp.path })),
),
)
expect(rows).toEqual([])
})
test("bootstraps a fresh database when the legacy credential file is malformed", async () => {
await using tmp = await tmpdir()
await Bun.write(path.join(tmp.path, "auth.json"), "not json")