import { z } from 'zod' import { defineRouteContract } from '@/lib/api/contracts/types' export const webhooksRouteQuerySchema = z.object({ workflowId: z.string().optional(), blockId: z.string().optional(), }) export const webhooksByBlockQuerySchema = z.object({ workflowId: z.string().min(1), blockId: z.string().min(1), }) export const webhookProviderConfigSchema = z.record(z.string(), z.unknown()) export const webhookDataSchema = z .object({ id: z.string(), path: z.string().optional(), providerConfig: webhookProviderConfigSchema.optional(), isActive: z.boolean().optional(), }) .passthrough() export type WebhookData = z.output export const webhookListItemSchema = z.object({ webhook: webhookDataSchema, workflow: z .object({ id: z.string(), name: z.string(), }) .optional(), }) export type WebhookListItem = z.output export const webhookUpsertBodySchema = z .object({ workflowId: z.string().optional(), path: z.string().optional(), provider: z.string().optional(), providerConfig: webhookProviderConfigSchema.optional(), blockId: z.string().optional(), }) .strict() export type WebhookUpsertBody = z.input export const webhookIdParamsSchema = z.object({ id: z.string().min(1), }) export const webhookPatchBodySchema = z .object({ isActive: z.boolean().optional(), failedCount: z .number({ error: 'failedCount must be a number' }) .finite('failedCount must be a valid number') .int('failedCount must be an integer') .min(0, 'failedCount must be at least 0') .optional(), }) .strict() export type WebhookPatchBody = z.input export const webhookPollingParamsSchema = z.object({ provider: z.string().min(1), }) export const webhookTriggerParamsSchema = z.object({ path: z.string().min(1), }) export const webhookSvixHeadersSchema = z.object({ 'svix-id': z.string().min(1), 'svix-timestamp': z.string().min(1), 'svix-signature': z.string().min(1), }) export const agentMailEnvelopeSchema = z .object({ event_type: z.string(), message: z.unknown().optional(), }) .passthrough() const agentMailAttachmentSchema = z .object({ attachment_id: z.string(), filename: z.string(), content_type: z.string(), size: z.number(), inline: z.boolean().optional(), }) .passthrough() export const agentMailMessageSchema = z .object({ message_id: z.string(), thread_id: z.string(), inbox_id: z.string(), organization_id: z.string().optional(), from: z.string(), to: z.array(z.string()), cc: z.array(z.string()).optional(), bcc: z.array(z.string()).optional(), reply_to: z.array(z.string()).optional(), subject: z.string().optional(), preview: z.string().optional(), text: z.string().nullable().optional(), html: z.string().nullable().optional(), attachments: z.array(agentMailAttachmentSchema).optional(), in_reply_to: z.string().optional(), references: z.array(z.string()).optional(), labels: z.array(z.string()).optional(), sort_key: z.string().optional(), updated_at: z.string().optional(), created_at: z.string(), }) .passthrough() export const listWebhooksByBlockResponseSchema = z.object({ webhooks: z.array(webhookListItemSchema), }) export type ListWebhooksByBlockResponse = z.output const listWebhooksResponseSchema = z.object({ webhooks: z.array(webhookListItemSchema), }) const upsertWebhookResponseSchema = z.object({ webhook: webhookDataSchema, }) const getWebhookResponseSchema = z.object({ webhook: webhookListItemSchema, }) const updateWebhookResponseSchema = z.object({ webhook: webhookDataSchema, }) const deleteWebhookResponseSchema = z.object({ success: z.literal(true), }) const webhookPollingResponseSchema = z .object({ success: z.boolean(), message: z.string(), requestId: z.string(), status: z.enum(['skip', 'completed']).optional(), total: z.number().optional(), successful: z.number().optional(), failed: z.number().optional(), error: z.string().optional(), }) .passthrough() export const listWebhooksByBlockContract = defineRouteContract({ method: 'GET', path: '/api/webhooks', query: webhooksByBlockQuerySchema, response: { mode: 'json', schema: listWebhooksByBlockResponseSchema, }, }) export const listWebhooksContract = defineRouteContract({ method: 'GET', path: '/api/webhooks', query: webhooksRouteQuerySchema, response: { mode: 'json', schema: listWebhooksResponseSchema, }, }) export const upsertWebhookContract = defineRouteContract({ method: 'POST', path: '/api/webhooks', body: webhookUpsertBodySchema, response: { mode: 'json', schema: upsertWebhookResponseSchema, }, }) export const getWebhookContract = defineRouteContract({ method: 'GET', path: '/api/webhooks/[id]', params: webhookIdParamsSchema, response: { mode: 'json', schema: getWebhookResponseSchema, }, }) export const updateWebhookContract = defineRouteContract({ method: 'PATCH', path: '/api/webhooks/[id]', params: webhookIdParamsSchema, body: webhookPatchBodySchema, response: { mode: 'json', schema: updateWebhookResponseSchema, }, }) export const deleteWebhookContract = defineRouteContract({ method: 'DELETE', path: '/api/webhooks/[id]', params: webhookIdParamsSchema, response: { mode: 'json', schema: deleteWebhookResponseSchema, }, }) export const webhookPollingContract = defineRouteContract({ method: 'GET', path: '/api/webhooks/poll/[provider]', params: webhookPollingParamsSchema, response: { mode: 'json', schema: webhookPollingResponseSchema, }, }) /** * Webhook trigger endpoints proxy provider responses back to the caller. The * payload shape varies per provider (challenge text, queued execution result, * pre-deployment verification, etc.) so the response is genuinely unbounded * and stays as `z.unknown()`. */ export const webhookTriggerGetContract = defineRouteContract({ method: 'GET', path: '/api/webhooks/trigger/[path]', params: webhookTriggerParamsSchema, response: { mode: 'json', // untyped-response: webhook trigger forwards arbitrary provider verification or workflow execution payloads schema: z.unknown(), }, }) export const webhookTriggerPostContract = defineRouteContract({ method: 'POST', path: '/api/webhooks/trigger/[path]', params: webhookTriggerParamsSchema, response: { mode: 'json', // untyped-response: webhook trigger forwards arbitrary provider challenge or workflow execution payloads schema: z.unknown(), }, }) /** * TikTok app-level webhook ingress. Signature is verified from the raw body * before this schema runs; `content` remains a JSON string per TikTok docs. */ export const tiktokWebhookEnvelopeSchema = z.object({ client_key: z.string().min(1).max(255), event: z.string().min(1).max(255), create_time: z.number().int().nonnegative(), user_openid: z.string().min(1).max(255), content: z.string().max(1_000_000), }) export type TikTokWebhookEnvelope = z.input export const tiktokWebhookHeadersSchema = z.object({ 'tiktok-signature': z.string().min(1), }) export const tiktokWebhookResponseSchema = z.union([ z.object({ ok: z.literal(true) }), z.object({ error: z.string().min(1) }), ]) export const tiktokWebhookContract = defineRouteContract({ method: 'POST', path: '/api/webhooks/tiktok', headers: tiktokWebhookHeadersSchema, // Body is validated after HMAC verification against the raw payload. body: tiktokWebhookEnvelopeSchema, response: { mode: 'json', schema: tiktokWebhookResponseSchema, }, })