mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-24 09:03:37 +00:00
fix: resolve console device URLs (#44029)
This commit is contained in:
parent
3a4c253969
commit
e00890c672
4 changed files with 108 additions and 10 deletions
|
|
@ -45,9 +45,18 @@ function oauth(http: HttpClient.HttpClient) {
|
|||
authorize: () =>
|
||||
Effect.gen(function* () {
|
||||
const device = yield* post(http, `${defaultServer}/auth/device/code`, { client_id: clientID }, Device)
|
||||
const verification = yield* Effect.try({
|
||||
try: () => {
|
||||
const url = new URL(device.verification_uri_complete, `${defaultServer}/`)
|
||||
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: `${defaultServer}${device.verification_uri_complete}`,
|
||||
url: verification.href,
|
||||
instructions: `Enter code: ${device.user_code}`,
|
||||
callback: poll(http, defaultServer, device.device_code, Duration.seconds(device.interval)),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { describe, expect } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { HttpClient, HttpClientResponse } from "effect/unstable/http"
|
||||
import { Catalog } from "@opencode-ai/core/catalog"
|
||||
import { Credential } from "@opencode-ai/core/credential"
|
||||
import { EventV2 } from "@opencode-ai/core/event"
|
||||
|
|
@ -14,14 +15,16 @@ import { PluginTestLayer } from "./fixture"
|
|||
|
||||
const it = testEffect(PluginTestLayer)
|
||||
|
||||
const addPlugin = Effect.fn(function* () {
|
||||
const addPlugin = Effect.fn(function* (http?: HttpClient.HttpClient) {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const host = yield* PluginHost.make(plugin)
|
||||
const events = yield* EventV2.Service
|
||||
const integration = yield* Integration.Service
|
||||
const client = yield* HttpClient.HttpClient
|
||||
yield* OpencodePlugin.effect(host).pipe(
|
||||
Effect.provideService(EventV2.Service, events),
|
||||
Effect.provideService(Integration.Service, integration),
|
||||
Effect.provideService(HttpClient.HttpClient, http ?? client),
|
||||
)
|
||||
})
|
||||
|
||||
|
|
@ -82,6 +85,63 @@ describe("OpencodePlugin", () => {
|
|||
}),
|
||||
)
|
||||
|
||||
it.effect("resolves origin-rooted device verification URLs", () =>
|
||||
Effect.gen(function* () {
|
||||
const http = HttpClient.make((request) =>
|
||||
Effect.succeed(
|
||||
HttpClientResponse.fromWeb(
|
||||
request,
|
||||
Response.json({
|
||||
device_code: "device",
|
||||
user_code: "user",
|
||||
verification_uri_complete: "/console/device?user_code=user&client_id=opencode-cli",
|
||||
expires_in: 60,
|
||||
interval: 60,
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
yield* addPlugin(http)
|
||||
const integration = yield* Integration.Service
|
||||
const attempt = yield* integration.connection.oauth({
|
||||
integrationID: Integration.ID.make("opencode"),
|
||||
methodID: Integration.MethodID.make("device"),
|
||||
inputs: {},
|
||||
})
|
||||
expect(attempt.url).toBe("https://opencode.ai/console/device?user_code=user&client_id=opencode-cli")
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("rejects malformed device verification URLs", () =>
|
||||
Effect.gen(function* () {
|
||||
const http = HttpClient.make((request) =>
|
||||
Effect.succeed(
|
||||
HttpClientResponse.fromWeb(
|
||||
request,
|
||||
Response.json({
|
||||
device_code: "device",
|
||||
user_code: "user",
|
||||
verification_uri_complete: "http://[::1",
|
||||
expires_in: 60,
|
||||
interval: 60,
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
yield* addPlugin(http)
|
||||
const integration = yield* Integration.Service
|
||||
const error = yield* integration.connection
|
||||
.oauth({
|
||||
integrationID: Integration.ID.make("opencode"),
|
||||
methodID: Integration.MethodID.make("device"),
|
||||
inputs: {},
|
||||
})
|
||||
.pipe(Effect.flip)
|
||||
expect(error).toBeInstanceOf(Integration.AuthorizationError)
|
||||
expect(String(error.cause)).toContain("Invalid device verification URL")
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("loads providers and models from the connected OpenCode server", () =>
|
||||
Effect.acquireUseRelease(
|
||||
Effect.sync(() => {
|
||||
|
|
|
|||
|
|
@ -396,10 +396,18 @@ const layer: Layer.Layer<Service, never, AccountRepo.Service | HttpClient.HttpCl
|
|||
const parsed = yield* HttpClientResponse.schemaBodyJson(DeviceAuth)(response).pipe(
|
||||
mapAccountServiceError("Failed to decode response"),
|
||||
)
|
||||
const verification = yield* Effect.try({
|
||||
try: () => {
|
||||
const url = new URL(parsed.verification_uri_complete, `${normalizedServer}/`)
|
||||
if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error("expected HTTP(S)")
|
||||
return url.href
|
||||
},
|
||||
catch: (cause) => new AccountServiceError({ message: "Invalid device verification URL", cause }),
|
||||
})
|
||||
return new Login({
|
||||
code: parsed.device_code,
|
||||
user: parsed.user_code,
|
||||
url: `${normalizedServer}${parsed.verification_uri_complete}`,
|
||||
url: verification,
|
||||
server: normalizedServer,
|
||||
expiry: parsed.expires_in,
|
||||
interval: parsed.interval,
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { Account } from "../../src/account/account"
|
|||
import {
|
||||
AccessToken,
|
||||
AccountID,
|
||||
AccountServiceError,
|
||||
AccountTransportError,
|
||||
DeviceCode,
|
||||
Login,
|
||||
|
|
@ -71,18 +72,18 @@ const deviceTokenClient = (body: unknown, status = 400) =>
|
|||
const poll = (body: unknown, status = 400) =>
|
||||
Account.Service.use((s) => s.poll(login())).pipe(Effect.provide(live(deviceTokenClient(body, status))))
|
||||
|
||||
it.live("login normalizes trailing slashes in the provided server URL", () =>
|
||||
it.live("login resolves origin-rooted verification URLs from servers with base paths", () =>
|
||||
Effect.gen(function* () {
|
||||
const seen: Array<string> = []
|
||||
const client = HttpClient.make((req) =>
|
||||
Effect.gen(function* () {
|
||||
seen.push(`${req.method} ${req.url}`)
|
||||
|
||||
if (req.url === "https://one.example.com/auth/device/code") {
|
||||
if (req.url === "https://one.example.com/console/auth/device/code") {
|
||||
return json(req, {
|
||||
device_code: "device-code",
|
||||
user_code: "user-code",
|
||||
verification_uri_complete: "/device?user_code=user-code",
|
||||
verification_uri_complete: "/console/device?user_code=user-code",
|
||||
expires_in: 600,
|
||||
interval: 5,
|
||||
})
|
||||
|
|
@ -92,11 +93,31 @@ it.live("login normalizes trailing slashes in the provided server URL", () =>
|
|||
}),
|
||||
)
|
||||
|
||||
const result = yield* Account.use.login("https://one.example.com/").pipe(Effect.provide(live(client)))
|
||||
const result = yield* Account.use.login("https://one.example.com/console/").pipe(Effect.provide(live(client)))
|
||||
|
||||
expect(seen).toEqual(["POST https://one.example.com/auth/device/code"])
|
||||
expect(result.server).toBe("https://one.example.com")
|
||||
expect(result.url).toBe("https://one.example.com/device?user_code=user-code")
|
||||
expect(seen).toEqual(["POST https://one.example.com/console/auth/device/code"])
|
||||
expect(result.server).toBe("https://one.example.com/console")
|
||||
expect(result.url).toBe("https://one.example.com/console/device?user_code=user-code")
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("login rejects malformed device verification URLs", () =>
|
||||
Effect.gen(function* () {
|
||||
const client = HttpClient.make((req) =>
|
||||
Effect.succeed(
|
||||
json(req, {
|
||||
device_code: "device-code",
|
||||
user_code: "user-code",
|
||||
verification_uri_complete: "http://[::1",
|
||||
expires_in: 600,
|
||||
interval: 5,
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
const error = yield* Effect.flip(Account.use.login("https://one.example.com").pipe(Effect.provide(live(client))))
|
||||
expect(error).toBeInstanceOf(AccountServiceError)
|
||||
if (error instanceof AccountServiceError) expect(error.message).toBe("Invalid device verification URL")
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue