@open-mercato/webhooks 0.6.5-develop.5033.1.c970204a3f → 0.6.5-develop.5099.1.85c0aff78c
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/subscribers/outbound-dispatch.js +1 -1
- package/dist/modules/webhooks/subscribers/outbound-dispatch.js.map +2 -2
- package/package.json +6 -6
- package/src/modules/webhooks/subscribers/__tests__/outbound-dispatch.test.ts +31 -0
- package/src/modules/webhooks/subscribers/outbound-dispatch.ts +1 -1
|
@@ -45,7 +45,7 @@ async function handler(payload, ctx) {
|
|
|
45
45
|
...organizationId ? { organizationId } : {}
|
|
46
46
|
},
|
|
47
47
|
{},
|
|
48
|
-
{ tenantId, organizationId: organizationId ??
|
|
48
|
+
{ tenantId, organizationId: organizationId ?? null }
|
|
49
49
|
);
|
|
50
50
|
if (!webhooks.length) return;
|
|
51
51
|
const matchingWebhooks = webhooks.filter(
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/webhooks/subscribers/outbound-dispatch.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { SubscriberContext } from '@open-mercato/events/types'\nimport { WebhookDeliveryEntity, WebhookEntity } from '../data/entities'\nimport { findWithDecryption, findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { matchAnyWebhookEventPattern } from '@open-mercato/shared/lib/events/patterns'\nimport { getDeclaredEvents } from '@open-mercato/shared/modules/events'\nimport { createWebhookDelivery } from '../lib/delivery'\nimport { enqueueWebhookDelivery } from '../lib/queue'\nimport { isWebhookIntegrationEnabled } from '../lib/integration-state'\n\nexport const metadata = {\n event: '*',\n persistent: true,\n id: 'webhooks:outbound-dispatch',\n}\n\nfunction shouldSkipOutboundDispatch(eventId: string): boolean {\n if (eventId.startsWith('webhooks.') || eventId.startsWith('application.')) return true\n\n const declaredEvent = getDeclaredEvents().find((event) => event.id === eventId)\n return declaredEvent?.excludeFromTriggers === true\n}\n\nfunction forkOutboundEntityManager(em: EntityManager): EntityManager {\n const fork = (em as unknown as { fork?: (options?: Record<string, unknown>) => EntityManager }).fork\n if (typeof fork !== 'function') return em\n return fork.call(em, { clear: true, useContext: false })\n}\n\nfunction integrationScopeKey(tenantId: string, organizationId: string): string {\n return `${tenantId}:${organizationId}`\n}\n\nexport default async function handler(\n payload: Record<string, unknown>,\n ctx: (SubscriberContext & { eventId?: string }) | { container?: { resolve: <T = unknown>(name: string) => T }; eventId?: string; eventName?: string; resolve?: <T = unknown>(name: string) => T },\n) {\n const eventId = ctx.eventId ?? ctx.eventName ?? (payload.eventId as string) ?? (payload.type as string)\n if (!eventId) return\n if (shouldSkipOutboundDispatch(eventId)) return\n\n const tenantId = payload.tenantId as string | undefined\n const organizationId = payload.organizationId as string | undefined\n if (!tenantId) return\n\n if (eventId.startsWith('webhooks.')) return\n if (eventId.startsWith('query_index.')) return\n\n\n const resolve = ('resolve' in ctx && typeof ctx.resolve === 'function')\n ? ctx.resolve\n : ('container' in ctx && ctx.container && typeof ctx.container.resolve === 'function')\n ? ctx.container.resolve.bind(ctx.container)\n : null\n\n if (!resolve) return\n\n const em = (resolve('em') as EntityManager).fork()\n\n const webhooks = await findWithDecryption(\n em,\n WebhookEntity,\n {\n isActive: true,\n deletedAt: null,\n tenantId,\n ...(organizationId ? { organizationId } : {}),\n },\n {},\n { tenantId, organizationId: organizationId ??
|
|
5
|
-
"mappings": "AAEA,SAAS,uBAAuB,qBAAqB;AACrD,SAAS,oBAAoB,6BAA6B;AAC1D,SAAS,mCAAmC;AAC5C,SAAS,yBAAyB;AAClC,SAAS,6BAA6B;AACtC,SAAS,8BAA8B;AACvC,SAAS,mCAAmC;AAErC,MAAM,WAAW;AAAA,EACtB,OAAO;AAAA,EACP,YAAY;AAAA,EACZ,IAAI;AACN;AAEA,SAAS,2BAA2B,SAA0B;AAC5D,MAAI,QAAQ,WAAW,WAAW,KAAK,QAAQ,WAAW,cAAc,EAAG,QAAO;AAElF,QAAM,gBAAgB,kBAAkB,EAAE,KAAK,CAAC,UAAU,MAAM,OAAO,OAAO;AAC9E,SAAO,eAAe,wBAAwB;AAChD;AAEA,SAAS,0BAA0B,IAAkC;AACnE,QAAM,OAAQ,GAAkF;AAChG,MAAI,OAAO,SAAS,WAAY,QAAO;AACvC,SAAO,KAAK,KAAK,IAAI,EAAE,OAAO,MAAM,YAAY,MAAM,CAAC;AACzD;AAEA,SAAS,oBAAoB,UAAkB,gBAAgC;AAC7E,SAAO,GAAG,QAAQ,IAAI,cAAc;AACtC;AAEA,eAAO,QACL,SACA,KACA;AACA,QAAM,UAAU,IAAI,WAAW,IAAI,aAAc,QAAQ,WAAuB,QAAQ;AACxF,MAAI,CAAC,QAAS;AACd,MAAI,2BAA2B,OAAO,EAAG;AAEzC,QAAM,WAAW,QAAQ;AACzB,QAAM,iBAAiB,QAAQ;AAC/B,MAAI,CAAC,SAAU;AAEf,MAAI,QAAQ,WAAW,WAAW,EAAG;AACrC,MAAI,QAAQ,WAAW,cAAc,EAAG;AAGxC,QAAM,UAAW,aAAa,OAAO,OAAO,IAAI,YAAY,aACxD,IAAI,UACH,eAAe,OAAO,IAAI,aAAa,OAAO,IAAI,UAAU,YAAY,aACvE,IAAI,UAAU,QAAQ,KAAK,IAAI,SAAS,IACxC;AAEN,MAAI,CAAC,QAAS;AAEd,QAAM,KAAM,QAAQ,IAAI,EAAoB,KAAK;AAEjD,QAAM,WAAW,MAAM;AAAA,IACrB;AAAA,IACA;AAAA,IACA;AAAA,MACE,UAAU;AAAA,MACV,WAAW;AAAA,MACX;AAAA,MACA,GAAI,iBAAiB,EAAE,eAAe,IAAI,CAAC;AAAA,IAC7C;AAAA,IACA,CAAC;AAAA,IACD,EAAE,UAAU,gBAAgB,kBAAkB,
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { SubscriberContext } from '@open-mercato/events/types'\nimport { WebhookDeliveryEntity, WebhookEntity } from '../data/entities'\nimport { findWithDecryption, findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { matchAnyWebhookEventPattern } from '@open-mercato/shared/lib/events/patterns'\nimport { getDeclaredEvents } from '@open-mercato/shared/modules/events'\nimport { createWebhookDelivery } from '../lib/delivery'\nimport { enqueueWebhookDelivery } from '../lib/queue'\nimport { isWebhookIntegrationEnabled } from '../lib/integration-state'\n\nexport const metadata = {\n event: '*',\n persistent: true,\n id: 'webhooks:outbound-dispatch',\n}\n\nfunction shouldSkipOutboundDispatch(eventId: string): boolean {\n if (eventId.startsWith('webhooks.') || eventId.startsWith('application.')) return true\n\n const declaredEvent = getDeclaredEvents().find((event) => event.id === eventId)\n return declaredEvent?.excludeFromTriggers === true\n}\n\nfunction forkOutboundEntityManager(em: EntityManager): EntityManager {\n const fork = (em as unknown as { fork?: (options?: Record<string, unknown>) => EntityManager }).fork\n if (typeof fork !== 'function') return em\n return fork.call(em, { clear: true, useContext: false })\n}\n\nfunction integrationScopeKey(tenantId: string, organizationId: string): string {\n return `${tenantId}:${organizationId}`\n}\n\nexport default async function handler(\n payload: Record<string, unknown>,\n ctx: (SubscriberContext & { eventId?: string }) | { container?: { resolve: <T = unknown>(name: string) => T }; eventId?: string; eventName?: string; resolve?: <T = unknown>(name: string) => T },\n) {\n const eventId = ctx.eventId ?? ctx.eventName ?? (payload.eventId as string) ?? (payload.type as string)\n if (!eventId) return\n if (shouldSkipOutboundDispatch(eventId)) return\n\n const tenantId = payload.tenantId as string | undefined\n const organizationId = payload.organizationId as string | undefined\n if (!tenantId) return\n\n if (eventId.startsWith('webhooks.')) return\n if (eventId.startsWith('query_index.')) return\n\n\n const resolve = ('resolve' in ctx && typeof ctx.resolve === 'function')\n ? ctx.resolve\n : ('container' in ctx && ctx.container && typeof ctx.container.resolve === 'function')\n ? ctx.container.resolve.bind(ctx.container)\n : null\n\n if (!resolve) return\n\n const em = (resolve('em') as EntityManager).fork()\n\n const webhooks = await findWithDecryption(\n em,\n WebhookEntity,\n {\n isActive: true,\n deletedAt: null,\n tenantId,\n ...(organizationId ? { organizationId } : {}),\n },\n {},\n { tenantId, organizationId: organizationId ?? null },\n )\n\n if (!webhooks.length) return\n\n const matchingWebhooks = webhooks.filter((webhook) =>\n matchAnyWebhookEventPattern(eventId, webhook.subscribedEvents),\n )\n\n if (!matchingWebhooks.length) return\n\n const integrationEnabledByScope = new Map<string, Promise<boolean>>()\n\n const resolveIntegrationEnabled = (webhook: WebhookEntity): Promise<boolean> => {\n const key = integrationScopeKey(webhook.tenantId, webhook.organizationId)\n let pending = integrationEnabledByScope.get(key)\n if (!pending) {\n pending = isWebhookIntegrationEnabled(em, {\n tenantId: webhook.tenantId,\n organizationId: webhook.organizationId,\n })\n integrationEnabledByScope.set(key, pending)\n }\n return pending\n }\n\n await Promise.all(\n matchingWebhooks.map(async (webhook) => {\n if (!(await resolveIntegrationEnabled(webhook))) return\n\n const webhookEm = forkOutboundEntityManager(em)\n let createdDeliveryId: string | null = null\n try {\n const delivery = await createWebhookDelivery({\n em: webhookEm,\n webhook,\n eventId,\n payload,\n })\n createdDeliveryId = delivery.id\n\n await enqueueWebhookDelivery({\n deliveryId: delivery.id,\n tenantId: delivery.tenantId,\n organizationId: delivery.organizationId,\n })\n } catch (error) {\n if (createdDeliveryId) {\n const failedDelivery = await findOneWithDecryption(\n webhookEm,\n WebhookDeliveryEntity,\n { id: createdDeliveryId, tenantId: webhook.tenantId, organizationId: webhook.organizationId },\n undefined,\n { tenantId: webhook.tenantId, organizationId: webhook.organizationId },\n )\n if (failedDelivery) {\n failedDelivery.status = 'failed'\n failedDelivery.errorMessage = error instanceof Error ? `Queue enqueue failed: ${error.message}` : 'Queue enqueue failed'\n failedDelivery.nextRetryAt = null\n await webhookEm.flush()\n }\n }\n console.error('[webhooks] Failed to enqueue outbound delivery', {\n webhookId: webhook.id,\n eventId,\n tenantId,\n organizationId: organizationId ?? webhook.organizationId,\n error: error instanceof Error ? error.message : String(error),\n })\n }\n }),\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/webhooks",
|
|
3
|
-
"version": "0.6.5-develop.
|
|
3
|
+
"version": "0.6.5-develop.5099.1.85c0aff78c",
|
|
4
4
|
"description": "Webhooks module for Open Mercato — Standard Webhooks compliant outbound/inbound delivery",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -69,19 +69,19 @@
|
|
|
69
69
|
}
|
|
70
70
|
},
|
|
71
71
|
"dependencies": {
|
|
72
|
-
"@open-mercato/core": "0.6.5-develop.
|
|
73
|
-
"@open-mercato/queue": "0.6.5-develop.
|
|
74
|
-
"@open-mercato/ui": "0.6.5-develop.
|
|
72
|
+
"@open-mercato/core": "0.6.5-develop.5099.1.85c0aff78c",
|
|
73
|
+
"@open-mercato/queue": "0.6.5-develop.5099.1.85c0aff78c",
|
|
74
|
+
"@open-mercato/ui": "0.6.5-develop.5099.1.85c0aff78c",
|
|
75
75
|
"svix": "^1.95.1"
|
|
76
76
|
},
|
|
77
77
|
"peerDependencies": {
|
|
78
78
|
"@mikro-orm/postgresql": "^7.0.14",
|
|
79
|
-
"@open-mercato/shared": "0.6.5-develop.
|
|
79
|
+
"@open-mercato/shared": "0.6.5-develop.5099.1.85c0aff78c",
|
|
80
80
|
"react": "^19.0.0",
|
|
81
81
|
"react-dom": "^19.0.0"
|
|
82
82
|
},
|
|
83
83
|
"devDependencies": {
|
|
84
|
-
"@open-mercato/shared": "0.6.5-develop.
|
|
84
|
+
"@open-mercato/shared": "0.6.5-develop.5099.1.85c0aff78c",
|
|
85
85
|
"@types/jest": "^30.0.0",
|
|
86
86
|
"@types/react": "^19.2.16",
|
|
87
87
|
"@types/react-dom": "^19.2.3",
|
|
@@ -102,6 +102,37 @@ describe('webhooks outbound dispatch subscriber', () => {
|
|
|
102
102
|
})
|
|
103
103
|
})
|
|
104
104
|
|
|
105
|
+
it('normalizes a missing organizationId to null in the decryption scope', async () => {
|
|
106
|
+
const { rootEm } = createDispatchEntityManagers()
|
|
107
|
+
|
|
108
|
+
;(findWithDecryption as jest.Mock).mockResolvedValue([])
|
|
109
|
+
|
|
110
|
+
await handler(
|
|
111
|
+
{
|
|
112
|
+
id: 'product-1',
|
|
113
|
+
tenantId: 'tenant-1',
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
eventName: 'catalog.product.deleted',
|
|
117
|
+
resolve: <T,>(name: string): T => {
|
|
118
|
+
if (name === 'em') return rootEm as T
|
|
119
|
+
throw new Error(`Unexpected dependency: ${name}`)
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
expect(findWithDecryption).toHaveBeenCalledWith(
|
|
125
|
+
expect.anything(),
|
|
126
|
+
expect.anything(),
|
|
127
|
+
expect.objectContaining({ tenantId: 'tenant-1' }),
|
|
128
|
+
expect.anything(),
|
|
129
|
+
{ tenantId: 'tenant-1', organizationId: null },
|
|
130
|
+
)
|
|
131
|
+
const decryptionScope = (findWithDecryption as jest.Mock).mock.calls[0][4]
|
|
132
|
+
expect(decryptionScope.organizationId).toBeNull()
|
|
133
|
+
expect(decryptionScope.organizationId).not.toBe('')
|
|
134
|
+
})
|
|
135
|
+
|
|
105
136
|
it('checks integration state once per organization when multiple webhooks match', async () => {
|
|
106
137
|
const { rootEm, handlerEm } = createDispatchEntityManagers()
|
|
107
138
|
|