cp_01 yesterday Add initial webhook receiver
A first pass that accepts provider webhooks and writes them to the database. This checkpoint is intentionally broad enough to review the shape of the feature before tightening the edge cases.
123456789101112131415161718192021222324252627282930313233343536import { json } from '@sveltejs/kit';import type { RequestHandler } from './$types';import { db } from '$lib/server/db';import { webhook_events } from '$lib/server/db/schema';
type GithubWebhook = { action?: string; repository?: { full_name?: string }; sender?: { login?: string }; pull_request?: { number?: number; title?: string; head?: { sha?: string }; };};
export const POST: RequestHandler = async ({ request }) => { const event_type = request.headers.get('x-github-event') ?? 'unknown'; const delivery_id = request.headers.get('x-github-delivery') ?? crypto.randomUUID(); const body = (await request.json()) as GithubWebhook;
await db.insert(webhook_events).values({ delivery_id, event_type, repository: body.repository?.full_name ?? 'unknown', action: body.action ?? 'unknown', actor: body.sender?.login ?? 'unknown', pull_request_number: body.pull_request?.number ?? null, pull_request_title: body.pull_request?.title ?? null, head_sha: body.pull_request?.head?.sha ?? null, payload: body });
return json({ ok: true });};
123456789101112131415import { jsonb, pgTable, text, timestamp, integer } from 'drizzle-orm/pg-core';
export const webhook_events = pgTable('webhook_events', { delivery_id: text('delivery_id').primaryKey(), event_type: text('event_type').notNull(), repository: text('repository').notNull(), action: text('action').notNull(), actor: text('actor').notNull(), pull_request_number: integer('pull_request_number'), pull_request_title: text('pull_request_title'), head_sha: text('head_sha'), payload: jsonb('payload').notNull(), created_at: timestamp('created_at', { withTimezone: true }).defaultNow().notNull()});
cp_02 yesterday Verify signatures before persisting
The second checkpoint isolates the security review. Reviewers can focus only on request authentication instead of re-reading the whole endpoint.
1123456789107 unmodified lines2021222023242526272829303132333435363738394041424344454647484950515253545556575859import { json } from '@sveltejs/kit';import { error, json } from '@sveltejs/kit';import type { RequestHandler } from './$types';import { GITHUB_WEBHOOK_SECRET } from '$env/static/private';import { db } from '$lib/server/db';import { webhook_events } from '$lib/server/db/schema';import { timingSafeEqual, createHmac } from 'node:crypto';
type GithubWebhook = { action?: string; repository?: { full_name?: string };7 unmodified linesexport const POST: RequestHandler = async ({ request }) => { const event_type = request.headers.get('x-github-event') ?? 'unknown'; const delivery_id = request.headers.get('x-github-delivery') ?? crypto.randomUUID(); const body = (await request.json()) as GithubWebhook; const signature = request.headers.get('x-hub-signature-256'); const raw_body = await request.text();
if (!signature || !verify_signature(raw_body, signature)) { error(401, 'Invalid webhook signature'); }
const body = JSON.parse(raw_body) as GithubWebhook;
await db.insert(webhook_events).values({ delivery_id, event_type, repository: body.repository?.full_name ?? 'unknown', action: body.action ?? 'unknown', actor: body.sender?.login ?? 'unknown', pull_request_number: body.pull_request?.number ?? null, pull_request_title: body.pull_request?.title ?? null, head_sha: body.pull_request?.head?.sha ?? null, payload: body });
return json({ ok: true });};
function verify_signature(raw_body: string, signature: string) { const expected = createHmac('sha256', GITHUB_WEBHOOK_SECRET).update(raw_body).digest('hex'); const actual = signature.replace(/^sha256=/, '');
const expected_buffer = Buffer.from(expected, 'hex'); const actual_buffer = Buffer.from(actual, 'hex');
return ( expected_buffer.length === actual_buffer.length && timingSafeEqual(expected_buffer, actual_buffer) );}
cp_03 yesterday Move parsing into a small adapter
This checkpoint turns the endpoint into orchestration code. The diff shows a clean extraction where the parsing rules can be reviewed independently from HTTP handling.
2 unmodified lines345678891011121314151617184 unmodified lines151629171819202134353637383940222313 unmodified lines3637382 unmodified linesimport type { RequestHandler } from './$types';import { GITHUB_WEBHOOK_SECRET } from '$env/static/private';import { db } from '$lib/server/db';import { parse_github_webhook } from '$lib/server/github/webhooks';import { webhook_events } from '$lib/server/db/schema';import { timingSafeEqual, createHmac } from 'node:crypto';
type GithubWebhook = { action?: string; repository?: { full_name?: string }; sender?: { login?: string }; pull_request?: { number?: number; title?: string; head?: { sha?: string }; };};4 unmodified lines }
const body = JSON.parse(raw_body) as GithubWebhook; const event = parse_github_webhook(JSON.parse(raw_body));
await db.insert(webhook_events).values({ delivery_id, event_type, repository: body.repository?.full_name ?? 'unknown', action: body.action ?? 'unknown', actor: body.sender?.login ?? 'unknown', pull_request_number: body.pull_request?.number ?? null, pull_request_title: body.pull_request?.title ?? null, head_sha: body.pull_request?.head?.sha ?? null, payload: body ...event });13 unmodified lines );}
12345678910111213141516171819202122232425type GithubWebhook = { action?: string; repository?: { full_name?: string }; sender?: { login?: string }; pull_request?: { number?: number; title?: string; head?: { sha?: string }; };};
export function parse_github_webhook(payload: unknown) { const body = payload as GithubWebhook;
return { repository: body.repository?.full_name ?? 'unknown', action: body.action ?? 'unknown', actor: body.sender?.login ?? 'unknown', pull_request_number: body.pull_request?.number ?? null, pull_request_title: body.pull_request?.title ?? null, head_sha: body.pull_request?.head?.sha ?? null, payload: body };}
cp_04 yesterday Add idempotency and parser tests
This checkpoint shows how tests can be reviewed after the production behavior exists. It catches duplicate deliveries and locks down payload normalization.
1 unmodified line2345678913 unmodified lines232425262728293031323334351 unmodified lineimport { error, json } from '@sveltejs/kit';import type { RequestHandler } from './$types';import { GITHUB_WEBHOOK_SECRET } from '$env/static/private';import { db } from '$lib/server/db';import { eq } from 'drizzle-orm';import { parse_github_webhook } from '$lib/server/github/webhooks';import { webhook_events } from '$lib/server/db/schema';import { timingSafeEqual, createHmac } from 'node:crypto';13 unmodified lines
const event = parse_github_webhook(JSON.parse(raw_body)); const existing = await db.query.webhook_events.findFirst({ where: eq(webhook_events.delivery_id, delivery_id) });
if (existing) { return json({ ok: true, duplicate: true }); }
await db.insert(webhook_events).values({ delivery_id, event_type,12345678910111213141516171819202122232425262728293031323334353637383940import { describe, expect, it } from 'vitest';import { parse_github_webhook } from './webhooks';
describe('parse_github_webhook', () => { it('normalizes pull request payloads', () => { const event = parse_github_webhook({ action: 'opened', repository: { full_name: 'letmediff/app' }, sender: { login: 'mona' }, pull_request: { number: 42, title: 'Add checkpoint review', head: { sha: 'abc123' } } });
expect(event).toMatchObject({ repository: 'letmediff/app', action: 'opened', actor: 'mona', pull_request_number: 42, pull_request_title: 'Add checkpoint review', head_sha: 'abc123' }); });
it('keeps partial payloads reviewable', () => { const event = parse_github_webhook({ action: 'ping' });
expect(event).toMatchObject({ repository: 'unknown', action: 'ping', actor: 'unknown', pull_request_number: null, pull_request_title: null, head_sha: null }); });});
cp_05 yesterday Surface webhook history in the UI
The final checkpoint is mostly presentation. Reviewers can compare it against the earlier server checkpoints and verify that stored events become an understandable audit trail.
1234567891011121314import { db } from '$lib/server/db';import { webhook_events } from '$lib/server/db/schema';import { desc } from 'drizzle-orm';
export async function load() { const events = await db .select() .from(webhook_events) .orderBy(desc(webhook_events.created_at)) .limit(50);
return { events };}
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970<script lang="ts"> import type { PageData } from './$types';
let { data }: { data: PageData } = $props();</script>
<svelte:head> <title>Webhook history</title></svelte:head>
<section class="webhook-history"> <header> <p class="eyebrow">Operations</p> <h1>Webhook history</h1> <p>Recent GitHub deliveries are grouped by checkpoint-friendly metadata so reviewers can trace what happened without opening the raw payload first.</p> </header>
<div class="events"> {#each data.events as event} <article class="event-card"> <div> <strong>{event.repository}</strong> <span>{event.event_type} / {event.action}</span> </div>
{#if event.pull_request_number} <p>#{event.pull_request_number}: {event.pull_request_title}</p> {:else} <p>No pull request attached to this delivery.</p> {/if}
<footer> <code>{event.head_sha ?? event.delivery_id}</code> <span>by {event.actor}</span> </footer> </article> {/each} </div></section>
<style> .webhook-history { display: grid; gap: 2rem; max-width: 58rem; margin: 0 auto; padding: 4rem 1rem; }
.eyebrow { margin: 0 0 0.5rem; color: #6b7280; font-size: 0.75rem; font-weight: 700; letter-spacing: 0.16em; text-transform: uppercase; }
.events { display: grid; gap: 1rem; }
.event-card { display: grid; gap: 0.75rem; padding: 1rem; border: 1px solid #e5e7eb; border-radius: 1rem; background: white;