From f2687cc30aca4db94482794b4e37c6ef2229b51c Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Fri, 13 Nov 2026 02:56:05 -0600 Subject: [PATCH] fix(core): import legacy auth.json only into the user's database file (#63716) --- packages/core/src/database/database.ts | 13 ++++++++----- packages/core/src/database/migration.ts | 4 ++-- packages/core/test/database-migration.test.ts | 16 ++++++++++++++++ 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/packages/core/src/database/database.ts b/packages/core/src/database/database.ts index b18521b3084..5cf243fd028 100644 --- a/packages/core/src/database/database.ts +++ b/packages/core/src/database/database.ts @@ -30,7 +30,9 @@ export class Service extends Context.Service()("@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) => +// 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, legacyCredentials = false) => Layer.effect( Service, Effect.gen(function* () { @@ -48,7 +50,7 @@ const databaseLayer = (lock: Effect.Effect) => // 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) diff --git a/packages/core/src/database/migration.ts b/packages/core/src/database/migration.ts index 9f006d1eb56..5d3512820b1 100644 --- a/packages/core/src/database/migration.ts +++ b/packages/core/src/database/migration.ts @@ -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)`, ) diff --git a/packages/core/test/database-migration.test.ts b/packages/core/test/database-migration.test.ts index bfdfd4adbfd..fd3bc387e27 100644 --- a/packages/core/test/database-migration.test.ts +++ b/packages/core/test/database-migration.test.ts @@ -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")