Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/device-authorization-url.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"@opencode-ai/core": patch
---

Fix OpenCode Console device authorization URLs when the server returns an origin-rooted verification path.
17 changes: 10 additions & 7 deletions packages/core/src/plugin/provider/opencode.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -46,15 +46,18 @@ 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:") {
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 ?? `${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)),
}
Expand Down
36 changes: 34 additions & 2 deletions packages/core/test/plugin/provider-opencode.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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,
})
Expand All@@ -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",
Expand All@@ -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()
Expand Down
Loading