import { Body, Controller, Headers, Logger, Post, Req, type RawBodyRequest } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; import { createHmac, timingSafeEqual } from "node:crypto"; import type { Request } from "express"; import { SkipCsrf } from "../common/decorators/skip-csrf.decorator"; import { GiteaPrWebhookDto } from "./dto/gitea-pr-webhook.dto"; import { GatekeeperService } from "./gatekeeper.service"; @Controller("gatekeeper/webhook") export class GatekeeperController { private readonly logger = new Logger(GatekeeperController.name); constructor( private readonly gatekeeperService: GatekeeperService, private readonly configService: ConfigService ) {} @SkipCsrf() @Post("gitea") handleWebhook( @Req() req: RawBodyRequest, @Body() body: GiteaPrWebhookDto, @Headers("x-gitea-signature") signature: string | undefined ): Promise<{ ok: boolean }> { const secret = this.configService.get("GITEA_WEBHOOK_SECRET"); if (secret && !this.isValidSignature(this.getRequestBody(req, body), signature, secret)) { this.logger.warn("Received invalid Gitea webhook signature"); return Promise.resolve({ ok: true }); } if (!secret) { this.logger.warn("GITEA_WEBHOOK_SECRET is not configured; accepting Gitea webhook"); } void this.gatekeeperService.handlePrEvent(body).catch((error: unknown) => { const message = error instanceof Error ? error.message : String(error); this.logger.error(`Failed to process Gitea PR webhook: ${message}`); }); return Promise.resolve({ ok: true }); } private getRequestBody(req: RawBodyRequest, body: GiteaPrWebhookDto): Buffer { if (Buffer.isBuffer(req.rawBody)) { return req.rawBody; } return Buffer.from(JSON.stringify(body)); } private isValidSignature(body: Buffer, signature: string | undefined, secret: string): boolean { if (!signature) { return false; } const expected = createHmac("sha256", secret).update(body).digest("hex"); const actual = Buffer.from(signature); const expectedBuffer = Buffer.from(expected); if (actual.length !== expectedBuffer.length) { return false; } return timingSafeEqual(actual, expectedBuffer); } }