Skip to content

Commit 3be5cf4

Browse files
ralyodioclaude
andcommitted
fix: webhook signature — strip whsecret_ prefix, exclude from middleware
- Strip the whsecret_ prefix from COINPAY_WEBHOOK_SECRET before using it as the HMAC key (CoinPay signs with bare hex) - Add signature debug logging (first 12 chars of received vs computed) - Exclude /api/webhooks/* from middleware to keep raw body intact Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 0fdeb46 commit 3be5cf4

2 files changed

Lines changed: 17 additions & 6 deletions

File tree

apps/web/app/api/webhooks/coinpay/route.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,35 @@
11
import { createHmac, timingSafeEqual } from 'crypto';
22
import { NextRequest, NextResponse } from 'next/server';
33

4-
const WEBHOOK_SECRET = process.env.COINPAY_WEBHOOK_SECRET!;
4+
const RAW_SECRET = process.env.COINPAY_WEBHOOK_SECRET!;
5+
// CoinPay signs with the bare hex key — strip the "whsecret_" prefix if present
6+
const WEBHOOK_SECRET = RAW_SECRET?.replace(/^whsecret_/, '') ?? RAW_SECRET;
57

68
function verifySignature(raw: string, header: string): boolean {
79
const parts: Record<string, string> = {};
810
for (const part of header.split(',')) {
911
const idx = part.indexOf('=');
10-
if (idx !== -1) parts[part.slice(0, idx)] = part.slice(idx + 1);
12+
if (idx !== -1) parts[part.slice(0, idx).trim()] = part.slice(idx + 1).trim();
1113
}
1214
const { t, v1 } = parts;
13-
if (!t || !v1) return false;
15+
if (!t || !v1) {
16+
console.error('CoinPay webhook: missing t or v1 in signature header', header);
17+
return false;
18+
}
1419

1520
const age = Math.floor(Date.now() / 1000) - parseInt(t, 10);
16-
if (Math.abs(age) > 300) return false;
21+
if (Math.abs(age) > 300) {
22+
console.error('CoinPay webhook: timestamp too old, age =', age);
23+
return false;
24+
}
1725

26+
const payload = `${t}.${raw}`;
1827
const computed = createHmac('sha256', WEBHOOK_SECRET)
19-
.update(`${t}.${raw}`)
28+
.update(payload)
2029
.digest('hex');
2130

31+
console.log('CoinPay webhook sig check — received:', v1.slice(0, 12), 'computed:', computed.slice(0, 12));
32+
2233
try {
2334
return timingSafeEqual(Buffer.from(v1, 'hex'), Buffer.from(computed, 'hex'));
2435
} catch {

apps/web/middleware.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,6 @@ export function middleware(req: NextRequest) {
2626

2727
export const config = {
2828
matcher: [
29-
'/((?!_next/static|_next/image|favicon\\.svg|favicon\\.ico|.*\\.png|.*\\.svg|.*\\.jpg|.*\\.webp).*)',
29+
'/((?!_next/static|_next/image|favicon\\.svg|favicon\\.ico|api/webhooks|.*\\.png|.*\\.svg|.*\\.jpg|.*\\.webp).*)',
3030
],
3131
};

0 commit comments

Comments
 (0)