mirror your GitHub repos to tangled.org automatically
1

Configure Feed

Select the types of activity you want to include in your feed.

1import type { InstallationEvent } from '@octokit/webhooks-types' 2import { verify } from '@octokit/webhooks-methods' 3import { sql } from 'drizzle-orm' 4import { installation, webhookEvent } from '~~/server/db/schema' 5 6const RECOGNISED_EVENTS = new Set([ 7 'push', 8 'create', 9 'delete', 10 'repository', 11 'installation', 12 'installation_repositories', 13]) 14 15export default defineEventHandler(async event => { 16 const { githubWebhookSecret } = useRuntimeConfig() 17 if (!githubWebhookSecret) { 18 throw createError({ statusCode: 500, statusMessage: 'webhook secret not configured' }) 19 } 20 21 const signature = getRequestHeader(event, 'x-hub-signature-256') 22 const deliveryHeader = getRequestHeader(event, 'x-github-delivery') 23 const eventName = getRequestHeader(event, 'x-github-event') 24 25 if (!signature || !deliveryHeader || !eventName) { 26 throw createError({ statusCode: 400, statusMessage: 'missing required headers' }) 27 } 28 29 const rawBody = await readRawBody(event, 'utf8') 30 if (!rawBody) { 31 throw createError({ statusCode: 400, statusMessage: 'empty body' }) 32 } 33 34 const valid = await verify(githubWebhookSecret, rawBody, signature) 35 if (!valid) { 36 throw createError({ statusCode: 401, statusMessage: 'invalid signature' }) 37 } 38 39 const deliveryId = deliveryHeader.toLowerCase() 40 41 // Idempotent insert. If this delivery has already been seen, the row is not 42 // re-inserted and `inserted` is empty; we 200 and skip downstream work. 43 const db = useDb() 44 const inserted = await db 45 .insert(webhookEvent) 46 .values({ 47 deliveryId, 48 source: 'github', 49 event: eventName, 50 }) 51 .onConflictDoNothing({ target: webhookEvent.deliveryId }) 52 .returning({ deliveryId: webhookEvent.deliveryId }) 53 54 if (inserted.length === 0) { 55 return { ok: true, duplicate: true } 56 } 57 58 // Bookkeeping for installation lifecycle events. Sync work itself is enqueued 59 // by later commits; for now we just keep the `installation` table in step so 60 // those commits can FK-reference rows that already exist. 61 if (eventName === 'installation') { 62 const body = await readBody<InstallationEvent>(event) 63 const action = body.action 64 65 if (action === 'created') { 66 const account = body.installation.account 67 // GitHub's `installation.account` is `User | Enterprise | null`, but the 68 // ones we accept are user/org accounts; bail loudly on anything else. 69 if (!account || !('login' in account) || !('type' in account)) { 70 throw createError({ statusCode: 400, statusMessage: 'unsupported installation account' }) 71 } 72 const accountType = account.type === 'Organization' ? 'Organization' : 'User' 73 await db.insert(installation).values({ 74 id: body.installation.id, 75 accountLogin: account.login, 76 accountId: account.id, 77 accountType, 78 }).onConflictDoNothing({ target: installation.id }) 79 } 80 else if (action === 'deleted') { 81 // installation row deletion cascades to user_identity, ssh_key, repo_mapping. 82 // The corresponding sh.tangled.publicKey record on the user's PDS is revoked 83 // in commit 15. 84 await db.delete(installation).where(sql`${installation.id} = ${body.installation.id}`) 85 } 86 else if (action === 'suspend') { 87 await db.update(installation) 88 .set({ suspendedAt: new Date() }) 89 .where(sql`${installation.id} = ${body.installation.id}`) 90 } 91 else if (action === 'unsuspend') { 92 await db.update(installation) 93 .set({ suspendedAt: null }) 94 .where(sql`${installation.id} = ${body.installation.id}`) 95 } 96 } 97 98 // TODO(commit 7): enqueue a job for recognised event types. 99 if (RECOGNISED_EVENTS.has(eventName)) { 100 // Will become: await enqueue({ kind: eventName, payload: <envelope> }) 101 } 102 103 return { ok: true, deliveryId } 104})