import { type Context, Hono } from 'hono' import * as z from 'zod/mini' import type * as App from '../../../App.js' import * as Db from '../../../db/Db.js' import * as Auth from '../../../internal/Auth.js' import * as OpenApi from '../../../internal/OpenApi.js' import * as Response from '../../../internal/Response.js' import * as WebhookDestination from '../../../internal/WebhookDestination.js' import * as Webhooks from '../../../internal/Webhooks.js' import * as DataWebhooks from '../../data/routes/webhooks.js' import type { Environment } from '../App.js' const defaultMaxPerOwner = 100 const redactedBetterstackToken = '[redacted]' const redactedSlackUrl = 'https://hooks.slack.com/…' /** OpenAPI schemas owned by console webhook management. */ export namespace schema { /** Public webhook subscription representation. */ export const Subscription = OpenApi.component( z .object({ ...DataWebhooks.schema.Subscription.shape, context: DataWebhooks.schema.Subscription.shape.context.check( z.meta({ examples: [{ title: 'Production transfers' }] }), ), destination: DataWebhooks.schema.Subscription.shape.destination.check( z.meta({ examples: [{ type: 'url', url: 'https://example.com/webhooks' }] }), ), }) .check(z.describe('An organization webhook subscription.')), 'OrganizationWebhook', ) /** Webhook delivery data returned in collection rows. */ export const Delivery = OpenApi.component( DataWebhooks.schema.Delivery, 'OrganizationWebhookDeliverySummary', ) /** Schemas for creating an organization webhook. */ export namespace createWebhook { const Environment = z .enum(['production', 'sandbox']) .check( z.describe('API-key environment whose private funding resources are delivered.'), z.meta({ examples: ['production'] }), ) /** Organization webhook creation requires an explicit network. */ export const Body = OpenApi.component( z .intersection( DataWebhooks.schema.createWebhook.Body, z.object({ chainId: DataWebhooks.schema.Subscription.shape.chainId, environment: z.optional(Environment), }), ) .check( z.refine( (body) => !DataWebhooks.isFundingEventType(body.eventType) || body.environment !== undefined, { error: '`environment` is required for funding subscriptions.' }, ), z.describe('Details for creating an organization webhook subscription.'), ), 'CreateOrganizationWebhookRequest', ) const secret = DataWebhooks.schema.createWebhook.Response.shape.secret const urlDestination = z .object({ type: z .literal('url') .check(z.describe('HTTPS destination.'), z.meta({ examples: ['url'] })), url: z .url() .check( z.describe('Your HTTPS endpoint where Tempo sends signed event POSTs.'), z.meta({ examples: ['https://example.com/webhooks'] }), ), }) .check( z.describe('HTTPS webhook destination.'), z.meta({ examples: [{ type: 'url', url: 'https://example.com/webhooks' }] }), ) const slackDestination = z .object({ type: z .literal('slack') .check(z.describe('Slack destination.'), z.meta({ examples: ['slack'] })), url: z .literal(redactedSlackUrl) .check( z.describe('Redacted Slack webhook URL.'), z.meta({ examples: [redactedSlackUrl] }), ), }) .check( z.describe('Slack webhook destination with its URL redacted.'), z.meta({ examples: [{ type: 'slack', url: redactedSlackUrl }] }), ) const betterstackDestination = z .object({ token: z .literal(redactedBetterstackToken) .check( z.describe('Redacted Better Stack source token.'), z.meta({ examples: [redactedBetterstackToken] }), ), type: z .literal('betterstack') .check(z.describe('Better Stack destination.'), z.meta({ examples: ['betterstack'] })), url: z .url() .check( z.describe('Your Better Stack source ingest host URL.'), z.meta({ examples: ['https://s1234567.eu-nbg-2.betterstackdata.com'] }), ), }) .check( z.describe('Better Stack destination with its token redacted.'), z.meta({ examples: [ { token: redactedBetterstackToken, type: 'betterstack', url: 'https://s1234567.eu-nbg-2.betterstackdata.com', }, ], }), ) const UrlResponse = OpenApi.component( z .object({ ...Subscription.shape, destination: urlDestination, secret }) .check(z.describe('Created HTTPS webhook with its one-time signing secret.')), 'CreateOrganizationWebhookUrlResponse', ) const SlackResponse = OpenApi.component( z .object({ ...Subscription.shape, destination: slackDestination }) .check(z.describe('Created Slack webhook with its destination redacted.')), 'CreateOrganizationWebhookSlackResponse', ) const BetterstackResponse = OpenApi.component( z .object({ ...Subscription.shape, destination: betterstackDestination }) .check(z.describe('Created Better Stack webhook with its credentials redacted.')), 'CreateOrganizationWebhookBetterstackResponse', ) /** Created webhook with transport credentials redacted. */ export const Response = OpenApi.component( z .union([UrlResponse, SlackResponse, BetterstackResponse]) .check( z.describe( 'The new webhook subscription. HTTPS endpoints include their one-time signing secret.', ), ), 'CreateOrganizationWebhookResponse', ) } /** Organization webhook collection parameters. */ export const Params = z .object({ orgId: z .string() .check( z.describe('Organization id (`org_…`).'), z.meta({ examples: ['org_1a2b3c4d5e6f7g8h9j0k1m2n'] }), ), }) .check(z.describe("Path parameters for an organization's webhooks.")) /** Parameters addressing one organization webhook. */ export const WebhookParams = z .object({ id: z .string() .check( z.describe('Webhook subscription id (`wh_…`).'), z.meta({ examples: ['wh_001718668800000_2ZPE2gvateYEQ0dQslgvkhjx'] }), ), orgId: Params.shape.orgId, }) .check(z.describe("Path parameters for one organization's webhook.")) /** Schemas for listing an organization webhook's delivery attempts. */ export namespace listWebhookDeliveries { export const Params = WebhookParams export const Query = DataWebhooks.schema.listWebhookDeliveries.Query export const Response = OpenApi.component( z .object({ data: z .array(Delivery) .check( z.describe('Webhook delivery attempts on this page.'), z.meta({ examples: [[]] }), ), meta: DataWebhooks.schema.listWebhookDeliveries.Response.shape.meta.check( z.meta({ examples: [{ totalCount: 1, totalCountCapped: false }] }), ), nextCursor: DataWebhooks.schema.listWebhookDeliveries.Response.shape.nextCursor, }) .check(z.describe("A page of an organization's webhook delivery attempts.")), 'OrganizationWebhookDeliveryList', ) } const DeliveryParams = z .object({ deliveryId: DataWebhooks.schema.retryWebhookDelivery.Params.shape.deliveryId, id: WebhookParams.shape.id, orgId: WebhookParams.shape.orgId, }) .check(z.describe("Path parameters for one organization's webhook delivery.")) /** Schemas for loading one organization webhook delivery. */ export namespace getWebhookDelivery { export const Params = DeliveryParams export const Response = OpenApi.component( z .object({ ...Delivery.shape, envelope: DataWebhooks.schema.Envelope, }) .check(z.describe('A webhook delivery attempt with the exact JSON payload Tempo sent.')), 'OrganizationWebhookDelivery', ) } /** Schemas for replaying one organization webhook delivery. */ export namespace retryWebhookDelivery { export const Params = DeliveryParams export const Response = OpenApi.component( DataWebhooks.schema.retryWebhookDelivery.Response, 'RetryOrganizationWebhookDeliveryResponse', ) } /** Non-paginated organization webhook list. */ export const ListResponse = OpenApi.component( z .object({ data: z .array(Subscription) .check( z.describe("The organization's webhooks, newest first."), z.meta({ examples: [[]] }), ), }) .check(z.describe("An organization's webhook subscriptions.")), 'OrganizationWebhookList', ) /** Lifecycle state editable from the console. */ export const UpdateBody = OpenApi.component( z .object({ status: DataWebhooks.schema.Status }) .check(z.describe('New delivery state for a webhook subscription.')), 'UpdateOrganizationWebhookRequest', ) /** Webhook deletion confirmation. */ export const DeleteResponse = OpenApi.component( z .object({ id: z .string() .check( z.describe('Deleted webhook subscription id.'), z.meta({ examples: ['wh_001718668800000_2ZPE2gvateYEQ0dQslgvkhjx'] }), ), }) .check(z.describe('Confirmation that a webhook subscription was deleted.')), 'DeleteOrganizationWebhookResponse', ) } const bodyValidation = { code: 'body_invalid', message: 'Check the request body and try again.', } as const const paramValidation = { code: 'param_invalid', message: 'Check the path parameters and try again.', } as const /** Mounts session-authenticated organization webhook management operations. */ export function webhooks(options: webhooks.Options = {}) { const hidden = !options.webhook return new Hono() .post( '/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/webhooks', Auth.policy({ session: true }), Auth.ensureOrg({ role: 'admin' }), OpenApi.validate('param', schema.Params, paramValidation), OpenApi.validate('json', schema.createWebhook.Body, bodyValidation), OpenApi.describeRoute({ hide: hidden, operationId: 'createOrgWebhook', responses: OpenApi.responses({ errors: { 400: { codes: [ 'body_invalid', 'chain_id_unsupported', 'event_type_unsupported', 'filters_invalid', 'param_invalid', 'url_invalid', ], description: 'The request or destination is invalid, or the chain is not polled.', }, 403: { codes: ['forbidden', 'limit_exceeded'], description: 'Admin access is required and the organization must be under its limit.', }, 404: { codes: ['organization_not_found', 'webhooks_not_enabled'], description: 'No accessible organization or webhook capability was found.', }, }, success: { description: 'Created webhook. HTTPS endpoints include a one-time signing secret.', schema: schema.createWebhook.Response, }, }), summary: 'Create webhook', tags: ['Webhooks'], }), async (c) => { if (Auth.narrowAccess) return Auth.superAdminAccessError(c) if (Auth.narrowScope) return Auth.ensureOrgError(c) if (Auth.narrowOrgRole) return Auth.ensureOrgRoleError(c) if (OpenApi.narrowValidation) return OpenApi.validationError(c, paramValidation) if (OpenApi.narrowValidation) return OpenApi.validationError(c, bodyValidation) if (!options.webhook) return notEnabled(c) const body = c.req.valid('json') if ( DataWebhooks.isFundingEventType(body.eventType) && !options.webhook.applicationEventTypes?.includes(body.eventType) ) return Response.error(c, { code: 'event_type_unsupported', message: 'Webhook event type is not available in this deployment', status: 400, }) const chainId = body.chainId const supportedChainIds = new Set(options.webhook.supportedChainIds) if (!supportedChainIds.has(chainId)) return Response.unsupportedChainId(c, chainId, supportedChainIds) try { // Preserve destination errors before the creation-time RPC dependency. WebhookDestination.assertDestination(body.destination) const db = Db.get(c.get('db')) const maxPerOwner = options.webhook.maxPerOwner ?? defaultMaxPerOwner const owner_ = owner(Auth.org(c).id) if ((await Webhooks.countSubscriptions(db, owner_)) >= maxPerOwner) throw new Webhooks.LimitExceededError(maxPerOwner) const startBlockNumber = DataWebhooks.isFundingEventType(body.eventType) ? undefined : Number(await c.get('getClient')(chainId).getBlockNumber()) const subscription = await Webhooks.createSubscription( db, { chainId, ...(body.context === undefined ? {} : { context: body.context }), destination: body.destination, ...(DataWebhooks.isFundingEventType(body.eventType) && body.environment !== undefined ? { environment: body.environment } : {}), eventType: body.eventType, filters: body.filters, owner: owner_, }, { maxPerOwner, ...(startBlockNumber === undefined ? {} : { startBlockNumber }), }, ) return c.json( Response.validated(schema.createWebhook.Response, created(subscription)), 200, ) } catch (cause) { return mutationError(c, cause) } }, ) .get( '/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/webhooks', Auth.policy({ session: true }), Auth.ensureOrg(), OpenApi.validate('param', schema.Params, paramValidation), OpenApi.describeRoute({ hide: hidden, operationId: 'listOrgWebhooks', responses: OpenApi.responses({ errors: { 400: { codes: ['param_invalid'], description: 'The path parameters are invalid.' }, 404: { codes: ['organization_not_found', 'webhooks_not_enabled'], description: 'No accessible organization or webhook capability was found.', }, }, success: { description: 'Organization webhooks.', schema: schema.ListResponse }, }), summary: 'List webhooks', tags: ['Webhooks'], }), async (c) => { if (Auth.narrowAccess) return Auth.superAdminAccessError(c) if (Auth.narrowScope) return Auth.ensureOrgError(c) if (OpenApi.narrowValidation) return OpenApi.validationError(c, paramValidation) if (!options.webhook) return notEnabled(c) try { const records = await Webhooks.listSubscriptions( Db.get(c.get('db')), owner(Auth.org(c).id), ) return c.json( Response.validated(schema.ListResponse, { data: records.map(publicSubscription) }), 200, ) } catch (cause) { return Response.upstream(c, cause) } }, ) .get( '/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/webhooks/:id{wh_[A-Za-z0-9_-]+}/deliveries', Auth.policy({ session: true }), Auth.ensureOrg(), OpenApi.validate('param', schema.listWebhookDeliveries.Params, paramValidation), OpenApi.validate('query', schema.listWebhookDeliveries.Query, { code: 'query_invalid', message: 'Check the query parameters and try again.', }), OpenApi.describeRoute({ hide: hidden, operationId: 'listOrgWebhookDeliveries', responses: OpenApi.responses({ errors: { 400: { codes: ['param_invalid', 'query_invalid'], description: 'The path or query parameters are invalid.', }, 404: { codes: ['organization_not_found', 'webhook_not_found', 'webhooks_not_enabled'], description: 'No accessible organization, webhook, or webhook capability was found.', }, }, success: { description: 'A newest-first page of webhook delivery attempts.', schema: schema.listWebhookDeliveries.Response, }, }), summary: 'List webhook deliveries', tags: ['Webhooks'], }), async (c) => { if (Auth.narrowAccess) return Auth.superAdminAccessError(c) if (Auth.narrowScope) return Auth.ensureOrgError(c) if (OpenApi.narrowValidation) return OpenApi.validationError(c, paramValidation) if (OpenApi.narrowValidation) return OpenApi.validationError(c, { code: 'query_invalid', message: 'Check the query parameters and try again.', }) if (!options.webhook) return notEnabled(c) const db = Db.get(c.get('db')) const { id } = c.req.valid('param') const { cursor, include, limit, page } = c.req.valid('query') try { const subscription = await Webhooks.getSubscription(db, owner(Auth.org(c).id), id) if (!subscription) return notFound(c) const countPromise = include.includes('totalCount') ? Webhooks.countDeliveries(db, id).catch(() => undefined) : undefined const rows = await Webhooks.listDeliveries(db, id, { cursor, limit: limit + 1, offset: page !== undefined && page > 1 ? (page - 1) * limit : undefined, }) const hasMore = rows.length > limit const data = hasMore ? rows.slice(0, limit) : rows const nextCursor = hasMore ? (data[data.length - 1]?.id ?? null) : null const totalCount = countPromise ? await countPromise : undefined return c.json( Response.validated(schema.listWebhookDeliveries.Response, { data: data.map((delivery) => publicDelivery(delivery, subscription)), ...(totalCount === undefined ? {} : { meta: { totalCountCapped: false, totalCount } }), nextCursor, }), 200, ) } catch (cause) { return Response.upstream(c, cause) } }, ) .get( '/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/webhooks/:id{wh_[A-Za-z0-9_-]+}/deliveries/:deliveryId', Auth.policy({ session: true }), Auth.ensureOrg(), OpenApi.validate('param', schema.getWebhookDelivery.Params, paramValidation), OpenApi.describeRoute({ hide: hidden, operationId: 'getOrgWebhookDelivery', responses: OpenApi.responses({ errors: { 400: { codes: ['param_invalid'], description: 'The path parameters are invalid.' }, 404: { codes: [ 'delivery_not_found', 'organization_not_found', 'webhook_not_found', 'webhooks_not_enabled', ], description: 'No accessible organization, webhook, delivery, or webhook capability was found.', }, }, success: { description: 'The delivery attempt and exact JSON payload Tempo sent.', schema: schema.getWebhookDelivery.Response, }, }), summary: 'Get webhook delivery', tags: ['Webhooks'], }), async (c) => { if (Auth.narrowAccess) return Auth.superAdminAccessError(c) if (Auth.narrowScope) return Auth.ensureOrgError(c) if (OpenApi.narrowValidation) return OpenApi.validationError(c, paramValidation) if (!options.webhook) return notEnabled(c) const db = Db.get(c.get('db')) const { deliveryId, id } = c.req.valid('param') try { const subscription = await Webhooks.getSubscription(db, owner(Auth.org(c).id), id) if (!subscription) return notFound(c) const delivery = await Webhooks.getDelivery(db, id, deliveryId) if (!delivery) return deliveryNotFound(c) return c.json( Response.validated(schema.getWebhookDelivery.Response, { ...publicDelivery(delivery, subscription), envelope: delivery.envelope, }), 200, ) } catch (cause) { return Response.upstream(c, cause) } }, ) .post( '/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/webhooks/:id{wh_[A-Za-z0-9_-]+}/deliveries/:deliveryId/retry', Auth.policy({ session: true }), Auth.ensureOrg({ role: 'admin' }), OpenApi.validate('param', schema.retryWebhookDelivery.Params, paramValidation), OpenApi.describeRoute({ hide: hidden, operationId: 'retryOrgWebhookDelivery', responses: OpenApi.responses({ errors: { 400: { codes: ['param_invalid'], description: 'The path parameters are invalid.' }, 403: { codes: ['forbidden'], description: 'Organization admin access is required.' }, 404: { codes: [ 'delivery_not_found', 'organization_not_found', 'webhook_not_found', 'webhooks_not_enabled', ], description: 'No accessible organization, webhook, delivery, or webhook capability was found.', }, }, success: { description: 'Result of replaying the webhook delivery.', schema: schema.retryWebhookDelivery.Response, }, }), summary: 'Retry webhook delivery', tags: ['Webhooks'], }), async (c) => { if (Auth.narrowAccess) return Auth.superAdminAccessError(c) if (Auth.narrowScope) return Auth.ensureOrgError(c) if (Auth.narrowOrgRole) return Auth.ensureOrgRoleError(c) if (OpenApi.narrowValidation) return OpenApi.validationError(c, paramValidation) if (!options.webhook) return notEnabled(c) const db = Db.get(c.get('db')) const { deliveryId, id } = c.req.valid('param') try { const subscription = await Webhooks.getSubscription(db, owner(Auth.org(c).id), id) if (!subscription) return notFound(c) const delivery = await Webhooks.getDelivery(db, id, deliveryId) if (!delivery) return deliveryNotFound(c) const result = await Webhooks.deliverAndRecord(db, subscription, delivery.envelope, { trigger: 'manual_retry', }) return c.json( Response.validated(schema.retryWebhookDelivery.Response, { delivered: result.ok, eventId: delivery.eventId, ...(result.error === undefined ? {} : { error: result.error }), ...(result.durationMs === undefined ? {} : { responseMs: result.durationMs }), ...(result.status === undefined ? {} : { responseStatus: result.status }), }), 200, ) } catch (cause) { return Response.upstream(c, cause) } }, ) .patch( '/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/webhooks/:id{wh_[A-Za-z0-9_-]+}', Auth.policy({ session: true }), Auth.ensureOrg({ role: 'admin' }), OpenApi.validate('param', schema.WebhookParams, paramValidation), OpenApi.validate('json', schema.UpdateBody, bodyValidation), OpenApi.describeRoute({ hide: hidden, operationId: 'updateOrgWebhook', responses: OpenApi.responses({ errors: { 400: { codes: ['body_invalid', 'param_invalid'], description: 'The path parameters or request body are invalid.', }, 403: { codes: ['forbidden'], description: 'Organization admin access is required.' }, 404: { codes: ['organization_not_found', 'webhook_not_found', 'webhooks_not_enabled'], description: 'No accessible organization, webhook, or webhook capability was found.', }, }, success: { description: 'Updated webhook.', schema: schema.Subscription, }, }), summary: 'Update webhook', tags: ['Webhooks'], }), async (c) => { if (Auth.narrowAccess) return Auth.superAdminAccessError(c) if (Auth.narrowScope) return Auth.ensureOrgError(c) if (Auth.narrowOrgRole) return Auth.ensureOrgRoleError(c) if (OpenApi.narrowValidation) return OpenApi.validationError(c, paramValidation) if (OpenApi.narrowValidation) return OpenApi.validationError(c, bodyValidation) if (!options.webhook) return notEnabled(c) try { const subscription = await Webhooks.updateSubscription( Db.get(c.get('db')), owner(Auth.org(c).id), c.req.valid('param').id, c.req.valid('json'), ) if (!subscription) return notFound(c) return c.json( Response.validated(schema.Subscription, publicSubscription(subscription)), 200, ) } catch (cause) { return Response.upstream(c, cause) } }, ) .delete( '/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/webhooks/:id{wh_[A-Za-z0-9_-]+}', Auth.policy({ session: true }), Auth.ensureOrg({ role: 'admin' }), OpenApi.validate('param', schema.WebhookParams, paramValidation), OpenApi.describeRoute({ hide: hidden, operationId: 'deleteOrgWebhook', responses: OpenApi.responses({ errors: { 400: { codes: ['param_invalid'], description: 'The path parameters are invalid.' }, 403: { codes: ['forbidden'], description: 'Organization admin access is required.' }, 404: { codes: ['organization_not_found', 'webhook_not_found', 'webhooks_not_enabled'], description: 'No accessible organization, webhook, or webhook capability was found.', }, }, success: { description: 'Deleted webhook.', schema: schema.DeleteResponse }, }), summary: 'Delete webhook', tags: ['Webhooks'], }), async (c) => { if (Auth.narrowAccess) return Auth.superAdminAccessError(c) if (Auth.narrowScope) return Auth.ensureOrgError(c) if (Auth.narrowOrgRole) return Auth.ensureOrgRoleError(c) if (OpenApi.narrowValidation) return OpenApi.validationError(c, paramValidation) if (!options.webhook) return notEnabled(c) try { const id = c.req.valid('param').id const removed = await Webhooks.deleteSubscription( Db.get(c.get('db')), owner(Auth.org(c).id), id, ) if (!removed) return notFound(c) return c.json(Response.validated(schema.DeleteResponse, { id }), 200) } catch (cause) { return Response.upstream(c, cause) } }, ) } export declare namespace webhooks { /** Options for organization webhook management routes. */ type Options = { /** Webhook capability shared with the data routes and host poller. */ webhook?: App.Webhook | undefined } } function owner(orgId: string): Webhooks.Owner { return { orgId, type: 'api_key' } } function publicSubscription(subscription: Webhooks.Subscription) { const destination = (() => { if (subscription.destination.type === 'slack') return { ...subscription.destination, url: redactedSlackUrl } if (subscription.destination.type === 'betterstack') return { ...subscription.destination, token: redactedBetterstackToken } return { ...subscription.destination, url: redactUrl(subscription.destination.url) } })() return { chainId: subscription.chainId, ...(subscription.context === undefined ? {} : { context: subscription.context }), createdAt: subscription.createdAt, destination, ...(DataWebhooks.isFundingEventType(subscription.eventType) && subscription.environment !== undefined ? { environment: subscription.environment } : {}), eventType: subscription.eventType, ...(subscription.expiresAt === undefined ? {} : { expiresAt: subscription.expiresAt }), failureCount: subscription.failureCount, filters: subscription.filters, id: subscription.id, ...(subscription.lastDeliveryAt === undefined ? {} : { lastDeliveryAt: subscription.lastDeliveryAt }), status: subscription.status, updatedAt: subscription.updatedAt, } } function created(subscription: Webhooks.Subscription) { const record = publicSubscription(subscription) if (subscription.destination.type !== 'url') return record return { ...record, destination: subscription.destination, secret: subscription.secret, } } function publicDelivery(delivery: Webhooks.Delivery, subscription: Webhooks.Subscription) { const { envelope: _, ...record } = delivery return { ...record, requestUrl: publicSubscription(subscription).destination.url, } } function redactUrl(input: string) { return `${new URL(input).origin}/…` } function mutationError(c: Context, cause: unknown) { if (cause instanceof Webhooks.InvalidUrlError) return Response.error(c, { code: 'url_invalid', message: cause.message, status: 400 }) if (cause instanceof Webhooks.InvalidFilterError) return Response.error(c, { code: 'filters_invalid', details: cause.details, message: cause.message, status: 400, }) if (cause instanceof Webhooks.LimitExceededError) return Response.error(c, { code: 'limit_exceeded', message: cause.message, status: 403 }) return Response.upstream(c, cause) } function notFound(c: Context) { return Response.error(c, { code: 'webhook_not_found', message: 'Webhook not found', status: 404, }) } function deliveryNotFound(c: Context) { return Response.error(c, { code: 'delivery_not_found', message: 'Delivery not found', status: 404, }) } function notEnabled(c: Context) { return Response.error(c, { code: 'webhooks_not_enabled', message: 'Webhooks are not enabled for this Tempo API deployment.', status: 404, }) }