Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions app/api/webhook/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { NextResponse } from 'next/server';
import crypto from 'crypto';

const WEBHOOK_SECRET = process.env.GITHUB_WEBHOOK_SECRET || 'development_secret';
const MAX_PAYLOAD_SIZE = 1024 * 1024; // 1MB

// In-memory rate limiting map: ip -> { count, resetTime }
const rateLimitMap = new Map<string, { count: number; resetTime: number }>();
const RATE_LIMIT_WINDOW = 60 * 1000; // 1 minute
const MAX_REQUESTS_PER_WINDOW = 10;

function checkRateLimit(ip: string): boolean {
const now = Date.now();
let record = rateLimitMap.get(ip);
if (!record || now > record.resetTime) {
record = { count: 1, resetTime: now + RATE_LIMIT_WINDOW };
rateLimitMap.set(ip, record);
return true;
}
record.count++;
if (record.count > MAX_REQUESTS_PER_WINDOW) {
return false;
}
return true;
}

export async function POST(req: Request) {
// 1. Rate Limiting
const ip = req.headers.get('x-forwarded-for') || 'unknown_ip';
if (!checkRateLimit(ip)) {
return NextResponse.json({ error: 'Too many requests' }, { status: 429 });
}

// 2. Payload Validation
const contentLength = Number(req.headers.get('content-length') || '0');
if (contentLength > MAX_PAYLOAD_SIZE) {
return NextResponse.json({ error: 'Payload too large' }, { status: 413 });
}

let bodyText: string;
try {
bodyText = await req.text();
} catch (error) {

Check warning on line 43 in app/api/webhook/route.ts

View workflow job for this annotation

GitHub Actions / Format · Lint · Typecheck · Test

'error' is defined but never used
return NextResponse.json({ error: 'Invalid payload' }, { status: 400 });
}

// Ensure it's not larger than 1MB even after reading
if (Buffer.byteLength(bodyText, 'utf8') > MAX_PAYLOAD_SIZE) {
return NextResponse.json({ error: 'Payload too large' }, { status: 413 });
}

// 3. Signature Verification
const signature = req.headers.get('x-hub-signature-256');
if (!signature) {
return NextResponse.json({ error: 'Missing signature' }, { status: 401 });
}

const hmac = crypto.createHmac('sha256', WEBHOOK_SECRET);
const digest = 'sha256=' + hmac.update(bodyText).digest('hex');

if (signature !== digest) {
return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
}

// Valid payload, proceed...
let payload;
try {
payload = JSON.parse(bodyText);

Check warning on line 68 in app/api/webhook/route.ts

View workflow job for this annotation

GitHub Actions / Format · Lint · Typecheck · Test

'payload' is assigned a value but never used
} catch (error) {

Check warning on line 69 in app/api/webhook/route.ts

View workflow job for this annotation

GitHub Actions / Format · Lint · Typecheck · Test

'error' is defined but never used
return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 });
}

// Handle payload...
return NextResponse.json(
{ success: true, message: 'Webhook received securely' },
{ status: 200 }
);
}
Loading