Skip to content

Commit 8c19bc7

Browse files
GeneAIclaude
authored andcommitted
feat: Add Vercel webhook endpoint for deployment notifications
Adds /api/webhooks/vercel endpoint that: - Verifies webhook signatures using HMAC-SHA1 - Handles deployment.created, deployment.ready, deployment.succeeded, deployment.error events - Logs deployment status for monitoring 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent 450f597 commit 8c19bc7

File tree

1 file changed

+171
-0
lines changed

1 file changed

+171
-0
lines changed
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
import { NextRequest, NextResponse } from 'next/server';
2+
import crypto from 'crypto';
3+
4+
// Vercel webhook event types
5+
interface VercelWebhookPayload {
6+
id: string;
7+
type: string;
8+
createdAt: number;
9+
payload: {
10+
deployment?: {
11+
id: string;
12+
name: string;
13+
url: string;
14+
meta?: Record<string, string>;
15+
};
16+
project?: {
17+
id: string;
18+
name: string;
19+
};
20+
team?: {
21+
id: string;
22+
name: string;
23+
};
24+
user?: {
25+
id: string;
26+
email: string;
27+
};
28+
target?: string;
29+
alias?: string[];
30+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
31+
[key: string]: any;
32+
};
33+
}
34+
35+
export const runtime = 'nodejs';
36+
37+
function verifySignature(body: string, signature: string | null, secret: string): boolean {
38+
if (!signature) return false;
39+
40+
const expectedSignature = crypto
41+
.createHmac('sha1', secret)
42+
.update(body)
43+
.digest('hex');
44+
45+
return crypto.timingSafeEqual(
46+
Buffer.from(signature),
47+
Buffer.from(expectedSignature)
48+
);
49+
}
50+
51+
export async function POST(req: NextRequest) {
52+
const body = await req.text();
53+
const signature = req.headers.get('x-vercel-signature');
54+
55+
const secret = process.env.VERCEL_WEBHOOK_SECRET;
56+
57+
if (!secret) {
58+
console.error('VERCEL_WEBHOOK_SECRET not configured');
59+
return NextResponse.json(
60+
{ error: 'Webhook secret not configured' },
61+
{ status: 500 }
62+
);
63+
}
64+
65+
// Verify the webhook signature
66+
if (!verifySignature(body, signature, secret)) {
67+
console.error('Invalid Vercel webhook signature');
68+
return NextResponse.json(
69+
{ error: 'Invalid signature' },
70+
{ status: 401 }
71+
);
72+
}
73+
74+
let event: VercelWebhookPayload;
75+
76+
try {
77+
event = JSON.parse(body);
78+
} catch {
79+
console.error('Failed to parse webhook body');
80+
return NextResponse.json(
81+
{ error: 'Invalid JSON' },
82+
{ status: 400 }
83+
);
84+
}
85+
86+
console.log('Vercel webhook received:', {
87+
id: event.id,
88+
type: event.type,
89+
createdAt: new Date(event.createdAt).toISOString(),
90+
});
91+
92+
try {
93+
switch (event.type) {
94+
case 'deployment.created': {
95+
console.log('Deployment created:', {
96+
id: event.payload.deployment?.id,
97+
name: event.payload.deployment?.name,
98+
url: event.payload.deployment?.url,
99+
});
100+
break;
101+
}
102+
103+
case 'deployment.ready': {
104+
console.log('Deployment ready:', {
105+
id: event.payload.deployment?.id,
106+
url: event.payload.deployment?.url,
107+
aliases: event.payload.alias,
108+
});
109+
// Could trigger post-deployment tasks here
110+
// e.g., warm up caches, run smoke tests, notify team
111+
break;
112+
}
113+
114+
case 'deployment.succeeded': {
115+
console.log('Deployment succeeded:', {
116+
id: event.payload.deployment?.id,
117+
url: event.payload.deployment?.url,
118+
});
119+
break;
120+
}
121+
122+
case 'deployment.error': {
123+
console.error('Deployment failed:', {
124+
id: event.payload.deployment?.id,
125+
name: event.payload.deployment?.name,
126+
});
127+
// Could send alert to Slack/Discord/email here
128+
break;
129+
}
130+
131+
case 'deployment.canceled': {
132+
console.log('Deployment canceled:', {
133+
id: event.payload.deployment?.id,
134+
});
135+
break;
136+
}
137+
138+
case 'project.created': {
139+
console.log('Project created:', {
140+
id: event.payload.project?.id,
141+
name: event.payload.project?.name,
142+
});
143+
break;
144+
}
145+
146+
case 'project.removed': {
147+
console.log('Project removed:', {
148+
id: event.payload.project?.id,
149+
name: event.payload.project?.name,
150+
});
151+
break;
152+
}
153+
154+
default:
155+
console.log(`Unhandled Vercel webhook event: ${event.type}`);
156+
}
157+
158+
return NextResponse.json({ received: true });
159+
} catch (error) {
160+
console.error('Error processing Vercel webhook:', error);
161+
return NextResponse.json(
162+
{ error: 'Webhook handler failed' },
163+
{ status: 500 }
164+
);
165+
}
166+
}
167+
168+
// Optionally handle GET for webhook verification (some services require this)
169+
export async function GET() {
170+
return NextResponse.json({ status: 'Vercel webhook endpoint active' });
171+
}

0 commit comments

Comments
 (0)