@@ -79,7 +79,19 @@ function createOAuthAbortError(): Error {
7979 return error
8080}
8181
82- async function postForm ( endpoint : string , values : Record < string , string > , signal ?: AbortSignal ) : Promise < Response > {
82+ /** A form POST whose cancellation contract outlives the response headers: `settle` detaches the
83+ * caller's abort forwarder once the body is consumed, so an abort during a body read cancels the
84+ * underlying response too. An abort detaches the forwarder by itself ({ once: true }). */
85+ interface CancellingPost {
86+ response : Response
87+ settle : ( ) => void
88+ }
89+
90+ async function postForm (
91+ endpoint : string ,
92+ values : Record < string , string > ,
93+ signal ?: AbortSignal ,
94+ ) : Promise < CancellingPost > {
8395 const controller = new AbortController ( )
8496 const forwardAbort = ( ) => controller . abort ( signal ?. reason )
8597 if ( signal ?. aborted ) forwardAbort ( )
@@ -88,8 +100,11 @@ async function postForm(endpoint: string, values: Record<string, string>, signal
88100 ( ) => controller . abort ( new Error ( "Kimi Code OAuth request timed out" ) ) ,
89101 OAUTH_REQUEST_TIMEOUT_MS ,
90102 )
103+ const settle = ( ) => {
104+ signal ?. removeEventListener ( "abort" , forwardAbort )
105+ }
91106 try {
92- return await fetch ( endpoint , {
107+ const response = await fetch ( endpoint , {
93108 method : "POST" ,
94109 headers : {
95110 Accept : "application/json" ,
@@ -98,7 +113,11 @@ async function postForm(endpoint: string, values: Record<string, string>, signal
98113 body : new URLSearchParams ( values ) . toString ( ) ,
99114 signal : controller . signal ,
100115 } )
116+ return { response, settle }
101117 } catch ( error ) {
118+ // Nothing will read a body from a POST that never produced a response, so the forwarder
119+ // is detached on this path and nothing else.
120+ settle ( )
102121 // Classify from the CALLER signal: only a rejection that came from the caller's own abort
103122 // is normalized. The caller's reason can be an ordinary Error and fetch forwards it
104123 // verbatim, and an independent OAuth/network failure that merely races the abort must be
@@ -113,7 +132,6 @@ async function postForm(endpoint: string, values: Record<string, string>, signal
113132 throw error
114133 } finally {
115134 clearTimeout ( timeout )
116- signal ?. removeEventListener ( "abort" , forwardAbort )
117135 }
118136}
119137/** Consume a response body under the caller's signal: an abort while the body is still
@@ -148,17 +166,21 @@ async function readOAuthError(response: Response, signal?: AbortSignal): Promise
148166}
149167
150168export async function requestDeviceAuthorization ( signal ?: AbortSignal ) {
151- const response = await postForm (
169+ const { response, settle } = await postForm (
152170 KIMI_CODE_OAUTH_CONFIG . deviceAuthorizationEndpoint ,
153171 { client_id : KIMI_CODE_OAUTH_CONFIG . clientId } ,
154172 signal ,
155173 )
156- if ( ! response . ok ) throw await readOAuthError ( response , signal )
157- return deviceAuthorizationSchema . parse ( await readBodyWithSignal ( ( ) => response . json ( ) , signal ) )
174+ try {
175+ if ( ! response . ok ) throw await readOAuthError ( response , signal )
176+ return deviceAuthorizationSchema . parse ( await readBodyWithSignal ( ( ) => response . json ( ) , signal ) )
177+ } finally {
178+ settle ( )
179+ }
158180}
159181
160182async function requestDeviceToken ( deviceCode : string , signal ?: AbortSignal ) : Promise < KimiCodeCredentials > {
161- const response = await postForm (
183+ const { response, settle } = await postForm (
162184 KIMI_CODE_OAUTH_CONFIG . tokenEndpoint ,
163185 {
164186 grant_type : KIMI_CODE_OAUTH_CONFIG . deviceGrantType ,
@@ -167,15 +189,19 @@ async function requestDeviceToken(deviceCode: string, signal?: AbortSignal): Pro
167189 } ,
168190 signal ,
169191 )
170- if ( ! response . ok ) throw await readOAuthError ( response , signal )
171- const tokens = tokenResponseSchema . parse ( await readBodyWithSignal ( ( ) => response . json ( ) , signal ) )
172- if ( ! tokens . refresh_token ) throw new Error ( "Kimi Code OAuth did not return a refresh token" )
173- return {
174- type : providerIdentifiers . kimiCode ,
175- accessToken : tokens . access_token ,
176- refreshToken : tokens . refresh_token ,
177- expiresAt : Date . now ( ) + tokens . expires_in * 1000 ,
178- tokenType : tokens . token_type ,
192+ try {
193+ if ( ! response . ok ) throw await readOAuthError ( response , signal )
194+ const tokens = tokenResponseSchema . parse ( await readBodyWithSignal ( ( ) => response . json ( ) , signal ) )
195+ if ( ! tokens . refresh_token ) throw new Error ( "Kimi Code OAuth did not return a refresh token" )
196+ return {
197+ type : providerIdentifiers . kimiCode ,
198+ accessToken : tokens . access_token ,
199+ refreshToken : tokens . refresh_token ,
200+ expiresAt : Date . now ( ) + tokens . expires_in * 1000 ,
201+ tokenType : tokens . token_type ,
202+ }
203+ } finally {
204+ settle ( )
179205 }
180206}
181207
@@ -185,7 +211,7 @@ export async function refreshKimiCodeAccessToken(
185211) : Promise < KimiCodeCredentials > {
186212 // The caller's signal aborts the token POST itself, not just the caller's wait: a cancelled
187213 // task must not keep a refresh request (and its rotated refresh token) in flight.
188- const response = await postForm (
214+ const { response, settle } = await postForm (
189215 KIMI_CODE_OAUTH_CONFIG . tokenEndpoint ,
190216 {
191217 grant_type : "refresh_token" ,
@@ -194,14 +220,18 @@ export async function refreshKimiCodeAccessToken(
194220 } ,
195221 signal ,
196222 )
197- if ( ! response . ok ) throw await readOAuthError ( response , signal )
198- const tokens = tokenResponseSchema . parse ( await readBodyWithSignal ( ( ) => response . json ( ) , signal ) )
199- return {
200- type : providerIdentifiers . kimiCode ,
201- accessToken : tokens . access_token ,
202- refreshToken : tokens . refresh_token ?? credentials . refreshToken ,
203- expiresAt : Date . now ( ) + tokens . expires_in * 1000 ,
204- tokenType : tokens . token_type ?? credentials . tokenType ,
223+ try {
224+ if ( ! response . ok ) throw await readOAuthError ( response , signal )
225+ const tokens = tokenResponseSchema . parse ( await readBodyWithSignal ( ( ) => response . json ( ) , signal ) )
226+ return {
227+ type : providerIdentifiers . kimiCode ,
228+ accessToken : tokens . access_token ,
229+ refreshToken : tokens . refresh_token ?? credentials . refreshToken ,
230+ expiresAt : Date . now ( ) + tokens . expires_in * 1000 ,
231+ tokenType : tokens . token_type ?? credentials . tokenType ,
232+ }
233+ } finally {
234+ settle ( )
205235 }
206236}
207237
0 commit comments