Skip to content
Open
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
14 changes: 13 additions & 1 deletion packages/opencode/src/account/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -396,10 +396,22 @@ 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
},
catch: (cause) =>
new AccountServiceError({
message: `Invalid device verification URL: ${cause instanceof Error ? cause.message : String(cause)}`,
cause,
}),
})
return new Login({
code: parsed.device_code,
user: parsed.user_code,
url: `${normalizedServer}${parsed.verification_uri_complete}`,
url: verification.href,
server: normalizedServer,
expiry: parsed.expires_in,
interval: parsed.interval,
Expand Down
88 changes: 88 additions & 0 deletions packages/opencode/test/account/service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Account } from "../../src/account/account"
import {
AccessToken,
AccountID,
AccountServiceError,
AccountTransportError,
DeviceCode,
Login,
Expand Down Expand Up @@ -100,6 +101,93 @@ it.live("login normalizes trailing slashes in the provided server URL", () =>
}),
)

it.live("login resolves the verification URL against the server origin", () =>
Effect.gen(function* () {
const client = HttpClient.make((req) =>
Effect.succeed(
json(req, {
device_code: "device-code",
user_code: "user-code",
verification_uri_complete: "/console/device?user_code=user-code",
expires_in: 600,
interval: 5,
}),
),
)

const result = yield* Account.use.login("https://one.example.com/console").pipe(Effect.provide(live(client)))

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 resolves relative verification URLs beneath the server path", () =>
Effect.gen(function* () {
const client = HttpClient.make((req) =>
Effect.succeed(
json(req, {
device_code: "device-code",
user_code: "user-code",
verification_uri_complete: "device?user_code=user-code",
expires_in: 600,
interval: 5,
}),
),
)

const result = yield* Account.use.login("https://one.example.com/console").pipe(Effect.provide(live(client)))

expect(result.url).toBe("https://one.example.com/console/device?user_code=user-code")
}),
)

it.live("login rejects malformed 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/console").pipe(Effect.provide(live(client))),
)

expect(error).toBeInstanceOf(AccountServiceError)
expect(error.message).toContain("Invalid device verification URL")
}),
)

it.live("login rejects non-HTTP 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: "file:///tmp/device",
expires_in: 600,
interval: 5,
}),
),
)

const error = yield* Effect.flip(
Account.use.login("https://one.example.com/console").pipe(Effect.provide(live(client))),
)

expect(error).toBeInstanceOf(AccountServiceError)
expect(error.message).toContain("Invalid device verification URL: expected HTTP(S)")
}),
)

it.live("login maps transport failures to account transport errors", () =>
Effect.gen(function* () {
const client = HttpClient.make((req) =>
Expand Down
Loading