@@ -15,6 +15,9 @@ const ACCESS_TOKEN_URL = "https://github.com/login/oauth/access_token";
1515const DEFAULT_SCOPE = "repo" ;
1616const DEFAULT_EXPIRES_IN_SECONDS = 900 ;
1717const DEFAULT_INTERVAL_SECONDS = 5 ;
18+ // #miner-github-read-timeouts: matches github-token-resolution.js's GITHUB_TOKEN_FETCH_TIMEOUT_MS -- a stalled
19+ // connection can't hang forever, here or anywhere else this package talks to GitHub.
20+ const DEVICE_FLOW_FETCH_TIMEOUT_MS = 10_000 ;
1821
1922/** The centrally-held loopover-ams App's OAuth client id -- public (not secret), so it's safe to read from a
2023 * plain env var. Empty/unset means device-flow authorization isn't available in this build/deployment. */
@@ -40,6 +43,7 @@ export async function requestDeviceCode({ clientId, scope = DEFAULT_SCOPE, fetch
4043 method : "POST" ,
4144 headers : { accept : "application/json" , "content-type" : "application/x-www-form-urlencoded" } ,
4245 body : new URLSearchParams ( { client_id : clientId , scope } ) . toString ( ) ,
46+ signal : AbortSignal . timeout ( DEVICE_FLOW_FETCH_TIMEOUT_MS ) ,
4347 } ) ;
4448 if ( ! res . ok ) throw new DeviceFlowError ( "device_code_request_failed" , `GitHub returned HTTP ${ res . status } requesting a device code` ) ;
4549 const data = await res . json ( ) ;
@@ -75,15 +79,23 @@ export async function pollForAccessToken({
7579 for ( ; ; ) {
7680 if ( now ( ) >= deadline ) throw new DeviceFlowError ( "expired_token" , "the device code expired before authorization completed" ) ;
7781 await sleepFn ( interval * 1000 ) ;
78- const res = await fetchFn ( ACCESS_TOKEN_URL , {
79- method : "POST" ,
80- headers : { accept : "application/json" , "content-type" : "application/x-www-form-urlencoded" } ,
81- body : new URLSearchParams ( {
82- client_id : clientId ,
83- device_code : deviceCode ,
84- grant_type : "urn:ietf:params:oauth:grant-type:device_code" ,
85- } ) . toString ( ) ,
86- } ) ;
82+ let res ;
83+ try {
84+ res = await fetchFn ( ACCESS_TOKEN_URL , {
85+ method : "POST" ,
86+ headers : { accept : "application/json" , "content-type" : "application/x-www-form-urlencoded" } ,
87+ body : new URLSearchParams ( {
88+ client_id : clientId ,
89+ device_code : deviceCode ,
90+ grant_type : "urn:ietf:params:oauth:grant-type:device_code" ,
91+ } ) . toString ( ) ,
92+ signal : AbortSignal . timeout ( DEVICE_FLOW_FETCH_TIMEOUT_MS ) ,
93+ } ) ;
94+ } catch {
95+ // A stalled/timed-out attempt is a per-attempt failure, not a fatal one -- the existing deadline check
96+ // at the top of the loop still bounds total polling time, so this just costs one wasted interval.
97+ continue ;
98+ }
8799 const data = await res . json ( ) . catch ( ( ) => ( { } ) ) ;
88100 if ( data && typeof data . access_token === "string" && data . access_token ) {
89101 return { accessToken : data . access_token , scope : typeof data . scope === "string" ? data . scope : "" } ;
0 commit comments