@@ -1193,6 +1193,38 @@ async function handleNetworkV1EnterpriseClientConnections(
11931193 return handleProxyReq ( req , res , next ) ;
11941194}
11951195
1196+ /**
1197+ * Helper to send request body, using raw bytes when available.
1198+ *
1199+ * For v4 HMAC authentication, we need to send the exact bytes that were
1200+ * received from the client to ensure the HMAC signature matches.
1201+ * The rawBodyBuffer is captured by body-parser's verify callback before
1202+ * JSON parsing, preserving exact whitespace, key ordering, etc.
1203+ *
1204+ * For v2/v3, sending the raw string also works because serializeRequestData
1205+ * now properly returns strings as-is for HMAC calculation.
1206+ *
1207+ * @param request - The superagent request object
1208+ * @param req - The Express request containing body and rawBodyBuffer
1209+ * @returns The request with body attached
1210+ */
1211+ function sendRequestBody ( request : ReturnType < BitGo [ 'post' ] > , req : express . Request ) {
1212+ if ( req . rawBodyBuffer ) {
1213+ // Preserve original Content-Type header from client
1214+ const contentTypeHeader = req . headers [ 'content-type' ] ;
1215+ if ( contentTypeHeader ) {
1216+ request . set ( 'Content-Type' , Array . isArray ( contentTypeHeader ) ? contentTypeHeader [ 0 ] : contentTypeHeader ) ;
1217+ }
1218+ // Send raw body as UTF-8 string to preserve exact bytes for HMAC.
1219+ // JSON is always UTF-8 (RFC 8259), so this is lossless for JSON bodies.
1220+ // serializeRequestData will return this string as-is for HMAC calculation.
1221+ return request . send ( req . rawBodyBuffer . toString ( 'utf8' ) ) ;
1222+ }
1223+
1224+ // Fall back to parsed body for backward compatibility (e.g., non-JSON bodies)
1225+ return request . send ( req . body ) ;
1226+ }
1227+
11961228/**
11971229 * Redirect a request using the bitgo request functions.
11981230 * @param bitgo
@@ -1215,19 +1247,19 @@ export function redirectRequest(
12151247 request = bitgo . get ( url ) ;
12161248 break ;
12171249 case 'POST' :
1218- request = bitgo . post ( url ) . send ( req . body ) ;
1250+ request = sendRequestBody ( bitgo . post ( url ) , req ) ;
12191251 break ;
12201252 case 'PUT' :
1221- request = bitgo . put ( url ) . send ( req . body ) ;
1253+ request = sendRequestBody ( bitgo . put ( url ) , req ) ;
12221254 break ;
12231255 case 'PATCH' :
1224- request = bitgo . patch ( url ) . send ( req . body ) ;
1256+ request = sendRequestBody ( bitgo . patch ( url ) , req ) ;
12251257 break ;
12261258 case 'OPTIONS' :
1227- request = bitgo . options ( url ) . send ( req . body ) ;
1259+ request = sendRequestBody ( bitgo . options ( url ) , req ) ;
12281260 break ;
12291261 case 'DELETE' :
1230- request = bitgo . del ( url ) . send ( req . body ) ;
1262+ request = sendRequestBody ( bitgo . del ( url ) , req ) ;
12311263 break ;
12321264 }
12331265
@@ -1268,7 +1300,12 @@ function apiResponse(status: number, result: any, message?: string): ApiResponse
12681300 return new ApiResponseError ( message , status , result ) ;
12691301}
12701302
1271- const expressJSONParser = bodyParser . json ( { limit : '20mb' } ) ;
1303+ const expressJSONParser = bodyParser . json ( {
1304+ limit : '20mb' ,
1305+ verify : ( req , res , buf ) => {
1306+ ( req as express . Request ) . rawBodyBuffer = buf ;
1307+ } ,
1308+ } ) ;
12721309
12731310/**
12741311 * Perform body parsing here only on routes we want
0 commit comments