@open-mercato/webhooks 0.6.6-develop.5491.1.469e89d368 → 0.6.6-develop.5505.1.f08e81a6fe

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,2 +1,2 @@
1
- [build:webhooks] found 67 entry points
1
+ [build:webhooks] found 68 entry points
2
2
  [build:webhooks] built successfully
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { assertStaticallySafeWebhookUrl, UnsafeWebhookUrlError } from "../lib/url-safety.js";
3
+ import { isReservedWebhookCustomHeader } from "../lib/custom-headers.js";
3
4
  const safeWebhookUrl = z.string().url().superRefine((value, ctx) => {
4
5
  try {
5
6
  assertStaticallySafeWebhookUrl(value);
@@ -8,13 +9,24 @@ const safeWebhookUrl = z.string().url().superRefine((value, ctx) => {
8
9
  ctx.addIssue({ code: z.ZodIssueCode.custom, message });
9
10
  }
10
11
  });
12
+ const webhookCustomHeaders = z.record(z.string(), z.string()).superRefine((value, ctx) => {
13
+ for (const name of Object.keys(value)) {
14
+ if (isReservedWebhookCustomHeader(name)) {
15
+ ctx.addIssue({
16
+ code: z.ZodIssueCode.custom,
17
+ message: `Header "${name}" is reserved for webhook signing and cannot be overridden`,
18
+ path: [name]
19
+ });
20
+ }
21
+ }
22
+ });
11
23
  const webhookCreateSchema = z.object({
12
24
  name: z.string().min(1).max(255),
13
25
  description: z.string().max(1e3).optional().nullable(),
14
26
  url: safeWebhookUrl,
15
27
  subscribedEvents: z.array(z.string().min(1)).min(1),
16
28
  httpMethod: z.enum(["POST", "PUT", "PATCH"]).default("POST"),
17
- customHeaders: z.record(z.string(), z.string()).optional().nullable(),
29
+ customHeaders: webhookCustomHeaders.optional().nullable(),
18
30
  deliveryStrategy: z.literal("http").default("http"),
19
31
  strategyConfig: z.record(z.string(), z.unknown()).optional().nullable(),
20
32
  maxRetries: z.number().int().min(0).max(30).default(10),
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/webhooks/data/validators.ts"],
4
- "sourcesContent": ["import { z } from 'zod'\nimport { assertStaticallySafeWebhookUrl, UnsafeWebhookUrlError } from '../lib/url-safety'\n\nconst safeWebhookUrl = z.string().url().superRefine((value, ctx) => {\n try {\n assertStaticallySafeWebhookUrl(value)\n } catch (error) {\n const message = error instanceof UnsafeWebhookUrlError\n ? error.message\n : 'Webhook URL is not allowed'\n ctx.addIssue({ code: z.ZodIssueCode.custom, message })\n }\n})\n\nexport const webhookCreateSchema = z.object({\n name: z.string().min(1).max(255),\n description: z.string().max(1000).optional().nullable(),\n url: safeWebhookUrl,\n subscribedEvents: z.array(z.string().min(1)).min(1),\n httpMethod: z.enum(['POST', 'PUT', 'PATCH'] as const).default('POST'),\n customHeaders: z.record(z.string(), z.string()).optional().nullable(),\n deliveryStrategy: z.literal('http').default('http'),\n strategyConfig: z.record(z.string(), z.unknown()).optional().nullable(),\n maxRetries: z.number().int().min(0).max(30).default(10),\n timeoutMs: z.number().int().min(1000).max(60000).default(15000),\n rateLimitPerMinute: z.number().int().min(0).max(10000).default(0),\n autoDisableThreshold: z.number().int().min(0).max(1000).default(100),\n integrationId: z.string().optional().nullable(),\n})\n\nexport type WebhookCreateInput = z.infer<typeof webhookCreateSchema>\n\nexport const webhookUpdateSchema = webhookCreateSchema.partial().extend({\n isActive: z.boolean().optional(),\n})\n\nexport type WebhookUpdateInput = z.infer<typeof webhookUpdateSchema>\n\nexport const webhookListQuerySchema = z.object({\n page: z.string().optional(),\n pageSize: z.string().optional(),\n search: z.string().optional(),\n isActive: z.string().optional(),\n})\n\nexport const webhookDeliveryQuerySchema = z.object({\n page: z.coerce.number().min(1).default(1),\n pageSize: z.coerce.number().min(1).max(100).default(50),\n webhookId: z.string().uuid().optional(),\n eventType: z.string().optional(),\n status: z.enum(['pending', 'sending', 'delivered', 'failed', 'expired'] as const).optional(),\n})\n"],
5
- "mappings": "AAAA,SAAS,SAAS;AAClB,SAAS,gCAAgC,6BAA6B;AAEtE,MAAM,iBAAiB,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY,CAAC,OAAO,QAAQ;AAClE,MAAI;AACF,mCAA+B,KAAK;AAAA,EACtC,SAAS,OAAO;AACd,UAAM,UAAU,iBAAiB,wBAC7B,MAAM,UACN;AACJ,QAAI,SAAS,EAAE,MAAM,EAAE,aAAa,QAAQ,QAAQ,CAAC;AAAA,EACvD;AACF,CAAC;AAEM,MAAM,sBAAsB,EAAE,OAAO;AAAA,EAC1C,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG;AAAA,EAC/B,aAAa,EAAE,OAAO,EAAE,IAAI,GAAI,EAAE,SAAS,EAAE,SAAS;AAAA,EACtD,KAAK;AAAA,EACL,kBAAkB,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC,EAAE,IAAI,CAAC;AAAA,EAClD,YAAY,EAAE,KAAK,CAAC,QAAQ,OAAO,OAAO,CAAU,EAAE,QAAQ,MAAM;AAAA,EACpE,eAAe,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,OAAO,CAAC,EAAE,SAAS,EAAE,SAAS;AAAA,EACpE,kBAAkB,EAAE,QAAQ,MAAM,EAAE,QAAQ,MAAM;AAAA,EAClD,gBAAgB,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,QAAQ,CAAC,EAAE,SAAS,EAAE,SAAS;AAAA,EACtE,YAAY,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,IAAI,EAAE,EAAE,QAAQ,EAAE;AAAA,EACtD,WAAW,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,GAAI,EAAE,IAAI,GAAK,EAAE,QAAQ,IAAK;AAAA,EAC9D,oBAAoB,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,IAAI,GAAK,EAAE,QAAQ,CAAC;AAAA,EAChE,sBAAsB,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,IAAI,GAAI,EAAE,QAAQ,GAAG;AAAA,EACnE,eAAe,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAChD,CAAC;AAIM,MAAM,sBAAsB,oBAAoB,QAAQ,EAAE,OAAO;AAAA,EACtE,UAAU,EAAE,QAAQ,EAAE,SAAS;AACjC,CAAC;AAIM,MAAM,yBAAyB,EAAE,OAAO;AAAA,EAC7C,MAAM,EAAE,OAAO,EAAE,SAAS;AAAA,EAC1B,UAAU,EAAE,OAAO,EAAE,SAAS;AAAA,EAC9B,QAAQ,EAAE,OAAO,EAAE,SAAS;AAAA,EAC5B,UAAU,EAAE,OAAO,EAAE,SAAS;AAChC,CAAC;AAEM,MAAM,6BAA6B,EAAE,OAAO;AAAA,EACjD,MAAM,EAAE,OAAO,OAAO,EAAE,IAAI,CAAC,EAAE,QAAQ,CAAC;AAAA,EACxC,UAAU,EAAE,OAAO,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,EAAE,QAAQ,EAAE;AAAA,EACtD,WAAW,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS;AAAA,EACtC,WAAW,EAAE,OAAO,EAAE,SAAS;AAAA,EAC/B,QAAQ,EAAE,KAAK,CAAC,WAAW,WAAW,aAAa,UAAU,SAAS,CAAU,EAAE,SAAS;AAC7F,CAAC;",
4
+ "sourcesContent": ["import { z } from 'zod'\nimport { assertStaticallySafeWebhookUrl, UnsafeWebhookUrlError } from '../lib/url-safety'\nimport { isReservedWebhookCustomHeader } from '../lib/custom-headers'\n\nconst safeWebhookUrl = z.string().url().superRefine((value, ctx) => {\n try {\n assertStaticallySafeWebhookUrl(value)\n } catch (error) {\n const message = error instanceof UnsafeWebhookUrlError\n ? error.message\n : 'Webhook URL is not allowed'\n ctx.addIssue({ code: z.ZodIssueCode.custom, message })\n }\n})\n\nconst webhookCustomHeaders = z.record(z.string(), z.string()).superRefine((value, ctx) => {\n for (const name of Object.keys(value)) {\n if (isReservedWebhookCustomHeader(name)) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: `Header \"${name}\" is reserved for webhook signing and cannot be overridden`,\n path: [name],\n })\n }\n }\n})\n\nexport const webhookCreateSchema = z.object({\n name: z.string().min(1).max(255),\n description: z.string().max(1000).optional().nullable(),\n url: safeWebhookUrl,\n subscribedEvents: z.array(z.string().min(1)).min(1),\n httpMethod: z.enum(['POST', 'PUT', 'PATCH'] as const).default('POST'),\n customHeaders: webhookCustomHeaders.optional().nullable(),\n deliveryStrategy: z.literal('http').default('http'),\n strategyConfig: z.record(z.string(), z.unknown()).optional().nullable(),\n maxRetries: z.number().int().min(0).max(30).default(10),\n timeoutMs: z.number().int().min(1000).max(60000).default(15000),\n rateLimitPerMinute: z.number().int().min(0).max(10000).default(0),\n autoDisableThreshold: z.number().int().min(0).max(1000).default(100),\n integrationId: z.string().optional().nullable(),\n})\n\nexport type WebhookCreateInput = z.infer<typeof webhookCreateSchema>\n\nexport const webhookUpdateSchema = webhookCreateSchema.partial().extend({\n isActive: z.boolean().optional(),\n})\n\nexport type WebhookUpdateInput = z.infer<typeof webhookUpdateSchema>\n\nexport const webhookListQuerySchema = z.object({\n page: z.string().optional(),\n pageSize: z.string().optional(),\n search: z.string().optional(),\n isActive: z.string().optional(),\n})\n\nexport const webhookDeliveryQuerySchema = z.object({\n page: z.coerce.number().min(1).default(1),\n pageSize: z.coerce.number().min(1).max(100).default(50),\n webhookId: z.string().uuid().optional(),\n eventType: z.string().optional(),\n status: z.enum(['pending', 'sending', 'delivered', 'failed', 'expired'] as const).optional(),\n})\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,17 @@
1
+ const RESERVED_HEADER_PREFIX = "webhook-";
2
+ const RESERVED_HEADER_NAMES = /* @__PURE__ */ new Set(["content-type"]);
3
+ function isReservedWebhookCustomHeader(name) {
4
+ const normalized = name.trim().toLowerCase();
5
+ return normalized.startsWith(RESERVED_HEADER_PREFIX) || RESERVED_HEADER_NAMES.has(normalized);
6
+ }
7
+ function sanitizeWebhookCustomHeaders(customHeaders) {
8
+ if (!customHeaders) return {};
9
+ return Object.fromEntries(
10
+ Object.entries(customHeaders).filter(([name]) => !isReservedWebhookCustomHeader(name))
11
+ );
12
+ }
13
+ export {
14
+ isReservedWebhookCustomHeader,
15
+ sanitizeWebhookCustomHeaders
16
+ };
17
+ //# sourceMappingURL=custom-headers.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/webhooks/lib/custom-headers.ts"],
4
+ "sourcesContent": ["const RESERVED_HEADER_PREFIX = 'webhook-'\nconst RESERVED_HEADER_NAMES = new Set(['content-type'])\n\nexport function isReservedWebhookCustomHeader(name: string): boolean {\n const normalized = name.trim().toLowerCase()\n return normalized.startsWith(RESERVED_HEADER_PREFIX) || RESERVED_HEADER_NAMES.has(normalized)\n}\n\nexport function sanitizeWebhookCustomHeaders(\n customHeaders: Record<string, string> | null | undefined,\n): Record<string, string> {\n if (!customHeaders) return {}\n return Object.fromEntries(\n Object.entries(customHeaders).filter(([name]) => !isReservedWebhookCustomHeader(name)),\n )\n}\n"],
5
+ "mappings": "AAAA,MAAM,yBAAyB;AAC/B,MAAM,wBAAwB,oBAAI,IAAI,CAAC,cAAc,CAAC;AAE/C,SAAS,8BAA8B,MAAuB;AACnE,QAAM,aAAa,KAAK,KAAK,EAAE,YAAY;AAC3C,SAAO,WAAW,WAAW,sBAAsB,KAAK,sBAAsB,IAAI,UAAU;AAC9F;AAEO,SAAS,6BACd,eACwB;AACxB,MAAI,CAAC,cAAe,QAAO,CAAC;AAC5B,SAAO,OAAO;AAAA,IACZ,OAAO,QAAQ,aAAa,EAAE,OAAO,CAAC,CAAC,IAAI,MAAM,CAAC,8BAA8B,IAAI,CAAC;AAAA,EACvF;AACF;",
6
+ "names": []
7
+ }
@@ -4,6 +4,7 @@ import { WebhookDeliveryEntity, WebhookEntity } from "../data/entities.js";
4
4
  import { emitWebhooksEvent } from "../events.js";
5
5
  import { enqueueWebhookDelivery } from "./queue.js";
6
6
  import { isWebhookIntegrationEnabled, WEBHOOK_INTEGRATION_DISABLED_MESSAGE } from "./integration-state.js";
7
+ import { sanitizeWebhookCustomHeaders } from "./custom-headers.js";
7
8
  import {
8
9
  assertSafeWebhookDeliveryUrl,
9
10
  safeWebhookFetch,
@@ -121,8 +122,8 @@ async function processWebhookDeliveryJob(em, job, options = {}) {
121
122
  redirect: "manual",
122
123
  headers: {
123
124
  "content-type": "application/json",
124
- ...headers,
125
- ...webhook.customHeaders ?? {}
125
+ ...sanitizeWebhookCustomHeaders(webhook.customHeaders),
126
+ ...headers
126
127
  },
127
128
  body,
128
129
  signal: controller.signal
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/webhooks/lib/delivery.ts"],
4
- "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { buildWebhookHeaders, generateMessageId } from '@open-mercato/shared/lib/webhooks'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { WebhookDeliveryEntity, WebhookEntity } from '../data/entities'\nimport { emitWebhooksEvent } from '../events'\nimport { enqueueWebhookDelivery } from './queue'\nimport { isWebhookIntegrationEnabled, WEBHOOK_INTEGRATION_DISABLED_MESSAGE } from './integration-state'\nimport {\n assertSafeWebhookDeliveryUrl,\n safeWebhookFetch,\n UnsafeWebhookUrlError,\n} from './url-safety'\n\nexport interface WebhookDeliveryJob {\n deliveryId: string\n tenantId: string\n organizationId: string\n}\n\nexport interface CreateWebhookDeliveryInput {\n em: EntityManager\n webhook: WebhookEntity\n eventId: string\n payload: Record<string, unknown>\n}\n\ntype ProcessWebhookDeliveryOptions = {\n scheduleRetries?: boolean\n}\n\ntype WebhookBody = {\n type: string\n timestamp: string\n data: Record<string, unknown>\n}\n\nexport async function createWebhookDelivery(input: CreateWebhookDeliveryInput): Promise<WebhookDeliveryEntity> {\n const bodyPayload: WebhookBody = {\n type: input.eventId,\n timestamp: new Date().toISOString(),\n data: input.payload,\n }\n const now = new Date()\n\n const delivery = input.em.create(WebhookDeliveryEntity, {\n webhookId: input.webhook.id,\n eventType: input.eventId,\n messageId: generateMessageId(),\n payload: bodyPayload,\n status: 'pending',\n attemptNumber: 0,\n maxAttempts: input.webhook.maxRetries,\n targetUrl: input.webhook.url,\n organizationId: input.webhook.organizationId,\n tenantId: input.webhook.tenantId,\n enqueuedAt: now,\n createdAt: now,\n updatedAt: now,\n })\n\n await input.em.flush()\n await emitWebhooksEvent('webhooks.delivery.enqueued', {\n deliveryId: delivery.id,\n webhookId: input.webhook.id,\n eventType: input.eventId,\n organizationId: input.webhook.organizationId,\n tenantId: input.webhook.tenantId,\n })\n return delivery\n}\n\nexport async function processWebhookDeliveryJob(\n em: EntityManager,\n job: WebhookDeliveryJob,\n options: ProcessWebhookDeliveryOptions = {},\n): Promise<{ status: string; deliveryId: string } | null> {\n const delivery = await em.findOne(WebhookDeliveryEntity, {\n id: job.deliveryId,\n tenantId: job.tenantId,\n organizationId: job.organizationId,\n })\n\n if (!delivery) return null\n\n const webhook = await findOneWithDecryption(\n em,\n WebhookEntity,\n {\n id: delivery.webhookId,\n tenantId: job.tenantId,\n organizationId: job.organizationId,\n deletedAt: null,\n },\n {},\n { tenantId: job.tenantId, organizationId: job.organizationId },\n )\n\n if (!webhook) {\n delivery.status = 'failed'\n delivery.errorMessage = 'Webhook not found'\n delivery.nextRetryAt = null\n await em.flush()\n return { status: delivery.status, deliveryId: delivery.id }\n }\n\n if (!webhook.isActive) {\n delivery.status = 'expired'\n delivery.errorMessage = 'Webhook is inactive'\n delivery.nextRetryAt = null\n await em.flush()\n return { status: delivery.status, deliveryId: delivery.id }\n }\n\n const integrationEnabled = await isWebhookIntegrationEnabled(em, {\n tenantId: webhook.tenantId,\n organizationId: webhook.organizationId,\n })\n\n if (!integrationEnabled) {\n delivery.status = 'expired'\n delivery.errorMessage = WEBHOOK_INTEGRATION_DISABLED_MESSAGE\n delivery.nextRetryAt = null\n await em.flush()\n return { status: delivery.status, deliveryId: delivery.id }\n }\n\n try {\n await assertSafeWebhookDeliveryUrl(webhook.url)\n } catch (error) {\n return await recordWebhookSafetyFailure({\n em,\n delivery,\n webhook,\n error,\n })\n }\n\n const bodyPayload = normalizeWebhookBody(delivery.eventType, delivery.payload)\n delivery.payload = bodyPayload\n delivery.status = 'sending'\n delivery.lastAttemptAt = new Date()\n delivery.errorMessage = null\n await em.flush()\n\n const body = JSON.stringify(bodyPayload)\n const timestamp = Math.floor(Date.now() / 1000)\n const headers = buildWebhookHeaders(\n delivery.messageId,\n timestamp,\n body,\n webhook.secret,\n webhook.previousSecret,\n )\n\n const attemptStartedAt = Date.now()\n\n try {\n const controller = new AbortController()\n const timeoutId = setTimeout(() => controller.abort(), webhook.timeoutMs)\n\n let response: Response\n try {\n response = await safeWebhookFetch(webhook.url, {\n method: webhook.httpMethod,\n redirect: 'manual',\n headers: {\n 'content-type': 'application/json',\n ...headers,\n ...(webhook.customHeaders ?? {}),\n },\n body,\n signal: controller.signal,\n })\n } finally {\n clearTimeout(timeoutId)\n }\n\n delivery.attemptNumber += 1\n delivery.responseStatus = response.status\n delivery.responseBody = (await response.text()).slice(0, 4096)\n delivery.responseHeaders = Object.fromEntries(response.headers.entries())\n delivery.durationMs = Date.now() - delivery.enqueuedAt.getTime()\n delivery.lastAttemptAt = new Date()\n\n if (response.ok) {\n delivery.status = 'delivered'\n delivery.deliveredAt = new Date()\n delivery.nextRetryAt = null\n webhook.consecutiveFailures = 0\n webhook.lastSuccessAt = new Date()\n\n await emitWebhooksEvent('webhooks.delivery.succeeded', {\n deliveryId: delivery.id,\n webhookId: webhook.id,\n eventType: delivery.eventType,\n organizationId: delivery.organizationId,\n tenantId: delivery.tenantId,\n })\n\n await em.flush()\n return { status: delivery.status, deliveryId: delivery.id }\n }\n\n webhook.consecutiveFailures += 1\n webhook.lastFailureAt = new Date()\n\n await handleFailedDelivery({\n em,\n webhook,\n delivery,\n canRetry: shouldRetryStatus(response.status),\n scheduleRetries: options.scheduleRetries !== false,\n fallbackMessage: `HTTP ${response.status}`,\n })\n\n await emitWebhooksEvent('webhooks.delivery.failed', {\n deliveryId: delivery.id,\n webhookId: webhook.id,\n eventType: delivery.eventType,\n responseStatus: response.status,\n organizationId: delivery.organizationId,\n tenantId: delivery.tenantId,\n willRetry: delivery.status === 'pending',\n })\n\n return { status: delivery.status, deliveryId: delivery.id }\n } catch (error) {\n if (error instanceof UnsafeWebhookUrlError) {\n // DNS rebinding caught between the upfront safety check and the pinned connect:\n // the same attacker-controlled DNS would defeat retries too, so fail terminally.\n const durationMs = Date.now() - delivery.enqueuedAt.getTime()\n delivery.durationMs = durationMs\n return await recordWebhookSafetyFailure({\n em,\n delivery,\n webhook,\n error,\n durationMs,\n })\n }\n\n delivery.attemptNumber += 1\n delivery.durationMs = Date.now() - delivery.enqueuedAt.getTime()\n delivery.lastAttemptAt = new Date()\n delivery.errorMessage = error instanceof Error ? error.message : 'Unknown delivery error'\n\n webhook.consecutiveFailures += 1\n webhook.lastFailureAt = new Date()\n\n await handleFailedDelivery({\n em,\n webhook,\n delivery,\n canRetry: true,\n scheduleRetries: options.scheduleRetries !== false,\n fallbackMessage: delivery.errorMessage,\n })\n\n await emitWebhooksEvent('webhooks.delivery.failed', {\n deliveryId: delivery.id,\n webhookId: webhook.id,\n eventType: delivery.eventType,\n errorMessage: delivery.errorMessage,\n durationMs: Date.now() - attemptStartedAt,\n organizationId: delivery.organizationId,\n tenantId: delivery.tenantId,\n willRetry: delivery.status === 'pending',\n })\n\n return { status: delivery.status, deliveryId: delivery.id }\n }\n}\n\ntype RecordWebhookSafetyFailureInput = {\n em: EntityManager\n delivery: WebhookDeliveryEntity\n webhook: WebhookEntity\n error: unknown\n durationMs?: number | null\n}\n\nasync function recordWebhookSafetyFailure(\n input: RecordWebhookSafetyFailureInput,\n): Promise<{ status: string; deliveryId: string }> {\n const { em, delivery, webhook, error } = input\n const message = error instanceof UnsafeWebhookUrlError\n ? error.message\n : 'Webhook URL rejected by safety check'\n\n delivery.status = 'failed'\n delivery.errorMessage = message\n delivery.nextRetryAt = null\n delivery.lastAttemptAt = new Date()\n delivery.attemptNumber += 1\n webhook.consecutiveFailures += 1\n webhook.lastFailureAt = new Date()\n await em.flush()\n\n await emitWebhooksEvent('webhooks.delivery.failed', {\n deliveryId: delivery.id,\n webhookId: webhook.id,\n eventType: delivery.eventType,\n errorMessage: message,\n durationMs: input.durationMs ?? 0,\n organizationId: delivery.organizationId,\n tenantId: delivery.tenantId,\n willRetry: false,\n })\n\n return { status: delivery.status, deliveryId: delivery.id }\n}\n\ntype HandleFailedDeliveryInput = {\n em: EntityManager\n webhook: WebhookEntity\n delivery: WebhookDeliveryEntity\n canRetry: boolean\n scheduleRetries: boolean\n fallbackMessage: string\n}\n\nasync function handleFailedDelivery(input: HandleFailedDeliveryInput): Promise<void> {\n const { delivery, webhook } = input\n const retriesRemaining = delivery.attemptNumber < Math.max(delivery.maxAttempts, 1)\n const shouldRetry = input.canRetry && retriesRemaining\n\n if (shouldRetry) {\n const nextRetryAt = calculateNextRetry(delivery.attemptNumber)\n delivery.status = 'pending'\n delivery.nextRetryAt = nextRetryAt\n\n await input.em.flush()\n\n if (input.scheduleRetries) {\n try {\n await enqueueWebhookDelivery(\n {\n deliveryId: delivery.id,\n tenantId: delivery.tenantId,\n organizationId: delivery.organizationId,\n },\n Math.max(nextRetryAt.getTime() - Date.now(), 0),\n )\n } catch (error) {\n delivery.status = 'failed'\n delivery.nextRetryAt = null\n delivery.errorMessage = error instanceof Error\n ? `Retry scheduling failed: ${error.message}`\n : 'Retry scheduling failed'\n }\n }\n } else {\n delivery.status = 'expired'\n delivery.nextRetryAt = null\n\n await emitWebhooksEvent('webhooks.delivery.exhausted', {\n deliveryId: delivery.id,\n webhookId: webhook.id,\n eventType: delivery.eventType,\n organizationId: delivery.organizationId,\n tenantId: delivery.tenantId,\n errorMessage: delivery.errorMessage ?? input.fallbackMessage,\n })\n }\n\n if (webhook.autoDisableThreshold > 0 && webhook.consecutiveFailures >= webhook.autoDisableThreshold) {\n webhook.isActive = false\n await emitWebhooksEvent('webhooks.webhook.disabled', {\n webhookId: webhook.id,\n organizationId: webhook.organizationId,\n tenantId: webhook.tenantId,\n consecutiveFailures: webhook.consecutiveFailures,\n })\n }\n\n await input.em.flush()\n}\n\nfunction normalizeWebhookBody(eventType: string, payload: Record<string, unknown>): WebhookBody {\n const candidateType = typeof payload.type === 'string' ? payload.type : null\n const candidateTimestamp = typeof payload.timestamp === 'string' ? payload.timestamp : null\n const candidateData = isRecord(payload.data) ? payload.data : null\n\n if (candidateType && candidateTimestamp && candidateData) {\n return {\n type: candidateType,\n timestamp: candidateTimestamp,\n data: candidateData,\n }\n }\n\n return {\n type: eventType,\n timestamp: new Date().toISOString(),\n data: payload,\n }\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\nfunction shouldRetryStatus(status: number): boolean {\n if (status >= 200 && status < 300) return false\n if (status === 408 || status === 429) return true\n return status >= 500\n}\n\nfunction calculateNextRetry(attemptNumber: number): Date {\n const baseDelayMs = 1000\n const jitterMs = Math.floor(Math.random() * 1000)\n const delayMs = baseDelayMs * Math.pow(2, Math.max(attemptNumber - 1, 0)) + jitterMs\n return new Date(Date.now() + delayMs)\n}\n"],
5
- "mappings": "AACA,SAAS,qBAAqB,yBAAyB;AACvD,SAAS,6BAA6B;AACtC,SAAS,uBAAuB,qBAAqB;AACrD,SAAS,yBAAyB;AAClC,SAAS,8BAA8B;AACvC,SAAS,6BAA6B,4CAA4C;AAClF;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAyBP,eAAsB,sBAAsB,OAAmE;AAC7G,QAAM,cAA2B;AAAA,IAC/B,MAAM,MAAM;AAAA,IACZ,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,IAClC,MAAM,MAAM;AAAA,EACd;AACA,QAAM,MAAM,oBAAI,KAAK;AAErB,QAAM,WAAW,MAAM,GAAG,OAAO,uBAAuB;AAAA,IACtD,WAAW,MAAM,QAAQ;AAAA,IACzB,WAAW,MAAM;AAAA,IACjB,WAAW,kBAAkB;AAAA,IAC7B,SAAS;AAAA,IACT,QAAQ;AAAA,IACR,eAAe;AAAA,IACf,aAAa,MAAM,QAAQ;AAAA,IAC3B,WAAW,MAAM,QAAQ;AAAA,IACzB,gBAAgB,MAAM,QAAQ;AAAA,IAC9B,UAAU,MAAM,QAAQ;AAAA,IACxB,YAAY;AAAA,IACZ,WAAW;AAAA,IACX,WAAW;AAAA,EACb,CAAC;AAED,QAAM,MAAM,GAAG,MAAM;AACrB,QAAM,kBAAkB,8BAA8B;AAAA,IACpD,YAAY,SAAS;AAAA,IACrB,WAAW,MAAM,QAAQ;AAAA,IACzB,WAAW,MAAM;AAAA,IACjB,gBAAgB,MAAM,QAAQ;AAAA,IAC9B,UAAU,MAAM,QAAQ;AAAA,EAC1B,CAAC;AACD,SAAO;AACT;AAEA,eAAsB,0BACpB,IACA,KACA,UAAyC,CAAC,GACc;AACxD,QAAM,WAAW,MAAM,GAAG,QAAQ,uBAAuB;AAAA,IACvD,IAAI,IAAI;AAAA,IACR,UAAU,IAAI;AAAA,IACd,gBAAgB,IAAI;AAAA,EACtB,CAAC;AAED,MAAI,CAAC,SAAU,QAAO;AAEtB,QAAM,UAAU,MAAM;AAAA,IACpB;AAAA,IACA;AAAA,IACA;AAAA,MACE,IAAI,SAAS;AAAA,MACb,UAAU,IAAI;AAAA,MACd,gBAAgB,IAAI;AAAA,MACpB,WAAW;AAAA,IACb;AAAA,IACA,CAAC;AAAA,IACD,EAAE,UAAU,IAAI,UAAU,gBAAgB,IAAI,eAAe;AAAA,EAC/D;AAEA,MAAI,CAAC,SAAS;AACZ,aAAS,SAAS;AAClB,aAAS,eAAe;AACxB,aAAS,cAAc;AACvB,UAAM,GAAG,MAAM;AACf,WAAO,EAAE,QAAQ,SAAS,QAAQ,YAAY,SAAS,GAAG;AAAA,EAC5D;AAEA,MAAI,CAAC,QAAQ,UAAU;AACrB,aAAS,SAAS;AAClB,aAAS,eAAe;AACxB,aAAS,cAAc;AACvB,UAAM,GAAG,MAAM;AACf,WAAO,EAAE,QAAQ,SAAS,QAAQ,YAAY,SAAS,GAAG;AAAA,EAC5D;AAEA,QAAM,qBAAqB,MAAM,4BAA4B,IAAI;AAAA,IAC/D,UAAU,QAAQ;AAAA,IAClB,gBAAgB,QAAQ;AAAA,EAC1B,CAAC;AAED,MAAI,CAAC,oBAAoB;AACvB,aAAS,SAAS;AAClB,aAAS,eAAe;AACxB,aAAS,cAAc;AACvB,UAAM,GAAG,MAAM;AACf,WAAO,EAAE,QAAQ,SAAS,QAAQ,YAAY,SAAS,GAAG;AAAA,EAC5D;AAEA,MAAI;AACF,UAAM,6BAA6B,QAAQ,GAAG;AAAA,EAChD,SAAS,OAAO;AACd,WAAO,MAAM,2BAA2B;AAAA,MACtC;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF,CAAC;AAAA,EACH;AAEA,QAAM,cAAc,qBAAqB,SAAS,WAAW,SAAS,OAAO;AAC7E,WAAS,UAAU;AACnB,WAAS,SAAS;AAClB,WAAS,gBAAgB,oBAAI,KAAK;AAClC,WAAS,eAAe;AACxB,QAAM,GAAG,MAAM;AAEf,QAAM,OAAO,KAAK,UAAU,WAAW;AACvC,QAAM,YAAY,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AAC9C,QAAM,UAAU;AAAA,IACd,SAAS;AAAA,IACT;AAAA,IACA;AAAA,IACA,QAAQ;AAAA,IACR,QAAQ;AAAA,EACV;AAEA,QAAM,mBAAmB,KAAK,IAAI;AAElC,MAAI;AACF,UAAM,aAAa,IAAI,gBAAgB;AACvC,UAAM,YAAY,WAAW,MAAM,WAAW,MAAM,GAAG,QAAQ,SAAS;AAExE,QAAI;AACJ,QAAI;AACF,iBAAW,MAAM,iBAAiB,QAAQ,KAAK;AAAA,QAC7C,QAAQ,QAAQ;AAAA,QAChB,UAAU;AAAA,QACV,SAAS;AAAA,UACP,gBAAgB;AAAA,UAChB,GAAG;AAAA,UACH,GAAI,QAAQ,iBAAiB,CAAC;AAAA,QAChC;AAAA,QACA;AAAA,QACA,QAAQ,WAAW;AAAA,MACrB,CAAC;AAAA,IACH,UAAE;AACA,mBAAa,SAAS;AAAA,IACxB;AAEA,aAAS,iBAAiB;AAC1B,aAAS,iBAAiB,SAAS;AACnC,aAAS,gBAAgB,MAAM,SAAS,KAAK,GAAG,MAAM,GAAG,IAAI;AAC7D,aAAS,kBAAkB,OAAO,YAAY,SAAS,QAAQ,QAAQ,CAAC;AACxE,aAAS,aAAa,KAAK,IAAI,IAAI,SAAS,WAAW,QAAQ;AAC/D,aAAS,gBAAgB,oBAAI,KAAK;AAElC,QAAI,SAAS,IAAI;AACf,eAAS,SAAS;AAClB,eAAS,cAAc,oBAAI,KAAK;AAChC,eAAS,cAAc;AACvB,cAAQ,sBAAsB;AAC9B,cAAQ,gBAAgB,oBAAI,KAAK;AAEjC,YAAM,kBAAkB,+BAA+B;AAAA,QACrD,YAAY,SAAS;AAAA,QACrB,WAAW,QAAQ;AAAA,QACnB,WAAW,SAAS;AAAA,QACpB,gBAAgB,SAAS;AAAA,QACzB,UAAU,SAAS;AAAA,MACrB,CAAC;AAED,YAAM,GAAG,MAAM;AACf,aAAO,EAAE,QAAQ,SAAS,QAAQ,YAAY,SAAS,GAAG;AAAA,IAC5D;AAEA,YAAQ,uBAAuB;AAC/B,YAAQ,gBAAgB,oBAAI,KAAK;AAEjC,UAAM,qBAAqB;AAAA,MACzB;AAAA,MACA;AAAA,MACA;AAAA,MACA,UAAU,kBAAkB,SAAS,MAAM;AAAA,MAC3C,iBAAiB,QAAQ,oBAAoB;AAAA,MAC7C,iBAAiB,QAAQ,SAAS,MAAM;AAAA,IAC1C,CAAC;AAED,UAAM,kBAAkB,4BAA4B;AAAA,MAClD,YAAY,SAAS;AAAA,MACrB,WAAW,QAAQ;AAAA,MACnB,WAAW,SAAS;AAAA,MACpB,gBAAgB,SAAS;AAAA,MACzB,gBAAgB,SAAS;AAAA,MACzB,UAAU,SAAS;AAAA,MACnB,WAAW,SAAS,WAAW;AAAA,IACjC,CAAC;AAED,WAAO,EAAE,QAAQ,SAAS,QAAQ,YAAY,SAAS,GAAG;AAAA,EAC5D,SAAS,OAAO;AACd,QAAI,iBAAiB,uBAAuB;AAG1C,YAAM,aAAa,KAAK,IAAI,IAAI,SAAS,WAAW,QAAQ;AAC5D,eAAS,aAAa;AACtB,aAAO,MAAM,2BAA2B;AAAA,QACtC;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF,CAAC;AAAA,IACH;AAEA,aAAS,iBAAiB;AAC1B,aAAS,aAAa,KAAK,IAAI,IAAI,SAAS,WAAW,QAAQ;AAC/D,aAAS,gBAAgB,oBAAI,KAAK;AAClC,aAAS,eAAe,iBAAiB,QAAQ,MAAM,UAAU;AAEjE,YAAQ,uBAAuB;AAC/B,YAAQ,gBAAgB,oBAAI,KAAK;AAEjC,UAAM,qBAAqB;AAAA,MACzB;AAAA,MACA;AAAA,MACA;AAAA,MACA,UAAU;AAAA,MACV,iBAAiB,QAAQ,oBAAoB;AAAA,MAC7C,iBAAiB,SAAS;AAAA,IAC5B,CAAC;AAED,UAAM,kBAAkB,4BAA4B;AAAA,MAClD,YAAY,SAAS;AAAA,MACrB,WAAW,QAAQ;AAAA,MACnB,WAAW,SAAS;AAAA,MACpB,cAAc,SAAS;AAAA,MACvB,YAAY,KAAK,IAAI,IAAI;AAAA,MACzB,gBAAgB,SAAS;AAAA,MACzB,UAAU,SAAS;AAAA,MACnB,WAAW,SAAS,WAAW;AAAA,IACjC,CAAC;AAED,WAAO,EAAE,QAAQ,SAAS,QAAQ,YAAY,SAAS,GAAG;AAAA,EAC5D;AACF;AAUA,eAAe,2BACb,OACiD;AACjD,QAAM,EAAE,IAAI,UAAU,SAAS,MAAM,IAAI;AACzC,QAAM,UAAU,iBAAiB,wBAC7B,MAAM,UACN;AAEJ,WAAS,SAAS;AAClB,WAAS,eAAe;AACxB,WAAS,cAAc;AACvB,WAAS,gBAAgB,oBAAI,KAAK;AAClC,WAAS,iBAAiB;AAC1B,UAAQ,uBAAuB;AAC/B,UAAQ,gBAAgB,oBAAI,KAAK;AACjC,QAAM,GAAG,MAAM;AAEf,QAAM,kBAAkB,4BAA4B;AAAA,IAClD,YAAY,SAAS;AAAA,IACrB,WAAW,QAAQ;AAAA,IACnB,WAAW,SAAS;AAAA,IACpB,cAAc;AAAA,IACd,YAAY,MAAM,cAAc;AAAA,IAChC,gBAAgB,SAAS;AAAA,IACzB,UAAU,SAAS;AAAA,IACnB,WAAW;AAAA,EACb,CAAC;AAED,SAAO,EAAE,QAAQ,SAAS,QAAQ,YAAY,SAAS,GAAG;AAC5D;AAWA,eAAe,qBAAqB,OAAiD;AACnF,QAAM,EAAE,UAAU,QAAQ,IAAI;AAC9B,QAAM,mBAAmB,SAAS,gBAAgB,KAAK,IAAI,SAAS,aAAa,CAAC;AAClF,QAAM,cAAc,MAAM,YAAY;AAEtC,MAAI,aAAa;AACf,UAAM,cAAc,mBAAmB,SAAS,aAAa;AAC7D,aAAS,SAAS;AAClB,aAAS,cAAc;AAEvB,UAAM,MAAM,GAAG,MAAM;AAErB,QAAI,MAAM,iBAAiB;AACzB,UAAI;AACF,cAAM;AAAA,UACJ;AAAA,YACE,YAAY,SAAS;AAAA,YACrB,UAAU,SAAS;AAAA,YACnB,gBAAgB,SAAS;AAAA,UAC3B;AAAA,UACA,KAAK,IAAI,YAAY,QAAQ,IAAI,KAAK,IAAI,GAAG,CAAC;AAAA,QAChD;AAAA,MACF,SAAS,OAAO;AACd,iBAAS,SAAS;AAClB,iBAAS,cAAc;AACvB,iBAAS,eAAe,iBAAiB,QACrC,4BAA4B,MAAM,OAAO,KACzC;AAAA,MACN;AAAA,IACF;AAAA,EACF,OAAO;AACL,aAAS,SAAS;AAClB,aAAS,cAAc;AAEvB,UAAM,kBAAkB,+BAA+B;AAAA,MACrD,YAAY,SAAS;AAAA,MACrB,WAAW,QAAQ;AAAA,MACnB,WAAW,SAAS;AAAA,MACpB,gBAAgB,SAAS;AAAA,MACzB,UAAU,SAAS;AAAA,MACnB,cAAc,SAAS,gBAAgB,MAAM;AAAA,IAC/C,CAAC;AAAA,EACH;AAEA,MAAI,QAAQ,uBAAuB,KAAK,QAAQ,uBAAuB,QAAQ,sBAAsB;AACnG,YAAQ,WAAW;AACnB,UAAM,kBAAkB,6BAA6B;AAAA,MACnD,WAAW,QAAQ;AAAA,MACnB,gBAAgB,QAAQ;AAAA,MACxB,UAAU,QAAQ;AAAA,MAClB,qBAAqB,QAAQ;AAAA,IAC/B,CAAC;AAAA,EACH;AAEA,QAAM,MAAM,GAAG,MAAM;AACvB;AAEA,SAAS,qBAAqB,WAAmB,SAA+C;AAC9F,QAAM,gBAAgB,OAAO,QAAQ,SAAS,WAAW,QAAQ,OAAO;AACxE,QAAM,qBAAqB,OAAO,QAAQ,cAAc,WAAW,QAAQ,YAAY;AACvF,QAAM,gBAAgB,SAAS,QAAQ,IAAI,IAAI,QAAQ,OAAO;AAE9D,MAAI,iBAAiB,sBAAsB,eAAe;AACxD,WAAO;AAAA,MACL,MAAM;AAAA,MACN,WAAW;AAAA,MACX,MAAM;AAAA,IACR;AAAA,EACF;AAEA,SAAO;AAAA,IACL,MAAM;AAAA,IACN,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,IAClC,MAAM;AAAA,EACR;AACF;AAEA,SAAS,SAAS,OAAkD;AAClE,SAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAC5E;AAEA,SAAS,kBAAkB,QAAyB;AAClD,MAAI,UAAU,OAAO,SAAS,IAAK,QAAO;AAC1C,MAAI,WAAW,OAAO,WAAW,IAAK,QAAO;AAC7C,SAAO,UAAU;AACnB;AAEA,SAAS,mBAAmB,eAA6B;AACvD,QAAM,cAAc;AACpB,QAAM,WAAW,KAAK,MAAM,KAAK,OAAO,IAAI,GAAI;AAChD,QAAM,UAAU,cAAc,KAAK,IAAI,GAAG,KAAK,IAAI,gBAAgB,GAAG,CAAC,CAAC,IAAI;AAC5E,SAAO,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO;AACtC;",
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { buildWebhookHeaders, generateMessageId } from '@open-mercato/shared/lib/webhooks'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { WebhookDeliveryEntity, WebhookEntity } from '../data/entities'\nimport { emitWebhooksEvent } from '../events'\nimport { enqueueWebhookDelivery } from './queue'\nimport { isWebhookIntegrationEnabled, WEBHOOK_INTEGRATION_DISABLED_MESSAGE } from './integration-state'\nimport { sanitizeWebhookCustomHeaders } from './custom-headers'\nimport {\n assertSafeWebhookDeliveryUrl,\n safeWebhookFetch,\n UnsafeWebhookUrlError,\n} from './url-safety'\n\nexport interface WebhookDeliveryJob {\n deliveryId: string\n tenantId: string\n organizationId: string\n}\n\nexport interface CreateWebhookDeliveryInput {\n em: EntityManager\n webhook: WebhookEntity\n eventId: string\n payload: Record<string, unknown>\n}\n\ntype ProcessWebhookDeliveryOptions = {\n scheduleRetries?: boolean\n}\n\ntype WebhookBody = {\n type: string\n timestamp: string\n data: Record<string, unknown>\n}\n\nexport async function createWebhookDelivery(input: CreateWebhookDeliveryInput): Promise<WebhookDeliveryEntity> {\n const bodyPayload: WebhookBody = {\n type: input.eventId,\n timestamp: new Date().toISOString(),\n data: input.payload,\n }\n const now = new Date()\n\n const delivery = input.em.create(WebhookDeliveryEntity, {\n webhookId: input.webhook.id,\n eventType: input.eventId,\n messageId: generateMessageId(),\n payload: bodyPayload,\n status: 'pending',\n attemptNumber: 0,\n maxAttempts: input.webhook.maxRetries,\n targetUrl: input.webhook.url,\n organizationId: input.webhook.organizationId,\n tenantId: input.webhook.tenantId,\n enqueuedAt: now,\n createdAt: now,\n updatedAt: now,\n })\n\n await input.em.flush()\n await emitWebhooksEvent('webhooks.delivery.enqueued', {\n deliveryId: delivery.id,\n webhookId: input.webhook.id,\n eventType: input.eventId,\n organizationId: input.webhook.organizationId,\n tenantId: input.webhook.tenantId,\n })\n return delivery\n}\n\nexport async function processWebhookDeliveryJob(\n em: EntityManager,\n job: WebhookDeliveryJob,\n options: ProcessWebhookDeliveryOptions = {},\n): Promise<{ status: string; deliveryId: string } | null> {\n const delivery = await em.findOne(WebhookDeliveryEntity, {\n id: job.deliveryId,\n tenantId: job.tenantId,\n organizationId: job.organizationId,\n })\n\n if (!delivery) return null\n\n const webhook = await findOneWithDecryption(\n em,\n WebhookEntity,\n {\n id: delivery.webhookId,\n tenantId: job.tenantId,\n organizationId: job.organizationId,\n deletedAt: null,\n },\n {},\n { tenantId: job.tenantId, organizationId: job.organizationId },\n )\n\n if (!webhook) {\n delivery.status = 'failed'\n delivery.errorMessage = 'Webhook not found'\n delivery.nextRetryAt = null\n await em.flush()\n return { status: delivery.status, deliveryId: delivery.id }\n }\n\n if (!webhook.isActive) {\n delivery.status = 'expired'\n delivery.errorMessage = 'Webhook is inactive'\n delivery.nextRetryAt = null\n await em.flush()\n return { status: delivery.status, deliveryId: delivery.id }\n }\n\n const integrationEnabled = await isWebhookIntegrationEnabled(em, {\n tenantId: webhook.tenantId,\n organizationId: webhook.organizationId,\n })\n\n if (!integrationEnabled) {\n delivery.status = 'expired'\n delivery.errorMessage = WEBHOOK_INTEGRATION_DISABLED_MESSAGE\n delivery.nextRetryAt = null\n await em.flush()\n return { status: delivery.status, deliveryId: delivery.id }\n }\n\n try {\n await assertSafeWebhookDeliveryUrl(webhook.url)\n } catch (error) {\n return await recordWebhookSafetyFailure({\n em,\n delivery,\n webhook,\n error,\n })\n }\n\n const bodyPayload = normalizeWebhookBody(delivery.eventType, delivery.payload)\n delivery.payload = bodyPayload\n delivery.status = 'sending'\n delivery.lastAttemptAt = new Date()\n delivery.errorMessage = null\n await em.flush()\n\n const body = JSON.stringify(bodyPayload)\n const timestamp = Math.floor(Date.now() / 1000)\n const headers = buildWebhookHeaders(\n delivery.messageId,\n timestamp,\n body,\n webhook.secret,\n webhook.previousSecret,\n )\n\n const attemptStartedAt = Date.now()\n\n try {\n const controller = new AbortController()\n const timeoutId = setTimeout(() => controller.abort(), webhook.timeoutMs)\n\n let response: Response\n try {\n response = await safeWebhookFetch(webhook.url, {\n method: webhook.httpMethod,\n redirect: 'manual',\n headers: {\n 'content-type': 'application/json',\n ...sanitizeWebhookCustomHeaders(webhook.customHeaders),\n ...headers,\n },\n body,\n signal: controller.signal,\n })\n } finally {\n clearTimeout(timeoutId)\n }\n\n delivery.attemptNumber += 1\n delivery.responseStatus = response.status\n delivery.responseBody = (await response.text()).slice(0, 4096)\n delivery.responseHeaders = Object.fromEntries(response.headers.entries())\n delivery.durationMs = Date.now() - delivery.enqueuedAt.getTime()\n delivery.lastAttemptAt = new Date()\n\n if (response.ok) {\n delivery.status = 'delivered'\n delivery.deliveredAt = new Date()\n delivery.nextRetryAt = null\n webhook.consecutiveFailures = 0\n webhook.lastSuccessAt = new Date()\n\n await emitWebhooksEvent('webhooks.delivery.succeeded', {\n deliveryId: delivery.id,\n webhookId: webhook.id,\n eventType: delivery.eventType,\n organizationId: delivery.organizationId,\n tenantId: delivery.tenantId,\n })\n\n await em.flush()\n return { status: delivery.status, deliveryId: delivery.id }\n }\n\n webhook.consecutiveFailures += 1\n webhook.lastFailureAt = new Date()\n\n await handleFailedDelivery({\n em,\n webhook,\n delivery,\n canRetry: shouldRetryStatus(response.status),\n scheduleRetries: options.scheduleRetries !== false,\n fallbackMessage: `HTTP ${response.status}`,\n })\n\n await emitWebhooksEvent('webhooks.delivery.failed', {\n deliveryId: delivery.id,\n webhookId: webhook.id,\n eventType: delivery.eventType,\n responseStatus: response.status,\n organizationId: delivery.organizationId,\n tenantId: delivery.tenantId,\n willRetry: delivery.status === 'pending',\n })\n\n return { status: delivery.status, deliveryId: delivery.id }\n } catch (error) {\n if (error instanceof UnsafeWebhookUrlError) {\n // DNS rebinding caught between the upfront safety check and the pinned connect:\n // the same attacker-controlled DNS would defeat retries too, so fail terminally.\n const durationMs = Date.now() - delivery.enqueuedAt.getTime()\n delivery.durationMs = durationMs\n return await recordWebhookSafetyFailure({\n em,\n delivery,\n webhook,\n error,\n durationMs,\n })\n }\n\n delivery.attemptNumber += 1\n delivery.durationMs = Date.now() - delivery.enqueuedAt.getTime()\n delivery.lastAttemptAt = new Date()\n delivery.errorMessage = error instanceof Error ? error.message : 'Unknown delivery error'\n\n webhook.consecutiveFailures += 1\n webhook.lastFailureAt = new Date()\n\n await handleFailedDelivery({\n em,\n webhook,\n delivery,\n canRetry: true,\n scheduleRetries: options.scheduleRetries !== false,\n fallbackMessage: delivery.errorMessage,\n })\n\n await emitWebhooksEvent('webhooks.delivery.failed', {\n deliveryId: delivery.id,\n webhookId: webhook.id,\n eventType: delivery.eventType,\n errorMessage: delivery.errorMessage,\n durationMs: Date.now() - attemptStartedAt,\n organizationId: delivery.organizationId,\n tenantId: delivery.tenantId,\n willRetry: delivery.status === 'pending',\n })\n\n return { status: delivery.status, deliveryId: delivery.id }\n }\n}\n\ntype RecordWebhookSafetyFailureInput = {\n em: EntityManager\n delivery: WebhookDeliveryEntity\n webhook: WebhookEntity\n error: unknown\n durationMs?: number | null\n}\n\nasync function recordWebhookSafetyFailure(\n input: RecordWebhookSafetyFailureInput,\n): Promise<{ status: string; deliveryId: string }> {\n const { em, delivery, webhook, error } = input\n const message = error instanceof UnsafeWebhookUrlError\n ? error.message\n : 'Webhook URL rejected by safety check'\n\n delivery.status = 'failed'\n delivery.errorMessage = message\n delivery.nextRetryAt = null\n delivery.lastAttemptAt = new Date()\n delivery.attemptNumber += 1\n webhook.consecutiveFailures += 1\n webhook.lastFailureAt = new Date()\n await em.flush()\n\n await emitWebhooksEvent('webhooks.delivery.failed', {\n deliveryId: delivery.id,\n webhookId: webhook.id,\n eventType: delivery.eventType,\n errorMessage: message,\n durationMs: input.durationMs ?? 0,\n organizationId: delivery.organizationId,\n tenantId: delivery.tenantId,\n willRetry: false,\n })\n\n return { status: delivery.status, deliveryId: delivery.id }\n}\n\ntype HandleFailedDeliveryInput = {\n em: EntityManager\n webhook: WebhookEntity\n delivery: WebhookDeliveryEntity\n canRetry: boolean\n scheduleRetries: boolean\n fallbackMessage: string\n}\n\nasync function handleFailedDelivery(input: HandleFailedDeliveryInput): Promise<void> {\n const { delivery, webhook } = input\n const retriesRemaining = delivery.attemptNumber < Math.max(delivery.maxAttempts, 1)\n const shouldRetry = input.canRetry && retriesRemaining\n\n if (shouldRetry) {\n const nextRetryAt = calculateNextRetry(delivery.attemptNumber)\n delivery.status = 'pending'\n delivery.nextRetryAt = nextRetryAt\n\n await input.em.flush()\n\n if (input.scheduleRetries) {\n try {\n await enqueueWebhookDelivery(\n {\n deliveryId: delivery.id,\n tenantId: delivery.tenantId,\n organizationId: delivery.organizationId,\n },\n Math.max(nextRetryAt.getTime() - Date.now(), 0),\n )\n } catch (error) {\n delivery.status = 'failed'\n delivery.nextRetryAt = null\n delivery.errorMessage = error instanceof Error\n ? `Retry scheduling failed: ${error.message}`\n : 'Retry scheduling failed'\n }\n }\n } else {\n delivery.status = 'expired'\n delivery.nextRetryAt = null\n\n await emitWebhooksEvent('webhooks.delivery.exhausted', {\n deliveryId: delivery.id,\n webhookId: webhook.id,\n eventType: delivery.eventType,\n organizationId: delivery.organizationId,\n tenantId: delivery.tenantId,\n errorMessage: delivery.errorMessage ?? input.fallbackMessage,\n })\n }\n\n if (webhook.autoDisableThreshold > 0 && webhook.consecutiveFailures >= webhook.autoDisableThreshold) {\n webhook.isActive = false\n await emitWebhooksEvent('webhooks.webhook.disabled', {\n webhookId: webhook.id,\n organizationId: webhook.organizationId,\n tenantId: webhook.tenantId,\n consecutiveFailures: webhook.consecutiveFailures,\n })\n }\n\n await input.em.flush()\n}\n\nfunction normalizeWebhookBody(eventType: string, payload: Record<string, unknown>): WebhookBody {\n const candidateType = typeof payload.type === 'string' ? payload.type : null\n const candidateTimestamp = typeof payload.timestamp === 'string' ? payload.timestamp : null\n const candidateData = isRecord(payload.data) ? payload.data : null\n\n if (candidateType && candidateTimestamp && candidateData) {\n return {\n type: candidateType,\n timestamp: candidateTimestamp,\n data: candidateData,\n }\n }\n\n return {\n type: eventType,\n timestamp: new Date().toISOString(),\n data: payload,\n }\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\nfunction shouldRetryStatus(status: number): boolean {\n if (status >= 200 && status < 300) return false\n if (status === 408 || status === 429) return true\n return status >= 500\n}\n\nfunction calculateNextRetry(attemptNumber: number): Date {\n const baseDelayMs = 1000\n const jitterMs = Math.floor(Math.random() * 1000)\n const delayMs = baseDelayMs * Math.pow(2, Math.max(attemptNumber - 1, 0)) + jitterMs\n return new Date(Date.now() + delayMs)\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/webhooks",
3
- "version": "0.6.6-develop.5491.1.469e89d368",
3
+ "version": "0.6.6-develop.5505.1.f08e81a6fe",
4
4
  "description": "Webhooks module for Open Mercato — Standard Webhooks compliant outbound/inbound delivery",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -69,19 +69,19 @@
69
69
  }
70
70
  },
71
71
  "dependencies": {
72
- "@open-mercato/core": "0.6.6-develop.5491.1.469e89d368",
73
- "@open-mercato/queue": "0.6.6-develop.5491.1.469e89d368",
74
- "@open-mercato/ui": "0.6.6-develop.5491.1.469e89d368",
72
+ "@open-mercato/core": "0.6.6-develop.5505.1.f08e81a6fe",
73
+ "@open-mercato/queue": "0.6.6-develop.5505.1.f08e81a6fe",
74
+ "@open-mercato/ui": "0.6.6-develop.5505.1.f08e81a6fe",
75
75
  "svix": "^1.95.2"
76
76
  },
77
77
  "peerDependencies": {
78
78
  "@mikro-orm/postgresql": "^7.0.14",
79
- "@open-mercato/shared": "0.6.6-develop.5491.1.469e89d368",
79
+ "@open-mercato/shared": "0.6.6-develop.5505.1.f08e81a6fe",
80
80
  "react": "^19.0.0",
81
81
  "react-dom": "^19.0.0"
82
82
  },
83
83
  "devDependencies": {
84
- "@open-mercato/shared": "0.6.6-develop.5491.1.469e89d368",
84
+ "@open-mercato/shared": "0.6.6-develop.5505.1.f08e81a6fe",
85
85
  "@types/jest": "^30.0.0",
86
86
  "@types/react": "^19.2.17",
87
87
  "@types/react-dom": "^19.2.3",
@@ -87,3 +87,29 @@ describe('webhookCreateSchema — URL safety', () => {
87
87
  expect(webhookCreateSchema.safeParse(baseInput({ url: 'https://user:pass@localhost/webhooks' })).success).toBe(false)
88
88
  })
89
89
  })
90
+
91
+ describe('webhookCreateSchema — reserved custom headers', () => {
92
+ it('rejects custom headers that shadow Standard Webhooks signature headers', () => {
93
+ const result = webhookCreateSchema.safeParse(baseInput({ customHeaders: { 'webhook-signature': 'forged' } }))
94
+ expect(result.success).toBe(false)
95
+ if (!result.success) {
96
+ expect(result.error.issues[0].message).toMatch(/reserved/i)
97
+ }
98
+ })
99
+
100
+ it('rejects reserved header names case-insensitively', () => {
101
+ expect(webhookCreateSchema.safeParse(baseInput({ customHeaders: { 'Webhook-Id': 'constant' } })).success).toBe(false)
102
+ expect(webhookCreateSchema.safeParse(baseInput({ customHeaders: { 'WEBHOOK-TIMESTAMP': '0' } })).success).toBe(false)
103
+ expect(webhookCreateSchema.safeParse(baseInput({ customHeaders: { 'Content-Type': 'text/plain' } })).success).toBe(false)
104
+ })
105
+
106
+ it('rejects reserved headers on update', () => {
107
+ const result = webhookUpdateSchema.safeParse({ customHeaders: { 'webhook-signature': 'forged' } })
108
+ expect(result.success).toBe(false)
109
+ })
110
+
111
+ it('accepts non-reserved custom headers', () => {
112
+ expect(webhookCreateSchema.safeParse(baseInput({ customHeaders: { 'x-api-key': 'value', authorization: 'Bearer token' } })).success).toBe(true)
113
+ expect(webhookUpdateSchema.safeParse({ customHeaders: { 'x-api-key': 'value' } }).success).toBe(true)
114
+ })
115
+ })
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod'
2
2
  import { assertStaticallySafeWebhookUrl, UnsafeWebhookUrlError } from '../lib/url-safety'
3
+ import { isReservedWebhookCustomHeader } from '../lib/custom-headers'
3
4
 
4
5
  const safeWebhookUrl = z.string().url().superRefine((value, ctx) => {
5
6
  try {
@@ -12,13 +13,25 @@ const safeWebhookUrl = z.string().url().superRefine((value, ctx) => {
12
13
  }
13
14
  })
14
15
 
16
+ const webhookCustomHeaders = z.record(z.string(), z.string()).superRefine((value, ctx) => {
17
+ for (const name of Object.keys(value)) {
18
+ if (isReservedWebhookCustomHeader(name)) {
19
+ ctx.addIssue({
20
+ code: z.ZodIssueCode.custom,
21
+ message: `Header "${name}" is reserved for webhook signing and cannot be overridden`,
22
+ path: [name],
23
+ })
24
+ }
25
+ }
26
+ })
27
+
15
28
  export const webhookCreateSchema = z.object({
16
29
  name: z.string().min(1).max(255),
17
30
  description: z.string().max(1000).optional().nullable(),
18
31
  url: safeWebhookUrl,
19
32
  subscribedEvents: z.array(z.string().min(1)).min(1),
20
33
  httpMethod: z.enum(['POST', 'PUT', 'PATCH'] as const).default('POST'),
21
- customHeaders: z.record(z.string(), z.string()).optional().nullable(),
34
+ customHeaders: webhookCustomHeaders.optional().nullable(),
22
35
  deliveryStrategy: z.literal('http').default('http'),
23
36
  strategyConfig: z.record(z.string(), z.unknown()).optional().nullable(),
24
37
  maxRetries: z.number().int().min(0).max(30).default(10),
@@ -0,0 +1,169 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql'
2
+ import { processWebhookDeliveryJob } from '../delivery'
3
+ import { isReservedWebhookCustomHeader, sanitizeWebhookCustomHeaders } from '../custom-headers'
4
+
5
+ jest.mock('../../events', () => ({
6
+ emitWebhooksEvent: jest.fn(async () => undefined),
7
+ }))
8
+
9
+ jest.mock('@open-mercato/shared/lib/encryption/find', () => ({
10
+ findOneWithDecryption: jest.fn(),
11
+ }))
12
+
13
+ jest.mock('../integration-state', () => ({
14
+ isWebhookIntegrationEnabled: jest.fn(async () => true),
15
+ WEBHOOK_INTEGRATION_DISABLED_MESSAGE: 'disabled',
16
+ }))
17
+
18
+ jest.mock('@open-mercato/shared/lib/webhooks', () => ({
19
+ buildWebhookHeaders: jest.fn(() => ({
20
+ 'webhook-id': 'msg-1',
21
+ 'webhook-timestamp': '1700000000',
22
+ 'webhook-signature': 'v1,legitimate-signature',
23
+ })),
24
+ generateMessageId: jest.fn(() => 'msg-1'),
25
+ }))
26
+
27
+ import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
28
+
29
+ const findOneWithDecryptionMock = findOneWithDecryption as jest.MockedFunction<typeof findOneWithDecryption>
30
+
31
+ describe('isReservedWebhookCustomHeader', () => {
32
+ it('reserves webhook-* and content-type case-insensitively', () => {
33
+ expect(isReservedWebhookCustomHeader('webhook-signature')).toBe(true)
34
+ expect(isReservedWebhookCustomHeader('Webhook-Id')).toBe(true)
35
+ expect(isReservedWebhookCustomHeader('WEBHOOK-TIMESTAMP')).toBe(true)
36
+ expect(isReservedWebhookCustomHeader(' webhook-signature ')).toBe(true)
37
+ expect(isReservedWebhookCustomHeader('Content-Type')).toBe(true)
38
+ expect(isReservedWebhookCustomHeader('x-custom-header')).toBe(false)
39
+ expect(isReservedWebhookCustomHeader('authorization')).toBe(false)
40
+ })
41
+ })
42
+
43
+ describe('sanitizeWebhookCustomHeaders', () => {
44
+ it('strips reserved headers and keeps the rest', () => {
45
+ expect(sanitizeWebhookCustomHeaders({
46
+ 'Webhook-Signature': 'forged',
47
+ 'webhook-id': 'constant',
48
+ 'Content-Type': 'text/plain',
49
+ 'x-api-key': 'value',
50
+ })).toEqual({ 'x-api-key': 'value' })
51
+ })
52
+
53
+ it('returns an empty object for null or undefined', () => {
54
+ expect(sanitizeWebhookCustomHeaders(null)).toEqual({})
55
+ expect(sanitizeWebhookCustomHeaders(undefined)).toEqual({})
56
+ })
57
+ })
58
+
59
+ describe('processWebhookDeliveryJob — custom headers cannot override signed headers', () => {
60
+ const originalFetch = globalThis.fetch
61
+
62
+ beforeEach(() => {
63
+ jest.clearAllMocks()
64
+ process.env.OM_WEBHOOKS_ALLOW_PRIVATE_URLS = '1'
65
+ globalThis.fetch = jest.fn(async () => new Response('ok', { status: 200 })) as unknown as typeof fetch
66
+ })
67
+
68
+ afterEach(() => {
69
+ globalThis.fetch = originalFetch
70
+ delete process.env.OM_WEBHOOKS_ALLOW_PRIVATE_URLS
71
+ })
72
+
73
+ function buildDelivery() {
74
+ return {
75
+ id: 'delivery-1',
76
+ tenantId: 't',
77
+ organizationId: 'o',
78
+ webhookId: 'w-1',
79
+ status: 'pending',
80
+ payload: { type: 'x', timestamp: new Date().toISOString(), data: {} },
81
+ enqueuedAt: new Date(),
82
+ eventType: 'x',
83
+ maxAttempts: 3,
84
+ attemptNumber: 0,
85
+ messageId: 'msg-1',
86
+ responseBody: null,
87
+ responseHeaders: null,
88
+ responseStatus: null,
89
+ nextRetryAt: null,
90
+ errorMessage: null,
91
+ lastAttemptAt: null,
92
+ deliveredAt: null,
93
+ durationMs: null,
94
+ }
95
+ }
96
+
97
+ function buildWebhook(customHeaders: Record<string, string> | null) {
98
+ return {
99
+ id: 'w-1',
100
+ url: 'http://127.0.0.1:3001/hook',
101
+ isActive: true,
102
+ timeoutMs: 1000,
103
+ httpMethod: 'POST',
104
+ customHeaders,
105
+ consecutiveFailures: 0,
106
+ lastFailureAt: null,
107
+ lastSuccessAt: null,
108
+ autoDisableThreshold: 0,
109
+ maxRetries: 3,
110
+ secret: 'secret',
111
+ previousSecret: null,
112
+ tenantId: 't',
113
+ organizationId: 'o',
114
+ }
115
+ }
116
+
117
+ function buildEm(delivery: Record<string, unknown>) {
118
+ return {
119
+ findOne: jest.fn(async () => delivery),
120
+ flush: jest.fn(async () => undefined),
121
+ } as unknown as EntityManager
122
+ }
123
+
124
+ it('sends signed headers even when customHeaders tries to override them', async () => {
125
+ const delivery = buildDelivery()
126
+ const webhook = buildWebhook({
127
+ 'webhook-signature': 'forged-signature',
128
+ 'Webhook-Id': 'pinned-id',
129
+ 'WEBHOOK-TIMESTAMP': '0',
130
+ 'content-type': 'text/plain',
131
+ 'x-api-key': 'tenant-value',
132
+ })
133
+ findOneWithDecryptionMock.mockResolvedValueOnce(webhook as never)
134
+
135
+ const em = buildEm(delivery)
136
+ const result = await processWebhookDeliveryJob(em, {
137
+ deliveryId: 'delivery-1',
138
+ tenantId: 't',
139
+ organizationId: 'o',
140
+ }, { scheduleRetries: false })
141
+
142
+ expect(result?.status).toBe('delivered')
143
+ expect((globalThis.fetch as jest.Mock).mock.calls).toHaveLength(1)
144
+ const sentHeaders = new Headers((globalThis.fetch as jest.Mock).mock.calls[0][1].headers)
145
+ expect(sentHeaders.get('webhook-signature')).toBe('v1,legitimate-signature')
146
+ expect(sentHeaders.get('webhook-id')).toBe('msg-1')
147
+ expect(sentHeaders.get('webhook-timestamp')).toBe('1700000000')
148
+ expect(sentHeaders.get('content-type')).toBe('application/json')
149
+ expect(sentHeaders.get('x-api-key')).toBe('tenant-value')
150
+ })
151
+
152
+ it('still applies non-reserved custom headers', async () => {
153
+ const delivery = buildDelivery()
154
+ const webhook = buildWebhook({ authorization: 'Bearer receiver-token' })
155
+ findOneWithDecryptionMock.mockResolvedValueOnce(webhook as never)
156
+
157
+ const em = buildEm(delivery)
158
+ const result = await processWebhookDeliveryJob(em, {
159
+ deliveryId: 'delivery-1',
160
+ tenantId: 't',
161
+ organizationId: 'o',
162
+ }, { scheduleRetries: false })
163
+
164
+ expect(result?.status).toBe('delivered')
165
+ const sentHeaders = new Headers((globalThis.fetch as jest.Mock).mock.calls[0][1].headers)
166
+ expect(sentHeaders.get('authorization')).toBe('Bearer receiver-token')
167
+ expect(sentHeaders.get('webhook-signature')).toBe('v1,legitimate-signature')
168
+ })
169
+ })
@@ -0,0 +1,16 @@
1
+ const RESERVED_HEADER_PREFIX = 'webhook-'
2
+ const RESERVED_HEADER_NAMES = new Set(['content-type'])
3
+
4
+ export function isReservedWebhookCustomHeader(name: string): boolean {
5
+ const normalized = name.trim().toLowerCase()
6
+ return normalized.startsWith(RESERVED_HEADER_PREFIX) || RESERVED_HEADER_NAMES.has(normalized)
7
+ }
8
+
9
+ export function sanitizeWebhookCustomHeaders(
10
+ customHeaders: Record<string, string> | null | undefined,
11
+ ): Record<string, string> {
12
+ if (!customHeaders) return {}
13
+ return Object.fromEntries(
14
+ Object.entries(customHeaders).filter(([name]) => !isReservedWebhookCustomHeader(name)),
15
+ )
16
+ }
@@ -5,6 +5,7 @@ import { WebhookDeliveryEntity, WebhookEntity } from '../data/entities'
5
5
  import { emitWebhooksEvent } from '../events'
6
6
  import { enqueueWebhookDelivery } from './queue'
7
7
  import { isWebhookIntegrationEnabled, WEBHOOK_INTEGRATION_DISABLED_MESSAGE } from './integration-state'
8
+ import { sanitizeWebhookCustomHeaders } from './custom-headers'
8
9
  import {
9
10
  assertSafeWebhookDeliveryUrl,
10
11
  safeWebhookFetch,
@@ -165,8 +166,8 @@ export async function processWebhookDeliveryJob(
165
166
  redirect: 'manual',
166
167
  headers: {
167
168
  'content-type': 'application/json',
169
+ ...sanitizeWebhookCustomHeaders(webhook.customHeaders),
168
170
  ...headers,
169
- ...(webhook.customHeaders ?? {}),
170
171
  },
171
172
  body,
172
173
  signal: controller.signal,