/opt/mawid/apps/api/src/whatsapp
Edit: /opt/mawid/apps/api/src/whatsapp/wa-webhook.controller.ts (2308B)
import {
BadRequestException,
Controller,
ForbiddenException,
Get,
HttpCode,
Logger,
Post,
Query,
Req,
type RawBodyRequest,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Throttle } from '@nestjs/throttler';
import type { FastifyRequest } from 'fastify';
import { Public } from '../auth/public.decorator';
import { WaInboundService } from './wa-inbound.service';
import { verifyWaSignature } from './wa-signature';
import type { WaWebhookPayload } from './wa-types';
// Meta bursts webhook deliveries — allow more headroom than the API default.
@Throttle({ default: { ttl: 60_000, limit: 600 } })
@Public()
@Controller('webhooks/whatsapp')
export class WaWebhookController {
private readonly logger = new Logger(WaWebhookController.name);
constructor(
private readonly config: ConfigService,
private readonly inbound: WaInboundService,
) {}
/** Meta subscription verification handshake. */
@Get()
verify(
@Query('hub.mode') mode?: string,
@Query('hub.verify_token') token?: string,
@Query('hub.challenge') challenge?: string,
): string {
if (mode === 'subscribe' && token === this.config.get('WA_WEBHOOK_VERIFY_TOKEN') && challenge) {
return challenge;
}
throw new ForbiddenException('Webhook verification failed');
}
@Post()
@HttpCode(200) // Meta expects 200, not Nest's default 201 for POST
async receive(@Req() req: RawBodyRequest
): Promise<{ received: true }> {
const signature = req.headers['x-hub-signature-256'] as string | undefined;
const appSecret = this.config.get('WA_APP_SECRET') ?? '';
if (!verifyWaSignature(req.rawBody, signature, appSecret)) {
throw new ForbiddenException('Invalid webhook signature');
}
const payload = req.body as WaWebhookPayload;
if (payload?.object !== 'whatsapp_business_account') {
throw new BadRequestException('Unexpected webhook object');
}
// Ack immediately — agent turns take seconds of LLM calls, and holding
// Meta's webhook open past its timeout triggers redelivery storms.
void Promise.resolve(this.inbound.process(payload)).catch((err) =>
this.logger.error(`Webhook processing failed: ${String(err)}`),
);
return { received: true };
}
}