Files
zeekay 3578254c60 Tie a webhook's eSign identity to its team as well as its URL, and follow no redirect with it
HANZO_WEBHOOKS entries are <team url>=<url>: another team pointing at a listed URL is posted as a stranger. A call made as eSign does not follow a redirect, so the token reaches the listed URL and nowhere else.
2026-10-20 18:21:45 -07:00

200 lines
5.6 KiB
TypeScript

// esign ZAP handlers — webhook router (8/8 procedures).
//
// Verbatim tRPC procedure bodies rewired to (ctx, raw). Input is validated by
// the SAME Zod schema the tRPC procedure used; server-only functions and the
// per-route exported helpers (findWebhookCalls / resendWebhookCallRoute body)
// are reused unchanged. Inline-bodied procedures (getTeamWebhooks,
// getWebhookById, createWebhook, deleteWebhook, editWebhook, testWebhook) plus
// the two file-backed routes (calls.find / calls.resend) have their bodies
// ported verbatim. Route keys mirror the tRPC nested router shape (see
// webhook-router.zap).
import { WebhookCallStatus } from '@prisma/client';
import { TEAM_MEMBER_ROLE_PERMISSIONS_MAP } from '@hanzo/esign-lib/constants/teams';
import { AppError, AppErrorCode } from '@hanzo/esign-lib/errors/app-error';
import { createWebhook } from '@hanzo/esign-lib/server-only/webhooks/create-webhook';
import { deleteWebhookById } from '@hanzo/esign-lib/server-only/webhooks/delete-webhook-by-id';
import { editWebhook } from '@hanzo/esign-lib/server-only/webhooks/edit-webhook';
import { getWebhookById } from '@hanzo/esign-lib/server-only/webhooks/get-webhook-by-id';
import { getWebhooksByTeamId } from '@hanzo/esign-lib/server-only/webhooks/get-webhooks-by-team-id';
import { sendWebhook } from '@hanzo/esign-lib/server-only/webhooks/send';
import { triggerTestWebhook } from '@hanzo/esign-lib/server-only/webhooks/trigger-test-webhook';
import { buildTeamWhereQuery } from '@hanzo/esign-lib/utils/teams';
import { prisma } from '@hanzo/esign-prisma';
import { findWebhookCalls } from '../../../server/webhook-router/find-webhook-calls';
import { ZFindWebhookCallsRequestSchema } from '../../../server/webhook-router/find-webhook-calls.types';
import { ZResendWebhookCallRequestSchema } from '../../../server/webhook-router/resend-webhook-call.types';
import {
ZCreateWebhookRequestSchema,
ZDeleteWebhookRequestSchema,
ZEditWebhookRequestSchema,
ZGetWebhookByIdRequestSchema,
ZTriggerTestWebhookRequestSchema,
} from '../../../server/webhook-router/schema';
import type { ZapContext } from '../context';
import type { ZapRouteMap } from '../dispatch';
export const webhookRoutes: ZapRouteMap = {
'webhook.calls.find': async (ctx: ZapContext, raw) => {
const { webhookId, page, perPage, status, query, events } =
ZFindWebhookCallsRequestSchema.parse(raw);
ctx.logger.info({
input: { webhookId, status },
});
return await findWebhookCalls({
userId: ctx.user.id,
teamId: ctx.teamId,
webhookId,
page,
perPage,
status,
query,
events,
});
},
'webhook.calls.resend': async (ctx: ZapContext, raw) => {
const { teamId, user } = ctx;
const { webhookId, webhookCallId } = ZResendWebhookCallRequestSchema.parse(raw);
ctx.logger.info({
input: { webhookId, webhookCallId },
});
const webhookCall = await prisma.webhookCall.findFirst({
where: {
id: webhookCallId,
webhook: {
id: webhookId,
team: buildTeamWhereQuery({
teamId,
userId: user.id,
roles: TEAM_MEMBER_ROLE_PERMISSIONS_MAP.MANAGE_TEAM,
}),
},
},
include: {
webhook: true,
},
});
if (!webhookCall) {
throw new AppError(AppErrorCode.NOT_FOUND);
}
const { webhook } = webhookCall;
const sent = await sendWebhook({
url: webhookCall.url,
body: webhookCall.requestBody,
secret: webhook.secret,
teamId: webhook.teamId,
});
return await prisma.webhookCall.update({
where: {
id: webhookCall.id,
},
data: {
status: sent.ok ? WebhookCallStatus.SUCCESS : WebhookCallStatus.FAILED,
responseCode: sent.status,
responseBody: sent.responseBody,
responseHeaders: sent.responseHeaders,
},
});
},
'webhook.getTeamWebhooks': async (ctx: ZapContext) => {
ctx.logger.info({
input: {
teamId: ctx.teamId,
},
});
return await getWebhooksByTeamId(ctx.teamId, ctx.user.id);
},
'webhook.getWebhookById': async (ctx: ZapContext, raw) => {
const { id } = ZGetWebhookByIdRequestSchema.parse(raw);
ctx.logger.info({
input: {
id,
},
});
return await getWebhookById({
id,
userId: ctx.user.id,
teamId: ctx.teamId,
});
},
'webhook.createWebhook': async (ctx: ZapContext, raw) => {
const { enabled, eventTriggers, secret, webhookUrl } = ZCreateWebhookRequestSchema.parse(raw);
return await createWebhook({
enabled,
secret,
webhookUrl,
eventTriggers,
teamId: ctx.teamId,
userId: ctx.user.id,
});
},
'webhook.deleteWebhook': async (ctx: ZapContext, raw) => {
const { id } = ZDeleteWebhookRequestSchema.parse(raw);
ctx.logger.info({
input: {
id,
},
});
return await deleteWebhookById({
id,
teamId: ctx.teamId,
userId: ctx.user.id,
});
},
'webhook.editWebhook': async (ctx: ZapContext, raw) => {
const { id, ...data } = ZEditWebhookRequestSchema.parse(raw);
ctx.logger.info({
input: {
id,
},
});
return await editWebhook({
id,
data,
userId: ctx.user.id,
teamId: ctx.teamId,
});
},
'webhook.testWebhook': async (ctx: ZapContext, raw) => {
const { id, event } = ZTriggerTestWebhookRequestSchema.parse(raw);
ctx.logger.info({
input: {
id,
event,
},
});
return await triggerTestWebhook({
id,
event,
userId: ctx.user.id,
teamId: ctx.teamId,
});
},
};