@@ -16,6 +16,60 @@ import { type Environment, envGetEffect, getApiUrl } from "./environment";
1616// Minimum seconds before expiry to consider token valid for a request
1717const TOKEN_EXPIRY_BUFFER_SECONDS = 30 ;
1818
19+ // Header names (lowercased) that must be redacted from debug output and
20+ // the --include envelope to prevent leaking tokens, cookies, or secrets.
21+ const SENSITIVE_HEADER_PARTS = [
22+ "authorization" ,
23+ "cookie" ,
24+ "set-cookie" ,
25+ "token" ,
26+ "secret" ,
27+ "api-key" ,
28+ "apikey" ,
29+ "x-auth" ,
30+ ] as const ;
31+
32+ function isSensitiveHeader ( headerName : string ) : boolean {
33+ const lower = headerName . toLowerCase ( ) ;
34+ return SENSITIVE_HEADER_PARTS . some ( ( part ) => lower . includes ( part ) ) ;
35+ }
36+
37+ /**
38+ * Return a copy of headers with sensitive values replaced by "[REDACTED]".
39+ */
40+ export { sanitizeHeaders as sanitizeResponseHeaders } ;
41+
42+ function sanitizeHeaders (
43+ headers : Record < string , string > ,
44+ ) : Record < string , string > {
45+ const sanitized : Record < string , string > = { } ;
46+ for ( const [ key , value ] of Object . entries ( headers ) ) {
47+ sanitized [ key ] = isSensitiveHeader ( key ) ? "[REDACTED]" : value ;
48+ }
49+ return sanitized ;
50+ }
51+
52+ /**
53+ * Redact values whose keys look like they contain secrets.
54+ */
55+ function redactSensitiveBodyFields ( body : string ) : string {
56+ try {
57+ const parsed = JSON . parse ( body ) ;
58+ if ( typeof parsed !== "object" || parsed === null ) return body ;
59+ const redacted : Record < string , unknown > = { } ;
60+ for ( const [ key , value ] of Object . entries ( parsed ) ) {
61+ const lower = key . toLowerCase ( ) ;
62+ const isSensitive = SENSITIVE_HEADER_PARTS . some ( ( part ) =>
63+ lower . includes ( part ) ,
64+ ) || lower . includes ( "password" ) || lower . includes ( "credential" ) ;
65+ redacted [ key ] = isSensitive ? "[REDACTED]" : value ;
66+ }
67+ return JSON . stringify ( redacted ) ;
68+ } catch {
69+ return body ;
70+ }
71+ }
72+
1973export type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" ;
2074
2175export interface ApiRequestOptions {
@@ -265,13 +319,12 @@ export function apiRequestEffect(
265319
266320 if ( debug ) {
267321 console . error ( `> ${ method } ${ url } ` ) ;
268- for ( const [ key , value ] of Object . entries ( requestHeaders ) ) {
269- const displayValue =
270- key . toLowerCase ( ) === "authorization" ? "Bearer [REDACTED]" : value ;
271- console . error ( `> ${ key } : ${ displayValue } ` ) ;
322+ const sanitizedRequestHeaders = sanitizeHeaders ( requestHeaders ) ;
323+ for ( const [ key , value ] of Object . entries ( sanitizedRequestHeaders ) ) {
324+ console . error ( `> ${ key } : ${ value } ` ) ;
272325 }
273326 if ( requestBody ) {
274- console . error ( `> Body: ${ requestBody } ` ) ;
327+ console . error ( `> Body: ${ redactSensitiveBodyFields ( requestBody ) } ` ) ;
275328 }
276329 console . error ( "" ) ;
277330 }
@@ -302,7 +355,8 @@ export function apiRequestEffect(
302355
303356 if ( debug ) {
304357 console . error ( `< ${ response . status } ${ response . statusText } ` ) ;
305- for ( const [ key , value ] of Object . entries ( responseHeaders ) ) {
358+ const sanitizedResponseHeaders = sanitizeHeaders ( responseHeaders ) ;
359+ for ( const [ key , value ] of Object . entries ( sanitizedResponseHeaders ) ) {
306360 console . error ( `< ${ key } : ${ value } ` ) ;
307361 }
308362 console . error ( "" ) ;
@@ -340,7 +394,9 @@ export function apiRequestEffect(
340394
341395 // Check for error status codes
342396 if ( ! response . ok ) {
343- const errorMessage =
397+ // Internal message includes the raw server payload for debugging;
398+ // userMessage is a safe, generic description shown to users/agents.
399+ const internalDetail =
344400 typeof data === "object" && data !== null
345401 ? JSON . stringify ( data )
346402 : String ( data || response . statusText ) ;
@@ -349,7 +405,7 @@ export function apiRequestEffect(
349405 if ( response . status === 401 ) {
350406 return yield * Effect . fail (
351407 new AuthenticationError ( {
352- message : `Authentication failed (401): ${ errorMessage } ` ,
408+ message : `Authentication failed (401): ${ internalDetail } ` ,
353409 userMessage :
354410 "Your session has expired or is invalid. Run 'godaddy auth login' to re-authenticate." ,
355411 } ) ,
@@ -360,7 +416,7 @@ export function apiRequestEffect(
360416 if ( response . status === 403 ) {
361417 return yield * Effect . fail (
362418 new AuthenticationError ( {
363- message : `Access denied (403): ${ errorMessage } ` ,
419+ message : `Access denied (403): ${ internalDetail } ` ,
364420 userMessage :
365421 "You don't have permission to access this resource. Check your account permissions." ,
366422 } ) ,
@@ -369,7 +425,7 @@ export function apiRequestEffect(
369425
370426 return yield * Effect . fail (
371427 new NetworkError ( {
372- message : `API error (${ response . status } ): ${ errorMessage } ` ,
428+ message : `API error (${ response . status } ): ${ internalDetail } ` ,
373429 userMessage : `API request failed with status ${ response . status } : ${ response . statusText } ` ,
374430 } ) ,
375431 ) ;
0 commit comments