@@ -1192,6 +1192,37 @@ async function handleNetworkV1EnterpriseClientConnections(
11921192 return handleProxyReq ( req , res , next ) ;
11931193}
11941194
1195+ /**
1196+ * Helper to send request body, using raw bytes when available.
1197+ *
1198+ * For v4 HMAC authentication, we need to send the exact bytes that were
1199+ * received from the client to ensure the HMAC signature matches.
1200+ * The rawBodyBuffer is captured by body-parser's verify callback before
1201+ * JSON parsing, preserving exact whitespace, key ordering, etc.
1202+ *
1203+ * For v2/v3, sending the raw string also works because serializeRequestData
1204+ * now properly returns strings as-is for HMAC calculation.
1205+ *
1206+ * @param request - The superagent request object
1207+ * @param req - The Express request containing body and rawBodyBuffer
1208+ * @returns The request with body attached
1209+ */
1210+ function sendRequestBody ( request : ReturnType < BitGo [ 'post' ] > , req : express . Request ) {
1211+ if ( req . rawBodyBuffer ) {
1212+ // Preserve original Content-Type header from client
1213+ const contentTypeHeader = req . headers [ 'content-type' ] ;
1214+ if ( contentTypeHeader ) {
1215+ request . set ( 'Content-Type' , Array . isArray ( contentTypeHeader ) ? contentTypeHeader [ 0 ] : contentTypeHeader ) ;
1216+ }
1217+ // Send raw body as UTF-8 string to preserve exact bytes for HMAC.
1218+ // JSON is always UTF-8 (RFC 8259), so this is lossless for JSON bodies.
1219+ // serializeRequestData will return this string as-is for HMAC calculation.
1220+ return request . send ( req . rawBodyBuffer . toString ( 'utf8' ) ) ;
1221+ }
1222+ // Fall back to parsed body for backward compatibility (e.g., non-JSON bodies)
1223+ return request . send ( req . body ) ;
1224+ }
1225+
11951226/**
11961227 * Redirect a request using the bitgo request functions.
11971228 * @param bitgo
@@ -1214,19 +1245,19 @@ export function redirectRequest(
12141245 request = bitgo . get ( url ) ;
12151246 break ;
12161247 case 'POST' :
1217- request = bitgo . post ( url ) . send ( req . body ) ;
1248+ request = sendRequestBody ( bitgo . post ( url ) , req ) ;
12181249 break ;
12191250 case 'PUT' :
1220- request = bitgo . put ( url ) . send ( req . body ) ;
1251+ request = sendRequestBody ( bitgo . put ( url ) , req ) ;
12211252 break ;
12221253 case 'PATCH' :
1223- request = bitgo . patch ( url ) . send ( req . body ) ;
1254+ request = sendRequestBody ( bitgo . patch ( url ) , req ) ;
12241255 break ;
12251256 case 'OPTIONS' :
1226- request = bitgo . options ( url ) . send ( req . body ) ;
1257+ request = sendRequestBody ( bitgo . options ( url ) , req ) ;
12271258 break ;
12281259 case 'DELETE' :
1229- request = bitgo . del ( url ) . send ( req . body ) ;
1260+ request = sendRequestBody ( bitgo . del ( url ) , req ) ;
12301261 break ;
12311262 }
12321263
@@ -1267,7 +1298,12 @@ function apiResponse(status: number, result: any, message?: string): ApiResponse
12671298 return new ApiResponseError ( message , status , result ) ;
12681299}
12691300
1270- const expressJSONParser = bodyParser . json ( { limit : '20mb' } ) ;
1301+ const expressJSONParser = bodyParser . json ( {
1302+ limit : '20mb' ,
1303+ verify : ( req , res , buf ) => {
1304+ ( req as express . Request ) . rawBodyBuffer = buf ;
1305+ } ,
1306+ } ) ;
12711307
12721308/**
12731309 * Perform body parsing here only on routes we want
0 commit comments