@@ -24,6 +24,7 @@ function secureResponse(params: {
2424 ok : boolean
2525 status : number
2626 body ?: string
27+ responseBody ?: ReadableStream < Uint8Array > | null
2728 opcRequestId ?: string
2829} ) {
2930 return {
@@ -34,7 +35,7 @@ function secureResponse(params: {
3435 get : ( name : string ) =>
3536 name . toLowerCase ( ) === 'opc-request-id' ? ( params . opcRequestId ?? null ) : null ,
3637 } ,
37- body : null ,
38+ body : params . responseBody ?? null ,
3839 text : vi . fn ( ) . mockResolvedValue ( params . body ?? '' ) ,
3940 json : vi . fn ( ) ,
4041 arrayBuffer : vi . fn ( ) ,
@@ -235,19 +236,17 @@ describe('OCI request client', () => {
235236 expect ( ( failure as Error ) . message ) . not . toContain ( echoedAuthorization )
236237 } )
237238
238- it ( 'redacts encoded credentials and request URLs echoed by the provider ' , async ( ) => {
239+ it ( 'redacts encoded credentials instead of falling through to a status-only error ' , async ( ) => {
239240 const encodedFingerprint = encodeURIComponent ( credentials . fingerprint )
240- const requestUrl = `${ destination . origin } /n/`
241- const encodedRequestUrl = encodeURIComponent ( requestUrl )
242241 const escapedPassphrase = 'secret "pass"'
243- const escapedPassphraseEcho = JSON . stringify ( escapedPassphrase ) . slice ( 1 , - 1 )
242+ const encodedPassphrase = encodeURIComponent ( escapedPassphrase )
244243 secureFetchMock . mockResolvedValueOnce (
245244 secureResponse ( {
246245 ok : false ,
247246 status : 401 ,
248247 body : JSON . stringify ( {
249248 code : 'NotAuthenticated' ,
250- message : `provider echoed ${ encodedFingerprint } ${ encodedRequestUrl } ${ escapedPassphraseEcho } ` ,
249+ message : `provider echoed ${ encodedFingerprint } ${ encodedPassphrase } ` ,
251250 } ) ,
252251 } )
253252 )
@@ -259,12 +258,63 @@ describe('OCI request client', () => {
259258 timeout : 10_000 ,
260259 maxResponseBytes : 65_536 ,
261260 } ) . catch ( ( error : unknown ) => error )
261+ expect ( ( failure as Error ) . message ) . toContain ( 'provider echoed' )
262+ expect ( ( failure as Error ) . message ) . toContain ( '[REDACTED]' )
262263 expect ( ( failure as Error ) . message ) . not . toContain ( encodedFingerprint )
263- expect ( ( failure as Error ) . message ) . not . toContain ( encodedRequestUrl )
264- expect ( ( failure as Error ) . message ) . not . toContain ( escapedPassphraseEcho )
264+ expect ( ( failure as Error ) . message ) . not . toContain ( encodedPassphrase )
265265 expect ( ( failure as Error ) . message ) . not . toContain ( escapedPassphrase )
266266 } )
267267
268+ it ( 'redacts an encoded signed request URL instead of returning it' , async ( ) => {
269+ const encodedRequestUrl = encodeURIComponent ( `${ destination . origin } /n/` )
270+ secureFetchMock . mockResolvedValueOnce (
271+ secureResponse ( {
272+ ok : false ,
273+ status : 401 ,
274+ body : JSON . stringify ( {
275+ code : 'NotAuthenticated' ,
276+ message : `provider echoed ${ encodedRequestUrl } ` ,
277+ } ) ,
278+ } )
279+ )
280+ const failure = await sendOciRequest ( {
281+ destination,
282+ credentials,
283+ method : 'GET' ,
284+ encodedPath : '/n/' ,
285+ timeout : 10_000 ,
286+ maxResponseBytes : 65_536 ,
287+ } ) . catch ( ( error : unknown ) => error )
288+ expect ( ( failure as Error ) . message ) . toContain ( 'provider echoed' )
289+ expect ( ( failure as Error ) . message ) . toContain ( '[REDACTED]' )
290+ expect ( ( failure as Error ) . message ) . not . toContain ( encodedRequestUrl )
291+ } )
292+
293+ it ( 'redacts caller-supplied service header values echoed by the provider' , async ( ) => {
294+ const serviceHeaderSecret = 'opaque-service-header-secret'
295+ secureFetchMock . mockResolvedValueOnce (
296+ secureResponse ( {
297+ ok : false ,
298+ status : 401 ,
299+ body : JSON . stringify ( {
300+ code : 'NotAuthenticated' ,
301+ message : `provider echoed ${ serviceHeaderSecret } ` ,
302+ } ) ,
303+ } )
304+ )
305+ const failure = await sendOciRequest ( {
306+ destination,
307+ credentials,
308+ method : 'GET' ,
309+ encodedPath : '/n/' ,
310+ timeout : 10_000 ,
311+ maxResponseBytes : 65_536 ,
312+ serviceHeaders : { 'opc-client-info' : serviceHeaderSecret } ,
313+ } ) . catch ( ( error : unknown ) => error )
314+ expect ( ( failure as Error ) . message ) . toContain ( '[REDACTED]' )
315+ expect ( ( failure as Error ) . message ) . not . toContain ( serviceHeaderSecret )
316+ } )
317+
268318 it ( 'redacts an echoed finalized request body from provider diagnostics' , async ( ) => {
269319 const requestBody = 'opaque-request-body-secret'
270320 secureFetchMock . mockResolvedValueOnce (
@@ -376,6 +426,7 @@ describe('OCI request client', () => {
376426 'signing-string-value' ,
377427 'private-key-value' ,
378428 'api-key-value' ,
429+ 'signature-value' ,
379430 ]
380431 secureFetchMock . mockResolvedValueOnce (
381432 secureResponse ( {
@@ -391,6 +442,7 @@ describe('OCI request client', () => {
391442 signing_string : echoedSecrets [ 4 ] ,
392443 'private key' : echoedSecrets [ 5 ] ,
393444 'api key' : echoedSecrets [ 6 ] ,
445+ signature : echoedSecrets [ 7 ] ,
394446 } ) ,
395447 } ) ,
396448 } )
@@ -407,6 +459,58 @@ describe('OCI request client', () => {
407459 for ( const secret of echoedSecrets ) expect ( ( failure as Error ) . message ) . not . toContain ( secret )
408460 } )
409461
462+ it ( 'fails closed for authorization signatures with flexible parameter spacing' , async ( ) => {
463+ const echoedSignature = 'unknown-provider-signature'
464+ secureFetchMock . mockResolvedValueOnce (
465+ secureResponse ( {
466+ ok : false ,
467+ status : 401 ,
468+ body : JSON . stringify ( {
469+ code : 'NotAuthenticated' ,
470+ message : `provider echoed Signature version = "1", keyId = "unknown", signature = "${ echoedSignature } "` ,
471+ } ) ,
472+ } )
473+ )
474+ const failure = await sendOciRequest ( {
475+ destination,
476+ credentials,
477+ method : 'GET' ,
478+ encodedPath : '/n/' ,
479+ timeout : 10_000 ,
480+ maxResponseBytes : 65_536 ,
481+ } ) . catch ( ( error : unknown ) => error )
482+ expect ( ( failure as Error ) . message ) . toBe ( 'OCI request failed with status 401' )
483+ expect ( ( failure as Error ) . message ) . not . toContain ( echoedSignature )
484+ } )
485+
486+ it ( 'bounds non-success response bodies independently of the caller response ceiling' , async ( ) => {
487+ const cancel = vi . fn ( )
488+ const response = secureResponse ( {
489+ ok : false ,
490+ status : 502 ,
491+ opcRequestId : 'request-oversized' ,
492+ responseBody : new ReadableStream < Uint8Array > ( {
493+ start ( controller ) {
494+ controller . enqueue ( new Uint8Array ( 65_537 ) )
495+ } ,
496+ cancel,
497+ } ) ,
498+ } )
499+ secureFetchMock . mockResolvedValueOnce ( response )
500+ const failure = await sendOciRequest ( {
501+ destination,
502+ credentials,
503+ method : 'GET' ,
504+ encodedPath : '/n/' ,
505+ timeout : 10_000 ,
506+ maxResponseBytes : 1024 * 1024 ,
507+ } ) . catch ( ( error : unknown ) => error )
508+ expect ( ( failure as Error ) . message ) . toBe ( 'OCI request failed with status 502' )
509+ expect ( ( failure as OciRequestError ) . opcRequestId ) . toBe ( 'request-oversized' )
510+ expect ( cancel ) . toHaveBeenCalledOnce ( )
511+ expect ( response . text ) . not . toHaveBeenCalled ( )
512+ } )
513+
410514 it ( 'fails closed for a percent-encoded sensitive JSON key' , async ( ) => {
411515 secureFetchMock . mockResolvedValueOnce (
412516 secureResponse ( {
0 commit comments