@@ -181,3 +181,85 @@ describe("GitHub Copilot OAuth model policy", () => {
181181 expect ( cancel ) . toHaveBeenCalledTimes ( 1 ) ;
182182 } ) ;
183183} ) ;
184+
185+ describe ( "GitHub Copilot OAuth bounded reads" , ( ) => {
186+ it ( "caps oversized OAuth JSON responses instead of buffering the full body" , async ( ) => {
187+ // 18 MiB body in 1 MiB chunks exceeds the 16 MiB default cap on
188+ // the shared readProviderJsonResponse reader.
189+ const CHUNK = 1024 * 1024 ;
190+ const CHUNK_COUNT = 18 ;
191+ let pulls = 0 ;
192+ const encoder = new TextEncoder ( ) ;
193+ const stream = new ReadableStream < Uint8Array > ( {
194+ pull ( controller ) {
195+ if ( pulls >= CHUNK_COUNT ) {
196+ controller . close ( ) ;
197+ return ;
198+ }
199+ pulls += 1 ;
200+ controller . enqueue ( encoder . encode ( "a" . repeat ( CHUNK ) ) ) ;
201+ } ,
202+ } ) ;
203+ vi . stubGlobal (
204+ "fetch" ,
205+ vi . fn (
206+ async ( ) =>
207+ new Response ( stream , {
208+ status : 200 ,
209+ headers : { "Content-Type" : "application/json" } ,
210+ } ) ,
211+ ) ,
212+ ) ;
213+
214+ await expect ( refreshGitHubCopilotToken ( "refresh-token" ) ) . rejects . toThrow (
215+ "GitHub Copilot token refresh request: JSON response exceeds 16777216 bytes" ,
216+ ) ;
217+ } ) ;
218+
219+ it ( "parses normal-size OAuth JSON responses under the byte cap" , async ( ) => {
220+ vi . stubGlobal (
221+ "fetch" ,
222+ vi . fn (
223+ async ( ) =>
224+ new Response (
225+ JSON . stringify ( {
226+ token : "copilot-token" ,
227+ expires_at : Math . floor ( Date . now ( ) / 1000 ) + 3600 ,
228+ } ) ,
229+ { status : 200 , headers : { "Content-Type" : "application/json" } } ,
230+ ) ,
231+ ) ,
232+ ) ;
233+
234+ const result = await refreshGitHubCopilotToken ( "refresh-token" ) ;
235+ expect ( result . access ) . toBe ( "copilot-token" ) ;
236+ expect ( typeof result . expires ) . toBe ( "number" ) ;
237+ } ) ;
238+
239+ it ( "cancels the upstream body when the bounded reader overflows" , async ( ) => {
240+ const cancel = vi . fn ( async ( ) => undefined ) ;
241+ const encoder = new TextEncoder ( ) ;
242+ const source = new ReadableStream < Uint8Array > ( {
243+ pull ( controller ) {
244+ controller . enqueue ( encoder . encode ( "a" . repeat ( 1024 * 1024 ) ) ) ;
245+ } ,
246+ cancel,
247+ } ) ;
248+ vi . stubGlobal (
249+ "fetch" ,
250+ vi . fn (
251+ async ( ) =>
252+ new Response ( source , {
253+ status : 200 ,
254+ headers : { "Content-Type" : "application/json" } ,
255+ } ) ,
256+ ) ,
257+ ) ;
258+
259+ await expect ( refreshGitHubCopilotToken ( "refresh-token" ) ) . rejects . toThrow (
260+ "GitHub Copilot token refresh request" ,
261+ ) ;
262+
263+ expect ( cancel ) . toHaveBeenCalled ( ) ;
264+ } ) ;
265+ } ) ;
0 commit comments