@open-mercato/webhooks 0.6.7-develop.6726.1.983ae8a07e → 0.6.7-develop.6749.1.6b54c56dfe
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/.turbo/turbo-build.log +1 -1
- package/dist/modules/webhooks/api/inbound/[endpointId]/route.js +113 -4
- package/dist/modules/webhooks/api/inbound/[endpointId]/route.js.map +3 -3
- package/dist/modules/webhooks/data/entities.js +101 -1
- package/dist/modules/webhooks/data/entities.js.map +2 -2
- package/dist/modules/webhooks/encryption.js +7 -0
- package/dist/modules/webhooks/encryption.js.map +2 -2
- package/dist/modules/webhooks/events.js +2 -0
- package/dist/modules/webhooks/events.js.map +2 -2
- package/dist/modules/webhooks/generators.js +57 -0
- package/dist/modules/webhooks/generators.js.map +7 -0
- package/dist/modules/webhooks/lib/inbound-dispatch.js +105 -0
- package/dist/modules/webhooks/lib/inbound-dispatch.js.map +7 -0
- package/dist/modules/webhooks/lib/inbound-registry.js +76 -0
- package/dist/modules/webhooks/lib/inbound-registry.js.map +7 -0
- package/dist/modules/webhooks/lib/module-webhook-registry.js +12 -0
- package/dist/modules/webhooks/lib/module-webhook-registry.js.map +7 -0
- package/dist/modules/webhooks/lib/queue.js +47 -0
- package/dist/modules/webhooks/lib/queue.js.map +2 -2
- package/dist/modules/webhooks/migrations/Migration20260617141327_webhooks.js +16 -0
- package/dist/modules/webhooks/migrations/Migration20260617141327_webhooks.js.map +7 -0
- package/dist/modules/webhooks/workers/inbound-dispatch.js +29 -0
- package/dist/modules/webhooks/workers/inbound-dispatch.js.map +7 -0
- package/generated/entities/inbound_endpoint_config_entity/index.ts +8 -0
- package/generated/entities/webhook_ingestion_entity/index.ts +16 -0
- package/generated/entities.ids.generated.ts +3 -1
- package/generated/entity-fields-registry.ts +28 -0
- package/package.json +6 -6
- package/src/modules/webhooks/api/inbound/[endpointId]/__tests__/route.unify.test.ts +108 -0
- package/src/modules/webhooks/api/inbound/[endpointId]/route.ts +136 -4
- package/src/modules/webhooks/data/entities.ts +84 -0
- package/src/modules/webhooks/encryption.ts +7 -0
- package/src/modules/webhooks/events.ts +2 -0
- package/src/modules/webhooks/generators.ts +57 -0
- package/src/modules/webhooks/lib/__tests__/inbound-dispatch.test.ts +186 -0
- package/src/modules/webhooks/lib/__tests__/inbound-registry.test.ts +102 -0
- package/src/modules/webhooks/lib/__tests__/module-webhook-registry.test.ts +53 -0
- package/src/modules/webhooks/lib/inbound-dispatch.ts +147 -0
- package/src/modules/webhooks/lib/inbound-registry.ts +92 -0
- package/src/modules/webhooks/lib/module-webhook-registry.ts +33 -0
- package/src/modules/webhooks/lib/queue.ts +60 -0
- package/src/modules/webhooks/migrations/.snapshot-open-mercato.json +907 -338
- package/src/modules/webhooks/migrations/Migration20260617141327_webhooks.ts +16 -0
- package/src/modules/webhooks/workers/inbound-dispatch.ts +31 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
2
|
+
import { WebhookIngestionEntity } from "../data/entities.js";
|
|
3
|
+
import { emitWebhooksEvent } from "../events.js";
|
|
4
|
+
import { resolveWebhookHandlers } from "./inbound-registry.js";
|
|
5
|
+
const MAX_HANDLER_RESULTS = 50;
|
|
6
|
+
const MAX_ERROR_MESSAGE_LENGTH = 1024;
|
|
7
|
+
function truncateError(message) {
|
|
8
|
+
return message.length > MAX_ERROR_MESSAGE_LENGTH ? `${message.slice(0, MAX_ERROR_MESSAGE_LENGTH)}\u2026` : message;
|
|
9
|
+
}
|
|
10
|
+
function moduleOfHandler(handlerId) {
|
|
11
|
+
const separatorIndex = handlerId.indexOf(":");
|
|
12
|
+
return separatorIndex > 0 ? handlerId.slice(0, separatorIndex) : handlerId;
|
|
13
|
+
}
|
|
14
|
+
async function processInboundDispatchJob(em, job, ctx) {
|
|
15
|
+
const startedAtMs = Date.now();
|
|
16
|
+
const ingestion = await findOneWithDecryption(
|
|
17
|
+
em,
|
|
18
|
+
WebhookIngestionEntity,
|
|
19
|
+
{
|
|
20
|
+
id: job.ingestionId,
|
|
21
|
+
tenantId: job.tenantId,
|
|
22
|
+
organizationId: job.organizationId
|
|
23
|
+
},
|
|
24
|
+
{}
|
|
25
|
+
);
|
|
26
|
+
if (!ingestion || ingestion.status === "processed") return;
|
|
27
|
+
ingestion.status = "processing";
|
|
28
|
+
await em.flush();
|
|
29
|
+
const handlers = resolveWebhookHandlers(job.sourceKey, job.eventType);
|
|
30
|
+
const payload = {
|
|
31
|
+
data: ingestion.payload,
|
|
32
|
+
eventType: job.eventType,
|
|
33
|
+
sourceKey: job.sourceKey,
|
|
34
|
+
headers: ingestion.headers ?? {},
|
|
35
|
+
ingestionId: job.ingestionId,
|
|
36
|
+
tenantId: job.tenantId,
|
|
37
|
+
organizationId: job.organizationId
|
|
38
|
+
};
|
|
39
|
+
const previousResults = Array.isArray(ingestion.handlerResults) ? ingestion.handlerResults : [];
|
|
40
|
+
const alreadySucceeded = new Map(
|
|
41
|
+
previousResults.filter((result) => result?.status === "success").map((result) => [result.handlerId, result])
|
|
42
|
+
);
|
|
43
|
+
const results = [];
|
|
44
|
+
let failedCount = 0;
|
|
45
|
+
for (const entry of handlers) {
|
|
46
|
+
const previousSuccess = alreadySucceeded.get(entry.meta.id);
|
|
47
|
+
if (previousSuccess) {
|
|
48
|
+
results.push(previousSuccess);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const handlerStartedMs = Date.now();
|
|
52
|
+
const handlerStartedAt = new Date(handlerStartedMs).toISOString();
|
|
53
|
+
try {
|
|
54
|
+
const mod = await entry.handler();
|
|
55
|
+
await mod.default(payload, ctx);
|
|
56
|
+
results.push({
|
|
57
|
+
handlerId: entry.meta.id,
|
|
58
|
+
module: moduleOfHandler(entry.meta.id),
|
|
59
|
+
status: "success",
|
|
60
|
+
durationMs: Date.now() - handlerStartedMs,
|
|
61
|
+
startedAt: handlerStartedAt
|
|
62
|
+
});
|
|
63
|
+
} catch (error) {
|
|
64
|
+
failedCount += 1;
|
|
65
|
+
const message = truncateError(error instanceof Error ? error.message : String(error));
|
|
66
|
+
results.push({
|
|
67
|
+
handlerId: entry.meta.id,
|
|
68
|
+
module: moduleOfHandler(entry.meta.id),
|
|
69
|
+
status: "failed",
|
|
70
|
+
errorMessage: message,
|
|
71
|
+
durationMs: Date.now() - handlerStartedMs,
|
|
72
|
+
startedAt: handlerStartedAt
|
|
73
|
+
});
|
|
74
|
+
await emitWebhooksEvent("webhooks.inbound.handler_failed", {
|
|
75
|
+
ingestionId: job.ingestionId,
|
|
76
|
+
sourceKey: job.sourceKey,
|
|
77
|
+
eventType: job.eventType,
|
|
78
|
+
handlerId: entry.meta.id,
|
|
79
|
+
errorMessage: message,
|
|
80
|
+
tenantId: job.tenantId,
|
|
81
|
+
organizationId: job.organizationId
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
ingestion.handlerCount = handlers.length;
|
|
86
|
+
ingestion.handlerResults = results.slice(0, MAX_HANDLER_RESULTS);
|
|
87
|
+
ingestion.status = failedCount > 0 ? "failed" : "processed";
|
|
88
|
+
ingestion.processedAt = /* @__PURE__ */ new Date();
|
|
89
|
+
ingestion.durationMs = Date.now() - startedAtMs;
|
|
90
|
+
ingestion.errorMessage = failedCount > 0 ? `${failedCount}/${handlers.length} handlers failed` : null;
|
|
91
|
+
await em.flush();
|
|
92
|
+
await emitWebhooksEvent("webhooks.inbound.processed", {
|
|
93
|
+
ingestionId: job.ingestionId,
|
|
94
|
+
sourceKey: job.sourceKey,
|
|
95
|
+
eventType: job.eventType,
|
|
96
|
+
handlerCount: handlers.length,
|
|
97
|
+
failedCount,
|
|
98
|
+
tenantId: job.tenantId,
|
|
99
|
+
organizationId: job.organizationId
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
export {
|
|
103
|
+
processInboundDispatchJob
|
|
104
|
+
};
|
|
105
|
+
//# sourceMappingURL=inbound-dispatch.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/webhooks/lib/inbound-dispatch.ts"],
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport type {\n WebhookHandlerContext,\n WebhookHandlerPayload,\n WebhookHandlerResult,\n} from '@open-mercato/shared/lib/webhooks'\nimport { WebhookIngestionEntity } from '../data/entities'\nimport { emitWebhooksEvent } from '../events'\nimport { resolveWebhookHandlers } from './inbound-registry'\n\nconst MAX_HANDLER_RESULTS = 50\nconst MAX_ERROR_MESSAGE_LENGTH = 1024\n\nexport type InboundDispatchJob = {\n ingestionId: string\n sourceKey: string\n eventType: string\n tenantId: string\n organizationId: string\n}\n\nfunction truncateError(message: string): string {\n return message.length > MAX_ERROR_MESSAGE_LENGTH\n ? `${message.slice(0, MAX_ERROR_MESSAGE_LENGTH)}\u2026`\n : message\n}\n\nfunction moduleOfHandler(handlerId: string): string {\n const separatorIndex = handlerId.indexOf(':')\n return separatorIndex > 0 ? handlerId.slice(0, separatorIndex) : handlerId\n}\n\n/**\n * Process a queued inbound-dispatch job: load the ingestion, run every matching\n * handler independently (a failing handler never blocks the others), record\n * per-handler results, and emit lifecycle events. The payload and headers are\n * read from the (decrypted) ingestion row rather than carried on the job, so the\n * queue never stores the raw webhook body. Idempotent \u2014 a job whose ingestion is\n * already `processed` returns early, and a retry after a partial failure re-runs\n * only the handlers that have not already succeeded.\n */\nexport async function processInboundDispatchJob(\n em: EntityManager,\n job: InboundDispatchJob,\n ctx: WebhookHandlerContext,\n): Promise<void> {\n const startedAtMs = Date.now()\n const ingestion = await findOneWithDecryption(\n em,\n WebhookIngestionEntity,\n {\n id: job.ingestionId,\n tenantId: job.tenantId,\n organizationId: job.organizationId,\n },\n {},\n )\n\n if (!ingestion || ingestion.status === 'processed') return\n\n ingestion.status = 'processing'\n await em.flush()\n\n const handlers = resolveWebhookHandlers(job.sourceKey, job.eventType)\n const payload: WebhookHandlerPayload = {\n data: ingestion.payload,\n eventType: job.eventType,\n sourceKey: job.sourceKey,\n headers: ingestion.headers ?? {},\n ingestionId: job.ingestionId,\n tenantId: job.tenantId,\n organizationId: job.organizationId,\n }\n\n const previousResults = Array.isArray(ingestion.handlerResults) ? ingestion.handlerResults : []\n const alreadySucceeded = new Map(\n previousResults\n .filter((result) => result?.status === 'success')\n .map((result) => [result.handlerId, result]),\n )\n\n const results: WebhookHandlerResult[] = []\n let failedCount = 0\n\n for (const entry of handlers) {\n const previousSuccess = alreadySucceeded.get(entry.meta.id)\n if (previousSuccess) {\n results.push(previousSuccess)\n continue\n }\n\n const handlerStartedMs = Date.now()\n const handlerStartedAt = new Date(handlerStartedMs).toISOString()\n try {\n const mod = await entry.handler()\n await mod.default(payload, ctx)\n results.push({\n handlerId: entry.meta.id,\n module: moduleOfHandler(entry.meta.id),\n status: 'success',\n durationMs: Date.now() - handlerStartedMs,\n startedAt: handlerStartedAt,\n })\n } catch (error) {\n failedCount += 1\n const message = truncateError(error instanceof Error ? error.message : String(error))\n results.push({\n handlerId: entry.meta.id,\n module: moduleOfHandler(entry.meta.id),\n status: 'failed',\n errorMessage: message,\n durationMs: Date.now() - handlerStartedMs,\n startedAt: handlerStartedAt,\n })\n await emitWebhooksEvent('webhooks.inbound.handler_failed', {\n ingestionId: job.ingestionId,\n sourceKey: job.sourceKey,\n eventType: job.eventType,\n handlerId: entry.meta.id,\n errorMessage: message,\n tenantId: job.tenantId,\n organizationId: job.organizationId,\n })\n }\n }\n\n ingestion.handlerCount = handlers.length\n ingestion.handlerResults = results.slice(0, MAX_HANDLER_RESULTS)\n ingestion.status = failedCount > 0 ? 'failed' : 'processed'\n ingestion.processedAt = new Date()\n ingestion.durationMs = Date.now() - startedAtMs\n ingestion.errorMessage = failedCount > 0\n ? `${failedCount}/${handlers.length} handlers failed`\n : null\n await em.flush()\n\n await emitWebhooksEvent('webhooks.inbound.processed', {\n ingestionId: job.ingestionId,\n sourceKey: job.sourceKey,\n eventType: job.eventType,\n handlerCount: handlers.length,\n failedCount,\n tenantId: job.tenantId,\n organizationId: job.organizationId,\n })\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { matchWebhookEventPattern } from "@open-mercato/shared/lib/events/patterns";
|
|
2
|
+
const WEBHOOK_SOURCES_KEY = "__openMercatoWebhookSources__";
|
|
3
|
+
const WEBHOOK_HANDLERS_KEY = "__openMercatoWebhookHandlers__";
|
|
4
|
+
function getSourceRegistry() {
|
|
5
|
+
const globalState = globalThis;
|
|
6
|
+
if (!globalState[WEBHOOK_SOURCES_KEY]) {
|
|
7
|
+
globalState[WEBHOOK_SOURCES_KEY] = /* @__PURE__ */ new Map();
|
|
8
|
+
}
|
|
9
|
+
return globalState[WEBHOOK_SOURCES_KEY];
|
|
10
|
+
}
|
|
11
|
+
function getHandlerRegistry() {
|
|
12
|
+
const globalState = globalThis;
|
|
13
|
+
if (!globalState[WEBHOOK_HANDLERS_KEY]) {
|
|
14
|
+
globalState[WEBHOOK_HANDLERS_KEY] = [];
|
|
15
|
+
}
|
|
16
|
+
return globalState[WEBHOOK_HANDLERS_KEY];
|
|
17
|
+
}
|
|
18
|
+
function registerWebhookSource(config) {
|
|
19
|
+
const registry = getSourceRegistry();
|
|
20
|
+
registry.set(config.key, config);
|
|
21
|
+
return () => {
|
|
22
|
+
if (registry.get(config.key) === config) registry.delete(config.key);
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function setWebhookSources(configs) {
|
|
26
|
+
const registry = getSourceRegistry();
|
|
27
|
+
registry.clear();
|
|
28
|
+
for (const config of configs) registry.set(config.key, config);
|
|
29
|
+
}
|
|
30
|
+
function getWebhookSource(sourceKey) {
|
|
31
|
+
return getSourceRegistry().get(sourceKey);
|
|
32
|
+
}
|
|
33
|
+
function listWebhookSources() {
|
|
34
|
+
return Array.from(getSourceRegistry().values());
|
|
35
|
+
}
|
|
36
|
+
function clearWebhookSources() {
|
|
37
|
+
getSourceRegistry().clear();
|
|
38
|
+
}
|
|
39
|
+
function registerWebhookHandler(entry) {
|
|
40
|
+
const registry = getHandlerRegistry();
|
|
41
|
+
registry.push(entry);
|
|
42
|
+
return () => {
|
|
43
|
+
const index = registry.indexOf(entry);
|
|
44
|
+
if (index >= 0) registry.splice(index, 1);
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function setWebhookHandlers(entries) {
|
|
48
|
+
const registry = getHandlerRegistry();
|
|
49
|
+
registry.length = 0;
|
|
50
|
+
registry.push(...entries);
|
|
51
|
+
}
|
|
52
|
+
function listWebhookHandlers() {
|
|
53
|
+
return [...getHandlerRegistry()];
|
|
54
|
+
}
|
|
55
|
+
function clearWebhookHandlers() {
|
|
56
|
+
getHandlerRegistry().length = 0;
|
|
57
|
+
}
|
|
58
|
+
function resolveWebhookHandlers(sourceKey, eventType) {
|
|
59
|
+
return getHandlerRegistry().filter((entry) => {
|
|
60
|
+
if (entry.meta.source !== sourceKey) return false;
|
|
61
|
+
return matchWebhookEventPattern(eventType, entry.meta.event);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
export {
|
|
65
|
+
clearWebhookHandlers,
|
|
66
|
+
clearWebhookSources,
|
|
67
|
+
getWebhookSource,
|
|
68
|
+
listWebhookHandlers,
|
|
69
|
+
listWebhookSources,
|
|
70
|
+
registerWebhookHandler,
|
|
71
|
+
registerWebhookSource,
|
|
72
|
+
resolveWebhookHandlers,
|
|
73
|
+
setWebhookHandlers,
|
|
74
|
+
setWebhookSources
|
|
75
|
+
};
|
|
76
|
+
//# sourceMappingURL=inbound-registry.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/webhooks/lib/inbound-registry.ts"],
|
|
4
|
+
"sourcesContent": ["import { matchWebhookEventPattern } from '@open-mercato/shared/lib/events/patterns'\nimport type {\n WebhookHandlerRegistryEntry,\n WebhookSourceConfig,\n} from '@open-mercato/shared/lib/webhooks'\n\nconst WEBHOOK_SOURCES_KEY = '__openMercatoWebhookSources__'\nconst WEBHOOK_HANDLERS_KEY = '__openMercatoWebhookHandlers__'\n\ntype GlobalState = typeof globalThis & {\n [WEBHOOK_SOURCES_KEY]?: Map<string, WebhookSourceConfig>\n [WEBHOOK_HANDLERS_KEY]?: WebhookHandlerRegistryEntry[]\n}\n\nfunction getSourceRegistry(): Map<string, WebhookSourceConfig> {\n const globalState = globalThis as GlobalState\n if (!globalState[WEBHOOK_SOURCES_KEY]) {\n globalState[WEBHOOK_SOURCES_KEY] = new Map<string, WebhookSourceConfig>()\n }\n return globalState[WEBHOOK_SOURCES_KEY]\n}\n\nfunction getHandlerRegistry(): WebhookHandlerRegistryEntry[] {\n const globalState = globalThis as GlobalState\n if (!globalState[WEBHOOK_HANDLERS_KEY]) {\n globalState[WEBHOOK_HANDLERS_KEY] = []\n }\n return globalState[WEBHOOK_HANDLERS_KEY]\n}\n\nexport function registerWebhookSource(config: WebhookSourceConfig): () => void {\n const registry = getSourceRegistry()\n registry.set(config.key, config)\n return () => {\n if (registry.get(config.key) === config) registry.delete(config.key)\n }\n}\n\nexport function setWebhookSources(configs: WebhookSourceConfig[]): void {\n const registry = getSourceRegistry()\n registry.clear()\n for (const config of configs) registry.set(config.key, config)\n}\n\nexport function getWebhookSource(sourceKey: string): WebhookSourceConfig | undefined {\n return getSourceRegistry().get(sourceKey)\n}\n\nexport function listWebhookSources(): WebhookSourceConfig[] {\n return Array.from(getSourceRegistry().values())\n}\n\nexport function clearWebhookSources(): void {\n getSourceRegistry().clear()\n}\n\nexport function registerWebhookHandler(entry: WebhookHandlerRegistryEntry): () => void {\n const registry = getHandlerRegistry()\n registry.push(entry)\n return () => {\n const index = registry.indexOf(entry)\n if (index >= 0) registry.splice(index, 1)\n }\n}\n\nexport function setWebhookHandlers(entries: WebhookHandlerRegistryEntry[]): void {\n const registry = getHandlerRegistry()\n registry.length = 0\n registry.push(...entries)\n}\n\nexport function listWebhookHandlers(): WebhookHandlerRegistryEntry[] {\n return [...getHandlerRegistry()]\n}\n\nexport function clearWebhookHandlers(): void {\n getHandlerRegistry().length = 0\n}\n\n/**\n * Resolve the handlers that match an inbound webhook's source key and event type.\n * Event matching reuses the outbound prefix-wildcard semantics (`*`, `payment_intent.*`).\n */\nexport function resolveWebhookHandlers(\n sourceKey: string,\n eventType: string,\n): WebhookHandlerRegistryEntry[] {\n return getHandlerRegistry().filter((entry) => {\n if (entry.meta.source !== sourceKey) return false\n return matchWebhookEventPattern(eventType, entry.meta.event)\n })\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,gCAAgC;AAMzC,MAAM,sBAAsB;AAC5B,MAAM,uBAAuB;AAO7B,SAAS,oBAAsD;AAC7D,QAAM,cAAc;AACpB,MAAI,CAAC,YAAY,mBAAmB,GAAG;AACrC,gBAAY,mBAAmB,IAAI,oBAAI,IAAiC;AAAA,EAC1E;AACA,SAAO,YAAY,mBAAmB;AACxC;AAEA,SAAS,qBAAoD;AAC3D,QAAM,cAAc;AACpB,MAAI,CAAC,YAAY,oBAAoB,GAAG;AACtC,gBAAY,oBAAoB,IAAI,CAAC;AAAA,EACvC;AACA,SAAO,YAAY,oBAAoB;AACzC;AAEO,SAAS,sBAAsB,QAAyC;AAC7E,QAAM,WAAW,kBAAkB;AACnC,WAAS,IAAI,OAAO,KAAK,MAAM;AAC/B,SAAO,MAAM;AACX,QAAI,SAAS,IAAI,OAAO,GAAG,MAAM,OAAQ,UAAS,OAAO,OAAO,GAAG;AAAA,EACrE;AACF;AAEO,SAAS,kBAAkB,SAAsC;AACtE,QAAM,WAAW,kBAAkB;AACnC,WAAS,MAAM;AACf,aAAW,UAAU,QAAS,UAAS,IAAI,OAAO,KAAK,MAAM;AAC/D;AAEO,SAAS,iBAAiB,WAAoD;AACnF,SAAO,kBAAkB,EAAE,IAAI,SAAS;AAC1C;AAEO,SAAS,qBAA4C;AAC1D,SAAO,MAAM,KAAK,kBAAkB,EAAE,OAAO,CAAC;AAChD;AAEO,SAAS,sBAA4B;AAC1C,oBAAkB,EAAE,MAAM;AAC5B;AAEO,SAAS,uBAAuB,OAAgD;AACrF,QAAM,WAAW,mBAAmB;AACpC,WAAS,KAAK,KAAK;AACnB,SAAO,MAAM;AACX,UAAM,QAAQ,SAAS,QAAQ,KAAK;AACpC,QAAI,SAAS,EAAG,UAAS,OAAO,OAAO,CAAC;AAAA,EAC1C;AACF;AAEO,SAAS,mBAAmB,SAA8C;AAC/E,QAAM,WAAW,mBAAmB;AACpC,WAAS,SAAS;AAClB,WAAS,KAAK,GAAG,OAAO;AAC1B;AAEO,SAAS,sBAAqD;AACnE,SAAO,CAAC,GAAG,mBAAmB,CAAC;AACjC;AAEO,SAAS,uBAA6B;AAC3C,qBAAmB,EAAE,SAAS;AAChC;AAMO,SAAS,uBACd,WACA,WAC+B;AAC/B,SAAO,mBAAmB,EAAE,OAAO,CAAC,UAAU;AAC5C,QAAI,MAAM,KAAK,WAAW,UAAW,QAAO;AAC5C,WAAO,yBAAyB,WAAW,MAAM,KAAK,KAAK;AAAA,EAC7D,CAAC;AACH;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { setWebhookHandlers, setWebhookSources } from "./inbound-registry.js";
|
|
2
|
+
function registerWebhookSourceEntries(entries) {
|
|
3
|
+
setWebhookSources(entries.flatMap((entry) => entry.sources ?? []));
|
|
4
|
+
}
|
|
5
|
+
function registerWebhookHandlerEntries(entries) {
|
|
6
|
+
setWebhookHandlers(entries.flatMap((entry) => entry.handlers ?? []));
|
|
7
|
+
}
|
|
8
|
+
export {
|
|
9
|
+
registerWebhookHandlerEntries,
|
|
10
|
+
registerWebhookSourceEntries
|
|
11
|
+
};
|
|
12
|
+
//# sourceMappingURL=module-webhook-registry.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/webhooks/lib/module-webhook-registry.ts"],
|
|
4
|
+
"sourcesContent": ["import type {\n WebhookHandlerRegistryEntry,\n WebhookSourceConfig,\n} from '@open-mercato/shared/lib/webhooks'\nimport { setWebhookHandlers, setWebhookSources } from './inbound-registry'\n\nexport type WebhookSourceModuleEntry = {\n moduleId: string\n sources: WebhookSourceConfig[]\n}\n\nexport type WebhookHandlerModuleEntry = {\n moduleId: string\n handlers: WebhookHandlerRegistryEntry[]\n}\n\n/**\n * Bootstrap-time registration for module-declared webhook sources.\n * Driven by the `webhooks.sources` generator plugin via\n * `bootstrap-registrations.generated.ts` \u2014 modules contribute a\n * `webhook-sources.ts` and are wired here without bootstrap.ts edits.\n */\nexport function registerWebhookSourceEntries(entries: WebhookSourceModuleEntry[]): void {\n setWebhookSources(entries.flatMap((entry) => entry.sources ?? []))\n}\n\n/**\n * Bootstrap-time registration for module-declared webhook handlers.\n * Driven by the `webhooks.handlers` generator plugin.\n */\nexport function registerWebhookHandlerEntries(entries: WebhookHandlerModuleEntry[]): void {\n setWebhookHandlers(entries.flatMap((entry) => entry.handlers ?? []))\n}\n"],
|
|
5
|
+
"mappings": "AAIA,SAAS,oBAAoB,yBAAyB;AAkB/C,SAAS,6BAA6B,SAA2C;AACtF,oBAAkB,QAAQ,QAAQ,CAAC,UAAU,MAAM,WAAW,CAAC,CAAC,CAAC;AACnE;AAMO,SAAS,8BAA8B,SAA4C;AACxF,qBAAmB,QAAQ,QAAQ,CAAC,UAAU,MAAM,YAAY,CAAC,CAAC,CAAC;AACrE;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -2,8 +2,11 @@ import { createModuleQueue } from "@open-mercato/queue";
|
|
|
2
2
|
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
3
3
|
const logger = createLogger("webhooks");
|
|
4
4
|
const queues = /* @__PURE__ */ new Map();
|
|
5
|
+
const inboundQueues = /* @__PURE__ */ new Map();
|
|
5
6
|
const LOCAL_WORKER_PROMISE_KEY = "__openMercatoWebhookLocalWorkerPromise__";
|
|
7
|
+
const LOCAL_INBOUND_WORKER_PROMISE_KEY = "__openMercatoWebhookLocalInboundWorkerPromise__";
|
|
6
8
|
const WEBHOOK_DELIVERIES_QUEUE = "webhook-deliveries";
|
|
9
|
+
const WEBHOOK_INBOUND_DISPATCH_QUEUE = "webhook-inbound-dispatch";
|
|
7
10
|
function getWebhookQueue(queueName = WEBHOOK_DELIVERIES_QUEUE) {
|
|
8
11
|
const existing = queues.get(queueName);
|
|
9
12
|
if (existing) return existing;
|
|
@@ -43,9 +46,53 @@ async function enqueueWebhookDelivery(job, delayMs) {
|
|
|
43
46
|
await ensureLocalWebhookQueueWorkerStarted();
|
|
44
47
|
return jobId;
|
|
45
48
|
}
|
|
49
|
+
function getInboundDispatchQueue(queueName = WEBHOOK_INBOUND_DISPATCH_QUEUE) {
|
|
50
|
+
const existing = inboundQueues.get(queueName);
|
|
51
|
+
if (existing) return existing;
|
|
52
|
+
const concurrency = Math.max(1, Number.parseInt(process.env.WEBHOOK_INBOUND_QUEUE_CONCURRENCY ?? "5", 10) || 5);
|
|
53
|
+
const created = createModuleQueue(queueName, { concurrency });
|
|
54
|
+
inboundQueues.set(queueName, created);
|
|
55
|
+
return created;
|
|
56
|
+
}
|
|
57
|
+
async function ensureLocalInboundQueueWorkerStarted() {
|
|
58
|
+
if (process.env.QUEUE_STRATEGY === "async") return;
|
|
59
|
+
const globalStore = globalThis;
|
|
60
|
+
if (globalStore[LOCAL_INBOUND_WORKER_PROMISE_KEY]) {
|
|
61
|
+
await globalStore[LOCAL_INBOUND_WORKER_PROMISE_KEY];
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
globalStore[LOCAL_INBOUND_WORKER_PROMISE_KEY] = (async () => {
|
|
65
|
+
const queue = getInboundDispatchQueue();
|
|
66
|
+
await queue.process(async (job) => {
|
|
67
|
+
const [{ createRequestContainer }, { processInboundDispatchJob }] = await Promise.all([
|
|
68
|
+
import("@open-mercato/shared/lib/di/container"),
|
|
69
|
+
import("./inbound-dispatch.js")
|
|
70
|
+
]);
|
|
71
|
+
const container = await createRequestContainer();
|
|
72
|
+
const em = container.resolve("em").fork();
|
|
73
|
+
await processInboundDispatchJob(em, job.payload, {
|
|
74
|
+
resolve: (name) => container.resolve(name)
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
})().catch((error) => {
|
|
78
|
+
delete globalStore[LOCAL_INBOUND_WORKER_PROMISE_KEY];
|
|
79
|
+
logger.error("Failed to start local inbound dispatch worker", { err: error });
|
|
80
|
+
throw error;
|
|
81
|
+
});
|
|
82
|
+
await globalStore[LOCAL_INBOUND_WORKER_PROMISE_KEY];
|
|
83
|
+
}
|
|
84
|
+
async function enqueueInboundDispatch(job) {
|
|
85
|
+
const queue = getInboundDispatchQueue();
|
|
86
|
+
const jobId = await queue.enqueue(job);
|
|
87
|
+
await ensureLocalInboundQueueWorkerStarted();
|
|
88
|
+
return jobId;
|
|
89
|
+
}
|
|
46
90
|
export {
|
|
47
91
|
WEBHOOK_DELIVERIES_QUEUE,
|
|
92
|
+
WEBHOOK_INBOUND_DISPATCH_QUEUE,
|
|
93
|
+
enqueueInboundDispatch,
|
|
48
94
|
enqueueWebhookDelivery,
|
|
95
|
+
getInboundDispatchQueue,
|
|
49
96
|
getWebhookQueue
|
|
50
97
|
};
|
|
51
98
|
//# sourceMappingURL=queue.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/webhooks/lib/queue.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { createModuleQueue, type Queue } from '@open-mercato/queue'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport type { WebhookDeliveryJob } from './delivery'\n\nconst logger = createLogger('webhooks')\n\nconst queues = new Map<string, Queue<WebhookDeliveryJob>>()\nconst LOCAL_WORKER_PROMISE_KEY = '__openMercatoWebhookLocalWorkerPromise__'\n\nexport const WEBHOOK_DELIVERIES_QUEUE = 'webhook-deliveries'\n\nexport function getWebhookQueue(queueName: string = WEBHOOK_DELIVERIES_QUEUE): Queue<WebhookDeliveryJob> {\n const existing = queues.get(queueName)\n if (existing) return existing\n\n const concurrency = Math.max(1, Number.parseInt(process.env.WEBHOOK_QUEUE_CONCURRENCY ?? '10', 10) || 10)\n const created = createModuleQueue<WebhookDeliveryJob>(queueName, { concurrency })\n\n queues.set(queueName, created)\n return created\n}\n\nasync function ensureLocalWebhookQueueWorkerStarted(): Promise<void> {\n if (process.env.QUEUE_STRATEGY === 'async') return\n\n const globalStore = globalThis as typeof globalThis & {\n [LOCAL_WORKER_PROMISE_KEY]?: Promise<void>\n }\n\n if (globalStore[LOCAL_WORKER_PROMISE_KEY]) {\n await globalStore[LOCAL_WORKER_PROMISE_KEY]\n return\n }\n\n globalStore[LOCAL_WORKER_PROMISE_KEY] = (async () => {\n const queue = getWebhookQueue()\n\n await queue.process(async (job) => {\n const [{ createRequestContainer }, { processWebhookDeliveryJob }] = await Promise.all([\n import('@open-mercato/shared/lib/di/container'),\n import('./delivery'),\n ])\n\n const container = await createRequestContainer()\n const em = (container.resolve('em') as EntityManager).fork()\n await processWebhookDeliveryJob(em, job.payload)\n })\n })().catch((error) => {\n delete globalStore[LOCAL_WORKER_PROMISE_KEY]\n logger.error('Failed to start local delivery worker', { err: error })\n throw error\n })\n\n await globalStore[LOCAL_WORKER_PROMISE_KEY]\n}\n\nexport async function enqueueWebhookDelivery(job: WebhookDeliveryJob, delayMs?: number): Promise<string> {\n const queue = getWebhookQueue()\n const jobId = await queue.enqueue(job, delayMs && delayMs > 0 ? { delayMs } : undefined)\n await ensureLocalWebhookQueueWorkerStarted()\n return jobId\n}\n"],
|
|
5
|
-
"mappings": "AACA,SAAS,yBAAqC;AAC9C,SAAS,oBAAoB;
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { createModuleQueue, type Queue } from '@open-mercato/queue'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport type { WebhookDeliveryJob } from './delivery'\nimport type { InboundDispatchJob } from './inbound-dispatch'\n\nconst logger = createLogger('webhooks')\n\nconst queues = new Map<string, Queue<WebhookDeliveryJob>>()\nconst inboundQueues = new Map<string, Queue<InboundDispatchJob>>()\nconst LOCAL_WORKER_PROMISE_KEY = '__openMercatoWebhookLocalWorkerPromise__'\nconst LOCAL_INBOUND_WORKER_PROMISE_KEY = '__openMercatoWebhookLocalInboundWorkerPromise__'\n\nexport const WEBHOOK_DELIVERIES_QUEUE = 'webhook-deliveries'\nexport const WEBHOOK_INBOUND_DISPATCH_QUEUE = 'webhook-inbound-dispatch'\n\nexport function getWebhookQueue(queueName: string = WEBHOOK_DELIVERIES_QUEUE): Queue<WebhookDeliveryJob> {\n const existing = queues.get(queueName)\n if (existing) return existing\n\n const concurrency = Math.max(1, Number.parseInt(process.env.WEBHOOK_QUEUE_CONCURRENCY ?? '10', 10) || 10)\n const created = createModuleQueue<WebhookDeliveryJob>(queueName, { concurrency })\n\n queues.set(queueName, created)\n return created\n}\n\nasync function ensureLocalWebhookQueueWorkerStarted(): Promise<void> {\n if (process.env.QUEUE_STRATEGY === 'async') return\n\n const globalStore = globalThis as typeof globalThis & {\n [LOCAL_WORKER_PROMISE_KEY]?: Promise<void>\n }\n\n if (globalStore[LOCAL_WORKER_PROMISE_KEY]) {\n await globalStore[LOCAL_WORKER_PROMISE_KEY]\n return\n }\n\n globalStore[LOCAL_WORKER_PROMISE_KEY] = (async () => {\n const queue = getWebhookQueue()\n\n await queue.process(async (job) => {\n const [{ createRequestContainer }, { processWebhookDeliveryJob }] = await Promise.all([\n import('@open-mercato/shared/lib/di/container'),\n import('./delivery'),\n ])\n\n const container = await createRequestContainer()\n const em = (container.resolve('em') as EntityManager).fork()\n await processWebhookDeliveryJob(em, job.payload)\n })\n })().catch((error) => {\n delete globalStore[LOCAL_WORKER_PROMISE_KEY]\n logger.error('Failed to start local delivery worker', { err: error })\n throw error\n })\n\n await globalStore[LOCAL_WORKER_PROMISE_KEY]\n}\n\nexport async function enqueueWebhookDelivery(job: WebhookDeliveryJob, delayMs?: number): Promise<string> {\n const queue = getWebhookQueue()\n const jobId = await queue.enqueue(job, delayMs && delayMs > 0 ? { delayMs } : undefined)\n await ensureLocalWebhookQueueWorkerStarted()\n return jobId\n}\n\nexport function getInboundDispatchQueue(\n queueName: string = WEBHOOK_INBOUND_DISPATCH_QUEUE,\n): Queue<InboundDispatchJob> {\n const existing = inboundQueues.get(queueName)\n if (existing) return existing\n\n const concurrency = Math.max(1, Number.parseInt(process.env.WEBHOOK_INBOUND_QUEUE_CONCURRENCY ?? '5', 10) || 5)\n const created = createModuleQueue<InboundDispatchJob>(queueName, { concurrency })\n\n inboundQueues.set(queueName, created)\n return created\n}\n\nasync function ensureLocalInboundQueueWorkerStarted(): Promise<void> {\n if (process.env.QUEUE_STRATEGY === 'async') return\n\n const globalStore = globalThis as typeof globalThis & {\n [LOCAL_INBOUND_WORKER_PROMISE_KEY]?: Promise<void>\n }\n\n if (globalStore[LOCAL_INBOUND_WORKER_PROMISE_KEY]) {\n await globalStore[LOCAL_INBOUND_WORKER_PROMISE_KEY]\n return\n }\n\n globalStore[LOCAL_INBOUND_WORKER_PROMISE_KEY] = (async () => {\n const queue = getInboundDispatchQueue()\n\n await queue.process(async (job) => {\n const [{ createRequestContainer }, { processInboundDispatchJob }] = await Promise.all([\n import('@open-mercato/shared/lib/di/container'),\n import('./inbound-dispatch'),\n ])\n\n const container = await createRequestContainer()\n const em = (container.resolve('em') as EntityManager).fork()\n await processInboundDispatchJob(em, job.payload, {\n resolve: <T,>(name: string) => container.resolve(name) as T,\n })\n })\n })().catch((error) => {\n delete globalStore[LOCAL_INBOUND_WORKER_PROMISE_KEY]\n logger.error('Failed to start local inbound dispatch worker', { err: error })\n throw error\n })\n\n await globalStore[LOCAL_INBOUND_WORKER_PROMISE_KEY]\n}\n\nexport async function enqueueInboundDispatch(job: InboundDispatchJob): Promise<string> {\n const queue = getInboundDispatchQueue()\n const jobId = await queue.enqueue(job)\n await ensureLocalInboundQueueWorkerStarted()\n return jobId\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Migration } from "@mikro-orm/migrations";
|
|
2
|
+
class Migration20260617141327_webhooks extends Migration {
|
|
3
|
+
up() {
|
|
4
|
+
this.addSql(`create table "webhook_inbound_configs" ("id" uuid not null default gen_random_uuid(), "source_key" text not null, "is_active" boolean not null default true, "integration_id" text null, "organization_id" uuid not null, "tenant_id" uuid not null, "created_at" timestamptz not null, "updated_at" timestamptz not null, primary key ("id"));`);
|
|
5
|
+
this.addSql(`create index "webhook_inbound_configs_source_key_is_active_index" on "webhook_inbound_configs" ("source_key", "is_active");`);
|
|
6
|
+
this.addSql(`alter table "webhook_inbound_configs" add constraint "webhook_inbound_configs_source_scope_unique" unique ("source_key", "organization_id", "tenant_id");`);
|
|
7
|
+
this.addSql(`create table "webhook_ingestions" ("id" uuid not null default gen_random_uuid(), "source_key" text not null, "event_type" text not null, "external_message_id" text null, "payload" jsonb not null, "headers" jsonb null, "status" text not null default 'received', "error_message" text null, "processed_at" timestamptz null, "handler_count" int not null default 0, "handler_results" jsonb null, "duration_ms" int null, "organization_id" uuid not null, "tenant_id" uuid not null, "created_at" timestamptz not null, "updated_at" timestamptz not null, primary key ("id"));`);
|
|
8
|
+
this.addSql(`create index "webhook_ingestions_external_message_id_index" on "webhook_ingestions" ("external_message_id");`);
|
|
9
|
+
this.addSql(`create index "webhook_ingestions_organization_id_tenant_id_created_at_index" on "webhook_ingestions" ("organization_id", "tenant_id", "created_at");`);
|
|
10
|
+
this.addSql(`create index "webhook_ingestions_source_key_status_created_at_index" on "webhook_ingestions" ("source_key", "status", "created_at");`);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export {
|
|
14
|
+
Migration20260617141327_webhooks
|
|
15
|
+
};
|
|
16
|
+
//# sourceMappingURL=Migration20260617141327_webhooks.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/webhooks/migrations/Migration20260617141327_webhooks.ts"],
|
|
4
|
+
"sourcesContent": ["import { Migration } from '@mikro-orm/migrations';\n\nexport class Migration20260617141327_webhooks extends Migration {\n\n override up(): void | Promise<void> {\n this.addSql(`create table \"webhook_inbound_configs\" (\"id\" uuid not null default gen_random_uuid(), \"source_key\" text not null, \"is_active\" boolean not null default true, \"integration_id\" text null, \"organization_id\" uuid not null, \"tenant_id\" uuid not null, \"created_at\" timestamptz not null, \"updated_at\" timestamptz not null, primary key (\"id\"));`);\n this.addSql(`create index \"webhook_inbound_configs_source_key_is_active_index\" on \"webhook_inbound_configs\" (\"source_key\", \"is_active\");`);\n this.addSql(`alter table \"webhook_inbound_configs\" add constraint \"webhook_inbound_configs_source_scope_unique\" unique (\"source_key\", \"organization_id\", \"tenant_id\");`);\n\n this.addSql(`create table \"webhook_ingestions\" (\"id\" uuid not null default gen_random_uuid(), \"source_key\" text not null, \"event_type\" text not null, \"external_message_id\" text null, \"payload\" jsonb not null, \"headers\" jsonb null, \"status\" text not null default 'received', \"error_message\" text null, \"processed_at\" timestamptz null, \"handler_count\" int not null default 0, \"handler_results\" jsonb null, \"duration_ms\" int null, \"organization_id\" uuid not null, \"tenant_id\" uuid not null, \"created_at\" timestamptz not null, \"updated_at\" timestamptz not null, primary key (\"id\"));`);\n this.addSql(`create index \"webhook_ingestions_external_message_id_index\" on \"webhook_ingestions\" (\"external_message_id\");`);\n this.addSql(`create index \"webhook_ingestions_organization_id_tenant_id_created_at_index\" on \"webhook_ingestions\" (\"organization_id\", \"tenant_id\", \"created_at\");`);\n this.addSql(`create index \"webhook_ingestions_source_key_status_created_at_index\" on \"webhook_ingestions\" (\"source_key\", \"status\", \"created_at\");`);\n }\n\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,iBAAiB;AAEnB,MAAM,yCAAyC,UAAU;AAAA,EAErD,KAA2B;AAClC,SAAK,OAAO,iVAAiV;AAC7V,SAAK,OAAO,6HAA6H;AACzI,SAAK,OAAO,2JAA2J;AAEvK,SAAK,OAAO,ujBAAujB;AACnkB,SAAK,OAAO,8GAA8G;AAC1H,SAAK,OAAO,sJAAsJ;AAClK,SAAK,OAAO,sIAAsI;AAAA,EACpJ;AAEF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
2
|
+
import { processInboundDispatchJob } from "../lib/inbound-dispatch.js";
|
|
3
|
+
const logger = createLogger("webhooks");
|
|
4
|
+
const metadata = {
|
|
5
|
+
queue: "webhook-inbound-dispatch",
|
|
6
|
+
id: "webhooks:inbound-dispatch-worker",
|
|
7
|
+
concurrency: 5
|
|
8
|
+
};
|
|
9
|
+
async function handler(job, ctx) {
|
|
10
|
+
const em = ctx.resolve("em").fork();
|
|
11
|
+
try {
|
|
12
|
+
await processInboundDispatchJob(em, job.data, {
|
|
13
|
+
resolve: (name) => ctx.resolve(name)
|
|
14
|
+
});
|
|
15
|
+
} catch (error) {
|
|
16
|
+
logger.error("Inbound dispatch job processing failed", {
|
|
17
|
+
ingestionId: job.data.ingestionId,
|
|
18
|
+
sourceKey: job.data.sourceKey,
|
|
19
|
+
tenantId: job.data.tenantId,
|
|
20
|
+
err: error
|
|
21
|
+
});
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export {
|
|
26
|
+
handler as default,
|
|
27
|
+
metadata
|
|
28
|
+
};
|
|
29
|
+
//# sourceMappingURL=inbound-dispatch.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/webhooks/workers/inbound-dispatch.ts"],
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { processInboundDispatchJob, type InboundDispatchJob } from '../lib/inbound-dispatch'\n\nconst logger = createLogger('webhooks')\n\nexport const metadata = {\n queue: 'webhook-inbound-dispatch',\n id: 'webhooks:inbound-dispatch-worker',\n concurrency: 5,\n}\n\nexport default async function handler(\n job: { data: InboundDispatchJob },\n ctx: { resolve: <T = unknown>(name: string) => T },\n) {\n const em = (ctx.resolve('em') as EntityManager).fork()\n try {\n await processInboundDispatchJob(em, job.data, {\n resolve: <T,>(name: string) => ctx.resolve(name) as T,\n })\n } catch (error) {\n logger.error('Inbound dispatch job processing failed', {\n ingestionId: job.data.ingestionId,\n sourceKey: job.data.sourceKey,\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;AAE/B,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,MAAM;AAAA,MAC5C,SAAS,CAAK,SAAiB,IAAI,QAAQ,IAAI;AAAA,IACjD,CAAC;AAAA,EACH,SAAS,OAAO;AACd,WAAO,MAAM,0CAA0C;AAAA,MACrD,aAAa,IAAI,KAAK;AAAA,MACtB,WAAW,IAAI,KAAK;AAAA,MACpB,UAAU,IAAI,KAAK;AAAA,MACnB,KAAK;AAAA,IACP,CAAC;AACD,UAAM;AAAA,EACR;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export const id = "id";
|
|
2
|
+
export const source_key = "source_key";
|
|
3
|
+
export const is_active = "is_active";
|
|
4
|
+
export const integration_id = "integration_id";
|
|
5
|
+
export const organization_id = "organization_id";
|
|
6
|
+
export const tenant_id = "tenant_id";
|
|
7
|
+
export const created_at = "created_at";
|
|
8
|
+
export const updated_at = "updated_at";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export const id = "id";
|
|
2
|
+
export const source_key = "source_key";
|
|
3
|
+
export const event_type = "event_type";
|
|
4
|
+
export const external_message_id = "external_message_id";
|
|
5
|
+
export const payload = "payload";
|
|
6
|
+
export const headers = "headers";
|
|
7
|
+
export const status = "status";
|
|
8
|
+
export const error_message = "error_message";
|
|
9
|
+
export const processed_at = "processed_at";
|
|
10
|
+
export const handler_count = "handler_count";
|
|
11
|
+
export const handler_results = "handler_results";
|
|
12
|
+
export const duration_ms = "duration_ms";
|
|
13
|
+
export const organization_id = "organization_id";
|
|
14
|
+
export const tenant_id = "tenant_id";
|
|
15
|
+
export const created_at = "created_at";
|
|
16
|
+
export const updated_at = "updated_at";
|
|
@@ -5,7 +5,9 @@ export const M = {
|
|
|
5
5
|
"webhooks": {
|
|
6
6
|
"webhook_entity": "webhooks:webhook_entity",
|
|
7
7
|
"webhook_delivery_entity": "webhooks:webhook_delivery_entity",
|
|
8
|
-
"webhook_inbound_receipt_entity": "webhooks:webhook_inbound_receipt_entity"
|
|
8
|
+
"webhook_inbound_receipt_entity": "webhooks:webhook_inbound_receipt_entity",
|
|
9
|
+
"webhook_ingestion_entity": "webhooks:webhook_ingestion_entity",
|
|
10
|
+
"inbound_endpoint_config_entity": "webhooks:inbound_endpoint_config_entity"
|
|
9
11
|
}
|
|
10
12
|
} as const;
|
|
11
13
|
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
// AUTO-GENERATED by mercato generate entity-ids
|
|
2
2
|
// Static registry for entity fields - eliminates dynamic imports for Turbopack compatibility
|
|
3
3
|
export const entityFieldsRegistry: Record<string, Record<string, string>> = {
|
|
4
|
+
"inbound_endpoint_config_entity": {
|
|
5
|
+
"id": "id",
|
|
6
|
+
"source_key": "source_key",
|
|
7
|
+
"is_active": "is_active",
|
|
8
|
+
"integration_id": "integration_id",
|
|
9
|
+
"organization_id": "organization_id",
|
|
10
|
+
"tenant_id": "tenant_id",
|
|
11
|
+
"created_at": "created_at",
|
|
12
|
+
"updated_at": "updated_at"
|
|
13
|
+
},
|
|
4
14
|
"webhook_delivery_entity": {
|
|
5
15
|
"id": "id",
|
|
6
16
|
"webhook_id": "webhook_id",
|
|
@@ -62,6 +72,24 @@ export const entityFieldsRegistry: Record<string, Record<string, string>> = {
|
|
|
62
72
|
"organization_id": "organization_id",
|
|
63
73
|
"tenant_id": "tenant_id",
|
|
64
74
|
"created_at": "created_at"
|
|
75
|
+
},
|
|
76
|
+
"webhook_ingestion_entity": {
|
|
77
|
+
"id": "id",
|
|
78
|
+
"source_key": "source_key",
|
|
79
|
+
"event_type": "event_type",
|
|
80
|
+
"external_message_id": "external_message_id",
|
|
81
|
+
"payload": "payload",
|
|
82
|
+
"headers": "headers",
|
|
83
|
+
"status": "status",
|
|
84
|
+
"error_message": "error_message",
|
|
85
|
+
"processed_at": "processed_at",
|
|
86
|
+
"handler_count": "handler_count",
|
|
87
|
+
"handler_results": "handler_results",
|
|
88
|
+
"duration_ms": "duration_ms",
|
|
89
|
+
"organization_id": "organization_id",
|
|
90
|
+
"tenant_id": "tenant_id",
|
|
91
|
+
"created_at": "created_at",
|
|
92
|
+
"updated_at": "updated_at"
|
|
65
93
|
}
|
|
66
94
|
};
|
|
67
95
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/webhooks",
|
|
3
|
-
"version": "0.6.7-develop.
|
|
3
|
+
"version": "0.6.7-develop.6749.1.6b54c56dfe",
|
|
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.7-develop.
|
|
74
|
-
"@open-mercato/queue": "0.6.7-develop.
|
|
75
|
-
"@open-mercato/ui": "0.6.7-develop.
|
|
73
|
+
"@open-mercato/core": "0.6.7-develop.6749.1.6b54c56dfe",
|
|
74
|
+
"@open-mercato/queue": "0.6.7-develop.6749.1.6b54c56dfe",
|
|
75
|
+
"@open-mercato/ui": "0.6.7-develop.6749.1.6b54c56dfe",
|
|
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.7-develop.
|
|
80
|
+
"@open-mercato/shared": "0.6.7-develop.6749.1.6b54c56dfe",
|
|
81
81
|
"react": "^19.0.0",
|
|
82
82
|
"react-dom": "^19.0.0"
|
|
83
83
|
},
|
|
84
84
|
"devDependencies": {
|
|
85
|
-
"@open-mercato/shared": "0.6.7-develop.
|
|
85
|
+
"@open-mercato/shared": "0.6.7-develop.6749.1.6b54c56dfe",
|
|
86
86
|
"@types/jest": "^30.0.0",
|
|
87
87
|
"@types/react": "^19.2.17",
|
|
88
88
|
"@types/react-dom": "^19.2.3",
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { WebhookSourceConfig } from '@open-mercato/shared/lib/webhooks'
|
|
2
|
+
|
|
3
|
+
const enqueueInboundDispatch = jest.fn(async () => 'job-1')
|
|
4
|
+
const emitWebhooksEvent = jest.fn(async () => undefined)
|
|
5
|
+
const findWithDecryption = jest.fn()
|
|
6
|
+
const credentialsResolve = jest.fn(async () => ({ webhookSigningSecret: 'whsec' }))
|
|
7
|
+
|
|
8
|
+
const flush = jest.fn(async () => undefined)
|
|
9
|
+
const persist = jest.fn()
|
|
10
|
+
const em = {
|
|
11
|
+
fork: () => em,
|
|
12
|
+
create: (_entity: unknown, data: Record<string, unknown>) => ({ id: 'ing-1', ...data }),
|
|
13
|
+
persist,
|
|
14
|
+
flush,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
jest.mock('@open-mercato/shared/lib/di/container', () => ({
|
|
18
|
+
createRequestContainer: async () => ({
|
|
19
|
+
resolve: (name: string) => {
|
|
20
|
+
if (name === 'em') return em
|
|
21
|
+
if (name === 'integrationCredentialsService') return { resolve: credentialsResolve }
|
|
22
|
+
throw new Error(`[internal] no mock for ${name}`)
|
|
23
|
+
},
|
|
24
|
+
}),
|
|
25
|
+
}))
|
|
26
|
+
jest.mock('@open-mercato/shared/lib/i18n/server', () => ({
|
|
27
|
+
resolveTranslations: async () => ({ translate: (_key: string, fallback: string) => fallback }),
|
|
28
|
+
}))
|
|
29
|
+
jest.mock('@open-mercato/shared/lib/encryption/find', () => ({
|
|
30
|
+
findWithDecryption: (...args: unknown[]) => findWithDecryption(...args),
|
|
31
|
+
}))
|
|
32
|
+
jest.mock('../../../../events', () => ({
|
|
33
|
+
emitWebhooksEvent: (...args: unknown[]) => emitWebhooksEvent(...args),
|
|
34
|
+
}))
|
|
35
|
+
jest.mock('../../../../lib/queue', () => ({
|
|
36
|
+
enqueueInboundDispatch: (...args: unknown[]) => enqueueInboundDispatch(...args),
|
|
37
|
+
}))
|
|
38
|
+
|
|
39
|
+
import { POST } from '../route'
|
|
40
|
+
import { clearWebhookSources, registerWebhookSource } from '../../../../lib/inbound-registry'
|
|
41
|
+
|
|
42
|
+
function makeSource(overrides: Partial<WebhookSourceConfig> = {}): WebhookSourceConfig {
|
|
43
|
+
return {
|
|
44
|
+
key: 'stripe',
|
|
45
|
+
label: 'Stripe',
|
|
46
|
+
verifier: async () => true,
|
|
47
|
+
eventTypeExtractor: (body) => String((body as { type?: string }).type ?? ''),
|
|
48
|
+
messageIdExtractor: (body) => String((body as { id?: string }).id ?? ''),
|
|
49
|
+
...overrides,
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function postTo(endpointId: string, body = '{"type":"payment_intent.succeeded","id":"evt_1"}') {
|
|
54
|
+
const request = new Request(`http://localhost/api/webhooks/inbound/${endpointId}`, {
|
|
55
|
+
method: 'POST',
|
|
56
|
+
body,
|
|
57
|
+
headers: { 'content-type': 'application/json' },
|
|
58
|
+
})
|
|
59
|
+
return POST(request, { params: Promise.resolve({ endpointId }) })
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
beforeEach(() => {
|
|
63
|
+
clearWebhookSources()
|
|
64
|
+
enqueueInboundDispatch.mockClear()
|
|
65
|
+
emitWebhooksEvent.mockClear()
|
|
66
|
+
persist.mockClear()
|
|
67
|
+
flush.mockReset()
|
|
68
|
+
flush.mockResolvedValue(undefined)
|
|
69
|
+
findWithDecryption.mockReset()
|
|
70
|
+
findWithDecryption.mockResolvedValue([{ organizationId: 'o1', tenantId: 't1', sourceKey: 'stripe', isActive: true }])
|
|
71
|
+
credentialsResolve.mockClear()
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('returns 404 when neither a source nor an adapter matches the segment', async () => {
|
|
75
|
+
const res = await postTo('totally-unknown')
|
|
76
|
+
expect(res.status).toBe(404)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('accepts a valid source webhook: records ingestion, enqueues dispatch, emits received', async () => {
|
|
80
|
+
registerWebhookSource(makeSource({ verifier: async () => true }))
|
|
81
|
+
const res = await postTo('stripe')
|
|
82
|
+
expect(res.status).toBe(200)
|
|
83
|
+
await expect(res.json()).resolves.toEqual({ ok: true })
|
|
84
|
+
expect(enqueueInboundDispatch).toHaveBeenCalledWith(
|
|
85
|
+
expect.objectContaining({ ingestionId: 'ing-1', sourceKey: 'stripe', eventType: 'payment_intent.succeeded', tenantId: 't1', organizationId: 'o1' }),
|
|
86
|
+
)
|
|
87
|
+
expect(emitWebhooksEvent).toHaveBeenCalledWith(
|
|
88
|
+
'webhooks.inbound.received',
|
|
89
|
+
expect.objectContaining({ endpointId: 'stripe', eventType: 'payment_intent.succeeded' }),
|
|
90
|
+
{ persistent: true },
|
|
91
|
+
)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('rejects with 401 when no candidate scope verifies', async () => {
|
|
95
|
+
registerWebhookSource(makeSource({ verifier: async () => false }))
|
|
96
|
+
const res = await postTo('stripe')
|
|
97
|
+
expect(res.status).toBe(401)
|
|
98
|
+
expect(enqueueInboundDispatch).not.toHaveBeenCalled()
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('returns duplicate when the receipt unique constraint is violated', async () => {
|
|
102
|
+
registerWebhookSource(makeSource())
|
|
103
|
+
flush.mockRejectedValueOnce({ code: '23505' })
|
|
104
|
+
const res = await postTo('stripe')
|
|
105
|
+
expect(res.status).toBe(200)
|
|
106
|
+
await expect(res.json()).resolves.toEqual({ ok: true, duplicate: true })
|
|
107
|
+
expect(enqueueInboundDispatch).not.toHaveBeenCalled()
|
|
108
|
+
})
|