Compare commits

...

2 Commits

Author SHA1 Message Date
Kit Langton 46c32883fb fix(opencode): validate device login URL 2026-08-22 00:10:11 +00:00
Slickstef11 a5685ebb52 fix(opencode): resolve console device login URL 2026-08-21 19:35:45 +00:00
2 changed files with 101 additions and 1 deletions
+13 -1
View File
@@ -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,
@@ -10,6 +10,7 @@ import { Account } from "../../src/account/account"
import {
AccessToken,
AccountID,
AccountServiceError,
AccountTransportError,
DeviceCode,
Login,
@@ -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) =>