| 377 | } |
| 378 | |
| 379 | async waitForGithubDeviceFlow( |
| 380 | flowId: string, |
| 381 | opts?: { timeoutMs?: number; userAgent?: string; ipAddress?: string } |
| 382 | ): Promise<Result<{ sessionId: string; sessionToken: string }, string>> { |
| 383 | const flow = this.githubDeviceFlows.get(flowId); |
| 384 | if (!flow) { |
| 385 | return Err("Device flow not found"); |
| 386 | } |
| 387 | |
| 388 | // Keep the first non-empty metadata from callers so the created session can be labeled. |
| 389 | flow.userAgent ??= normalizeOptionalString(opts?.userAgent); |
| 390 | flow.ipAddress ??= normalizeIpAddress(opts?.ipAddress); |
| 391 | |
| 392 | if (!flow.pollingStarted) { |
| 393 | flow.pollingStarted = true; |
| 394 | void this.pollGithubDeviceFlow(flow); |
| 395 | } |
| 396 | |
| 397 | const timeoutMs = opts?.timeoutMs ?? DEFAULT_DEVICE_FLOW_TIMEOUT_MS; |
| 398 | |
| 399 | let timeoutHandle: ReturnType<typeof setTimeout> | null = null; |
| 400 | const timeoutPromise = new Promise<Result<{ sessionId: string; sessionToken: string }, string>>( |
| 401 | (resolve) => { |
| 402 | timeoutHandle = setTimeout(() => { |
| 403 | resolve(Err("Timed out waiting for GitHub authorization")); |
| 404 | }, timeoutMs); |
| 405 | } |
| 406 | ); |
| 407 | |
| 408 | const result = await Promise.race([flow.resultPromise, timeoutPromise]); |
| 409 | |
| 410 | if (timeoutHandle !== null) { |
| 411 | clearTimeout(timeoutHandle); |
| 412 | } |
| 413 | |
| 414 | if (!result.success) { |
| 415 | this.finishGithubDeviceFlow(flowId, result); |
| 416 | } |
| 417 | |
| 418 | return result; |
| 419 | } |
| 420 | |
| 421 | cancelGithubDeviceFlow(flowId: string): void { |
| 422 | void this.finishGithubDeviceFlow(flowId, Err("Device flow cancelled")); |