From 1dd168cb2912a685c4fab0c4a7f356eea0f4953c Mon Sep 17 00:00:00 2001 From: DexterStorey Date: Wed, 2 Oct 2024 21:26:38 -0400 Subject: [PATCH] init --- .github/workflows/publish.yml | 16 ++++++ .gitignore | 17 ++++++ .vscode/settings.json | 21 ++++++++ CHANGELOG.md | 3 ++ biome.json | 3 ++ index.ts | 7 +++ lib/route.ts | 36 +++++++++++++ lib/types.ts | 65 +++++++++++++++++++++++ lib/utils.ts | 99 +++++++++++++++++++++++++++++++++++ package.json | 24 +++++++++ 10 files changed, 291 insertions(+) create mode 100644 .github/workflows/publish.yml create mode 100644 .gitignore create mode 100644 .vscode/settings.json create mode 100644 CHANGELOG.md create mode 100644 biome.json create mode 100644 index.ts create mode 100644 lib/route.ts create mode 100644 lib/types.ts create mode 100644 lib/utils.ts create mode 100644 package.json diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..078bacf --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,16 @@ +name: Release Package + +on: + push: + branches: + - main + +permissions: + contents: read + packages: write + id-token: write + +jobs: + release: + uses: rubriclab/package/.github/workflows/release-package.yml@main + secrets: inherit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..677225b --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +# Dependency directories +node_modules/ +dist +.DS_Store + +# Output of 'npm pack' +*.tgz + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# turbo +.turbo \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..6a581e6 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,21 @@ +{ + "editor.defaultFormatter": "biomejs.biome", + "editor.formatOnSave": true, + "editor.formatOnType": true, + "[typescript]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[typescriptreact]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[json]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "editor.codeActionsOnSave": { + "quickfix.biome": "explicit", + "source.organizeImports.biome": "explicit" + }, + "[prisma]": { + "editor.defaultFormatter": "Prisma.prisma" + } +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..3122959 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3 @@ +- [2024-10-02] [init](https://github.com/RubricLab/webhooks/commit/fa8f2f83d7cbc48865f343e7af69fd7fa8ec2d37) +# Changelog + diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..e8d99dd --- /dev/null +++ b/biome.json @@ -0,0 +1,3 @@ +{ + "extends": ["@rubriclab/config/biome"] +} diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..65c8b82 --- /dev/null +++ b/index.ts @@ -0,0 +1,7 @@ +export type { + WebhookEventHandler, + WebhookProvider, + WebhookEvent, + WebhookEventMap, + WebhookEventType +} from './lib/types' diff --git a/lib/route.ts b/lib/route.ts new file mode 100644 index 0000000..ed9ff6c --- /dev/null +++ b/lib/route.ts @@ -0,0 +1,36 @@ +import { type NextRequest, NextResponse } from 'next/server' +import type { WebhookProviders } from './types' + +export function webhookHandler({ webhookProviders }: { webhookProviders: WebhookProviders }) { + return async ( + request: NextRequest, + { params }: { params: { provider: keyof WebhookProviders } } + ) => { + const provider = webhookProviders[params.provider] + if (!provider) { + return NextResponse.json({ error: 'Invalid provider' }, { status: 400 }) + } + + const payload = await request.text() + const parsedPayload = JSON.parse(payload) + + try { + const isValid = await provider.verifyWebhook({ + payload, + headers: request.headers + }) + if (!isValid) { + return NextResponse.json({ error: 'Invalid signature' }, { status: 403 }) + } + + const response = await provider.handleWebhook({ + payload: parsedPayload, + headers: request.headers + }) + return NextResponse.json(response.body, { status: response.statusCode }) + } catch (error) { + console.error(`Error in ${provider.provider} webhook:`, error) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } + } +} diff --git a/lib/types.ts b/lib/types.ts new file mode 100644 index 0000000..e70fe1c --- /dev/null +++ b/lib/types.ts @@ -0,0 +1,65 @@ +import type { PrismaClient } from '@prisma/client' + +export interface WebhookProvider { + provider: string + enable: ({ + userId, + accountId + }: { + userId: string + accountId: string + }) => Promise + disable: ({ + userId, + accountId + }: { + userId: string + accountId: string + }) => Promise + handleWebhook: ({ + payload, + headers + }: { + // biome-ignore lint/suspicious/noExplicitAny: + payload: any + headers: Headers + }) => Promise + verifyWebhook: ({ + payload, + headers + }: { + // biome-ignore lint/suspicious/noExplicitAny: + payload: any + headers: Headers + }) => Promise + eventHandlers: T +} + +export type WebhookProviders = { + [provider in string]?: WebhookProvider +} + +export interface WebhookResponse { + statusCode: number + // biome-ignore lint/suspicious/noExplicitAny: + body: any +} + +// TODO: remove string type +export type WebhookEventType = keyof WebhookEventMap | string + +export interface WebhookEvent { + type: WebhookEventType + data: D + username?: string + action?: T +} + +export type WebhookEventHandler = (data: D, username: string) => Promise + +export type WebhookEventMap = { + // biome-ignore lint/suspicious/noExplicitAny: + [action in string]?: WebhookEventHandler +} + +export type DB = PrismaClient diff --git a/lib/utils.ts b/lib/utils.ts new file mode 100644 index 0000000..a8727f6 --- /dev/null +++ b/lib/utils.ts @@ -0,0 +1,99 @@ +import type { DB, WebhookProviders } from './types' + +export function createWebhookActions({ + webhookProviders, + db +}: { webhookProviders: WebhookProviders; db: DB }) { + return { + async getWebhooksForProvider({ + userId, + provider, + accountId + }: { + userId: string + provider: string + accountId: string + }) { + const webhookProvider = webhookProviders[provider] + + if (!webhookProvider) { + throw new Error(`Webhook provider not found for ${provider}`) + } + + const webhooks = await db.$transaction( + Object.keys(webhookProvider.eventHandlers).map(webhookKey => + db.webhook.findUnique({ + where: { + userId_provider_accountId_webhook: { + userId, + provider: webhookProvider.provider, + accountId, + webhook: webhookKey + } + } + }) + ) + ) + + return Object.keys(webhookProvider.eventHandlers).map((webhookKey, index) => { + const existingWebhook = webhooks[index] + return ( + existingWebhook || { + webhook: webhookKey, + enabled: false, + config: null, + provider: webhookProvider.provider, + accountId, + userId + } + ) + }) + }, + async toggleWebhook({ + provider, + webhook, + userId, + accountId, + enable + }: { + provider: string + webhook: string + + userId: string + accountId: string + + enable: boolean + }) { + const webhookProvider = webhookProviders[provider] + + if (!webhookProvider) { + throw new Error(`Webhook provider not found for ${provider}`) + } + + if (enable) { + await webhookProvider.enable({ userId, accountId }) + } else { + await webhookProvider.disable({ userId, accountId }) + } + + await db.webhook.upsert({ + where: { + userId_provider_accountId_webhook: { + userId, + provider: webhookProvider.provider, + accountId, + webhook + } + }, + create: { + userId, + provider: webhookProvider.provider, + accountId, + webhook, + enabled: enable + }, + update: { enabled: enable } + }) + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..c35f483 --- /dev/null +++ b/package.json @@ -0,0 +1,24 @@ +{ + "scripts": { + "generate": "bun scripts/generate.ts", + "watch": "bun scripts/watch.ts", + "prepare": "bun x simple-git-hooks", + "bleed": "bun x npm-check-updates -u && bun i", + "clean": "rm -rf .next && rm -rf node_modules", + "format": "bun x biome format --write .", + "lint": "bun x biome check .", + "lint:fix": "bun x biome lint . --write --unsafe" + }, + "name": "@rubriclab/webhooks", + "version": "0.0.1", + "main": "index.ts", + "dependencies": { + "@rubriclab/package": "*" + }, + "simple-git-hooks": { + "post-commit": "bun run rubriclab-postcommit" + }, + "publishConfig": { + "access": "public" + } +}