From d561c65a38d29b8cb023dff6d90a9f51ceb90f5c Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 21 Aug 2026 19:08:49 -0400 Subject: [PATCH 1/2] fix(core): resolve device verification URL --- .changeset/device-authorization-url.md | 5 +++++ packages/core/src/plugin/provider/opencode.ts | 8 +++----- packages/core/test/plugin/provider-opencode.test.ts | 4 ++-- 3 files changed, 10 insertions(+), 7 deletions(-) create mode 100644 .changeset/device-authorization-url.md diff --git a/.changeset/device-authorization-url.md b/.changeset/device-authorization-url.md new file mode 100644 index 000000000000..2bbf73cb05d6 --- /dev/null +++ b/.changeset/device-authorization-url.md @@ -0,0 +1,5 @@ +--- +"@opencode-ai/core": patch +--- + +Fix OpenCode Console device authorization URLs when the server returns an origin-rooted verification path. diff --git a/packages/core/src/plugin/provider/opencode.ts b/packages/core/src/plugin/provider/opencode.ts index f65feb9b229d..91a61ddcdb33 100644 --- a/packages/core/src/plugin/provider/opencode.ts +++ b/packages/core/src/plugin/provider/opencode.ts @@ -46,15 +46,13 @@ function oauth(http: HttpClient.HttpClient) { Effect.gen(function* () { const server = yield* normalizeServer(answer.server ?? defaultServer) const device = yield* post(http, `${server}/auth/device/code`, { client_id: clientID }, Device) - const verification = URL.canParse(device.verification_uri_complete) - ? new URL(device.verification_uri_complete) - : undefined - if (verification && verification.protocol !== "http:" && verification.protocol !== "https:") { + const verification = new URL(device.verification_uri_complete, `${server}/`) + if (verification.protocol !== "http:" && verification.protocol !== "https:") { return yield* Effect.fail(new Error("Invalid device verification URL: expected HTTP(S)")) } return { mode: "auto" as const, - url: verification?.href ?? `${server}/${device.verification_uri_complete.replace(/^\/+/, "")}`, + url: verification.href, instructions: `Enter code: ${device.user_code}`, callback: poll(http, server, device.device_code, Duration.seconds(device.interval)), } diff --git a/packages/core/test/plugin/provider-opencode.test.ts b/packages/core/test/plugin/provider-opencode.test.ts index 784196cc7d70..0c70061f4e2e 100644 --- a/packages/core/test/plugin/provider-opencode.test.ts +++ b/packages/core/test/plugin/provider-opencode.test.ts @@ -105,7 +105,7 @@ describe("OpencodePlugin", () => { return Response.json({ device_code: "device", user_code: "user", - verification_uri_complete: `${url.origin}/verify`, + verification_uri_complete: "/console/device?user_code=user&client_id=opencode-cli", expires_in: 60, interval: 0, }) @@ -130,7 +130,7 @@ describe("OpencodePlugin", () => { methodID: Integration.MethodID.make("device"), answer: { server: `${server.url.origin}/console///?ignored=true#ignored` }, }) - expect(attempt.url).toBe(`${server.url.origin}/verify`) + expect(attempt.url).toBe(`${server.url.origin}/console/device?user_code=user&client_id=opencode-cli`) yield* eventually( integrations.oauth.status({ integrationID, attemptID: attempt.attemptID }), (status) => status.status === "complete", From f8cebc76c00213f9bdfc17e871a0d4af77d7036d Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 21 Aug 2026 19:14:43 -0400 Subject: [PATCH 2/2] fix(core): handle malformed verification URLs --- packages/core/src/plugin/provider/opencode.ts | 13 +++++--- .../test/plugin/provider-opencode.test.ts | 32 +++++++++++++++++++ 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/packages/core/src/plugin/provider/opencode.ts b/packages/core/src/plugin/provider/opencode.ts index 91a61ddcdb33..c3b7f1ce915f 100644 --- a/packages/core/src/plugin/provider/opencode.ts +++ b/packages/core/src/plugin/provider/opencode.ts @@ -46,10 +46,15 @@ function oauth(http: HttpClient.HttpClient) { Effect.gen(function* () { const server = yield* normalizeServer(answer.server ?? defaultServer) const device = yield* post(http, `${server}/auth/device/code`, { client_id: clientID }, Device) - const verification = new URL(device.verification_uri_complete, `${server}/`) - if (verification.protocol !== "http:" && verification.protocol !== "https:") { - return yield* Effect.fail(new Error("Invalid device verification URL: expected HTTP(S)")) - } + const verification = yield* Effect.try({ + try: () => { + const url = new URL(device.verification_uri_complete, `${server}/`) + if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error("expected HTTP(S)") + return url + }, + catch: (cause) => + new Error(`Invalid device verification URL: ${cause instanceof Error ? cause.message : String(cause)}`), + }) return { mode: "auto" as const, url: verification.href, diff --git a/packages/core/test/plugin/provider-opencode.test.ts b/packages/core/test/plugin/provider-opencode.test.ts index 0c70061f4e2e..2a1fac997813 100644 --- a/packages/core/test/plugin/provider-opencode.test.ts +++ b/packages/core/test/plugin/provider-opencode.test.ts @@ -148,6 +148,38 @@ describe("OpencodePlugin", () => { ), ) + it.live("rejects malformed device verification URLs", () => + Effect.acquireUseRelease( + Effect.sync(() => + Bun.serve({ + port: 0, + fetch: () => + Response.json({ + device_code: "device", + user_code: "user", + verification_uri_complete: "http://[::1", + expires_in: 60, + interval: 0, + }), + }), + ), + (server) => + Effect.gen(function* () { + yield* addPlugin() + const error = yield* (yield* Integration.Service).oauth + .connect({ + integrationID: Integration.ID.make("opencode"), + methodID: Integration.MethodID.make("device"), + answer: { server: server.url.origin }, + }) + .pipe(Effect.flip) + expect(error).toBeInstanceOf(Integration.AuthorizationError) + expect(String(error.cause)).toContain("Invalid device verification URL") + }), + (server) => Effect.promise(() => server.stop(true)), + ), + ) + it.effect("rejects non-HTTP OpenCode servers", () => Effect.gen(function* () { yield* addPlugin()