mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-06-03 19:25:19 +02:00
fix and enable composio webhook verification
This commit is contained in:
parent
23d88aa7c0
commit
d074281280
4 changed files with 39 additions and 33 deletions
|
|
@ -10,7 +10,7 @@ export async function POST(request: Request) {
|
|||
const logger = new PrefixLogger(`composio-webhook-[${id}]`);
|
||||
const payload = await request.text();
|
||||
const headers = Object.fromEntries(request.headers.entries());
|
||||
logger.log('received event', JSON.stringify(headers));
|
||||
logger.log('received event', JSON.stringify(headers), payload);
|
||||
|
||||
// handle webhook
|
||||
try {
|
||||
|
|
|
|||
23
apps/rowboat/package-lock.json
generated
23
apps/rowboat/package-lock.json
generated
|
|
@ -60,7 +60,6 @@
|
|||
"remark-gfm": "^4.0.1",
|
||||
"rowboat-shared": "github:rowboatlabs/shared",
|
||||
"sharp": "^0.33.4",
|
||||
"standardwebhooks": "^1.0.0",
|
||||
"styled-components": "^5.3.11",
|
||||
"swr": "^2.2.5",
|
||||
"tailwind-merge": "^2.5.5",
|
||||
|
|
@ -7267,12 +7266,6 @@
|
|||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@stablelib/base64": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz",
|
||||
"integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@styled-system/background": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@styled-system/background/-/background-5.1.2.tgz",
|
||||
|
|
@ -11398,12 +11391,6 @@
|
|||
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/fast-sha256": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz",
|
||||
"integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==",
|
||||
"license": "Unlicense"
|
||||
},
|
||||
"node_modules/fast-xml-parser": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz",
|
||||
|
|
@ -16859,16 +16846,6 @@
|
|||
"integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/standardwebhooks": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz",
|
||||
"integrity": "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@stablelib/base64": "^1.0.0",
|
||||
"fast-sha256": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/statuses": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
|
||||
|
|
|
|||
|
|
@ -68,7 +68,6 @@
|
|||
"remark-gfm": "^4.0.1",
|
||||
"rowboat-shared": "github:rowboatlabs/shared",
|
||||
"sharp": "^0.33.4",
|
||||
"standardwebhooks": "^1.0.0",
|
||||
"styled-components": "^5.3.11",
|
||||
"swr": "^2.2.5",
|
||||
"tailwind-merge": "^2.5.5",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { IJobsRepository } from "@/src/application/repositories/jobs.repository.interface";
|
||||
import { IComposioTriggerDeploymentsRepository } from "@/src/application/repositories/composio-trigger-deployments.repository.interface";
|
||||
import { Webhook } from "standardwebhooks";
|
||||
import { createHmac, timingSafeEqual } from "crypto";
|
||||
import { z } from "zod";
|
||||
import { BadRequestError } from "@/src/entities/errors/common";
|
||||
import { UserMessage } from "@/app/lib/types/types";
|
||||
|
|
@ -52,7 +52,7 @@ export class HandleCompsioWebhookRequestUseCase implements IHandleCompsioWebhook
|
|||
private readonly jobsRepository: IJobsRepository;
|
||||
private readonly projectsRepository: IProjectsRepository;
|
||||
private readonly pubSubService: IPubSubService;
|
||||
private webhook;
|
||||
// no external webhook verifier; using HMAC-SHA256 verification
|
||||
|
||||
constructor({
|
||||
composioTriggerDeploymentsRepository,
|
||||
|
|
@ -69,18 +69,17 @@ export class HandleCompsioWebhookRequestUseCase implements IHandleCompsioWebhook
|
|||
this.jobsRepository = jobsRepository;
|
||||
this.projectsRepository = projectsRepository;
|
||||
this.pubSubService = pubSubService;
|
||||
this.webhook = new Webhook(WEBHOOK_SECRET);
|
||||
}
|
||||
|
||||
async execute(request: z.infer<typeof requestSchema>): Promise<void> {
|
||||
const { headers, payload } = request;
|
||||
|
||||
// verify payload
|
||||
// try {
|
||||
// this.webhook.verify(payload, headers);
|
||||
// } catch (error) {
|
||||
// throw new BadRequestError("Payload verification failed");
|
||||
// }
|
||||
try {
|
||||
this.verifySignature(headers, payload);
|
||||
} catch (error) {
|
||||
throw new BadRequestError("Payload verification failed");
|
||||
}
|
||||
|
||||
// parse event
|
||||
let event: z.infer<typeof payloadSchema>;
|
||||
|
|
@ -148,4 +147,35 @@ export class HandleCompsioWebhookRequestUseCase implements IHandleCompsioWebhook
|
|||
|
||||
logger.log(`Created ${jobs} jobs for trigger ${event.data.trigger_nano_id}`);
|
||||
}
|
||||
|
||||
private verifySignature(headers: Record<string, string>, payload: string): void {
|
||||
const normalizedHeaders = Object.fromEntries(
|
||||
Object.entries(headers).map(([key, value]) => [key.toLowerCase(), value])
|
||||
) as Record<string, string>;
|
||||
|
||||
const webhookId = normalizedHeaders["webhook-id"];
|
||||
const webhookTimestamp = normalizedHeaders["webhook-timestamp"];
|
||||
const webhookSignature = normalizedHeaders["webhook-signature"];
|
||||
|
||||
if (!webhookId || !webhookTimestamp || !webhookSignature) {
|
||||
throw new BadRequestError("Missing required webhook headers");
|
||||
}
|
||||
|
||||
const toSign = `${webhookId}.${webhookTimestamp}.${payload}`;
|
||||
const expectedSignature = createHmac("sha256", WEBHOOK_SECRET)
|
||||
.update(toSign)
|
||||
.digest("base64");
|
||||
const expectedFullSignature = `v1,${expectedSignature}`;
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
const expectedBytes = encoder.encode(expectedFullSignature);
|
||||
const actualBytes = encoder.encode(webhookSignature);
|
||||
|
||||
const isValid =
|
||||
expectedBytes.length === actualBytes.length && timingSafeEqual(expectedBytes, actualBytes);
|
||||
|
||||
if (!isValid) {
|
||||
throw new BadRequestError("Invalid webhook signature");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue