@@ -566,15 +566,16 @@ export class SingleAgentClient {
566566 taskType : string ,
567567 operationId : string ,
568568 signature ?: string ,
569- timestamp ?: string | number
569+ timestamp ?: string | number ,
570+ rawBody ?: string
570571 ) : Promise < boolean > {
571572 // Verify signature if secret is configured
572573 if ( this . config . webhookSecret ) {
573574 if ( ! signature || ! timestamp ) {
574575 throw new Error ( 'Webhook signature and timestamp required but not provided' ) ;
575576 }
576577
577- const isValid = this . verifyWebhookSignature ( payload , signature , timestamp ) ;
578+ const isValid = this . verifyWebhookSignature ( rawBody ?? payload , signature , timestamp ) ;
578579 if ( ! isValid ) {
579580 throw new Error ( 'Invalid webhook signature or timestamp too old' ) ;
580581 }
@@ -783,11 +784,16 @@ export class SingleAgentClient {
783784 const signature = req . headers [ 'x-adcp-signature' ] || req . headers [ 'X-ADCP-Signature' ] ;
784785 const timestamp = req . headers [ 'x-adcp-timestamp' ] || req . headers [ 'X-ADCP-Timestamp' ] ;
785786
786- // Parse body if needed
787+ // Capture raw body for signature verification, then parse
788+ const rawBody = typeof req . body === 'string' ? req . body : JSON . stringify ( req . body ) ;
787789 const payload = typeof req . body === 'string' ? JSON . parse ( req . body ) : req . body ;
788790
789- // Handle webhook with automatic verification
790- const handled = await this . handleWebhook ( payload , signature , timestamp ) ;
791+ // Extract routing params if available (e.g., Express route params)
792+ const taskType = req . params ?. task_type || req . params ?. taskType || 'unknown' ;
793+ const operationId = req . params ?. operation_id || req . params ?. operationId || 'unknown' ;
794+
795+ // Handle webhook with automatic verification using raw body bytes
796+ const handled = await this . handleWebhook ( payload , taskType , operationId , signature , timestamp , rawBody ) ;
791797
792798 // Return success
793799 if ( res . json ) {
@@ -811,17 +817,26 @@ export class SingleAgentClient {
811817 }
812818
813819 /**
814- * Verify webhook signature using HMAC-SHA256 per AdCP PR #86 spec
820+ * Verify webhook signature using HMAC-SHA256 per AdCP spec.
821+ *
822+ * HMAC is computed over the **raw HTTP body bytes** — the exact bytes received
823+ * on the wire, before JSON parsing. This ensures cross-language interop since
824+ * different JSON serializers may produce different byte representations of the
825+ * same logical payload.
826+ *
827+ * For backward compatibility, a parsed object is still accepted but will be
828+ * re-serialized with JSON.stringify, which may not match the sender's bytes.
829+ * Always prefer passing the raw body string.
815830 *
816831 * Signature format: sha256={hex_signature}
817- * Message format: {timestamp}.{json_payload }
832+ * Message format: {timestamp}.{raw_body }
818833 *
819- * @param payload - Webhook payload object
834+ * @param rawBodyOrPayload - Raw HTTP body string (preferred) or parsed payload object (deprecated)
820835 * @param signature - X-ADCP-Signature header value (format: "sha256=...")
821836 * @param timestamp - X-ADCP-Timestamp header value (Unix timestamp)
822837 * @returns true if signature is valid
823838 */
824- verifyWebhookSignature ( payload : any , signature : string , timestamp : string | number ) : boolean {
839+ verifyWebhookSignature ( rawBodyOrPayload : string | any , signature : string , timestamp : string | number ) : boolean {
825840 if ( ! this . config . webhookSecret ) {
826841 return false ;
827842 }
@@ -834,8 +849,11 @@ export class SingleAgentClient {
834849 return false ; // Request too old or from future
835850 }
836851
837- // Build message per AdCP spec: {timestamp}.{json_payload}
838- const message = `${ ts } .${ JSON . stringify ( payload ) } ` ;
852+ // Use raw body bytes when available; fall back to JSON.stringify for backward compat
853+ const body = typeof rawBodyOrPayload === 'string' ? rawBodyOrPayload : JSON . stringify ( rawBodyOrPayload ) ;
854+
855+ // Build message per AdCP spec: {timestamp}.{raw_body}
856+ const message = `${ ts } .${ body } ` ;
839857
840858 // Calculate expected signature
841859 const hmac = crypto . createHmac ( 'sha256' , this . config . webhookSecret ) ;
0 commit comments