@open-mercato/webhooks 0.6.6 → 0.6.7-canary.316.1.e6c38e0bef

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.
@@ -5,7 +5,7 @@ import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
5
5
  import { checkRateLimit, getClientIp, RATE_LIMIT_ERROR_FALLBACK, RATE_LIMIT_ERROR_KEY } from "@open-mercato/shared/lib/ratelimit/helpers";
6
6
  import { emitWebhooksEvent } from "../../../events.js";
7
7
  import { getWebhookEndpointAdapter } from "../../../lib/adapter-registry.js";
8
- import { isWebhookIntegrationEnabled } from "../../../lib/integration-state.js";
8
+ import { isWebhookIntegrationEnabled, WEBHOOK_INTEGRATION_DISABLED_MESSAGE } from "../../../lib/integration-state.js";
9
9
  import { json } from "../../helpers.js";
10
10
  import { WebhookInboundReceiptEntity } from "../../../data/entities.js";
11
11
  const metadata = {
@@ -47,14 +47,19 @@ async function POST(request, context) {
47
47
  } catch {
48
48
  return json({ error: "Verification failed" }, { status: 400 });
49
49
  }
50
- if (verified.tenantId && verified.organizationId) {
51
- const integrationEnabled = await isWebhookIntegrationEnabled(em, {
52
- tenantId: verified.tenantId,
53
- organizationId: verified.organizationId
54
- });
50
+ const hasTenantId = Boolean(verified.tenantId);
51
+ const hasOrganizationId = Boolean(verified.organizationId);
52
+ if (hasTenantId !== hasOrganizationId) {
53
+ return json({ error: WEBHOOK_INTEGRATION_DISABLED_MESSAGE }, { status: 503 });
54
+ }
55
+ const integrationScope = hasTenantId && hasOrganizationId ? { tenantId: verified.tenantId, organizationId: verified.organizationId } : null;
56
+ if (integrationScope) {
57
+ const integrationEnabled = await isWebhookIntegrationEnabled(em, integrationScope);
55
58
  if (!integrationEnabled) {
56
- return json({ error: "Custom Webhooks integration is disabled" }, { status: 503 });
59
+ return json({ error: WEBHOOK_INTEGRATION_DISABLED_MESSAGE }, { status: 503 });
57
60
  }
61
+ } else if (!adapter.allowUnscopedInbound) {
62
+ return json({ error: WEBHOOK_INTEGRATION_DISABLED_MESSAGE }, { status: 503 });
58
63
  }
59
64
  const messageId = resolveInboundReceiptMessageId({
60
65
  endpointId: params.endpointId,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/webhooks/api/inbound/%5BendpointId%5D/route.ts"],
4
- "sourcesContent": ["import { z } from 'zod'\nimport { createHash } from 'node:crypto'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { checkRateLimit, getClientIp, RATE_LIMIT_ERROR_FALLBACK, RATE_LIMIT_ERROR_KEY } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport type { RateLimiterService } from '@open-mercato/shared/lib/ratelimit/service'\nimport { emitWebhooksEvent } from '../../../events'\nimport { getWebhookEndpointAdapter } from '../../../lib/adapter-registry'\nimport { isWebhookIntegrationEnabled } from '../../../lib/integration-state'\nimport { json } from '../../helpers'\nimport { WebhookInboundReceiptEntity } from '../../../data/entities'\n\nexport const metadata = {\n POST: { requireAuth: false },\n}\n\ninterface RouteContext {\n params: Promise<{ endpointId: string }>\n}\n\nconst inboundResponseSchema = z.object({\n ok: z.boolean(),\n duplicate: z.boolean().optional(),\n})\n\nconst errorSchema = z.object({ error: z.string() })\n\nexport async function POST(request: Request, context: RouteContext): Promise<Response> {\n const params = await context.params\n const adapter = getWebhookEndpointAdapter(params.endpointId)\n const { translate } = await resolveTranslations()\n\n if (!adapter) {\n return json({ error: 'Webhook endpoint not found' }, { status: 404 })\n }\n\n const container = await createRequestContainer()\n const em = (container.resolve('em') as EntityManager).fork()\n const rateLimiterService = tryResolve<RateLimiterService>(container, 'rateLimiterService')\n\n if (rateLimiterService) {\n const rateLimitResponse = await checkRateLimit(\n rateLimiterService,\n { points: 60, duration: 60, keyPrefix: `webhooks:inbound:${params.endpointId}` },\n buildInboundRateLimitKey(params.endpointId, request, rateLimiterService.trustProxyDepth),\n translate(RATE_LIMIT_ERROR_KEY, RATE_LIMIT_ERROR_FALLBACK),\n )\n\n if (rateLimitResponse) return rateLimitResponse\n }\n\n const body = await request.text()\n const headers = Object.fromEntries(request.headers.entries())\n let verified: Awaited<ReturnType<typeof adapter.verifyWebhook>>\n try {\n verified = await adapter.verifyWebhook({\n headers,\n body,\n method: request.method,\n })\n } catch {\n return json({ error: 'Verification failed' }, { status: 400 })\n }\n\n if (verified.tenantId && verified.organizationId) {\n const integrationEnabled = await isWebhookIntegrationEnabled(em, {\n tenantId: verified.tenantId,\n organizationId: verified.organizationId,\n })\n\n if (!integrationEnabled) {\n return json({ error: 'Custom Webhooks integration is disabled' }, { status: 503 })\n }\n }\n\n const messageId = resolveInboundReceiptMessageId({\n endpointId: params.endpointId,\n providerKey: adapter.providerKey,\n headers,\n body,\n })\n try {\n em.persist(em.create(WebhookInboundReceiptEntity, {\n endpointId: params.endpointId,\n messageId,\n providerKey: adapter.providerKey,\n eventType: verified.eventType,\n tenantId: verified.tenantId ?? null,\n organizationId: verified.organizationId ?? null,\n createdAt: new Date(),\n }))\n await em.flush()\n } catch (error) {\n if (isUniqueViolation(error)) {\n return json({ ok: true, duplicate: true })\n }\n throw error\n }\n\n await emitWebhooksEvent('webhooks.inbound.received', {\n providerKey: adapter.providerKey,\n endpointId: params.endpointId,\n messageId,\n eventType: verified.eventType,\n payload: verified.payload,\n tenantId: verified.tenantId ?? null,\n organizationId: verified.organizationId ?? null,\n }, { persistent: true })\n\n return json({ ok: true })\n}\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Receive inbound webhook',\n description: 'Verifies, rate limits, deduplicates, and processes an inbound webhook using the registered endpoint adapter.',\n methods: {\n POST: {\n summary: 'Receive inbound webhook',\n description: 'Endpoint ids currently resolve to registered adapter provider keys.',\n pathParams: z.object({ endpointId: z.string().min(1) }),\n responses: [{ status: 200, description: 'Inbound webhook accepted', schema: inboundResponseSchema }],\n errors: [\n { status: 400, description: 'Verification failed', schema: errorSchema },\n { status: 404, description: 'Endpoint not found', schema: errorSchema },\n { status: 429, description: 'Rate limit exceeded', schema: errorSchema },\n { status: 503, description: 'Webhook integration disabled', schema: errorSchema },\n ],\n },\n },\n}\n\nfunction tryResolve<T>(container: { resolve: (name: string) => unknown }, name: string): T | null {\n try {\n return container.resolve(name) as T\n } catch {\n return null\n }\n}\n\nfunction isUniqueViolation(error: unknown): boolean {\n if (!error || typeof error !== 'object') return false\n const maybeError = error as { code?: string; cause?: unknown }\n if (maybeError.code === '23505') return true\n if (!maybeError.cause || typeof maybeError.cause !== 'object') return false\n return (maybeError.cause as { code?: string }).code === '23505'\n}\n\ntype ResolveInboundReceiptMessageIdInput = {\n endpointId: string\n providerKey: string\n headers: Record<string, string>\n body: string\n}\n\nexport function buildInboundRateLimitKey(endpointId: string, request: Request, trustProxyDepth: number): string {\n const clientIp = getClientIp(request, trustProxyDepth)\n return clientIp ? `${endpointId}:ip:${clientIp}` : `${endpointId}:global`\n}\n\nexport function resolveInboundReceiptMessageId(\n input: ResolveInboundReceiptMessageIdInput\n): string {\n const explicitMessageId = input.headers['webhook-id'] ?? input.headers['svix-id'] ?? null\n if (typeof explicitMessageId === 'string' && explicitMessageId.trim().length > 0) {\n return explicitMessageId.trim()\n }\n\n const timestamp =\n input.headers['webhook-timestamp'] ??\n input.headers['svix-timestamp'] ??\n null\n\n if (typeof timestamp === 'string' && timestamp.trim().length > 0) {\n const digest = createHash('sha256')\n .update(input.providerKey)\n .update(':')\n .update(input.endpointId)\n .update(':')\n .update(timestamp.trim())\n .update(':')\n .update(input.body)\n .digest('hex')\n\n return `derived:${timestamp.trim()}:${digest}`\n }\n\n const digest = createHash('sha256')\n .update(input.providerKey)\n .update(':')\n .update(input.endpointId)\n .update(':')\n .update(input.body)\n .digest('hex')\n\n return `derived:no-timestamp:${digest}`\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { z } from 'zod'\nimport { createHash } from 'node:crypto'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { checkRateLimit, getClientIp, RATE_LIMIT_ERROR_FALLBACK, RATE_LIMIT_ERROR_KEY } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport type { RateLimiterService } from '@open-mercato/shared/lib/ratelimit/service'\nimport { emitWebhooksEvent } from '../../../events'\nimport { getWebhookEndpointAdapter } from '../../../lib/adapter-registry'\nimport { isWebhookIntegrationEnabled, WEBHOOK_INTEGRATION_DISABLED_MESSAGE } from '../../../lib/integration-state'\nimport { json } from '../../helpers'\nimport { WebhookInboundReceiptEntity } from '../../../data/entities'\n\nexport const metadata = {\n POST: { requireAuth: false },\n}\n\ninterface RouteContext {\n params: Promise<{ endpointId: string }>\n}\n\nconst inboundResponseSchema = z.object({\n ok: z.boolean(),\n duplicate: z.boolean().optional(),\n})\n\nconst errorSchema = z.object({ error: z.string() })\n\nexport async function POST(request: Request, context: RouteContext): Promise<Response> {\n const params = await context.params\n const adapter = getWebhookEndpointAdapter(params.endpointId)\n const { translate } = await resolveTranslations()\n\n if (!adapter) {\n return json({ error: 'Webhook endpoint not found' }, { status: 404 })\n }\n\n const container = await createRequestContainer()\n const em = (container.resolve('em') as EntityManager).fork()\n const rateLimiterService = tryResolve<RateLimiterService>(container, 'rateLimiterService')\n\n if (rateLimiterService) {\n const rateLimitResponse = await checkRateLimit(\n rateLimiterService,\n { points: 60, duration: 60, keyPrefix: `webhooks:inbound:${params.endpointId}` },\n buildInboundRateLimitKey(params.endpointId, request, rateLimiterService.trustProxyDepth),\n translate(RATE_LIMIT_ERROR_KEY, RATE_LIMIT_ERROR_FALLBACK),\n )\n\n if (rateLimitResponse) return rateLimitResponse\n }\n\n const body = await request.text()\n const headers = Object.fromEntries(request.headers.entries())\n let verified: Awaited<ReturnType<typeof adapter.verifyWebhook>>\n try {\n verified = await adapter.verifyWebhook({\n headers,\n body,\n method: request.method,\n })\n } catch {\n return json({ error: 'Verification failed' }, { status: 400 })\n }\n\n const hasTenantId = Boolean(verified.tenantId)\n const hasOrganizationId = Boolean(verified.organizationId)\n if (hasTenantId !== hasOrganizationId) {\n return json({ error: WEBHOOK_INTEGRATION_DISABLED_MESSAGE }, { status: 503 })\n }\n\n const integrationScope = hasTenantId && hasOrganizationId\n ? { tenantId: verified.tenantId as string, organizationId: verified.organizationId as string }\n : null\n\n if (integrationScope) {\n const integrationEnabled = await isWebhookIntegrationEnabled(em, integrationScope)\n if (!integrationEnabled) {\n return json({ error: WEBHOOK_INTEGRATION_DISABLED_MESSAGE }, { status: 503 })\n }\n } else if (!adapter.allowUnscopedInbound) {\n return json({ error: WEBHOOK_INTEGRATION_DISABLED_MESSAGE }, { status: 503 })\n }\n\n const messageId = resolveInboundReceiptMessageId({\n endpointId: params.endpointId,\n providerKey: adapter.providerKey,\n headers,\n body,\n })\n try {\n em.persist(em.create(WebhookInboundReceiptEntity, {\n endpointId: params.endpointId,\n messageId,\n providerKey: adapter.providerKey,\n eventType: verified.eventType,\n tenantId: verified.tenantId ?? null,\n organizationId: verified.organizationId ?? null,\n createdAt: new Date(),\n }))\n await em.flush()\n } catch (error) {\n if (isUniqueViolation(error)) {\n return json({ ok: true, duplicate: true })\n }\n throw error\n }\n\n await emitWebhooksEvent('webhooks.inbound.received', {\n providerKey: adapter.providerKey,\n endpointId: params.endpointId,\n messageId,\n eventType: verified.eventType,\n payload: verified.payload,\n tenantId: verified.tenantId ?? null,\n organizationId: verified.organizationId ?? null,\n }, { persistent: true })\n\n return json({ ok: true })\n}\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Receive inbound webhook',\n description: 'Verifies, rate limits, deduplicates, and processes an inbound webhook using the registered endpoint adapter.',\n methods: {\n POST: {\n summary: 'Receive inbound webhook',\n description: 'Endpoint ids currently resolve to registered adapter provider keys.',\n pathParams: z.object({ endpointId: z.string().min(1) }),\n responses: [{ status: 200, description: 'Inbound webhook accepted', schema: inboundResponseSchema }],\n errors: [\n { status: 400, description: 'Verification failed', schema: errorSchema },\n { status: 404, description: 'Endpoint not found', schema: errorSchema },\n { status: 429, description: 'Rate limit exceeded', schema: errorSchema },\n { status: 503, description: 'Webhook integration disabled', schema: errorSchema },\n ],\n },\n },\n}\n\nfunction tryResolve<T>(container: { resolve: (name: string) => unknown }, name: string): T | null {\n try {\n return container.resolve(name) as T\n } catch {\n return null\n }\n}\n\nfunction isUniqueViolation(error: unknown): boolean {\n if (!error || typeof error !== 'object') return false\n const maybeError = error as { code?: string; cause?: unknown }\n if (maybeError.code === '23505') return true\n if (!maybeError.cause || typeof maybeError.cause !== 'object') return false\n return (maybeError.cause as { code?: string }).code === '23505'\n}\n\ntype ResolveInboundReceiptMessageIdInput = {\n endpointId: string\n providerKey: string\n headers: Record<string, string>\n body: string\n}\n\nexport function buildInboundRateLimitKey(endpointId: string, request: Request, trustProxyDepth: number): string {\n const clientIp = getClientIp(request, trustProxyDepth)\n return clientIp ? `${endpointId}:ip:${clientIp}` : `${endpointId}:global`\n}\n\nexport function resolveInboundReceiptMessageId(\n input: ResolveInboundReceiptMessageIdInput\n): string {\n const explicitMessageId = input.headers['webhook-id'] ?? input.headers['svix-id'] ?? null\n if (typeof explicitMessageId === 'string' && explicitMessageId.trim().length > 0) {\n return explicitMessageId.trim()\n }\n\n const timestamp =\n input.headers['webhook-timestamp'] ??\n input.headers['svix-timestamp'] ??\n null\n\n if (typeof timestamp === 'string' && timestamp.trim().length > 0) {\n const digest = createHash('sha256')\n .update(input.providerKey)\n .update(':')\n .update(input.endpointId)\n .update(':')\n .update(timestamp.trim())\n .update(':')\n .update(input.body)\n .digest('hex')\n\n return `derived:${timestamp.trim()}:${digest}`\n }\n\n const digest = createHash('sha256')\n .update(input.providerKey)\n .update(':')\n .update(input.endpointId)\n .update(':')\n .update(input.body)\n .digest('hex')\n\n return `derived:no-timestamp:${digest}`\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["digest"]
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/webhooks/lib/adapter-registry.ts"],
4
- "sourcesContent": ["const WEBHOOK_ENDPOINT_ADAPTERS_KEY = '__openMercatoWebhookEndpointAdapters__'\n\nexport interface WebhookEndpointAdapter {\n readonly providerKey: string\n readonly subscribedEvents: string[]\n\n formatPayload?(event: { type: string; data: unknown }): Promise<{\n url: string\n headers: Record<string, string>\n body: Record<string, unknown>\n method: 'POST' | 'PUT' | 'PATCH'\n }>\n\n verifyWebhook(input: {\n headers: Record<string, string>\n body: string\n method: string\n }): Promise<{\n eventType: string\n payload: Record<string, unknown>\n tenantId?: string\n organizationId?: string\n }>\n\n processInbound(event: {\n eventType: string\n payload: Record<string, unknown>\n tenantId?: string\n organizationId?: string\n providerKey: string\n }): Promise<void>\n}\n\nfunction getRegistry(): Map<string, WebhookEndpointAdapter> {\n const globalState = globalThis as typeof globalThis & {\n [WEBHOOK_ENDPOINT_ADAPTERS_KEY]?: Map<string, WebhookEndpointAdapter>\n }\n\n if (!globalState[WEBHOOK_ENDPOINT_ADAPTERS_KEY]) {\n globalState[WEBHOOK_ENDPOINT_ADAPTERS_KEY] = new Map<string, WebhookEndpointAdapter>()\n }\n\n return globalState[WEBHOOK_ENDPOINT_ADAPTERS_KEY]\n}\n\nexport function registerWebhookEndpointAdapter(adapter: WebhookEndpointAdapter): () => void {\n const registry = getRegistry()\n registry.set(adapter.providerKey, adapter)\n return () => {\n registry.delete(adapter.providerKey)\n }\n}\n\nexport function getWebhookEndpointAdapter(providerKey: string): WebhookEndpointAdapter | undefined {\n return getRegistry().get(providerKey)\n}\n\nexport function listWebhookEndpointAdapters(): WebhookEndpointAdapter[] {\n return Array.from(getRegistry().values())\n}\n\nexport function clearWebhookEndpointAdapters(): void {\n getRegistry().clear()\n}\n"],
5
- "mappings": "AAAA,MAAM,gCAAgC;AAiCtC,SAAS,cAAmD;AAC1D,QAAM,cAAc;AAIpB,MAAI,CAAC,YAAY,6BAA6B,GAAG;AAC/C,gBAAY,6BAA6B,IAAI,oBAAI,IAAoC;AAAA,EACvF;AAEA,SAAO,YAAY,6BAA6B;AAClD;AAEO,SAAS,+BAA+B,SAA6C;AAC1F,QAAM,WAAW,YAAY;AAC7B,WAAS,IAAI,QAAQ,aAAa,OAAO;AACzC,SAAO,MAAM;AACX,aAAS,OAAO,QAAQ,WAAW;AAAA,EACrC;AACF;AAEO,SAAS,0BAA0B,aAAyD;AACjG,SAAO,YAAY,EAAE,IAAI,WAAW;AACtC;AAEO,SAAS,8BAAwD;AACtE,SAAO,MAAM,KAAK,YAAY,EAAE,OAAO,CAAC;AAC1C;AAEO,SAAS,+BAAqC;AACnD,cAAY,EAAE,MAAM;AACtB;",
4
+ "sourcesContent": ["const WEBHOOK_ENDPOINT_ADAPTERS_KEY = '__openMercatoWebhookEndpointAdapters__'\n\nexport interface WebhookEndpointAdapter {\n readonly providerKey: string\n readonly subscribedEvents: string[]\n /**\n * Opt-in for adapters whose verified events intentionally cannot be mapped to\n * tenant/organization scope. Unscoped events cannot be checked against the\n * Custom Webhooks integration enabled state, so the inbound route rejects them\n * by default unless an adapter explicitly declares this behavior.\n */\n readonly allowUnscopedInbound?: boolean\n\n formatPayload?(event: { type: string; data: unknown }): Promise<{\n url: string\n headers: Record<string, string>\n body: Record<string, unknown>\n method: 'POST' | 'PUT' | 'PATCH'\n }>\n\n verifyWebhook(input: {\n headers: Record<string, string>\n body: string\n method: string\n }): Promise<{\n eventType: string\n payload: Record<string, unknown>\n tenantId?: string\n organizationId?: string\n }>\n\n processInbound(event: {\n eventType: string\n payload: Record<string, unknown>\n tenantId?: string\n organizationId?: string\n providerKey: string\n }): Promise<void>\n}\n\nfunction getRegistry(): Map<string, WebhookEndpointAdapter> {\n const globalState = globalThis as typeof globalThis & {\n [WEBHOOK_ENDPOINT_ADAPTERS_KEY]?: Map<string, WebhookEndpointAdapter>\n }\n\n if (!globalState[WEBHOOK_ENDPOINT_ADAPTERS_KEY]) {\n globalState[WEBHOOK_ENDPOINT_ADAPTERS_KEY] = new Map<string, WebhookEndpointAdapter>()\n }\n\n return globalState[WEBHOOK_ENDPOINT_ADAPTERS_KEY]\n}\n\nexport function registerWebhookEndpointAdapter(adapter: WebhookEndpointAdapter): () => void {\n const registry = getRegistry()\n registry.set(adapter.providerKey, adapter)\n return () => {\n registry.delete(adapter.providerKey)\n }\n}\n\nexport function getWebhookEndpointAdapter(providerKey: string): WebhookEndpointAdapter | undefined {\n return getRegistry().get(providerKey)\n}\n\nexport function listWebhookEndpointAdapters(): WebhookEndpointAdapter[] {\n return Array.from(getRegistry().values())\n}\n\nexport function clearWebhookEndpointAdapters(): void {\n getRegistry().clear()\n}\n"],
5
+ "mappings": "AAAA,MAAM,gCAAgC;AAwCtC,SAAS,cAAmD;AAC1D,QAAM,cAAc;AAIpB,MAAI,CAAC,YAAY,6BAA6B,GAAG;AAC/C,gBAAY,6BAA6B,IAAI,oBAAI,IAAoC;AAAA,EACvF;AAEA,SAAO,YAAY,6BAA6B;AAClD;AAEO,SAAS,+BAA+B,SAA6C;AAC1F,QAAM,WAAW,YAAY;AAC7B,WAAS,IAAI,QAAQ,aAAa,OAAO;AACzC,SAAO,MAAM;AACX,aAAS,OAAO,QAAQ,WAAW;AAAA,EACrC;AACF;AAEO,SAAS,0BAA0B,aAAyD;AACjG,SAAO,YAAY,EAAE,IAAI,WAAW;AACtC;AAEO,SAAS,8BAAwD;AACtE,SAAO,MAAM,KAAK,YAAY,EAAE,OAAO,CAAC;AAC1C;AAEO,SAAS,+BAAqC;AACnD,cAAY,EAAE,MAAM;AACtB;",
6
6
  "names": []
7
7
  }
@@ -9,11 +9,11 @@ const metadata = {
9
9
  async function handler(job, ctx) {
10
10
  const em = ctx.resolve("em").fork();
11
11
  try {
12
- await processWebhookDeliveryJob(em, job.data);
12
+ await processWebhookDeliveryJob(em, job.payload);
13
13
  } catch (error) {
14
14
  logger.error("Job processing failed", {
15
- deliveryId: job.data.deliveryId,
16
- tenantId: job.data.tenantId,
15
+ deliveryId: job.payload?.deliveryId,
16
+ tenantId: job.payload?.tenantId,
17
17
  err: error
18
18
  });
19
19
  throw error;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/webhooks/workers/webhook-delivery.ts"],
4
- "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { processWebhookDeliveryJob, type WebhookDeliveryJob } from '../lib/delivery'\n\nconst logger = createLogger('webhooks').child({ component: 'delivery' })\n\nexport const metadata = {\n queue: 'webhook-deliveries',\n id: 'webhooks:delivery-worker',\n concurrency: 10,\n}\n\nexport default async function handler(\n job: { data: WebhookDeliveryJob },\n ctx: { resolve: <T = unknown>(name: string) => T },\n) {\n const em = (ctx.resolve('em') as EntityManager).fork()\n try {\n await processWebhookDeliveryJob(em, job.data)\n } catch (error) {\n logger.error('Job processing failed', {\n deliveryId: job.data.deliveryId,\n tenantId: job.data.tenantId,\n err: error,\n })\n throw error\n }\n}\n"],
5
- "mappings": "AACA,SAAS,oBAAoB;AAC7B,SAAS,iCAA0D;AAEnE,MAAM,SAAS,aAAa,UAAU,EAAE,MAAM,EAAE,WAAW,WAAW,CAAC;AAEhE,MAAM,WAAW;AAAA,EACtB,OAAO;AAAA,EACP,IAAI;AAAA,EACJ,aAAa;AACf;AAEA,eAAO,QACL,KACA,KACA;AACA,QAAM,KAAM,IAAI,QAAQ,IAAI,EAAoB,KAAK;AACrD,MAAI;AACF,UAAM,0BAA0B,IAAI,IAAI,IAAI;AAAA,EAC9C,SAAS,OAAO;AACd,WAAO,MAAM,yBAAyB;AAAA,MACpC,YAAY,IAAI,KAAK;AAAA,MACrB,UAAU,IAAI,KAAK;AAAA,MACnB,KAAK;AAAA,IACP,CAAC;AACD,UAAM;AAAA,EACR;AACF;",
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { processWebhookDeliveryJob, type WebhookDeliveryJob } from '../lib/delivery'\n\nconst logger = createLogger('webhooks').child({ component: 'delivery' })\n\nexport const metadata = {\n queue: 'webhook-deliveries',\n id: 'webhooks:delivery-worker',\n concurrency: 10,\n}\n\nexport default async function handler(\n job: { payload: WebhookDeliveryJob },\n ctx: { resolve: <T = unknown>(name: string) => T },\n) {\n const em = (ctx.resolve('em') as EntityManager).fork()\n try {\n await processWebhookDeliveryJob(em, job.payload)\n } catch (error) {\n logger.error('Job processing failed', {\n deliveryId: job.payload?.deliveryId,\n tenantId: job.payload?.tenantId,\n err: error,\n })\n throw error\n }\n}\n"],
5
+ "mappings": "AACA,SAAS,oBAAoB;AAC7B,SAAS,iCAA0D;AAEnE,MAAM,SAAS,aAAa,UAAU,EAAE,MAAM,EAAE,WAAW,WAAW,CAAC;AAEhE,MAAM,WAAW;AAAA,EACtB,OAAO;AAAA,EACP,IAAI;AAAA,EACJ,aAAa;AACf;AAEA,eAAO,QACL,KACA,KACA;AACA,QAAM,KAAM,IAAI,QAAQ,IAAI,EAAoB,KAAK;AACrD,MAAI;AACF,UAAM,0BAA0B,IAAI,IAAI,OAAO;AAAA,EACjD,SAAS,OAAO;AACd,WAAO,MAAM,yBAAyB;AAAA,MACpC,YAAY,IAAI,SAAS;AAAA,MACzB,UAAU,IAAI,SAAS;AAAA,MACvB,KAAK;AAAA,IACP,CAAC;AACD,UAAM;AAAA,EACR;AACF;",
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",
3
+ "version": "0.6.7-canary.316.1.e6c38e0bef",
4
4
  "license": "MIT",
5
5
  "description": "Webhooks module for Open Mercato — Standard Webhooks compliant outbound/inbound delivery",
6
6
  "type": "module",
@@ -70,19 +70,19 @@
70
70
  }
71
71
  },
72
72
  "dependencies": {
73
- "@open-mercato/core": "0.6.6",
74
- "@open-mercato/queue": "0.6.6",
75
- "@open-mercato/ui": "0.6.6",
73
+ "@open-mercato/core": "0.6.7-canary.316.1.e6c38e0bef",
74
+ "@open-mercato/queue": "0.6.7-canary.316.1.e6c38e0bef",
75
+ "@open-mercato/ui": "0.6.7-canary.316.1.e6c38e0bef",
76
76
  "svix": "^1.96.1"
77
77
  },
78
78
  "peerDependencies": {
79
79
  "@mikro-orm/postgresql": "^7.0.14",
80
- "@open-mercato/shared": "0.6.6",
80
+ "@open-mercato/shared": "0.6.7-canary.316.1.e6c38e0bef",
81
81
  "react": "^19.0.0",
82
82
  "react-dom": "^19.0.0"
83
83
  },
84
84
  "devDependencies": {
85
- "@open-mercato/shared": "0.6.6",
85
+ "@open-mercato/shared": "0.6.7-canary.316.1.e6c38e0bef",
86
86
  "@types/jest": "^30.0.0",
87
87
  "@types/react": "^19.2.17",
88
88
  "@types/react-dom": "^19.2.3",
@@ -92,7 +92,7 @@
92
92
  "react": "19.2.7",
93
93
  "react-dom": "19.2.7",
94
94
  "ts-jest": "^29.4.11",
95
- "typescript": "^6.0.3"
95
+ "typescript": "7.0.2"
96
96
  },
97
97
  "publishConfig": {
98
98
  "access": "public"
@@ -101,5 +101,6 @@
101
101
  "type": "git",
102
102
  "url": "https://github.com/open-mercato/open-mercato",
103
103
  "directory": "packages/webhooks"
104
- }
104
+ },
105
+ "stableVersion": "0.6.6"
105
106
  }
@@ -1,4 +1,244 @@
1
- import { buildInboundRateLimitKey, resolveInboundReceiptMessageId } from '../route'
1
+ /** @jest-environment node */
2
+
3
+ const mockCreateRequestContainer = jest.fn()
4
+ const mockResolveTranslations = jest.fn()
5
+ const mockEmitWebhooksEvent = jest.fn()
6
+ const mockGetWebhookEndpointAdapter = jest.fn()
7
+ const mockIsWebhookIntegrationEnabled = jest.fn()
8
+
9
+ const mockPersistedEntities: unknown[] = []
10
+
11
+ const mockRouteEm = {
12
+ create: jest.fn((_entity: unknown, data: Record<string, unknown>) => ({ ...data })),
13
+ persist: jest.fn((entity: unknown) => {
14
+ mockPersistedEntities.push(entity)
15
+ return mockRouteEm
16
+ }),
17
+ flush: jest.fn(async () => undefined),
18
+ }
19
+
20
+ const mockRootEm = {
21
+ fork: jest.fn(() => mockRouteEm),
22
+ }
23
+
24
+ const mockContainer = {
25
+ resolve: jest.fn((name: string) => {
26
+ if (name === 'em') return mockRootEm
27
+ throw new Error(`Unexpected dependency: ${name}`)
28
+ }),
29
+ }
30
+
31
+ jest.mock('@open-mercato/shared/lib/di/container', () => ({
32
+ createRequestContainer: () => mockCreateRequestContainer(),
33
+ }))
34
+
35
+ jest.mock('@open-mercato/shared/lib/i18n/server', () => ({
36
+ resolveTranslations: () => mockResolveTranslations(),
37
+ }))
38
+
39
+ jest.mock('../../../../events', () => ({
40
+ emitWebhooksEvent: (...args: unknown[]) => mockEmitWebhooksEvent(...args),
41
+ }))
42
+
43
+ jest.mock('../../../../lib/adapter-registry', () => ({
44
+ getWebhookEndpointAdapter: (...args: unknown[]) => mockGetWebhookEndpointAdapter(...args),
45
+ }))
46
+
47
+ jest.mock('../../../../lib/integration-state', () => ({
48
+ isWebhookIntegrationEnabled: (...args: unknown[]) => mockIsWebhookIntegrationEnabled(...args),
49
+ WEBHOOK_INTEGRATION_DISABLED_MESSAGE: 'Custom Webhooks integration is disabled',
50
+ }))
51
+
52
+ jest.mock('../../../helpers', () => ({
53
+ json: (payload: unknown, init: ResponseInit = { status: 200 }) =>
54
+ new Response(JSON.stringify(payload), {
55
+ ...init,
56
+ headers: { 'content-type': 'application/json' },
57
+ }),
58
+ }))
59
+
60
+ import type { WebhookEndpointAdapter } from '../../../../lib/adapter-registry'
61
+ import { POST, buildInboundRateLimitKey, resolveInboundReceiptMessageId } from '../route'
62
+
63
+ const routeContext = { params: Promise.resolve({ endpointId: 'mock_inbound' }) }
64
+
65
+ function createRequest(body: string = '{"ok":true}') {
66
+ return new Request('http://localhost/api/webhooks/inbound/mock_inbound', {
67
+ method: 'POST',
68
+ headers: {
69
+ 'content-type': 'application/json',
70
+ 'webhook-id': 'msg-1',
71
+ },
72
+ body,
73
+ })
74
+ }
75
+
76
+ function createAdapter(
77
+ verified: Awaited<ReturnType<WebhookEndpointAdapter['verifyWebhook']>>,
78
+ overrides: Partial<WebhookEndpointAdapter> = {},
79
+ ): WebhookEndpointAdapter {
80
+ return {
81
+ providerKey: 'mock_inbound',
82
+ subscribedEvents: ['*'],
83
+ verifyWebhook: jest.fn(async () => verified),
84
+ processInbound: jest.fn(async () => undefined),
85
+ ...overrides,
86
+ }
87
+ }
88
+
89
+ describe('POST', () => {
90
+ beforeEach(() => {
91
+ jest.clearAllMocks()
92
+ mockPersistedEntities.length = 0
93
+ mockCreateRequestContainer.mockResolvedValue(mockContainer)
94
+ mockResolveTranslations.mockResolvedValue({
95
+ translate: (_key: string, fallback?: string) => fallback ?? '',
96
+ })
97
+ mockIsWebhookIntegrationEnabled.mockResolvedValue(true)
98
+ })
99
+
100
+ it('rejects verified inbound webhooks without tenant and organization scope before side effects', async () => {
101
+ mockGetWebhookEndpointAdapter.mockReturnValue(createAdapter({
102
+ eventType: 'mock.inbound.received',
103
+ payload: { ok: true },
104
+ }))
105
+
106
+ const response = await POST(createRequest(), routeContext)
107
+
108
+ expect(response.status).toBe(503)
109
+ await expect(response.json()).resolves.toEqual({
110
+ error: 'Custom Webhooks integration is disabled',
111
+ })
112
+ expect(mockIsWebhookIntegrationEnabled).not.toHaveBeenCalled()
113
+ expect(mockRouteEm.persist).not.toHaveBeenCalled()
114
+ expect(mockRouteEm.flush).not.toHaveBeenCalled()
115
+ expect(mockEmitWebhooksEvent).not.toHaveBeenCalled()
116
+ })
117
+
118
+ it.each([
119
+ ['tenant only', { tenantId: 'tenant-1' }],
120
+ ['organization only', { organizationId: 'org-1' }],
121
+ ] as const)('rejects verified inbound webhooks with partial scope before side effects: %s', async (_label, partialScope) => {
122
+ mockGetWebhookEndpointAdapter.mockReturnValue(createAdapter({
123
+ eventType: 'mock.inbound.received',
124
+ payload: { ok: true },
125
+ ...partialScope,
126
+ }))
127
+
128
+ const response = await POST(createRequest(), routeContext)
129
+
130
+ expect(response.status).toBe(503)
131
+ await expect(response.json()).resolves.toEqual({
132
+ error: 'Custom Webhooks integration is disabled',
133
+ })
134
+ expect(mockIsWebhookIntegrationEnabled).not.toHaveBeenCalled()
135
+ expect(mockRouteEm.persist).not.toHaveBeenCalled()
136
+ expect(mockRouteEm.flush).not.toHaveBeenCalled()
137
+ expect(mockEmitWebhooksEvent).not.toHaveBeenCalled()
138
+ })
139
+
140
+ it('allows adapters that explicitly opt in to unscoped inbound events', async () => {
141
+ mockGetWebhookEndpointAdapter.mockReturnValue(createAdapter(
142
+ {
143
+ eventType: 'mock.inbound.received',
144
+ payload: { ok: true },
145
+ },
146
+ { allowUnscopedInbound: true },
147
+ ))
148
+
149
+ const response = await POST(createRequest(), routeContext)
150
+
151
+ expect(response.status).toBe(200)
152
+ await expect(response.json()).resolves.toEqual({ ok: true })
153
+ expect(mockIsWebhookIntegrationEnabled).not.toHaveBeenCalled()
154
+ expect(mockPersistedEntities).toEqual([
155
+ expect.objectContaining({
156
+ endpointId: 'mock_inbound',
157
+ messageId: 'msg-1',
158
+ providerKey: 'mock_inbound',
159
+ eventType: 'mock.inbound.received',
160
+ tenantId: null,
161
+ organizationId: null,
162
+ }),
163
+ ])
164
+ expect(mockEmitWebhooksEvent).toHaveBeenCalledWith(
165
+ 'webhooks.inbound.received',
166
+ expect.objectContaining({
167
+ providerKey: 'mock_inbound',
168
+ endpointId: 'mock_inbound',
169
+ messageId: 'msg-1',
170
+ eventType: 'mock.inbound.received',
171
+ tenantId: null,
172
+ organizationId: null,
173
+ }),
174
+ { persistent: true },
175
+ )
176
+ })
177
+
178
+ it('rejects scoped inbound webhooks before side effects when the integration is disabled', async () => {
179
+ mockIsWebhookIntegrationEnabled.mockResolvedValue(false)
180
+ mockGetWebhookEndpointAdapter.mockReturnValue(createAdapter({
181
+ eventType: 'mock.inbound.received',
182
+ payload: { ok: true },
183
+ tenantId: 'tenant-1',
184
+ organizationId: 'org-1',
185
+ }))
186
+
187
+ const response = await POST(createRequest(), routeContext)
188
+
189
+ expect(response.status).toBe(503)
190
+ await expect(response.json()).resolves.toEqual({
191
+ error: 'Custom Webhooks integration is disabled',
192
+ })
193
+ expect(mockIsWebhookIntegrationEnabled).toHaveBeenCalledWith(mockRouteEm, {
194
+ tenantId: 'tenant-1',
195
+ organizationId: 'org-1',
196
+ })
197
+ expect(mockRouteEm.persist).not.toHaveBeenCalled()
198
+ expect(mockRouteEm.flush).not.toHaveBeenCalled()
199
+ expect(mockEmitWebhooksEvent).not.toHaveBeenCalled()
200
+ })
201
+
202
+ it('persists and emits scoped inbound webhooks when the integration is enabled', async () => {
203
+ mockGetWebhookEndpointAdapter.mockReturnValue(createAdapter({
204
+ eventType: 'mock.inbound.received',
205
+ payload: { ok: true },
206
+ tenantId: 'tenant-1',
207
+ organizationId: 'org-1',
208
+ }))
209
+
210
+ const response = await POST(createRequest(), routeContext)
211
+
212
+ expect(response.status).toBe(200)
213
+ await expect(response.json()).resolves.toEqual({ ok: true })
214
+ expect(mockIsWebhookIntegrationEnabled).toHaveBeenCalledWith(mockRouteEm, {
215
+ tenantId: 'tenant-1',
216
+ organizationId: 'org-1',
217
+ })
218
+ expect(mockPersistedEntities).toEqual([
219
+ expect.objectContaining({
220
+ endpointId: 'mock_inbound',
221
+ messageId: 'msg-1',
222
+ providerKey: 'mock_inbound',
223
+ eventType: 'mock.inbound.received',
224
+ tenantId: 'tenant-1',
225
+ organizationId: 'org-1',
226
+ }),
227
+ ])
228
+ expect(mockEmitWebhooksEvent).toHaveBeenCalledWith(
229
+ 'webhooks.inbound.received',
230
+ expect.objectContaining({
231
+ providerKey: 'mock_inbound',
232
+ endpointId: 'mock_inbound',
233
+ messageId: 'msg-1',
234
+ eventType: 'mock.inbound.received',
235
+ tenantId: 'tenant-1',
236
+ organizationId: 'org-1',
237
+ }),
238
+ { persistent: true },
239
+ )
240
+ })
241
+ })
2
242
 
3
243
  describe('buildInboundRateLimitKey', () => {
4
244
  it('uses an endpoint-global bucket when proxy headers are untrusted', () => {
@@ -8,7 +8,7 @@ import { checkRateLimit, getClientIp, RATE_LIMIT_ERROR_FALLBACK, RATE_LIMIT_ERRO
8
8
  import type { RateLimiterService } from '@open-mercato/shared/lib/ratelimit/service'
9
9
  import { emitWebhooksEvent } from '../../../events'
10
10
  import { getWebhookEndpointAdapter } from '../../../lib/adapter-registry'
11
- import { isWebhookIntegrationEnabled } from '../../../lib/integration-state'
11
+ import { isWebhookIntegrationEnabled, WEBHOOK_INTEGRATION_DISABLED_MESSAGE } from '../../../lib/integration-state'
12
12
  import { json } from '../../helpers'
13
13
  import { WebhookInboundReceiptEntity } from '../../../data/entities'
14
14
 
@@ -64,15 +64,23 @@ export async function POST(request: Request, context: RouteContext): Promise<Res
64
64
  return json({ error: 'Verification failed' }, { status: 400 })
65
65
  }
66
66
 
67
- if (verified.tenantId && verified.organizationId) {
68
- const integrationEnabled = await isWebhookIntegrationEnabled(em, {
69
- tenantId: verified.tenantId,
70
- organizationId: verified.organizationId,
71
- })
67
+ const hasTenantId = Boolean(verified.tenantId)
68
+ const hasOrganizationId = Boolean(verified.organizationId)
69
+ if (hasTenantId !== hasOrganizationId) {
70
+ return json({ error: WEBHOOK_INTEGRATION_DISABLED_MESSAGE }, { status: 503 })
71
+ }
72
+
73
+ const integrationScope = hasTenantId && hasOrganizationId
74
+ ? { tenantId: verified.tenantId as string, organizationId: verified.organizationId as string }
75
+ : null
72
76
 
77
+ if (integrationScope) {
78
+ const integrationEnabled = await isWebhookIntegrationEnabled(em, integrationScope)
73
79
  if (!integrationEnabled) {
74
- return json({ error: 'Custom Webhooks integration is disabled' }, { status: 503 })
80
+ return json({ error: WEBHOOK_INTEGRATION_DISABLED_MESSAGE }, { status: 503 })
75
81
  }
82
+ } else if (!adapter.allowUnscopedInbound) {
83
+ return json({ error: WEBHOOK_INTEGRATION_DISABLED_MESSAGE }, { status: 503 })
76
84
  }
77
85
 
78
86
  const messageId = resolveInboundReceiptMessageId({
@@ -1,104 +1,104 @@
1
1
  {
2
- "webhooks.deliveries.actions.retry": "Retry",
2
+ "webhooks.deliveries.actions.retry": "Ponów",
3
3
  "webhooks.deliveries.actions.viewDetails": "Zobacz szczegóły",
4
- "webhooks.deliveries.autoRefreshHint": "The delivery log refreshes automatically while this page is open.",
5
- "webhooks.deliveries.columns.attempts": "Attempts",
6
- "webhooks.deliveries.columns.createdAt": "Time",
7
- "webhooks.deliveries.columns.duration": "Duration",
8
- "webhooks.deliveries.columns.enqueuedAt": "Enqueued At",
9
- "webhooks.deliveries.columns.event": "Event",
10
- "webhooks.deliveries.columns.responseStatus": "HTTP Status",
4
+ "webhooks.deliveries.autoRefreshHint": "Log dostaw odświeża się automatycznie, dopóki ta strona jest otwarta.",
5
+ "webhooks.deliveries.columns.attempts": "Próby",
6
+ "webhooks.deliveries.columns.createdAt": "Czas",
7
+ "webhooks.deliveries.columns.duration": "Czas trwania",
8
+ "webhooks.deliveries.columns.enqueuedAt": "Dodano do kolejki",
9
+ "webhooks.deliveries.columns.event": "Zdarzenie",
10
+ "webhooks.deliveries.columns.responseStatus": "Status HTTP",
11
11
  "webhooks.deliveries.columns.status": "Status",
12
- "webhooks.deliveries.detailTitle": "Delivery Detail",
13
- "webhooks.deliveries.empty": "No delivery attempts yet.",
14
- "webhooks.deliveries.loadError": "Failed to load delivery detail.",
15
- "webhooks.deliveries.refresh": "Refresh",
16
- "webhooks.deliveries.refreshing": "Refreshing...",
17
- "webhooks.deliveries.requestBody": "Request Body",
18
- "webhooks.deliveries.responseBody": "Response Body",
19
- "webhooks.deliveries.responseHeaders": "Response Headers",
20
- "webhooks.deliveries.retryError": "Failed to retry delivery.",
21
- "webhooks.deliveries.retrySuccess": "Delivery re-enqueued successfully.",
22
- "webhooks.deliveries.status.delivered": "Delivered",
23
- "webhooks.deliveries.status.expired": "Expired",
24
- "webhooks.deliveries.status.failed": "Failed",
25
- "webhooks.deliveries.status.pending": "Pending",
26
- "webhooks.deliveries.status.sending": "Sending",
27
- "webhooks.deliveries.title": "Delivery Log",
28
- "webhooks.detail.actions.activate": "Activate",
29
- "webhooks.detail.actions.deactivate": "Deactivate",
30
- "webhooks.detail.actions.rotateSecret": "Rotate Secret",
31
- "webhooks.detail.actions.test": "Send Test",
12
+ "webhooks.deliveries.detailTitle": "Szczegóły dostawy",
13
+ "webhooks.deliveries.empty": "Nie ma jeszcze żadnych prób dostawy.",
14
+ "webhooks.deliveries.loadError": "Nie udało się załadować szczegółów dostawy.",
15
+ "webhooks.deliveries.refresh": "Odśwież",
16
+ "webhooks.deliveries.refreshing": "Odświeżanie...",
17
+ "webhooks.deliveries.requestBody": "Treść żądania",
18
+ "webhooks.deliveries.responseBody": "Treść odpowiedzi",
19
+ "webhooks.deliveries.responseHeaders": "Nagłówki odpowiedzi",
20
+ "webhooks.deliveries.retryError": "Nie udało się ponowić dostawy.",
21
+ "webhooks.deliveries.retrySuccess": "Dostawa została ponownie dodana do kolejki.",
22
+ "webhooks.deliveries.status.delivered": "Dostarczono",
23
+ "webhooks.deliveries.status.expired": "Wygasła",
24
+ "webhooks.deliveries.status.failed": "Niepowodzenie",
25
+ "webhooks.deliveries.status.pending": "Oczekuje",
26
+ "webhooks.deliveries.status.sending": "Wysyłanie",
27
+ "webhooks.deliveries.title": "Log dostaw",
28
+ "webhooks.detail.actions.activate": "Aktywuj",
29
+ "webhooks.detail.actions.deactivate": "Dezaktywuj",
30
+ "webhooks.detail.actions.rotateSecret": "Zmień sekret",
31
+ "webhooks.detail.actions.test": "Wyślij test",
32
32
  "webhooks.detail.backToList": "Wróć do webhooków",
33
- "webhooks.detail.consecutiveFailures": "Consecutive Failures",
34
- "webhooks.detail.deliveryTip": "Start with a test delivery before enabling a broad event pattern. The delivery log below shows exactly what your consumer returned.",
35
- "webhooks.detail.loadError": "Failed to load webhook.",
36
- "webhooks.detail.loading": "Loading webhook details...",
37
- "webhooks.detail.previousSecretSetAt": "Previous Secret Active Since",
38
- "webhooks.detail.readOnlyTip": "You currently have read-only access. Ask an administrator for manage, test, or secret rotation permissions if you need to change this endpoint.",
39
- "webhooks.detail.rotateError": "Failed to rotate webhook secret.",
40
- "webhooks.detail.rotateSuccess": "Webhook secret rotated",
41
- "webhooks.detail.signatureTip": "Consumers should verify the Standard Webhooks headers: webhook-id, webhook-timestamp, and webhook-signature.",
42
- "webhooks.detail.testError": "Failed to send test delivery.",
43
- "webhooks.detail.testQueued": "Test delivery finished with a non-success status.",
44
- "webhooks.detail.testResult": "Latest Test Result",
45
- "webhooks.detail.testSuccess": "Test delivery succeeded.",
46
- "webhooks.detail.title": "Webhook Detail",
47
- "webhooks.errors.forbidden": "You do not have permission to perform this action.",
48
- "webhooks.errors.notFound": "Webhook not found.",
49
- "webhooks.errors.orgRequired": "Organization context required.",
50
- "webhooks.errors.tenantRequired": "Tenant context required.",
51
- "webhooks.form.advancedStrategyTip": "HTTP delivery is active today. Use custom headers for routing hints, and keep secrets out of request headers.",
52
- "webhooks.form.autoDisableThreshold": "Auto-Disable Threshold",
53
- "webhooks.form.autoDisableThresholdHint": "Disable the webhook after this many consecutive failures (0 disables auto-disable).",
54
- "webhooks.form.createError": "Failed to create webhook.",
55
- "webhooks.form.createSuccess": "Webhook created successfully.",
56
- "webhooks.form.customHeaders": "Custom Headers",
57
- "webhooks.form.customHeadersHint": "Optional JSON object of additional headers to send with each webhook request.",
58
- "webhooks.form.customHeadersInvalid": "Custom headers must be a valid JSON object with string values.",
59
- "webhooks.form.customHeadersTip": "Use headers for static metadata such as environment or source identifiers. Do not put access tokens or secrets here.",
60
- "webhooks.form.deliveryDefaultsTip": "The defaults are a safe starting point for most consumers. Increase retries or timeout only if the receiver is known to be slow.",
61
- "webhooks.form.description": "Description",
62
- "webhooks.form.descriptionPlaceholder": "Optional description for this webhook",
63
- "webhooks.form.events": "Subscribed Events",
64
- "webhooks.form.eventsHint": "Select which events should trigger this webhook.",
65
- "webhooks.form.eventsPatternTip": "Tip: you can pick exact events from the list or type wildcard patterns like customers.* or * to subscribe broadly.",
66
- "webhooks.form.eventsPlaceholder": "Select events...",
67
- "webhooks.form.eventsRequired": "Select at least one event.",
68
- "webhooks.form.group.advanced": "Advanced",
69
- "webhooks.form.group.delivery": "Delivery Settings",
70
- "webhooks.form.group.events": "Event Subscription",
71
- "webhooks.form.group.general": "General",
72
- "webhooks.form.guidanceBody": "Configure where events should be delivered, choose the event patterns you need, then use Send Test to validate the consumer before going live.",
73
- "webhooks.form.guidanceSecurityBody": "A signing secret is generated automatically. You will only see it in full after create or rotate, so store it in your receiver before closing the page.",
74
- "webhooks.form.guidanceSecurityTitle": "Security Checklist",
75
- "webhooks.form.guidanceTitle": "Setup Guide",
76
- "webhooks.form.httpMethod": "HTTP Method",
77
- "webhooks.form.isActive": "Active",
78
- "webhooks.form.maxRetries": "Max Retries",
79
- "webhooks.form.maxRetriesHint": "Maximum number of delivery retry attempts (0-30).",
80
- "webhooks.form.name": "Name",
81
- "webhooks.form.namePlaceholder": "My Webhook",
82
- "webhooks.form.rateLimitPerMinute": "Rate Limit Per Minute",
83
- "webhooks.form.rateLimitPerMinuteHint": "Maximum deliveries per minute for this endpoint (0 means unlimited).",
84
- "webhooks.form.secret": "Signing Secret",
85
- "webhooks.form.secretCopied": "Secret copied to clipboard.",
86
- "webhooks.form.secretCopy": "Copy Secret",
87
- "webhooks.form.secretHint": "This secret is used to sign webhook payloads. Keep it safe.",
88
- "webhooks.form.secretMissing": "Webhook created but secret was not returned.",
89
- "webhooks.form.secretRotationTip": "Rotate the secret only after your consumer accepts signatures generated with both the old and new secret during the transition window.",
90
- "webhooks.form.secretUsageTip": "Store this secret in your receiving service now. Future reads only show a masked value.",
91
- "webhooks.form.secretVerificationTip": "Use this value to verify the webhook-signature header together with webhook-id and webhook-timestamp.",
92
- "webhooks.form.secretVisibleOnce": "This is the only time you can see this secret. Store it securely now.",
93
- "webhooks.form.subscribedEvents": "Subscribed Events",
94
- "webhooks.form.timeoutMs": "Timeout (ms)",
95
- "webhooks.form.timeoutMsHint": "Request timeout in milliseconds (1000-60000).",
96
- "webhooks.form.title.create": "Create Webhook",
97
- "webhooks.form.title.edit": "Edit Webhook",
98
- "webhooks.form.updateError": "Failed to update webhook.",
99
- "webhooks.form.updateSuccess": "Webhook updated successfully.",
100
- "webhooks.form.url": "Endpoint URL",
101
- "webhooks.form.urlHint": "The URL that will receive webhook payloads via HTTP POST.",
33
+ "webhooks.detail.consecutiveFailures": "Kolejne niepowodzenia",
34
+ "webhooks.detail.deliveryTip": "Zanim włączysz szeroki wzorzec zdarzeń, zacznij od testowej dostawy. Log dostaw poniżej pokazuje dokładnie, co zwrócił Twój odbiorca.",
35
+ "webhooks.detail.loadError": "Nie udało się załadować webhooka.",
36
+ "webhooks.detail.loading": "Ładowanie szczegółów webhooka...",
37
+ "webhooks.detail.previousSecretSetAt": "Poprzedni sekret aktywny od",
38
+ "webhooks.detail.readOnlyTip": "Masz obecnie dostęp tylko do odczytu. Jeśli chcesz zmienić ten endpoint, poproś administratora o uprawnienia do zarządzania, testowania lub zmiany sekretu.",
39
+ "webhooks.detail.rotateError": "Nie udało się zmienić sekretu webhooka.",
40
+ "webhooks.detail.rotateSuccess": "Zmieniono sekret webhooka",
41
+ "webhooks.detail.signatureTip": "Odbiorcy powinni weryfikować nagłówki Standard Webhooks: webhook-id, webhook-timestamp i webhook-signature.",
42
+ "webhooks.detail.testError": "Nie udało się wysłać testowej dostawy.",
43
+ "webhooks.detail.testQueued": "Testowa dostawa zakończyła się statusem innym niż sukces.",
44
+ "webhooks.detail.testResult": "Wynik ostatniego testu",
45
+ "webhooks.detail.testSuccess": "Testowa dostawa powiodła się.",
46
+ "webhooks.detail.title": "Szczegóły webhooka",
47
+ "webhooks.errors.forbidden": "Nie masz uprawnień do wykonania tej akcji.",
48
+ "webhooks.errors.notFound": "Nie znaleziono webhooka.",
49
+ "webhooks.errors.orgRequired": "Wymagany jest kontekst organizacji.",
50
+ "webhooks.errors.tenantRequired": "Wymagany jest kontekst najemcy.",
51
+ "webhooks.form.advancedStrategyTip": "Obecnie działa dostawa przez HTTP. Nagłówków własnych używaj do wskazówek routingu i nie umieszczaj w nich sekretów.",
52
+ "webhooks.form.autoDisableThreshold": "Próg automatycznego wyłączenia",
53
+ "webhooks.form.autoDisableThresholdHint": "Wyłącz webhook po tylu kolejnych niepowodzeniach (0 wyłącza automatyczne wyłączanie).",
54
+ "webhooks.form.createError": "Nie udało się utworzyć webhooka.",
55
+ "webhooks.form.createSuccess": "Utworzono webhook.",
56
+ "webhooks.form.customHeaders": "Nagłówki własne",
57
+ "webhooks.form.customHeadersHint": "Opcjonalny obiekt JSON z dodatkowymi nagłówkami wysyłanymi z każdym żądaniem webhooka.",
58
+ "webhooks.form.customHeadersInvalid": "Nagłówki własne muszą być poprawnym obiektem JSON z wartościami tekstowymi.",
59
+ "webhooks.form.customHeadersTip": "Używaj nagłówków do statycznych metadanych, takich jak środowisko czy identyfikator źródła. Nie umieszczaj w nich tokenów dostępu ani sekretów.",
60
+ "webhooks.form.deliveryDefaultsTip": "Wartości domyślne są bezpiecznym punktem wyjścia dla większości odbiorców. Zwiększaj liczbę ponowień lub limit czasu tylko wtedy, gdy wiesz, że odbiorca działa wolno.",
61
+ "webhooks.form.description": "Opis",
62
+ "webhooks.form.descriptionPlaceholder": "Opcjonalny opis tego webhooka",
63
+ "webhooks.form.events": "Subskrybowane zdarzenia",
64
+ "webhooks.form.eventsHint": "Wybierz, które zdarzenia mają wyzwalać ten webhook.",
65
+ "webhooks.form.eventsPatternTip": "Wskazówka: możesz wybrać konkretne zdarzenia z listy albo wpisać wzorce z gwiazdką, np. customers.* lub *, aby subskrybować szerzej.",
66
+ "webhooks.form.eventsPlaceholder": "Wybierz zdarzenia...",
67
+ "webhooks.form.eventsRequired": "Wybierz co najmniej jedno zdarzenie.",
68
+ "webhooks.form.group.advanced": "Zaawansowane",
69
+ "webhooks.form.group.delivery": "Ustawienia dostawy",
70
+ "webhooks.form.group.events": "Subskrypcja zdarzeń",
71
+ "webhooks.form.group.general": "Ogólne",
72
+ "webhooks.form.guidanceBody": "Skonfiguruj, dokąd mają trafiać zdarzenia, wybierz potrzebne wzorce zdarzeń, a następnie użyj opcji Wyślij test, aby sprawdzić odbiorcę przed uruchomieniem produkcyjnym.",
73
+ "webhooks.form.guidanceSecurityBody": "Sekret podpisujący jest generowany automatycznie. Zobaczysz go w całości tylko po utworzeniu lub zmianie, więc zapisz go w swoim odbiorcy przed zamknięciem strony.",
74
+ "webhooks.form.guidanceSecurityTitle": "Lista kontrolna bezpieczeństwa",
75
+ "webhooks.form.guidanceTitle": "Przewodnik konfiguracji",
76
+ "webhooks.form.httpMethod": "Metoda HTTP",
77
+ "webhooks.form.isActive": "Aktywny",
78
+ "webhooks.form.maxRetries": "Maks. liczba ponowień",
79
+ "webhooks.form.maxRetriesHint": "Maksymalna liczba prób ponowienia dostawy (0-30).",
80
+ "webhooks.form.name": "Nazwa",
81
+ "webhooks.form.namePlaceholder": "Mój webhook",
82
+ "webhooks.form.rateLimitPerMinute": "Limit na minutę",
83
+ "webhooks.form.rateLimitPerMinuteHint": "Maksymalna liczba dostaw na minutę dla tego endpointu (0 oznacza brak limitu).",
84
+ "webhooks.form.secret": "Sekret podpisujący",
85
+ "webhooks.form.secretCopied": "Skopiowano sekret do schowka.",
86
+ "webhooks.form.secretCopy": "Kopiuj sekret",
87
+ "webhooks.form.secretHint": "Ten sekret służy do podpisywania payloadów webhooków. Przechowuj go bezpiecznie.",
88
+ "webhooks.form.secretMissing": "Webhook został utworzony, ale sekret nie został zwrócony.",
89
+ "webhooks.form.secretRotationTip": "Zmieniaj sekret dopiero wtedy, gdy Twój odbiorca akceptuje podpisy wygenerowane starym i nowym sekretem w okresie przejściowym.",
90
+ "webhooks.form.secretUsageTip": "Zapisz ten sekret w swojej usłudze odbiorczej teraz. Przy kolejnych odczytach zobaczysz tylko wartość zamaskowaną.",
91
+ "webhooks.form.secretVerificationTip": "Użyj tej wartości do weryfikacji nagłówka webhook-signature razem z webhook-id i webhook-timestamp.",
92
+ "webhooks.form.secretVisibleOnce": "To jedyny moment, w którym możesz zobaczyć ten sekret. Zapisz go teraz w bezpiecznym miejscu.",
93
+ "webhooks.form.subscribedEvents": "Subskrybowane zdarzenia",
94
+ "webhooks.form.timeoutMs": "Limit czasu (ms)",
95
+ "webhooks.form.timeoutMsHint": "Limit czasu żądania w milisekundach (1000-60000).",
96
+ "webhooks.form.title.create": "Utwórz webhook",
97
+ "webhooks.form.title.edit": "Edytuj webhook",
98
+ "webhooks.form.updateError": "Nie udało się zaktualizować webhooka.",
99
+ "webhooks.form.updateSuccess": "Zaktualizowano webhook.",
100
+ "webhooks.form.url": "Adres URL endpointu",
101
+ "webhooks.form.urlHint": "Adres URL, który będzie odbierał payloady webhooków przez HTTP POST.",
102
102
  "webhooks.form.urlPlaceholder": "https://example.com/webhook",
103
103
  "webhooks.integrationDeliveries.actions.openWebhook": "Otwórz webhook",
104
104
  "webhooks.integrationDeliveries.columns.webhook": "Webhook",
@@ -129,33 +129,33 @@
129
129
  "webhooks.integrationSetup.summary": "Zarządzaj Custom Webhooks z dedykowanego ekranu webhooków. Wyłączenie tej integracji blokuje dostawy wychodzące, wysyłki testowe, ponowienia i odbiór webhooków przychodzących.",
130
130
  "webhooks.integrationSetup.title": "Konfiguracja webhooków",
131
131
  "webhooks.integrationTabs.settings": "Ustawienia",
132
- "webhooks.list.actions.delete": "Delete",
133
- "webhooks.list.actions.edit": "Edit",
134
- "webhooks.list.actions.test": "Test",
135
- "webhooks.list.actions.viewDeliveries": "View Deliveries",
136
- "webhooks.list.columns.createdAt": "Created",
137
- "webhooks.list.columns.events": "Events",
138
- "webhooks.list.columns.lastDelivery": "Last Delivery",
139
- "webhooks.list.columns.lastTriggered": "Last Triggered",
140
- "webhooks.list.columns.name": "Name",
132
+ "webhooks.list.actions.delete": "Usuń",
133
+ "webhooks.list.actions.edit": "Edytuj",
134
+ "webhooks.list.actions.test": "Testuj",
135
+ "webhooks.list.actions.viewDeliveries": "Zobacz dostawy",
136
+ "webhooks.list.columns.createdAt": "Utworzono",
137
+ "webhooks.list.columns.events": "Zdarzenia",
138
+ "webhooks.list.columns.lastDelivery": "Ostatnia dostawa",
139
+ "webhooks.list.columns.lastTriggered": "Ostatnie wyzwolenie",
140
+ "webhooks.list.columns.name": "Nazwa",
141
141
  "webhooks.list.columns.status": "Status",
142
- "webhooks.list.columns.successRate": "Health",
142
+ "webhooks.list.columns.successRate": "Kondycja",
143
143
  "webhooks.list.columns.url": "URL",
144
- "webhooks.list.confirmDelete": "Are you sure you want to delete this webhook? This action cannot be undone.",
145
- "webhooks.list.createFirst": "Create your first webhook to start receiving event notifications.",
146
- "webhooks.list.deleteError": "Failed to delete webhook.",
147
- "webhooks.list.deleteSuccess": "Webhook deleted successfully.",
148
- "webhooks.list.description": "Manage webhook endpoints for receiving platform events.",
149
- "webhooks.list.empty": "No webhooks configured yet.",
144
+ "webhooks.list.confirmDelete": "Czy na pewno chcesz usunąć ten webhook? Tej operacji nie można cofnąć.",
145
+ "webhooks.list.createFirst": "Utwórz pierwszy webhook, aby zacząć otrzymywać powiadomienia o zdarzeniach.",
146
+ "webhooks.list.deleteError": "Nie udało się usunąć webhooka.",
147
+ "webhooks.list.deleteSuccess": "Usunięto webhook.",
148
+ "webhooks.list.description": "Zarządzaj endpointami webhooków odbierającymi zdarzenia platformy.",
149
+ "webhooks.list.empty": "Nie skonfigurowano jeszcze żadnych webhooków.",
150
150
  "webhooks.list.filters.status": "Status",
151
- "webhooks.list.loadError": "Failed to load webhooks.",
152
- "webhooks.list.operatorTip": "Use exact events for narrow automations, or wildcard patterns when one consumer handles a whole domain. Check delivery logs after each rollout.",
153
- "webhooks.list.status.active": "Active",
154
- "webhooks.list.status.inactive": "Inactive",
155
- "webhooks.list.title": "Webhooks",
156
- "webhooks.nav.create": "Create Webhook",
157
- "webhooks.nav.group": "Integrations",
158
- "webhooks.nav.title": "Webhooks",
151
+ "webhooks.list.loadError": "Nie udało się załadować webhooków.",
152
+ "webhooks.list.operatorTip": "Do wąskich automatyzacji używaj konkretnych zdarzeń, a wzorców z gwiazdką wtedy, gdy jeden odbiorca obsługuje całą domenę. Po każdym wdrożeniu sprawdzaj logi dostaw.",
153
+ "webhooks.list.status.active": "Aktywny",
154
+ "webhooks.list.status.inactive": "Nieaktywny",
155
+ "webhooks.list.title": "Webhooki",
156
+ "webhooks.nav.create": "Utwórz webhook",
157
+ "webhooks.nav.group": "Integracje",
158
+ "webhooks.nav.title": "Webhooki",
159
159
  "webhooks.notifications.delivery.failed.body": "Webhook {webhookName} nie mógł dostarczyć zdarzenia {eventType}. {errorMessage}",
160
160
  "webhooks.notifications.delivery.failed.title": "Dostawa webhooka nie powiodła się dla {webhookName}"
161
161
  }
@@ -3,6 +3,13 @@ const WEBHOOK_ENDPOINT_ADAPTERS_KEY = '__openMercatoWebhookEndpointAdapters__'
3
3
  export interface WebhookEndpointAdapter {
4
4
  readonly providerKey: string
5
5
  readonly subscribedEvents: string[]
6
+ /**
7
+ * Opt-in for adapters whose verified events intentionally cannot be mapped to
8
+ * tenant/organization scope. Unscoped events cannot be checked against the
9
+ * Custom Webhooks integration enabled state, so the inbound route rejects them
10
+ * by default unless an adapter explicitly declares this behavior.
11
+ */
12
+ readonly allowUnscopedInbound?: boolean
6
13
 
7
14
  formatPayload?(event: { type: string; data: unknown }): Promise<{
8
15
  url: string
@@ -27,7 +27,7 @@ describe('webhooks delivery worker', () => {
27
27
  const em = { fork: jest.fn().mockReturnThis() } as unknown as EntityManager
28
28
  mockProcessWebhookDeliveryJob.mockResolvedValue(null)
29
29
 
30
- await handler({ data: jobData }, makeCtx(em))
30
+ await handler({ payload: jobData }, makeCtx(em))
31
31
 
32
32
  expect(mockProcessWebhookDeliveryJob).toHaveBeenCalledWith(em, jobData)
33
33
  })
@@ -37,6 +37,6 @@ describe('webhooks delivery worker', () => {
37
37
  const cause = new Error('DB connection lost')
38
38
  mockProcessWebhookDeliveryJob.mockRejectedValue(cause)
39
39
 
40
- await expect(handler({ data: jobData }, makeCtx(em))).rejects.toThrow('DB connection lost')
40
+ await expect(handler({ payload: jobData }, makeCtx(em))).rejects.toThrow('DB connection lost')
41
41
  })
42
42
  })
@@ -11,16 +11,16 @@ export const metadata = {
11
11
  }
12
12
 
13
13
  export default async function handler(
14
- job: { data: WebhookDeliveryJob },
14
+ job: { payload: WebhookDeliveryJob },
15
15
  ctx: { resolve: <T = unknown>(name: string) => T },
16
16
  ) {
17
17
  const em = (ctx.resolve('em') as EntityManager).fork()
18
18
  try {
19
- await processWebhookDeliveryJob(em, job.data)
19
+ await processWebhookDeliveryJob(em, job.payload)
20
20
  } catch (error) {
21
21
  logger.error('Job processing failed', {
22
- deliveryId: job.data.deliveryId,
23
- tenantId: job.data.tenantId,
22
+ deliveryId: job.payload?.deliveryId,
23
+ tenantId: job.payload?.tenantId,
24
24
  err: error,
25
25
  })
26
26
  throw error