Compare commits

...

1 Commits

Author SHA1 Message Date
Aiden Cline c4cba53f67 fix(core): refresh Console auth before catalog load 2026-08-04 20:00:21 +00:00
2 changed files with 103 additions and 1 deletions
+12 -1
View File
@@ -89,14 +89,24 @@ export const OpencodePlugin = define<HttpClient.HttpClient | Bus.Service | Scope
const http = yield* HttpClient.HttpClient
const loading = Semaphore.makeUnsafe(1)
let connected = false
let savedConnection = false
let providers: typeof ConfigV1.Info.Type.provider | undefined
const load = Effect.fn("OpencodePlugin.load")(function* () {
const connection = yield* ctx.integration.connection.active("opencode")
const credential = connection
const resolved = connection
? yield* ctx.integration.connection.resolve(connection).pipe(Effect.catch(() => Effect.succeed(undefined)))
: undefined
// Plugin activation batches transforms, so the first resolve can precede this plugin's OAuth registration.
const credential =
connection && resolved?.type === "oauth" && resolved.expires <= Date.now() + Duration.toMillis(Duration.minutes(5))
? yield* ctx.integration.reload().pipe(
Effect.andThen(ctx.integration.connection.resolve(connection)),
Effect.catch(() => Effect.succeed(undefined)),
)
: resolved
connected = connection !== undefined
savedConnection = connection?.type === "credential"
providers = credential
? yield* fetchProviders(http, credential).pipe(
Effect.catch((cause) =>
@@ -116,6 +126,7 @@ export const OpencodePlugin = define<HttpClient.HttpClient | Bus.Service | Scope
yield* load()
yield* ctx.catalog.transform((catalog) => {
if (savedConnection && providers === undefined) catalog.provider.remove(Provider.ID.opencode)
for (const [providerID, item] of Object.entries(providers ?? {})) {
catalog.provider.update(providerID, (provider) => {
provider.integrationID = Integration.ID.make("opencode")
@@ -10,6 +10,7 @@ import { Plugin } from "@opencode-ai/core/plugin"
import { PluginHost } from "@opencode-ai/core/plugin/host"
import { OpencodePlugin } from "@opencode-ai/core/plugin/provider/opencode"
import { Provider } from "@opencode-ai/core/provider"
import { State } from "@opencode-ai/core/state"
import { testEffect } from "../lib/effect"
import { PluginTestLayer } from "./fixture"
@@ -283,6 +284,96 @@ describe("OpencodePlugin", () => {
),
)
it.live("refreshes saved OAuth before loading the Console catalog on cold startup", () =>
Effect.acquireUseRelease(
Effect.sync(() => {
const requests: string[] = []
return {
requests,
server: Bun.serve({
port: 0,
fetch: (request) => {
const url = new URL(request.url)
if (url.pathname === "/auth/device/token") {
requests.push("refresh")
return Response.json({ access_token: "fresh", refresh_token: "next", expires_in: 600 })
}
if (url.pathname === "/api/config") {
requests.push(`config:${request.headers.get("authorization")}`)
return Response.json({
config: {
provider: {
console: {
name: "Console",
npm: "@ai-sdk/openai-compatible",
models: { current: { name: "Current" } },
},
},
},
})
}
return new Response("Not found", { status: 404 })
},
}),
}
}),
({ requests, server }) =>
Effect.gen(function* () {
yield* (yield* Credential.Service).create({
integrationID: Integration.ID.make("opencode"),
value: Credential.OAuth.make({
type: "oauth",
methodID: Integration.MethodID.make("device"),
access: "expired",
refresh: "refresh",
expires: 1,
metadata: { server: server.url.origin },
}),
})
yield* State.batch(addPlugin())
expect(requests).toEqual(["refresh", "config:Bearer fresh"])
expect(
yield* (yield* Catalog.Service).model.get(Provider.ID.make("console"), Model.ID.make("current")),
).toBeDefined()
}),
({ server }) => Effect.promise(() => server.stop(true)),
),
)
it.live("hides legacy fallback models when Console configuration fails", () =>
Effect.acquireUseRelease(
Effect.sync(() =>
Bun.serve({
port: 0,
fetch: () => new Response("Unauthorized", { status: 401 }),
}),
),
(server) =>
Effect.gen(function* () {
const catalog = yield* Catalog.Service
yield* catalog.transform((draft) => {
draft.provider.update(Provider.ID.opencode, () => {})
draft.model.update(Provider.ID.opencode, Model.ID.make("legacy"), () => {})
})
yield* (yield* Credential.Service).create({
integrationID: Integration.ID.make("opencode"),
value: Credential.Key.make({
type: "key",
key: "console-key",
metadata: { server: server.url.origin },
}),
})
yield* State.batch(addPlugin())
expect(yield* catalog.model.get(Provider.ID.opencode, Model.ID.make("legacy"))).toBeUndefined()
}),
(server) => Effect.promise(() => server.stop(true)),
),
)
it.effect("uses a public key and disables paid models without credentials", () =>
withEnv({ OPENCODE_API_KEY: undefined }, () =>
Effect.gen(function* () {