@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.
- package/dist/modules/webhooks/api/inbound/[endpointId]/route.js +12 -7
- package/dist/modules/webhooks/api/inbound/[endpointId]/route.js.map +2 -2
- package/dist/modules/webhooks/lib/adapter-registry.js.map +2 -2
- package/dist/modules/webhooks/workers/webhook-delivery.js +3 -3
- package/dist/modules/webhooks/workers/webhook-delivery.js.map +2 -2
- package/package.json +9 -8
- package/src/modules/webhooks/api/inbound/[endpointId]/__tests__/route.test.ts +241 -1
- package/src/modules/webhooks/api/inbound/[endpointId]/route.ts +15 -7
- package/src/modules/webhooks/i18n/pl.json +121 -121
- package/src/modules/webhooks/lib/adapter-registry.ts +7 -0
- package/src/modules/webhooks/workers/__tests__/webhook-delivery.test.ts +2 -2
- package/src/modules/webhooks/workers/webhook-delivery.ts +4 -4
|
@@ -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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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:
|
|
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
|
|
5
|
-
"mappings": "AAAA,SAAS,SAAS;AAClB,SAAS,kBAAkB;AAG3B,SAAS,8BAA8B;AACvC,SAAS,2BAA2B;AACpC,SAAS,gBAAgB,aAAa,2BAA2B,4BAA4B;AAE7F,SAAS,yBAAyB;AAClC,SAAS,iCAAiC;AAC1C,SAAS,
|
|
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;
|
|
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.
|
|
12
|
+
await processWebhookDeliveryJob(em, job.payload);
|
|
13
13
|
} catch (error) {
|
|
14
14
|
logger.error("Job processing failed", {
|
|
15
|
-
deliveryId: job.
|
|
16
|
-
tenantId: job.
|
|
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: {
|
|
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,
|
|
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.
|
|
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.
|
|
74
|
-
"@open-mercato/queue": "0.6.
|
|
75
|
-
"@open-mercato/ui": "0.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.
|
|
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.
|
|
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": "
|
|
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
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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:
|
|
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": "
|
|
2
|
+
"webhooks.deliveries.actions.retry": "Ponów",
|
|
3
3
|
"webhooks.deliveries.actions.viewDetails": "Zobacz szczegóły",
|
|
4
|
-
"webhooks.deliveries.autoRefreshHint": "
|
|
5
|
-
"webhooks.deliveries.columns.attempts": "
|
|
6
|
-
"webhooks.deliveries.columns.createdAt": "
|
|
7
|
-
"webhooks.deliveries.columns.duration": "
|
|
8
|
-
"webhooks.deliveries.columns.enqueuedAt": "
|
|
9
|
-
"webhooks.deliveries.columns.event": "
|
|
10
|
-
"webhooks.deliveries.columns.responseStatus": "HTTP
|
|
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": "
|
|
13
|
-
"webhooks.deliveries.empty": "
|
|
14
|
-
"webhooks.deliveries.loadError": "
|
|
15
|
-
"webhooks.deliveries.refresh": "
|
|
16
|
-
"webhooks.deliveries.refreshing": "
|
|
17
|
-
"webhooks.deliveries.requestBody": "
|
|
18
|
-
"webhooks.deliveries.responseBody": "
|
|
19
|
-
"webhooks.deliveries.responseHeaders": "
|
|
20
|
-
"webhooks.deliveries.retryError": "
|
|
21
|
-
"webhooks.deliveries.retrySuccess": "
|
|
22
|
-
"webhooks.deliveries.status.delivered": "
|
|
23
|
-
"webhooks.deliveries.status.expired": "
|
|
24
|
-
"webhooks.deliveries.status.failed": "
|
|
25
|
-
"webhooks.deliveries.status.pending": "
|
|
26
|
-
"webhooks.deliveries.status.sending": "
|
|
27
|
-
"webhooks.deliveries.title": "
|
|
28
|
-
"webhooks.detail.actions.activate": "
|
|
29
|
-
"webhooks.detail.actions.deactivate": "
|
|
30
|
-
"webhooks.detail.actions.rotateSecret": "
|
|
31
|
-
"webhooks.detail.actions.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": "
|
|
34
|
-
"webhooks.detail.deliveryTip": "
|
|
35
|
-
"webhooks.detail.loadError": "
|
|
36
|
-
"webhooks.detail.loading": "
|
|
37
|
-
"webhooks.detail.previousSecretSetAt": "
|
|
38
|
-
"webhooks.detail.readOnlyTip": "
|
|
39
|
-
"webhooks.detail.rotateError": "
|
|
40
|
-
"webhooks.detail.rotateSuccess": "
|
|
41
|
-
"webhooks.detail.signatureTip": "
|
|
42
|
-
"webhooks.detail.testError": "
|
|
43
|
-
"webhooks.detail.testQueued": "
|
|
44
|
-
"webhooks.detail.testResult": "
|
|
45
|
-
"webhooks.detail.testSuccess": "
|
|
46
|
-
"webhooks.detail.title": "
|
|
47
|
-
"webhooks.errors.forbidden": "
|
|
48
|
-
"webhooks.errors.notFound": "
|
|
49
|
-
"webhooks.errors.orgRequired": "
|
|
50
|
-
"webhooks.errors.tenantRequired": "
|
|
51
|
-
"webhooks.form.advancedStrategyTip": "
|
|
52
|
-
"webhooks.form.autoDisableThreshold": "
|
|
53
|
-
"webhooks.form.autoDisableThresholdHint": "
|
|
54
|
-
"webhooks.form.createError": "
|
|
55
|
-
"webhooks.form.createSuccess": "
|
|
56
|
-
"webhooks.form.customHeaders": "
|
|
57
|
-
"webhooks.form.customHeadersHint": "
|
|
58
|
-
"webhooks.form.customHeadersInvalid": "
|
|
59
|
-
"webhooks.form.customHeadersTip": "
|
|
60
|
-
"webhooks.form.deliveryDefaultsTip": "
|
|
61
|
-
"webhooks.form.description": "
|
|
62
|
-
"webhooks.form.descriptionPlaceholder": "
|
|
63
|
-
"webhooks.form.events": "
|
|
64
|
-
"webhooks.form.eventsHint": "
|
|
65
|
-
"webhooks.form.eventsPatternTip": "
|
|
66
|
-
"webhooks.form.eventsPlaceholder": "
|
|
67
|
-
"webhooks.form.eventsRequired": "
|
|
68
|
-
"webhooks.form.group.advanced": "
|
|
69
|
-
"webhooks.form.group.delivery": "
|
|
70
|
-
"webhooks.form.group.events": "
|
|
71
|
-
"webhooks.form.group.general": "
|
|
72
|
-
"webhooks.form.guidanceBody": "
|
|
73
|
-
"webhooks.form.guidanceSecurityBody": "
|
|
74
|
-
"webhooks.form.guidanceSecurityTitle": "
|
|
75
|
-
"webhooks.form.guidanceTitle": "
|
|
76
|
-
"webhooks.form.httpMethod": "HTTP
|
|
77
|
-
"webhooks.form.isActive": "
|
|
78
|
-
"webhooks.form.maxRetries": "
|
|
79
|
-
"webhooks.form.maxRetriesHint": "
|
|
80
|
-
"webhooks.form.name": "
|
|
81
|
-
"webhooks.form.namePlaceholder": "
|
|
82
|
-
"webhooks.form.rateLimitPerMinute": "
|
|
83
|
-
"webhooks.form.rateLimitPerMinuteHint": "
|
|
84
|
-
"webhooks.form.secret": "
|
|
85
|
-
"webhooks.form.secretCopied": "
|
|
86
|
-
"webhooks.form.secretCopy": "
|
|
87
|
-
"webhooks.form.secretHint": "
|
|
88
|
-
"webhooks.form.secretMissing": "Webhook
|
|
89
|
-
"webhooks.form.secretRotationTip": "
|
|
90
|
-
"webhooks.form.secretUsageTip": "
|
|
91
|
-
"webhooks.form.secretVerificationTip": "
|
|
92
|
-
"webhooks.form.secretVisibleOnce": "
|
|
93
|
-
"webhooks.form.subscribedEvents": "
|
|
94
|
-
"webhooks.form.timeoutMs": "
|
|
95
|
-
"webhooks.form.timeoutMsHint": "
|
|
96
|
-
"webhooks.form.title.create": "
|
|
97
|
-
"webhooks.form.title.edit": "
|
|
98
|
-
"webhooks.form.updateError": "
|
|
99
|
-
"webhooks.form.updateSuccess": "
|
|
100
|
-
"webhooks.form.url": "
|
|
101
|
-
"webhooks.form.urlHint": "
|
|
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": "
|
|
133
|
-
"webhooks.list.actions.edit": "
|
|
134
|
-
"webhooks.list.actions.test": "
|
|
135
|
-
"webhooks.list.actions.viewDeliveries": "
|
|
136
|
-
"webhooks.list.columns.createdAt": "
|
|
137
|
-
"webhooks.list.columns.events": "
|
|
138
|
-
"webhooks.list.columns.lastDelivery": "
|
|
139
|
-
"webhooks.list.columns.lastTriggered": "
|
|
140
|
-
"webhooks.list.columns.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": "
|
|
142
|
+
"webhooks.list.columns.successRate": "Kondycja",
|
|
143
143
|
"webhooks.list.columns.url": "URL",
|
|
144
|
-
"webhooks.list.confirmDelete": "
|
|
145
|
-
"webhooks.list.createFirst": "
|
|
146
|
-
"webhooks.list.deleteError": "
|
|
147
|
-
"webhooks.list.deleteSuccess": "
|
|
148
|
-
"webhooks.list.description": "
|
|
149
|
-
"webhooks.list.empty": "
|
|
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": "
|
|
152
|
-
"webhooks.list.operatorTip": "
|
|
153
|
-
"webhooks.list.status.active": "
|
|
154
|
-
"webhooks.list.status.inactive": "
|
|
155
|
-
"webhooks.list.title": "
|
|
156
|
-
"webhooks.nav.create": "
|
|
157
|
-
"webhooks.nav.group": "
|
|
158
|
-
"webhooks.nav.title": "
|
|
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({
|
|
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({
|
|
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: {
|
|
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.
|
|
19
|
+
await processWebhookDeliveryJob(em, job.payload)
|
|
20
20
|
} catch (error) {
|
|
21
21
|
logger.error('Job processing failed', {
|
|
22
|
-
deliveryId: job.
|
|
23
|
-
tenantId: job.
|
|
22
|
+
deliveryId: job.payload?.deliveryId,
|
|
23
|
+
tenantId: job.payload?.tenantId,
|
|
24
24
|
err: error,
|
|
25
25
|
})
|
|
26
26
|
throw error
|