@@ -13,7 +13,51 @@ describe('Webhook Signature Verification (PR #86 Spec)', () => {
1313
1414 const webhookSecret = 'test-secret-key-minimum-32-characters-long' ;
1515
16- test ( 'should verify valid webhook signature per PR #86 spec' , ( ) => {
16+ test ( 'should verify valid webhook signature using raw body string' , ( ) => {
17+ const client = new AdCPClient ( [ agent ] , { webhookSecret } ) ;
18+ const agentClient = client . agent ( 'test_agent' ) ;
19+
20+ const rawBody = '{"event":"creative.status_changed","creative_id":"creative_123","status":"approved","timestamp":"2025-10-08T22:30:00Z"}' ;
21+
22+ const timestamp = Math . floor ( Date . now ( ) / 1000 ) ;
23+
24+ // Generate signature per spec: sha256=HMAC({timestamp}.{raw_body})
25+ const message = `${ timestamp } .${ rawBody } ` ;
26+ const hmac = crypto . createHmac ( 'sha256' , webhookSecret ) ;
27+ hmac . update ( message ) ;
28+ const signature = `sha256=${ hmac . digest ( 'hex' ) } ` ;
29+
30+ // Verify signature using raw body string
31+ const isValid = agentClient . verifyWebhookSignature ( rawBody , signature , timestamp ) ;
32+ assert . strictEqual ( isValid , true ) ;
33+ } ) ;
34+
35+ test ( 'should verify signature when raw body has different formatting than JSON.stringify' , ( ) => {
36+ const client = new AdCPClient ( [ agent ] , { webhookSecret } ) ;
37+ const agentClient = client . agent ( 'test_agent' ) ;
38+
39+ // Raw body with extra spaces (as a Python sender might produce)
40+ const rawBody = '{"key": "value", "num": 1.0}' ;
41+
42+ const timestamp = Math . floor ( Date . now ( ) / 1000 ) ;
43+
44+ // Sender signs over raw body bytes
45+ const message = `${ timestamp } .${ rawBody } ` ;
46+ const hmac = crypto . createHmac ( 'sha256' , webhookSecret ) ;
47+ hmac . update ( message ) ;
48+ const signature = `sha256=${ hmac . digest ( 'hex' ) } ` ;
49+
50+ // Verification with raw body should succeed
51+ const isValid = agentClient . verifyWebhookSignature ( rawBody , signature , timestamp ) ;
52+ assert . strictEqual ( isValid , true ) ;
53+
54+ // Verification with parsed object would fail (different bytes)
55+ const parsed = JSON . parse ( rawBody ) ;
56+ const isValidParsed = agentClient . verifyWebhookSignature ( parsed , signature , timestamp ) ;
57+ assert . strictEqual ( isValidParsed , false , 'Parsed object re-serialization should not match raw body signature' ) ;
58+ } ) ;
59+
60+ test ( 'should still work with parsed object for backward compatibility' , ( ) => {
1761 const client = new AdCPClient ( [ agent ] , { webhookSecret } ) ;
1862 const agentClient = client . agent ( 'test_agent' ) ;
1963
@@ -26,13 +70,13 @@ describe('Webhook Signature Verification (PR #86 Spec)', () => {
2670
2771 const timestamp = Math . floor ( Date . now ( ) / 1000 ) ;
2872
29- // Generate signature per PR #86 spec: sha256=HMAC({timestamp}.{json_payload} )
73+ // Generate signature using JSON.stringify (old-style sender )
3074 const message = `${ timestamp } .${ JSON . stringify ( payload ) } ` ;
3175 const hmac = crypto . createHmac ( 'sha256' , webhookSecret ) ;
3276 hmac . update ( message ) ;
3377 const signature = `sha256=${ hmac . digest ( 'hex' ) } ` ;
3478
35- // Verify signature
79+ // Verify signature using parsed object (backward compat)
3680 const isValid = agentClient . verifyWebhookSignature ( payload , signature , timestamp ) ;
3781 assert . strictEqual ( isValid , true ) ;
3882 } ) ;
@@ -41,100 +85,84 @@ describe('Webhook Signature Verification (PR #86 Spec)', () => {
4185 const client = new AdCPClient ( [ agent ] , { webhookSecret } ) ;
4286 const agentClient = client . agent ( 'test_agent' ) ;
4387
44- const payload = {
45- event : 'creative.status_changed' ,
46- creative_id : 'creative_123' ,
47- status : 'approved' ,
48- } ;
88+ const rawBody = '{"event":"creative.status_changed","creative_id":"creative_123","status":"approved"}' ;
4989
5090 const timestamp = Math . floor ( Date . now ( ) / 1000 ) ;
5191 const invalidSignature = 'sha256=invalid_signature_here' ;
5292
53- const isValid = agentClient . verifyWebhookSignature ( payload , invalidSignature , timestamp ) ;
93+ const isValid = agentClient . verifyWebhookSignature ( rawBody , invalidSignature , timestamp ) ;
5494 assert . strictEqual ( isValid , false ) ;
5595 } ) ;
5696
5797 test ( 'should reject webhook with old timestamp (> 5 minutes)' , ( ) => {
5898 const client = new AdCPClient ( [ agent ] , { webhookSecret } ) ;
5999 const agentClient = client . agent ( 'test_agent' ) ;
60100
61- const payload = {
62- event : 'creative.status_changed' ,
63- creative_id : 'creative_123' ,
64- status : 'approved' ,
65- } ;
101+ const rawBody = '{"event":"creative.status_changed","creative_id":"creative_123","status":"approved"}' ;
66102
67103 // Timestamp from 10 minutes ago
68104 const oldTimestamp = Math . floor ( Date . now ( ) / 1000 ) - 600 ;
69105
70106 // Generate valid signature for old timestamp
71- const message = `${ oldTimestamp } .${ JSON . stringify ( payload ) } ` ;
107+ const message = `${ oldTimestamp } .${ rawBody } ` ;
72108 const hmac = crypto . createHmac ( 'sha256' , webhookSecret ) ;
73109 hmac . update ( message ) ;
74110 const signature = `sha256=${ hmac . digest ( 'hex' ) } ` ;
75111
76112 // Should reject due to timestamp being too old
77- const isValid = agentClient . verifyWebhookSignature ( payload , signature , oldTimestamp ) ;
113+ const isValid = agentClient . verifyWebhookSignature ( rawBody , signature , oldTimestamp ) ;
78114 assert . strictEqual ( isValid , false ) ;
79115 } ) ;
80116
81117 test ( 'should accept webhook within 5 minute window' , ( ) => {
82118 const client = new AdCPClient ( [ agent ] , { webhookSecret } ) ;
83119 const agentClient = client . agent ( 'test_agent' ) ;
84120
85- const payload = {
86- event : 'creative.status_changed' ,
87- creative_id : 'creative_123' ,
88- status : 'approved' ,
89- } ;
121+ const rawBody = '{"event":"creative.status_changed","creative_id":"creative_123","status":"approved"}' ;
90122
91123 // Timestamp from 2 minutes ago (within 5 minute window)
92124 const recentTimestamp = Math . floor ( Date . now ( ) / 1000 ) - 120 ;
93125
94126 // Generate valid signature
95- const message = `${ recentTimestamp } .${ JSON . stringify ( payload ) } ` ;
127+ const message = `${ recentTimestamp } .${ rawBody } ` ;
96128 const hmac = crypto . createHmac ( 'sha256' , webhookSecret ) ;
97129 hmac . update ( message ) ;
98130 const signature = `sha256=${ hmac . digest ( 'hex' ) } ` ;
99131
100132 // Should accept
101- const isValid = agentClient . verifyWebhookSignature ( payload , signature , recentTimestamp ) ;
133+ const isValid = agentClient . verifyWebhookSignature ( rawBody , signature , recentTimestamp ) ;
102134 assert . strictEqual ( isValid , true ) ;
103135 } ) ;
104136
105137 test ( 'should handle timestamp as string' , ( ) => {
106138 const client = new AdCPClient ( [ agent ] , { webhookSecret } ) ;
107139 const agentClient = client . agent ( 'test_agent' ) ;
108140
109- const payload = {
110- event : 'creative.status_changed' ,
111- creative_id : 'creative_123' ,
112- status : 'approved' ,
113- } ;
141+ const rawBody = '{"event":"creative.status_changed","creative_id":"creative_123","status":"approved"}' ;
114142
115143 const timestamp = Math . floor ( Date . now ( ) / 1000 ) ;
116144 const timestampStr = timestamp . toString ( ) ;
117145
118146 // Generate valid signature
119- const message = `${ timestamp } .${ JSON . stringify ( payload ) } ` ;
147+ const message = `${ timestamp } .${ rawBody } ` ;
120148 const hmac = crypto . createHmac ( 'sha256' , webhookSecret ) ;
121149 hmac . update ( message ) ;
122150 const signature = `sha256=${ hmac . digest ( 'hex' ) } ` ;
123151
124152 // Should accept string timestamp
125- const isValid = agentClient . verifyWebhookSignature ( payload , signature , timestampStr ) ;
153+ const isValid = agentClient . verifyWebhookSignature ( rawBody , signature , timestampStr ) ;
126154 assert . strictEqual ( isValid , true ) ;
127155 } ) ;
128156
129157 test ( 'should return false when webhookSecret not configured' , ( ) => {
130158 const client = new AdCPClient ( [ agent ] , { } ) ; // No webhookSecret
131159 const agentClient = client . agent ( 'test_agent' ) ;
132160
133- const payload = { event : ' test' } ;
161+ const rawBody = '{" event":" test"}' ;
134162 const timestamp = Math . floor ( Date . now ( ) / 1000 ) ;
135163 const signature = 'sha256=anything' ;
136164
137- const isValid = agentClient . verifyWebhookSignature ( payload , signature , timestamp ) ;
165+ const isValid = agentClient . verifyWebhookSignature ( rawBody , signature , timestamp ) ;
138166 assert . strictEqual ( isValid , false ) ;
139167 } ) ;
140168} ) ;
0 commit comments