@@ -1192,6 +1192,38 @@ 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+
1223+ // Fall back to parsed body for backward compatibility (e.g., non-JSON bodies)
1224+ return request . send ( req . body ) ;
1225+ }
1226+
11951227/**
11961228 * Redirect a request using the bitgo request functions.
11971229 * @param bitgo
@@ -1214,19 +1246,19 @@ export function redirectRequest(
12141246 request = bitgo . get ( url ) ;
12151247 break ;
12161248 case 'POST' :
1217- request = bitgo . post ( url ) . send ( req . body ) ;
1249+ request = sendRequestBody ( bitgo . post ( url ) , req ) ;
12181250 break ;
12191251 case 'PUT' :
1220- request = bitgo . put ( url ) . send ( req . body ) ;
1252+ request = sendRequestBody ( bitgo . put ( url ) , req ) ;
12211253 break ;
12221254 case 'PATCH' :
1223- request = bitgo . patch ( url ) . send ( req . body ) ;
1255+ request = sendRequestBody ( bitgo . patch ( url ) , req ) ;
12241256 break ;
12251257 case 'OPTIONS' :
1226- request = bitgo . options ( url ) . send ( req . body ) ;
1258+ request = sendRequestBody ( bitgo . options ( url ) , req ) ;
12271259 break ;
12281260 case 'DELETE' :
1229- request = bitgo . del ( url ) . send ( req . body ) ;
1261+ request = sendRequestBody ( bitgo . del ( url ) , req ) ;
12301262 break ;
12311263 }
12321264
@@ -1267,7 +1299,12 @@ function apiResponse(status: number, result: any, message?: string): ApiResponse
12671299 return new ApiResponseError ( message , status , result ) ;
12681300}
12691301
1270- const expressJSONParser = bodyParser . json ( { limit : '20mb' } ) ;
1302+ const expressJSONParser = bodyParser . json ( {
1303+ limit : '20mb' ,
1304+ verify : ( req , res , buf ) => {
1305+ ( req as express . Request ) . rawBodyBuffer = buf ;
1306+ } ,
1307+ } ) ;
12711308
12721309/**
12731310 * Perform body parsing here only on routes we want
0 commit comments