import { Body, Controller, Delete, Get, HttpCode, Inject, Param, Patch, Post, Query, Req } from "@nestjs/common"; import type { Request } from "express"; import { z } from "zod"; import { WebhooksService } from "./webhooks.service.js"; import { ZodValidate } from "../../common/pipes/zod-validation.pipe.js"; import { requireUser } from "../../common/types.js"; /** * Webhooks REST (TASK-0130) — docs/api/webhooks.md §1/§2/§6. * * POST /workspaces/:wid/webhooks create (secret shown once) * GET /workspaces/:wid/webhooks list (no secrets; prefix) * GET /workspaces/:wid/webhooks/:id detail (+recentDeliveries) * PATCH /workspaces/:wid/webhooks/:id merge-patch url/events/active * POST /workspaces/:wid/webhooks/:id/rotate-secret new secret once * POST /workspaces/:wid/webhooks/:id/test synchronous test send * POST /workspaces/:wid/webhooks/:id/resubmit re-queue failed/DLQ rows * DELETE /workspaces/:wid/webhooks/:id soft delete (204) * GET /workspaces/:wid/webhooks/:id/deliveries log (filters) * GET /workspaces/:wid/webhooks/:id/deliveries/:did single (payload) * POST /workspaces/:wid/webhooks/:id/deliveries/:did/retry * * Auth: user JWTs only (admin surface, same gate as the API-keys module — * `requireUser`); OWNER/ADMIN is enforced server-side in the service. */ const createSchema = z.object({ url: z.string().url().max(2048), events: z.array(z.string().min(1)).min(1).max(32).optional(), secret: z.string().min(16).max(256).optional(), active: z.boolean().optional() }); type CreateInput = z.infer; const updateSchema = z.object({ url: z.string().url().max(2048).optional(), events: z.array(z.string().min(1)).min(1).max(32).optional(), active: z.boolean().optional(), /** docs §1.4: `{ "rotateSecret": true }` → new secret returned once. */ rotateSecret: z.boolean().optional() }); type UpdateInput = z.infer; /** Task L349 resubmit body: one deliveryId, "all_dead", or a time window. */ const resubmitSchema = z .object({ deliveryId: z.string().min(1).optional(), all_dead: z.boolean().optional(), window: z.object({ since: z.string().optional(), until: z.string().optional() }).optional() }) .refine((v) => v.deliveryId || v.all_dead !== undefined || v.window, { message: "Provide deliveryId, all_dead, or window" }); type ResubmitInput = z.infer; @Controller("workspaces/:workspaceId/webhooks") export class WebhooksController { constructor(@Inject(WebhooksService) private readonly service: WebhooksService) {} @Post() create(@Param("workspaceId") wid: string, @Body(ZodValidate(createSchema)) body: CreateInput, @Req() req: Request) { return this.service.create(wid, requireUser(req), body, req); } @Get() list( @Param("workspaceId") wid: string, @Query() q: { cursor?: string; limit?: string }, @Req() req: Request ) { return this.service.list(wid, requireUser(req), q); } @Get(":webhookId") get(@Param("workspaceId") wid: string, @Param("webhookId") webhookId: string, @Req() req: Request) { return this.service.get(wid, webhookId, requireUser(req)); } @Patch(":webhookId") async update( @Param("workspaceId") wid: string, @Param("webhookId") webhookId: string, @Body(ZodValidate(updateSchema)) body: UpdateInput, @Req() req: Request ) { // docs §1.4: rotation rides the PATCH as `rotateSecret: true`. if (body.rotateSecret) { return this.service.rotateSecret(wid, webhookId, requireUser(req), req); } const { rotateSecret: _ignored, ...patch } = body; return this.service.update(wid, webhookId, requireUser(req), patch, req); } @Post(":webhookId/rotate-secret") rotateSecret(@Param("workspaceId") wid: string, @Param("webhookId") webhookId: string, @Req() req: Request) { return this.service.rotateSecret(wid, webhookId, requireUser(req), req); } @HttpCode(200) @Post(":webhookId/test") test(@Param("workspaceId") wid: string, @Param("webhookId") webhookId: string, @Req() req: Request) { return this.service.test(wid, webhookId, requireUser(req), req); } @HttpCode(200) @Post(":webhookId/resubmit") resubmit( @Param("workspaceId") wid: string, @Param("webhookId") webhookId: string, @Body(ZodValidate(resubmitSchema)) body: ResubmitInput, @Req() req: Request ) { return this.service.resubmit(wid, webhookId, requireUser(req), body, req); } @Delete(":webhookId") async remove(@Param("workspaceId") wid: string, @Param("webhookId") webhookId: string, @Req() req: Request) { await this.service.remove(wid, webhookId, requireUser(req), req); return; // 204 } @Get(":webhookId/deliveries") deliveries( @Param("workspaceId") wid: string, @Param("webhookId") webhookId: string, @Query() q: { cursor?: string; limit?: string; status?: string; event?: string; since?: string; until?: string }, @Req() req: Request ) { return this.service.deliveries(wid, webhookId, requireUser(req), q); } @Get(":webhookId/deliveries/:deliveryId") delivery( @Param("workspaceId") wid: string, @Param("webhookId") webhookId: string, @Param("deliveryId") deliveryId: string, @Req() req: Request ) { return this.service.delivery(wid, webhookId, deliveryId, requireUser(req)); } @HttpCode(200) @Post(":webhookId/deliveries/:deliveryId/retry") retryDelivery( @Param("workspaceId") wid: string, @Param("webhookId") webhookId: string, @Param("deliveryId") deliveryId: string, @Req() req: Request ) { return this.service.retryDelivery(wid, webhookId, deliveryId, requireUser(req), req); } }