| 299 | } |
| 300 | |
| 301 | async startGithubDeviceFlow(): Promise< |
| 302 | Result<{ flowId: string; verificationUri: string; userCode: string }, string> |
| 303 | > { |
| 304 | const owner = this.getAllowedGithubOwner(); |
| 305 | if (!owner) { |
| 306 | return Err("GitHub owner login is not configured"); |
| 307 | } |
| 308 | |
| 309 | const trackedFlows = this.getTrackedGithubDeviceFlowCount(); |
| 310 | if (trackedFlows + this.githubDeviceFlowStartsInFlight >= MAX_CONCURRENT_GITHUB_DEVICE_FLOWS) { |
| 311 | return Err("Too many concurrent GitHub login attempts. Please wait and try again."); |
| 312 | } |
| 313 | |
| 314 | this.githubDeviceFlowStartsInFlight += 1; |
| 315 | |
| 316 | const flowId = crypto.randomUUID(); |
| 317 | |
| 318 | try { |
| 319 | const response = await fetch(GITHUB_DEVICE_CODE_URL, { |
| 320 | method: "POST", |
| 321 | headers: { |
| 322 | Accept: "application/json", |
| 323 | "Content-Type": "application/x-www-form-urlencoded", |
| 324 | }, |
| 325 | body: new URLSearchParams({ |
| 326 | client_id: MUX_SERVER_GITHUB_CLIENT_ID, |
| 327 | scope: GITHUB_DEVICE_FLOW_SCOPE, |
| 328 | }), |
| 329 | }); |
| 330 | |
| 331 | if (!response.ok) { |
| 332 | const errorBody = await response.text().catch(() => ""); |
| 333 | return Err( |
| 334 | `GitHub device-code request failed (${response.status})${errorBody ? `: ${errorBody}` : ""}` |
| 335 | ); |
| 336 | } |
| 337 | |
| 338 | const json = (await response.json()) as unknown; |
| 339 | const payload = parseGithubDeviceCodeResponse(json); |
| 340 | if (!payload) { |
| 341 | return Err("GitHub device-code endpoint returned an invalid response"); |
| 342 | } |
| 343 | |
| 344 | const deferred = |
| 345 | createDeferred<Result<{ sessionId: string; sessionToken: string }, string>>(); |
| 346 | const timeout = setTimeout(() => { |
| 347 | this.finishGithubDeviceFlow(flowId, Err("Timed out waiting for GitHub authorization")); |
| 348 | }, DEFAULT_DEVICE_FLOW_TIMEOUT_MS); |
| 349 | |
| 350 | this.githubDeviceFlows.set(flowId, { |
| 351 | flowId, |
| 352 | deviceCode: payload.device_code, |
| 353 | intervalSeconds: payload.interval ?? 5, |
| 354 | cancelled: false, |
| 355 | pollingStarted: false, |
| 356 | timeout, |
| 357 | cleanupTimeout: null, |
| 358 | resultPromise: deferred.promise, |