Skip to content

Commit 6e09a1d

Browse files
authored
fix(account): handle pending console login polling (#18281)
1 parent 4f21757 commit 6e09a1d

2 files changed

Lines changed: 82 additions & 11 deletions

File tree

packages/opencode/src/account/effect.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,12 @@ export namespace AccountEffect {
148148
mapAccountServiceError("HTTP request failed"),
149149
)
150150

151+
const executeEffect = <E>(request: Effect.Effect<HttpClientRequest.HttpClientRequest, E>) =>
152+
request.pipe(
153+
Effect.flatMap((req) => http.execute(req)),
154+
mapAccountServiceError("HTTP request failed"),
155+
)
156+
151157
const resolveToken = Effect.fnUntraced(function* (row: AccountRow) {
152158
const now = yield* Clock.currentTimeMillis
153159
if (row.token_expiry && row.token_expiry > now) return row.access_token
@@ -290,7 +296,7 @@ export namespace AccountEffect {
290296
})
291297

292298
const poll = Effect.fn("Account.poll")(function* (input: Login) {
293-
const response = yield* executeEffectOk(
299+
const response = yield* executeEffect(
294300
HttpClientRequest.post(`${input.server}/auth/device/token`).pipe(
295301
HttpClientRequest.acceptJson,
296302
HttpClientRequest.schemaBodyJson(DeviceTokenRequest)(

packages/opencode/test/account/service.test.ts

Lines changed: 75 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,24 @@ const encodeOrg = Schema.encodeSync(Org)
3434

3535
const org = (id: string, name: string) => encodeOrg(new Org({ id: OrgID.make(id), name }))
3636

37+
const login = () =>
38+
new Login({
39+
code: DeviceCode.make("device-code"),
40+
user: UserCode.make("user-code"),
41+
url: "https://one.example.com/verify",
42+
server: "https://one.example.com",
43+
expiry: Duration.seconds(600),
44+
interval: Duration.seconds(5),
45+
})
46+
47+
const deviceTokenClient = (body: unknown, status = 400) =>
48+
HttpClient.make((req) =>
49+
Effect.succeed(req.url === "https://one.example.com/auth/device/token" ? json(req, body, status) : json(req, {}, 404)),
50+
)
51+
52+
const poll = (body: unknown, status = 400) =>
53+
AccountEffect.Service.use((s) => s.poll(login())).pipe(Effect.provide(live(deviceTokenClient(body, status))))
54+
3755
it.effect("orgsByAccount groups orgs per account", () =>
3856
Effect.gen(function* () {
3957
yield* AccountRepo.use((r) =>
@@ -172,15 +190,6 @@ it.effect("config sends the selected org header", () =>
172190

173191
it.effect("poll stores the account and first org on success", () =>
174192
Effect.gen(function* () {
175-
const login = new Login({
176-
code: DeviceCode.make("device-code"),
177-
user: UserCode.make("user-code"),
178-
url: "https://one.example.com/verify",
179-
server: "https://one.example.com",
180-
expiry: Duration.seconds(600),
181-
interval: Duration.seconds(5),
182-
})
183-
184193
const client = HttpClient.make((req) =>
185194
Effect.succeed(
186195
req.url === "https://one.example.com/auth/device/token"
@@ -198,7 +207,7 @@ it.effect("poll stores the account and first org on success", () =>
198207
),
199208
)
200209

201-
const res = yield* AccountEffect.Service.use((s) => s.poll(login)).pipe(Effect.provide(live(client)))
210+
const res = yield* AccountEffect.Service.use((s) => s.poll(login())).pipe(Effect.provide(live(client)))
202211

203212
expect(res._tag).toBe("PollSuccess")
204213
if (res._tag === "PollSuccess") {
@@ -215,3 +224,59 @@ it.effect("poll stores the account and first org on success", () =>
215224
)
216225
}),
217226
)
227+
228+
for (const [name, body, expectedTag] of [
229+
[
230+
"pending",
231+
{
232+
error: "authorization_pending",
233+
error_description: "The authorization request is still pending",
234+
},
235+
"PollPending",
236+
],
237+
[
238+
"slow",
239+
{
240+
error: "slow_down",
241+
error_description: "Polling too frequently, please slow down",
242+
},
243+
"PollSlow",
244+
],
245+
[
246+
"denied",
247+
{
248+
error: "access_denied",
249+
error_description: "The authorization request was denied",
250+
},
251+
"PollDenied",
252+
],
253+
[
254+
"expired",
255+
{
256+
error: "expired_token",
257+
error_description: "The device code has expired",
258+
},
259+
"PollExpired",
260+
],
261+
] as const) {
262+
it.effect(`poll returns ${name} for ${body.error}`, () =>
263+
Effect.gen(function* () {
264+
const result = yield* poll(body)
265+
expect(result._tag).toBe(expectedTag)
266+
}),
267+
)
268+
}
269+
270+
it.effect("poll returns poll error for other OAuth errors", () =>
271+
Effect.gen(function* () {
272+
const result = yield* poll({
273+
error: "server_error",
274+
error_description: "An unexpected error occurred",
275+
})
276+
277+
expect(result._tag).toBe("PollError")
278+
if (result._tag === "PollError") {
279+
expect(String(result.cause)).toContain("server_error")
280+
}
281+
}),
282+
)

0 commit comments

Comments
 (0)