refactor(api): extract webhook router support
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { z } from "zod";
|
||||
import { WEBHOOK_EVENTS } from "../lib/webhook-dispatcher.js";
|
||||
|
||||
export const webhookEventEnum = z.enum(WEBHOOK_EVENTS as unknown as [string, ...string[]]);
|
||||
|
||||
export const createWebhookInputSchema = z.object({
|
||||
name: z.string().min(1).max(200),
|
||||
url: z.string().url(),
|
||||
secret: z.string().optional(),
|
||||
events: z.array(webhookEventEnum).min(1),
|
||||
isActive: z.boolean().default(true),
|
||||
});
|
||||
|
||||
export const updateWebhookInputSchema = z.object({
|
||||
name: z.string().min(1).max(200).optional(),
|
||||
url: z.string().url().optional(),
|
||||
secret: z.string().nullish(),
|
||||
events: z.array(webhookEventEnum).min(1).optional(),
|
||||
isActive: z.boolean().optional(),
|
||||
});
|
||||
|
||||
type WebhookRecord = {
|
||||
id: string;
|
||||
name: string;
|
||||
url: string;
|
||||
secret?: string | null;
|
||||
events?: string[];
|
||||
isActive?: boolean;
|
||||
};
|
||||
|
||||
type WebhookDb = {
|
||||
webhook: {
|
||||
findUnique: (args: { where: { id: string } }) => Promise<WebhookRecord | null>;
|
||||
};
|
||||
};
|
||||
|
||||
export function buildWebhookCreateData(
|
||||
input: z.infer<typeof createWebhookInputSchema>,
|
||||
) {
|
||||
return {
|
||||
name: input.name,
|
||||
url: input.url,
|
||||
...(input.secret !== undefined ? { secret: input.secret } : {}),
|
||||
events: input.events,
|
||||
isActive: input.isActive,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildWebhookUpdateData(
|
||||
input: z.infer<typeof updateWebhookInputSchema>,
|
||||
) {
|
||||
return {
|
||||
...(input.name !== undefined ? { name: input.name } : {}),
|
||||
...(input.url !== undefined ? { url: input.url } : {}),
|
||||
...(input.secret !== undefined ? { secret: input.secret } : {}),
|
||||
...(input.events !== undefined ? { events: input.events } : {}),
|
||||
...(input.isActive !== undefined ? { isActive: input.isActive } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export async function loadWebhookOrThrow(
|
||||
db: WebhookDb,
|
||||
id: string,
|
||||
) {
|
||||
const webhook = await db.webhook.findUnique({ where: { id } });
|
||||
if (!webhook) {
|
||||
throw new TRPCError({ code: "NOT_FOUND", message: "Webhook not found" });
|
||||
}
|
||||
return webhook;
|
||||
}
|
||||
|
||||
export async function buildWebhookTestRequest(
|
||||
webhook: Pick<WebhookRecord, "id" | "name" | "secret">,
|
||||
timestamp = new Date().toISOString(),
|
||||
) {
|
||||
const payload = {
|
||||
event: "webhook.test",
|
||||
timestamp,
|
||||
payload: {
|
||||
webhookId: webhook.id,
|
||||
webhookName: webhook.name,
|
||||
message: "This is a test payload from CapaKraken.",
|
||||
},
|
||||
};
|
||||
|
||||
const body = JSON.stringify(payload);
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
"X-Webhook-Event": "webhook.test",
|
||||
};
|
||||
|
||||
if (webhook.secret) {
|
||||
const { createHmac } = await import("node:crypto");
|
||||
headers["X-Webhook-Signature"] = createHmac("sha256", webhook.secret)
|
||||
.update(body)
|
||||
.digest("hex");
|
||||
}
|
||||
|
||||
return { body, headers };
|
||||
}
|
||||
|
||||
export async function sendWebhookTestRequest(
|
||||
webhook: Pick<WebhookRecord, "id" | "name" | "url" | "secret">,
|
||||
options: {
|
||||
fetchImpl?: typeof fetch;
|
||||
timeoutMs?: number;
|
||||
timestamp?: string;
|
||||
} = {},
|
||||
): Promise<{ success: boolean; statusCode: number; statusText: string }> {
|
||||
const fetchImpl = options.fetchImpl ?? fetch;
|
||||
const timeoutMs = options.timeoutMs ?? 5_000;
|
||||
const { body, headers } = await buildWebhookTestRequest(webhook, options.timestamp);
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
||||
|
||||
try {
|
||||
const response = await fetchImpl(webhook.url, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body,
|
||||
signal: controller.signal,
|
||||
});
|
||||
return {
|
||||
success: response.ok,
|
||||
statusCode: response.status,
|
||||
statusText: response.statusText,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
statusCode: 0,
|
||||
statusText: error instanceof Error ? error.message : "Unknown error",
|
||||
};
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user