@open-mercato/webhooks 0.8.1-develop.7228.1.b7ea09d2d3 → 0.8.1-develop.7238.1.3abe669ea5

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.
Files changed (32) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +1 -0
  3. package/dist/modules/webhooks/api/webhooks/[id]/route.js +3 -0
  4. package/dist/modules/webhooks/api/webhooks/[id]/route.js.map +2 -2
  5. package/dist/modules/webhooks/api/webhooks/[id]/test/route.js +1 -1
  6. package/dist/modules/webhooks/api/webhooks/[id]/test/route.js.map +2 -2
  7. package/dist/modules/webhooks/api/webhooks/route.js +13 -0
  8. package/dist/modules/webhooks/api/webhooks/route.js.map +2 -2
  9. package/dist/modules/webhooks/lib/delivery.js +8 -1
  10. package/dist/modules/webhooks/lib/delivery.js.map +2 -2
  11. package/dist/modules/webhooks/lib/queue.js +1 -1
  12. package/dist/modules/webhooks/lib/queue.js.map +2 -2
  13. package/dist/modules/webhooks/lib/subscription-cache.js +93 -0
  14. package/dist/modules/webhooks/lib/subscription-cache.js.map +7 -0
  15. package/dist/modules/webhooks/subscribers/outbound-dispatch.js +34 -2
  16. package/dist/modules/webhooks/subscribers/outbound-dispatch.js.map +2 -2
  17. package/dist/modules/webhooks/workers/webhook-delivery.js +1 -1
  18. package/dist/modules/webhooks/workers/webhook-delivery.js.map +2 -2
  19. package/package.json +7 -6
  20. package/src/modules/webhooks/api/webhooks/[id]/__tests__/subscription-cache-invalidation.test.ts +107 -0
  21. package/src/modules/webhooks/api/webhooks/[id]/route.ts +5 -0
  22. package/src/modules/webhooks/api/webhooks/[id]/test/route.ts +1 -1
  23. package/src/modules/webhooks/api/webhooks/__tests__/subscription-cache-invalidation.test.ts +93 -0
  24. package/src/modules/webhooks/api/webhooks/route.ts +14 -0
  25. package/src/modules/webhooks/lib/__tests__/subscription-cache-invalidation.test.ts +207 -0
  26. package/src/modules/webhooks/lib/delivery.ts +12 -2
  27. package/src/modules/webhooks/lib/queue.ts +1 -1
  28. package/src/modules/webhooks/lib/subscription-cache.ts +135 -0
  29. package/src/modules/webhooks/subscribers/__tests__/outbound-dispatch.subscription-cache.test.ts +289 -0
  30. package/src/modules/webhooks/subscribers/outbound-dispatch.ts +49 -6
  31. package/src/modules/webhooks/workers/__tests__/webhook-delivery.test.ts +5 -1
  32. package/src/modules/webhooks/workers/webhook-delivery.ts +1 -1
@@ -1,2 +1,2 @@
1
- [build:webhooks] found 75 entry points
1
+ [build:webhooks] found 76 entry points
2
2
  [build:webhooks] built successfully
package/AGENTS.md CHANGED
@@ -12,6 +12,7 @@ Use `@open-mercato/webhooks` for Standard Webhooks delivery, inbound verificatio
12
12
  6. **MUST update both canonical and aliased API surfaces carefully** — `/api/webhooks/...` is the contract surface; compatibility aliases must keep working when present
13
13
  7. **MUST wire backend UI writes through shared CRUD helpers or guarded mutations** — do not add ad hoc fetch logic for create, update, retry, rotate, or test actions
14
14
  8. **MUST treat inbound adapters as provider-owned** — register `WebhookEndpointAdapter` in the provider module; do not hardcode provider behavior in the webhooks package
15
+ 9. **MUST invalidate the subscription cache on every webhook write** — `lib/subscription-cache.ts` caches active subscriptions per tenant/organization (`OM_WEBHOOKS_SUBSCRIPTION_CACHE_TTL_MS`, default `60000`); the `webhooks:outbound-dispatch` subscriber also clears it on `webhooks.webhook.*` events, but the TTL is a safety net, not the correctness mechanism
15
16
 
16
17
  ## Ask First
17
18
 
@@ -5,6 +5,7 @@ import { findScopedWebhook, json, resolveWebhookRequestScope, serializeWebhookDe
5
5
  import { webhookUpdateSchema } from "../../../data/validators.js";
6
6
  import { enforceCommandOptimisticLockWithGuards } from "@open-mercato/shared/lib/crud/optimistic-lock-command";
7
7
  import { isCrudHttpError } from "@open-mercato/shared/lib/crud/errors";
8
+ import { invalidateWebhookSubscriptionCacheFor } from "../../../lib/subscription-cache.js";
8
9
  const metadata = {
9
10
  GET: { requireAuth: true, requireFeatures: ["webhooks.view"] },
10
11
  PUT: { requireAuth: true, requireFeatures: ["webhooks.manage"] },
@@ -86,6 +87,7 @@ async function PUT(request, context) {
86
87
  if (input.integrationId !== void 0) webhook.integrationId = input.integrationId;
87
88
  if (input.isActive !== void 0) webhook.isActive = input.isActive;
88
89
  await em.flush();
90
+ await invalidateWebhookSubscriptionCacheFor(scope.container, webhook.tenantId);
89
91
  await emitWebhooksEvent("webhooks.webhook.updated", {
90
92
  webhookId: webhook.id,
91
93
  organizationId: webhook.organizationId,
@@ -116,6 +118,7 @@ async function DELETE(request, context) {
116
118
  }
117
119
  webhook.deletedAt = /* @__PURE__ */ new Date();
118
120
  await em.flush();
121
+ await invalidateWebhookSubscriptionCacheFor(scope.container, webhook.tenantId);
119
122
  await emitWebhooksEvent("webhooks.webhook.deleted", {
120
123
  webhookId: webhook.id,
121
124
  organizationId: webhook.organizationId,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/webhooks/api/webhooks/%5Bid%5D/route.ts"],
4
- "sourcesContent": ["import { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { emitWebhooksEvent } from '../../../events'\nimport { findScopedWebhook, json, resolveWebhookRequestScope, serializeWebhookDetail } from '../../helpers'\nimport { webhookUpdateSchema } from '../../../data/validators'\nimport { enforceCommandOptimisticLockWithGuards } from '@open-mercato/shared/lib/crud/optimistic-lock-command'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['webhooks.view'] },\n PUT: { requireAuth: true, requireFeatures: ['webhooks.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['webhooks.manage'] },\n}\n\ninterface RouteContext {\n params: Promise<{ id: string }>\n}\n\nconst webhookDetailResponseSchema = z.object({\n id: z.string(),\n name: z.string(),\n description: z.string().nullable(),\n url: z.string(),\n subscribedEvents: z.array(z.string()),\n httpMethod: z.string(),\n isActive: z.boolean(),\n deliveryStrategy: z.string(),\n maxRetries: z.number(),\n consecutiveFailures: z.number(),\n lastSuccessAt: z.string().nullable(),\n lastFailureAt: z.string().nullable(),\n createdAt: z.string(),\n updatedAt: z.string(),\n customHeaders: z.record(z.string(), z.string()).nullable(),\n strategyConfig: z.record(z.string(), z.unknown()).nullable(),\n timeoutMs: z.number(),\n rateLimitPerMinute: z.number(),\n autoDisableThreshold: z.number(),\n integrationId: z.string().nullable(),\n maskedSecret: z.string(),\n previousSecretSetAt: z.string().nullable(),\n})\n\nconst errorSchema = z.object({ error: z.string() })\nconst deleteResponseSchema = z.object({ success: z.literal(true) })\n\nexport async function GET(request: Request, context: RouteContext): Promise<Response> {\n const scope = await resolveWebhookRequestScope(request)\n if (scope instanceof Response) return scope\n\n const params = await context.params\n const webhook = await findScopedWebhook(scope.em.fork(), scope, params.id)\n\n if (!webhook) {\n return json({ error: 'Webhook not found' }, { status: 404 })\n }\n\n return json(serializeWebhookDetail(webhook))\n}\n\nexport async function PUT(request: Request, context: RouteContext): Promise<Response> {\n const scope = await resolveWebhookRequestScope(request)\n if (scope instanceof Response) return scope\n\n const params = await context.params\n const em = scope.em.fork()\n const webhook = await findScopedWebhook(em, scope, params.id)\n\n if (!webhook) {\n return json({ error: 'Webhook not found' }, { status: 404 })\n }\n\n try {\n await enforceCommandOptimisticLockWithGuards(scope.container, {\n resourceKind: 'webhooks.endpoint',\n resourceId: webhook.id,\n current: webhook.updatedAt ?? null,\n request,\n })\n } catch (err) {\n if (isCrudHttpError(err)) return json(err.body, { status: err.status })\n throw err\n }\n\n const parsed = webhookUpdateSchema.safeParse(await request.json().catch(() => null))\n if (!parsed.success) {\n return json({ error: 'Invalid request payload' }, { status: 400 })\n }\n\n const input = parsed.data\n if (input.name !== undefined) webhook.name = input.name\n if (input.description !== undefined) webhook.description = input.description\n if (input.url !== undefined) webhook.url = input.url\n if (input.subscribedEvents !== undefined) webhook.subscribedEvents = input.subscribedEvents\n if (input.httpMethod !== undefined) webhook.httpMethod = input.httpMethod\n if (input.customHeaders !== undefined) webhook.customHeaders = input.customHeaders\n if (input.deliveryStrategy !== undefined) webhook.deliveryStrategy = input.deliveryStrategy\n if (input.strategyConfig !== undefined) webhook.strategyConfig = input.strategyConfig\n if (input.maxRetries !== undefined) webhook.maxRetries = input.maxRetries\n if (input.timeoutMs !== undefined) webhook.timeoutMs = input.timeoutMs\n if (input.rateLimitPerMinute !== undefined) webhook.rateLimitPerMinute = input.rateLimitPerMinute\n if (input.autoDisableThreshold !== undefined) webhook.autoDisableThreshold = input.autoDisableThreshold\n if (input.integrationId !== undefined) webhook.integrationId = input.integrationId\n if (input.isActive !== undefined) webhook.isActive = input.isActive\n\n await em.flush()\n\n await emitWebhooksEvent('webhooks.webhook.updated', {\n webhookId: webhook.id,\n organizationId: webhook.organizationId,\n tenantId: webhook.tenantId,\n }, { persistent: true })\n\n return json(serializeWebhookDetail(webhook))\n}\n\nexport async function DELETE(request: Request, context: RouteContext): Promise<Response> {\n const scope = await resolveWebhookRequestScope(request)\n if (scope instanceof Response) return scope\n\n const params = await context.params\n const em = scope.em.fork()\n const webhook = await findScopedWebhook(em, scope, params.id)\n const { translate } = await resolveTranslations()\n\n if (!webhook) {\n return json({ error: translate('webhooks.errors.notFound', 'Webhook not found') }, { status: 404 })\n }\n\n try {\n await enforceCommandOptimisticLockWithGuards(scope.container, {\n resourceKind: 'webhooks.endpoint',\n resourceId: webhook.id,\n current: webhook.updatedAt ?? null,\n request,\n })\n } catch (err) {\n if (isCrudHttpError(err)) return json(err.body, { status: err.status })\n throw err\n }\n\n webhook.deletedAt = new Date()\n await em.flush()\n\n await emitWebhooksEvent('webhooks.webhook.deleted', {\n webhookId: webhook.id,\n organizationId: webhook.organizationId,\n tenantId: webhook.tenantId,\n }, { persistent: true })\n\n return json({ success: true })\n}\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Get webhook detail',\n description: 'Returns a single webhook configuration for the current tenant scope.',\n methods: {\n GET: {\n summary: 'Get webhook',\n description: 'Returns webhook configuration, masked secret metadata, and delivery settings.',\n pathParams: z.object({ id: z.string().uuid() }),\n responses: [{ status: 200, description: 'Webhook detail', schema: webhookDetailResponseSchema }],\n errors: [\n { status: 401, description: 'Unauthorized', schema: errorSchema },\n { status: 404, description: 'Webhook not found', schema: errorSchema },\n ],\n },\n PUT: {\n summary: 'Update webhook',\n description: 'Updates a single webhook configuration.',\n pathParams: z.object({ id: z.string().uuid() }),\n requestBody: {\n contentType: 'application/json',\n schema: webhookUpdateSchema,\n description: 'Webhook fields to update.',\n },\n responses: [{ status: 200, description: 'Webhook updated', schema: webhookDetailResponseSchema }],\n errors: [\n { status: 400, description: 'Invalid payload', schema: errorSchema },\n { status: 401, description: 'Unauthorized', schema: errorSchema },\n { status: 404, description: 'Webhook not found', schema: errorSchema },\n ],\n },\n DELETE: {\n summary: 'Delete webhook',\n description: 'Soft-deletes a webhook endpoint.',\n pathParams: z.object({ id: z.string().uuid() }),\n responses: [{ status: 200, description: 'Webhook deleted', schema: deleteResponseSchema }],\n errors: [\n { status: 401, description: 'Unauthorized', schema: errorSchema },\n { status: 404, description: 'Webhook not found', schema: errorSchema },\n ],\n },\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { emitWebhooksEvent } from '../../../events'\nimport { findScopedWebhook, json, resolveWebhookRequestScope, serializeWebhookDetail } from '../../helpers'\nimport { webhookUpdateSchema } from '../../../data/validators'\nimport { enforceCommandOptimisticLockWithGuards } from '@open-mercato/shared/lib/crud/optimistic-lock-command'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { invalidateWebhookSubscriptionCacheFor } from '../../../lib/subscription-cache'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['webhooks.view'] },\n PUT: { requireAuth: true, requireFeatures: ['webhooks.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['webhooks.manage'] },\n}\n\ninterface RouteContext {\n params: Promise<{ id: string }>\n}\n\nconst webhookDetailResponseSchema = z.object({\n id: z.string(),\n name: z.string(),\n description: z.string().nullable(),\n url: z.string(),\n subscribedEvents: z.array(z.string()),\n httpMethod: z.string(),\n isActive: z.boolean(),\n deliveryStrategy: z.string(),\n maxRetries: z.number(),\n consecutiveFailures: z.number(),\n lastSuccessAt: z.string().nullable(),\n lastFailureAt: z.string().nullable(),\n createdAt: z.string(),\n updatedAt: z.string(),\n customHeaders: z.record(z.string(), z.string()).nullable(),\n strategyConfig: z.record(z.string(), z.unknown()).nullable(),\n timeoutMs: z.number(),\n rateLimitPerMinute: z.number(),\n autoDisableThreshold: z.number(),\n integrationId: z.string().nullable(),\n maskedSecret: z.string(),\n previousSecretSetAt: z.string().nullable(),\n})\n\nconst errorSchema = z.object({ error: z.string() })\nconst deleteResponseSchema = z.object({ success: z.literal(true) })\n\nexport async function GET(request: Request, context: RouteContext): Promise<Response> {\n const scope = await resolveWebhookRequestScope(request)\n if (scope instanceof Response) return scope\n\n const params = await context.params\n const webhook = await findScopedWebhook(scope.em.fork(), scope, params.id)\n\n if (!webhook) {\n return json({ error: 'Webhook not found' }, { status: 404 })\n }\n\n return json(serializeWebhookDetail(webhook))\n}\n\nexport async function PUT(request: Request, context: RouteContext): Promise<Response> {\n const scope = await resolveWebhookRequestScope(request)\n if (scope instanceof Response) return scope\n\n const params = await context.params\n const em = scope.em.fork()\n const webhook = await findScopedWebhook(em, scope, params.id)\n\n if (!webhook) {\n return json({ error: 'Webhook not found' }, { status: 404 })\n }\n\n try {\n await enforceCommandOptimisticLockWithGuards(scope.container, {\n resourceKind: 'webhooks.endpoint',\n resourceId: webhook.id,\n current: webhook.updatedAt ?? null,\n request,\n })\n } catch (err) {\n if (isCrudHttpError(err)) return json(err.body, { status: err.status })\n throw err\n }\n\n const parsed = webhookUpdateSchema.safeParse(await request.json().catch(() => null))\n if (!parsed.success) {\n return json({ error: 'Invalid request payload' }, { status: 400 })\n }\n\n const input = parsed.data\n if (input.name !== undefined) webhook.name = input.name\n if (input.description !== undefined) webhook.description = input.description\n if (input.url !== undefined) webhook.url = input.url\n if (input.subscribedEvents !== undefined) webhook.subscribedEvents = input.subscribedEvents\n if (input.httpMethod !== undefined) webhook.httpMethod = input.httpMethod\n if (input.customHeaders !== undefined) webhook.customHeaders = input.customHeaders\n if (input.deliveryStrategy !== undefined) webhook.deliveryStrategy = input.deliveryStrategy\n if (input.strategyConfig !== undefined) webhook.strategyConfig = input.strategyConfig\n if (input.maxRetries !== undefined) webhook.maxRetries = input.maxRetries\n if (input.timeoutMs !== undefined) webhook.timeoutMs = input.timeoutMs\n if (input.rateLimitPerMinute !== undefined) webhook.rateLimitPerMinute = input.rateLimitPerMinute\n if (input.autoDisableThreshold !== undefined) webhook.autoDisableThreshold = input.autoDisableThreshold\n if (input.integrationId !== undefined) webhook.integrationId = input.integrationId\n if (input.isActive !== undefined) webhook.isActive = input.isActive\n\n await em.flush()\n\n await invalidateWebhookSubscriptionCacheFor(scope.container, webhook.tenantId)\n\n await emitWebhooksEvent('webhooks.webhook.updated', {\n webhookId: webhook.id,\n organizationId: webhook.organizationId,\n tenantId: webhook.tenantId,\n }, { persistent: true })\n\n return json(serializeWebhookDetail(webhook))\n}\n\nexport async function DELETE(request: Request, context: RouteContext): Promise<Response> {\n const scope = await resolveWebhookRequestScope(request)\n if (scope instanceof Response) return scope\n\n const params = await context.params\n const em = scope.em.fork()\n const webhook = await findScopedWebhook(em, scope, params.id)\n const { translate } = await resolveTranslations()\n\n if (!webhook) {\n return json({ error: translate('webhooks.errors.notFound', 'Webhook not found') }, { status: 404 })\n }\n\n try {\n await enforceCommandOptimisticLockWithGuards(scope.container, {\n resourceKind: 'webhooks.endpoint',\n resourceId: webhook.id,\n current: webhook.updatedAt ?? null,\n request,\n })\n } catch (err) {\n if (isCrudHttpError(err)) return json(err.body, { status: err.status })\n throw err\n }\n\n webhook.deletedAt = new Date()\n await em.flush()\n\n await invalidateWebhookSubscriptionCacheFor(scope.container, webhook.tenantId)\n\n await emitWebhooksEvent('webhooks.webhook.deleted', {\n webhookId: webhook.id,\n organizationId: webhook.organizationId,\n tenantId: webhook.tenantId,\n }, { persistent: true })\n\n return json({ success: true })\n}\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Get webhook detail',\n description: 'Returns a single webhook configuration for the current tenant scope.',\n methods: {\n GET: {\n summary: 'Get webhook',\n description: 'Returns webhook configuration, masked secret metadata, and delivery settings.',\n pathParams: z.object({ id: z.string().uuid() }),\n responses: [{ status: 200, description: 'Webhook detail', schema: webhookDetailResponseSchema }],\n errors: [\n { status: 401, description: 'Unauthorized', schema: errorSchema },\n { status: 404, description: 'Webhook not found', schema: errorSchema },\n ],\n },\n PUT: {\n summary: 'Update webhook',\n description: 'Updates a single webhook configuration.',\n pathParams: z.object({ id: z.string().uuid() }),\n requestBody: {\n contentType: 'application/json',\n schema: webhookUpdateSchema,\n description: 'Webhook fields to update.',\n },\n responses: [{ status: 200, description: 'Webhook updated', schema: webhookDetailResponseSchema }],\n errors: [\n { status: 400, description: 'Invalid payload', schema: errorSchema },\n { status: 401, description: 'Unauthorized', schema: errorSchema },\n { status: 404, description: 'Webhook not found', schema: errorSchema },\n ],\n },\n DELETE: {\n summary: 'Delete webhook',\n description: 'Soft-deletes a webhook endpoint.',\n pathParams: z.object({ id: z.string().uuid() }),\n responses: [{ status: 200, description: 'Webhook deleted', schema: deleteResponseSchema }],\n errors: [\n { status: 401, description: 'Unauthorized', schema: errorSchema },\n { status: 404, description: 'Webhook not found', schema: errorSchema },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -69,7 +69,7 @@ async function POST(request, context) {
69
69
  deliveryId: delivery.id,
70
70
  tenantId: delivery.tenantId,
71
71
  organizationId: delivery.organizationId
72
- }, { scheduleRetries: false });
72
+ }, { scheduleRetries: false, resolver: scope.container });
73
73
  await em.refresh(delivery);
74
74
  return json({
75
75
  success: true,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../../src/modules/webhooks/api/webhooks/%5Bid%5D/test/route.ts"],
4
- "sourcesContent": ["import { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { createWebhookDelivery, processWebhookDeliveryJob } from '../../../../lib/delivery'\nimport { isWebhookIntegrationEnabled } from '../../../../lib/integration-state'\nimport { findScopedWebhook, json, resolveWebhookRequestScope, serializeDeliveryDetail } from '../../../helpers'\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['webhooks.test'] },\n}\n\ninterface RouteContext {\n params: Promise<{ id: string }>\n}\n\nconst requestBodySchema = z.object({\n eventType: z.string().min(1).optional(),\n payload: z.record(z.string(), z.unknown()).optional(),\n})\n\nconst testResponseSchema = z.object({\n success: z.literal(true),\n delivery: z.object({\n id: z.string(),\n webhookId: z.string(),\n eventType: z.string(),\n messageId: z.string(),\n status: z.string(),\n responseStatus: z.number().nullable(),\n errorMessage: z.string().nullable(),\n attemptNumber: z.number(),\n maxAttempts: z.number(),\n targetUrl: z.string(),\n durationMs: z.number().nullable(),\n enqueuedAt: z.string(),\n lastAttemptAt: z.string().nullable(),\n deliveredAt: z.string().nullable(),\n createdAt: z.string(),\n payload: z.record(z.string(), z.unknown()),\n responseBody: z.string().nullable(),\n responseHeaders: z.record(z.string(), z.string()).nullable(),\n nextRetryAt: z.string().nullable(),\n updatedAt: z.string(),\n }),\n})\n\nconst errorSchema = z.object({ error: z.string() })\n\nexport async function POST(request: Request, context: RouteContext): Promise<Response> {\n const scope = await resolveWebhookRequestScope(request)\n if (scope instanceof Response) return scope\n\n const params = await context.params\n const em = scope.em.fork()\n const webhook = await findScopedWebhook(em, scope, params.id)\n\n if (!webhook) {\n return json({ error: 'Webhook not found' }, { status: 404 })\n }\n\n const integrationEnabled = await isWebhookIntegrationEnabled(em, {\n tenantId: webhook.tenantId,\n organizationId: webhook.organizationId,\n })\n if (!integrationEnabled) {\n return json({ error: 'Custom Webhooks integration is disabled' }, { status: 409 })\n }\n\n const parsed = requestBodySchema.safeParse(await request.json().catch(() => ({})))\n if (!parsed.success) {\n return json({ error: 'Invalid request payload' }, { status: 400 })\n }\n\n const delivery = await createWebhookDelivery({\n em,\n webhook,\n eventId: parsed.data.eventType ?? 'webhooks.test.ping',\n payload: parsed.data.payload ?? {\n ping: true,\n webhookId: webhook.id,\n testedAt: new Date().toISOString(),\n },\n })\n\n await processWebhookDeliveryJob(em, {\n deliveryId: delivery.id,\n tenantId: delivery.tenantId,\n organizationId: delivery.organizationId,\n }, { scheduleRetries: false })\n\n await em.refresh(delivery)\n\n return json({\n success: true,\n delivery: serializeDeliveryDetail(delivery),\n })\n}\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Send test webhook delivery',\n description: 'Creates and sends a synchronous test delivery for a webhook endpoint.',\n methods: {\n POST: {\n summary: 'Test webhook',\n description: 'Creates a synthetic event payload and delivers it immediately without using the queue.',\n pathParams: z.object({ id: z.string().uuid() }),\n requestBody: {\n contentType: 'application/json',\n schema: requestBodySchema,\n description: 'Optional custom event type and payload for the test delivery.',\n },\n responses: [{ status: 200, description: 'Test delivery result', schema: testResponseSchema }],\n errors: [\n { status: 400, description: 'Invalid request payload', schema: errorSchema },\n { status: 409, description: 'Webhook integration disabled', schema: errorSchema },\n { status: 401, description: 'Unauthorized', schema: errorSchema },\n { status: 404, description: 'Webhook not found', schema: errorSchema },\n ],\n },\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { createWebhookDelivery, processWebhookDeliveryJob } from '../../../../lib/delivery'\nimport { isWebhookIntegrationEnabled } from '../../../../lib/integration-state'\nimport { findScopedWebhook, json, resolveWebhookRequestScope, serializeDeliveryDetail } from '../../../helpers'\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['webhooks.test'] },\n}\n\ninterface RouteContext {\n params: Promise<{ id: string }>\n}\n\nconst requestBodySchema = z.object({\n eventType: z.string().min(1).optional(),\n payload: z.record(z.string(), z.unknown()).optional(),\n})\n\nconst testResponseSchema = z.object({\n success: z.literal(true),\n delivery: z.object({\n id: z.string(),\n webhookId: z.string(),\n eventType: z.string(),\n messageId: z.string(),\n status: z.string(),\n responseStatus: z.number().nullable(),\n errorMessage: z.string().nullable(),\n attemptNumber: z.number(),\n maxAttempts: z.number(),\n targetUrl: z.string(),\n durationMs: z.number().nullable(),\n enqueuedAt: z.string(),\n lastAttemptAt: z.string().nullable(),\n deliveredAt: z.string().nullable(),\n createdAt: z.string(),\n payload: z.record(z.string(), z.unknown()),\n responseBody: z.string().nullable(),\n responseHeaders: z.record(z.string(), z.string()).nullable(),\n nextRetryAt: z.string().nullable(),\n updatedAt: z.string(),\n }),\n})\n\nconst errorSchema = z.object({ error: z.string() })\n\nexport async function POST(request: Request, context: RouteContext): Promise<Response> {\n const scope = await resolveWebhookRequestScope(request)\n if (scope instanceof Response) return scope\n\n const params = await context.params\n const em = scope.em.fork()\n const webhook = await findScopedWebhook(em, scope, params.id)\n\n if (!webhook) {\n return json({ error: 'Webhook not found' }, { status: 404 })\n }\n\n const integrationEnabled = await isWebhookIntegrationEnabled(em, {\n tenantId: webhook.tenantId,\n organizationId: webhook.organizationId,\n })\n if (!integrationEnabled) {\n return json({ error: 'Custom Webhooks integration is disabled' }, { status: 409 })\n }\n\n const parsed = requestBodySchema.safeParse(await request.json().catch(() => ({})))\n if (!parsed.success) {\n return json({ error: 'Invalid request payload' }, { status: 400 })\n }\n\n const delivery = await createWebhookDelivery({\n em,\n webhook,\n eventId: parsed.data.eventType ?? 'webhooks.test.ping',\n payload: parsed.data.payload ?? {\n ping: true,\n webhookId: webhook.id,\n testedAt: new Date().toISOString(),\n },\n })\n\n await processWebhookDeliveryJob(em, {\n deliveryId: delivery.id,\n tenantId: delivery.tenantId,\n organizationId: delivery.organizationId,\n }, { scheduleRetries: false, resolver: scope.container })\n\n await em.refresh(delivery)\n\n return json({\n success: true,\n delivery: serializeDeliveryDetail(delivery),\n })\n}\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Send test webhook delivery',\n description: 'Creates and sends a synchronous test delivery for a webhook endpoint.',\n methods: {\n POST: {\n summary: 'Test webhook',\n description: 'Creates a synthetic event payload and delivers it immediately without using the queue.',\n pathParams: z.object({ id: z.string().uuid() }),\n requestBody: {\n contentType: 'application/json',\n schema: requestBodySchema,\n description: 'Optional custom event type and payload for the test delivery.',\n },\n responses: [{ status: 200, description: 'Test delivery result', schema: testResponseSchema }],\n errors: [\n { status: 400, description: 'Invalid request payload', schema: errorSchema },\n { status: 409, description: 'Webhook integration disabled', schema: errorSchema },\n { status: 401, description: 'Unauthorized', schema: errorSchema },\n { status: 404, description: 'Webhook not found', schema: errorSchema },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -6,6 +6,7 @@ import { generateWebhookSecret } from "@open-mercato/shared/lib/webhooks";
6
6
  import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
7
7
  import { escapeLikePattern } from "@open-mercato/shared/lib/db/escapeLikePattern";
8
8
  import { parseBooleanToken } from "@open-mercato/shared/lib/boolean";
9
+ import { invalidateWebhookSubscriptionCacheFor } from "../../lib/subscription-cache.js";
9
10
  function json(payload, init = { status: 200 }) {
10
11
  return new Response(JSON.stringify(payload), {
11
12
  ...init,
@@ -53,6 +54,11 @@ const webhookDetailResponseSchema = webhookListItemSchema.extend({
53
54
  });
54
55
  const deleteResponseSchema = z.object({ success: z.literal(true) });
55
56
  const errorSchema = z.object({ error: z.string() });
57
+ function resolveTenantIdForInvalidation(entity, ctx) {
58
+ const entityTenantId = entity?.tenantId;
59
+ if (typeof entityTenantId === "string" && entityTenantId.length > 0) return entityTenantId;
60
+ return ctx.auth?.tenantId ?? null;
61
+ }
56
62
  const crud = makeCrudRoute({
57
63
  metadata: {
58
64
  GET: { requireAuth: true, requireFeatures: ["webhooks.view"] },
@@ -195,6 +201,13 @@ const crud = makeCrudRoute({
195
201
  ;
196
202
  entity.__revealSecret = scopedCtx.__webhookSecret;
197
203
  }
204
+ await invalidateWebhookSubscriptionCacheFor(ctx.container, resolveTenantIdForInvalidation(entity, ctx));
205
+ },
206
+ afterUpdate: async (entity, ctx) => {
207
+ await invalidateWebhookSubscriptionCacheFor(ctx.container, resolveTenantIdForInvalidation(entity, ctx));
208
+ },
209
+ afterDelete: async (_id, ctx) => {
210
+ await invalidateWebhookSubscriptionCacheFor(ctx.container, ctx.auth?.tenantId ?? null);
198
211
  },
199
212
  beforeDelete: async (id, ctx) => {
200
213
  const auth = ctx.auth;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/webhooks/api/webhooks/route.ts"],
4
- "sourcesContent": ["import { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'\nimport type { CrudCtx } from '@open-mercato/shared/lib/crud/factory'\nimport { WebhookEntity } from '../../data/entities'\nimport { webhookCreateSchema, webhookUpdateSchema, webhookListQuerySchema } from '../../data/validators'\nimport { generateWebhookSecret } from '@open-mercato/shared/lib/webhooks'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'\nimport { parseBooleanToken } from '@open-mercato/shared/lib/boolean'\nimport type { WebhookCreateInput, WebhookUpdateInput } from '../../data/validators'\n\ntype WebhookCrudCtx = CrudCtx & {\n __webhookSecret?: string\n}\n\nfunction json(payload: unknown, init: ResponseInit = { status: 200 }) {\n return new Response(JSON.stringify(payload), {\n ...init,\n headers: { 'content-type': 'application/json', ...(init.headers || {}) },\n })\n}\n\nconst webhookListItemSchema = z.object({\n id: z.string(),\n name: z.string(),\n description: z.string().nullable(),\n url: z.string(),\n subscribedEvents: z.array(z.string()),\n httpMethod: z.string(),\n isActive: z.boolean(),\n deliveryStrategy: z.string(),\n maxRetries: z.number(),\n consecutiveFailures: z.number(),\n lastSuccessAt: z.string().nullable(),\n lastFailureAt: z.string().nullable(),\n createdAt: z.string(),\n updatedAt: z.string(),\n})\n\nconst webhookCollectionResponseSchema = z.object({\n items: z.array(webhookListItemSchema),\n total: z.number().int().nonnegative(),\n page: z.number().int().positive(),\n pageSize: z.number().int().positive(),\n totalPages: z.number().int().nonnegative(),\n})\n\nconst webhookCreateResponseSchema = z.object({\n id: z.string(),\n name: z.string(),\n url: z.string(),\n secret: z.string(),\n subscribedEvents: z.array(z.string()),\n isActive: z.boolean(),\n})\n\nconst webhookDetailResponseSchema = webhookListItemSchema.extend({\n customHeaders: z.record(z.string(), z.string()).nullable(),\n strategyConfig: z.record(z.string(), z.unknown()).nullable(),\n timeoutMs: z.number(),\n rateLimitPerMinute: z.number(),\n autoDisableThreshold: z.number(),\n integrationId: z.string().nullable(),\n})\n\nconst deleteResponseSchema = z.object({ success: z.literal(true) })\nconst errorSchema = z.object({ error: z.string() })\n\nconst crud = makeCrudRoute<WebhookCreateInput, WebhookUpdateInput, z.infer<typeof webhookListQuerySchema>>({\n metadata: {\n GET: { requireAuth: true, requireFeatures: ['webhooks.view'] },\n POST: { requireAuth: true, requireFeatures: ['webhooks.manage'] },\n PUT: { requireAuth: true, requireFeatures: ['webhooks.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['webhooks.manage'] },\n },\n orm: { entity: WebhookEntity, orgField: 'organizationId' },\n events: { module: 'webhooks', entity: 'webhook', persistent: true },\n list: { schema: webhookListQuerySchema },\n create: {\n schema: webhookCreateSchema,\n mapToEntity: (input, ctx) => {\n const scopedCtx = ctx as WebhookCrudCtx\n const secret = scopedCtx.__webhookSecret\n if (!secret) throw new Error('Webhook secret not prepared')\n return {\n name: input.name,\n description: input.description ?? null,\n url: input.url,\n secret,\n subscribedEvents: input.subscribedEvents,\n httpMethod: input.httpMethod ?? 'POST',\n customHeaders: input.customHeaders ?? null,\n deliveryStrategy: input.deliveryStrategy ?? 'http',\n strategyConfig: input.strategyConfig ?? null,\n maxRetries: input.maxRetries ?? 10,\n timeoutMs: input.timeoutMs ?? 15000,\n rateLimitPerMinute: input.rateLimitPerMinute ?? 0,\n autoDisableThreshold: input.autoDisableThreshold ?? 100,\n integrationId: input.integrationId ?? null,\n organizationId: ctx.auth?.orgId ?? '',\n tenantId: ctx.auth?.tenantId ?? '',\n }\n },\n response: (entity) => ({\n id: entity.id,\n name: entity.name,\n url: entity.url,\n secret: (entity as WebhookEntity & { __revealSecret?: string }).__revealSecret ?? '***',\n subscribedEvents: entity.subscribedEvents,\n isActive: entity.isActive,\n }),\n },\n update: {\n schema: webhookUpdateSchema,\n applyToEntity: (entity, input) => {\n if (input.name !== undefined) entity.name = input.name\n if (input.description !== undefined) entity.description = input.description\n if (input.url !== undefined) entity.url = input.url\n if (input.subscribedEvents !== undefined) entity.subscribedEvents = input.subscribedEvents\n if (input.httpMethod !== undefined) entity.httpMethod = input.httpMethod\n if (input.customHeaders !== undefined) entity.customHeaders = input.customHeaders\n if (input.deliveryStrategy !== undefined) entity.deliveryStrategy = input.deliveryStrategy\n if (input.strategyConfig !== undefined) entity.strategyConfig = input.strategyConfig\n if (input.maxRetries !== undefined) entity.maxRetries = input.maxRetries\n if (input.timeoutMs !== undefined) entity.timeoutMs = input.timeoutMs\n if (input.rateLimitPerMinute !== undefined) entity.rateLimitPerMinute = input.rateLimitPerMinute\n if (input.autoDisableThreshold !== undefined) entity.autoDisableThreshold = input.autoDisableThreshold\n if (input.integrationId !== undefined) entity.integrationId = input.integrationId\n if (input.isActive !== undefined) entity.isActive = input.isActive\n },\n },\n del: { idFrom: 'query' },\n hooks: {\n beforeList: async (query, ctx) => {\n const auth = ctx.auth\n const { translate } = await resolveTranslations()\n if (!auth?.tenantId) throw json({ error: translate('webhooks.errors.tenantRequired', 'Tenant context required') }, { status: 400 })\n\n const page = Math.max(parseInt(query.page ?? '1', 10) || 1, 1)\n const pageSize = Math.min(Math.max(parseInt(query.pageSize ?? '20', 10) || 20, 1), 100)\n const search = (query.search ?? '').trim().toLowerCase()\n\n const organizationIds = Array.isArray(ctx.organizationIds) ? ctx.organizationIds : null\n if (organizationIds && organizationIds.length === 0) {\n throw json({ items: [], total: 0, page, pageSize, totalPages: 0 })\n }\n\n const em = ctx.container.resolve('em') as EntityManager\n const qb = em.createQueryBuilder(WebhookEntity, 'w')\n qb.where({ deletedAt: null })\n qb.andWhere({ tenantId: auth.tenantId })\n\n if (organizationIds && organizationIds.length > 0) {\n qb.andWhere({ organizationId: { $in: organizationIds } })\n } else if (auth.orgId) {\n qb.andWhere({ organizationId: auth.orgId })\n }\n\n if (search) {\n const pattern = `%${escapeLikePattern(search)}%`\n qb.andWhere({\n $or: [\n { name: { $ilike: pattern } },\n { url: { $ilike: pattern } },\n ],\n })\n }\n\n if (query.isActive !== undefined && query.isActive !== '') {\n const active = parseBooleanToken(query.isActive)\n if (active !== null) {\n qb.andWhere({ isActive: active })\n }\n }\n\n qb.orderBy({ createdAt: 'desc' })\n qb.limit(pageSize).offset((page - 1) * pageSize)\n const [items, total] = await qb.getResultAndCount()\n\n const payload = {\n items: items.map((item) => ({\n id: item.id,\n name: item.name,\n description: item.description ?? null,\n url: item.url,\n subscribedEvents: item.subscribedEvents,\n httpMethod: item.httpMethod,\n isActive: item.isActive,\n deliveryStrategy: item.deliveryStrategy,\n maxRetries: item.maxRetries,\n consecutiveFailures: item.consecutiveFailures,\n lastSuccessAt: item.lastSuccessAt?.toISOString() ?? null,\n lastFailureAt: item.lastFailureAt?.toISOString() ?? null,\n createdAt: item.createdAt.toISOString(),\n updatedAt: item.updatedAt.toISOString(),\n })),\n total,\n page,\n pageSize,\n totalPages: Math.ceil(total / pageSize),\n }\n\n throw json(payload)\n },\n beforeCreate: async (input, ctx) => {\n const auth = ctx.auth\n const { translate } = await resolveTranslations()\n if (!auth?.tenantId) throw json({ error: translate('webhooks.errors.tenantRequired', 'Tenant context required') }, { status: 400 })\n if (!auth?.orgId) throw json({ error: translate('webhooks.errors.orgRequired', 'Organization context required') }, { status: 400 })\n\n const scopedCtx = ctx as WebhookCrudCtx\n scopedCtx.__webhookSecret = generateWebhookSecret()\n\n return input\n },\n afterCreate: async (entity, ctx) => {\n const scopedCtx = ctx as WebhookCrudCtx\n if (scopedCtx.__webhookSecret) {\n ;(entity as WebhookEntity & { __revealSecret?: string }).__revealSecret = scopedCtx.__webhookSecret\n }\n },\n beforeDelete: async (id, ctx) => {\n const auth = ctx.auth\n const { translate } = await resolveTranslations()\n if (!auth?.tenantId) throw json({ error: translate('webhooks.errors.tenantRequired', 'Tenant context required') }, { status: 400 })\n\n const em = ctx.container.resolve('em') as EntityManager\n const record = await em.findOne(WebhookEntity, { id, deletedAt: null, tenantId: auth.tenantId })\n if (!record) throw json({ error: translate('webhooks.errors.notFound', 'Webhook not found') }, { status: 404 })\n\n const allowedIds = ctx.organizationScope?.allowedIds ?? null\n if (record.organizationId && Array.isArray(allowedIds) && allowedIds.length > 0) {\n if (!allowedIds.includes(record.organizationId)) {\n throw json({ error: translate('webhooks.errors.forbidden', 'Forbidden') }, { status: 403 })\n }\n }\n },\n },\n})\n\nexport const metadata = crud.metadata\nexport const GET = crud.GET\nexport const POST = crud.POST\nexport const PUT = crud.PUT\nexport const DELETE = crud.DELETE\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Manage webhooks',\n description: 'CRUD operations for webhook endpoints following the Standard Webhooks specification.',\n methods: {\n GET: {\n summary: 'List webhooks',\n description: 'Returns paginated webhooks for the current tenant and organization.',\n query: webhookListQuerySchema,\n responses: [{ status: 200, description: 'Webhook collection', schema: webhookCollectionResponseSchema }],\n errors: [\n { status: 400, description: 'Tenant context missing', schema: errorSchema },\n { status: 401, description: 'Unauthorized', schema: errorSchema },\n ],\n },\n POST: {\n summary: 'Create webhook',\n description: 'Creates a new webhook endpoint. A signing secret (whsec_ prefixed) is auto-generated and returned once.',\n requestBody: { contentType: 'application/json', schema: webhookCreateSchema, description: 'Webhook configuration.' },\n responses: [{ status: 201, description: 'Webhook created', schema: webhookCreateResponseSchema }],\n errors: [\n { status: 400, description: 'Invalid payload', schema: errorSchema },\n { status: 401, description: 'Unauthorized', schema: errorSchema },\n ],\n },\n PUT: {\n summary: 'Update webhook',\n description: 'Updates an existing webhook configuration.',\n requestBody: { contentType: 'application/json', schema: webhookUpdateSchema, description: 'Fields to update.' },\n responses: [{ status: 200, description: 'Webhook updated', schema: webhookDetailResponseSchema }],\n errors: [\n { status: 400, description: 'Invalid payload', schema: errorSchema },\n { status: 404, description: 'Not found', schema: errorSchema },\n ],\n },\n DELETE: {\n summary: 'Delete webhook',\n description: 'Soft-deletes a webhook endpoint.',\n query: z.object({ id: z.string().uuid().describe('Webhook ID to delete') }),\n responses: [{ status: 200, description: 'Deleted', schema: deleteResponseSchema }],\n errors: [\n { status: 404, description: 'Not found', schema: errorSchema },\n { status: 403, description: 'Forbidden', schema: errorSchema },\n ],\n },\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'\nimport type { CrudCtx } from '@open-mercato/shared/lib/crud/factory'\nimport { WebhookEntity } from '../../data/entities'\nimport { webhookCreateSchema, webhookUpdateSchema, webhookListQuerySchema } from '../../data/validators'\nimport { generateWebhookSecret } from '@open-mercato/shared/lib/webhooks'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'\nimport { parseBooleanToken } from '@open-mercato/shared/lib/boolean'\nimport type { WebhookCreateInput, WebhookUpdateInput } from '../../data/validators'\nimport { invalidateWebhookSubscriptionCacheFor } from '../../lib/subscription-cache'\n\ntype WebhookCrudCtx = CrudCtx & {\n __webhookSecret?: string\n}\n\nfunction json(payload: unknown, init: ResponseInit = { status: 200 }) {\n return new Response(JSON.stringify(payload), {\n ...init,\n headers: { 'content-type': 'application/json', ...(init.headers || {}) },\n })\n}\n\nconst webhookListItemSchema = z.object({\n id: z.string(),\n name: z.string(),\n description: z.string().nullable(),\n url: z.string(),\n subscribedEvents: z.array(z.string()),\n httpMethod: z.string(),\n isActive: z.boolean(),\n deliveryStrategy: z.string(),\n maxRetries: z.number(),\n consecutiveFailures: z.number(),\n lastSuccessAt: z.string().nullable(),\n lastFailureAt: z.string().nullable(),\n createdAt: z.string(),\n updatedAt: z.string(),\n})\n\nconst webhookCollectionResponseSchema = z.object({\n items: z.array(webhookListItemSchema),\n total: z.number().int().nonnegative(),\n page: z.number().int().positive(),\n pageSize: z.number().int().positive(),\n totalPages: z.number().int().nonnegative(),\n})\n\nconst webhookCreateResponseSchema = z.object({\n id: z.string(),\n name: z.string(),\n url: z.string(),\n secret: z.string(),\n subscribedEvents: z.array(z.string()),\n isActive: z.boolean(),\n})\n\nconst webhookDetailResponseSchema = webhookListItemSchema.extend({\n customHeaders: z.record(z.string(), z.string()).nullable(),\n strategyConfig: z.record(z.string(), z.unknown()).nullable(),\n timeoutMs: z.number(),\n rateLimitPerMinute: z.number(),\n autoDisableThreshold: z.number(),\n integrationId: z.string().nullable(),\n})\n\nconst deleteResponseSchema = z.object({ success: z.literal(true) })\nconst errorSchema = z.object({ error: z.string() })\n\nfunction resolveTenantIdForInvalidation(entity: unknown, ctx: CrudCtx): string | null {\n const entityTenantId = (entity as { tenantId?: unknown } | null)?.tenantId\n if (typeof entityTenantId === 'string' && entityTenantId.length > 0) return entityTenantId\n return ctx.auth?.tenantId ?? null\n}\n\nconst crud = makeCrudRoute<WebhookCreateInput, WebhookUpdateInput, z.infer<typeof webhookListQuerySchema>>({\n metadata: {\n GET: { requireAuth: true, requireFeatures: ['webhooks.view'] },\n POST: { requireAuth: true, requireFeatures: ['webhooks.manage'] },\n PUT: { requireAuth: true, requireFeatures: ['webhooks.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['webhooks.manage'] },\n },\n orm: { entity: WebhookEntity, orgField: 'organizationId' },\n events: { module: 'webhooks', entity: 'webhook', persistent: true },\n list: { schema: webhookListQuerySchema },\n create: {\n schema: webhookCreateSchema,\n mapToEntity: (input, ctx) => {\n const scopedCtx = ctx as WebhookCrudCtx\n const secret = scopedCtx.__webhookSecret\n if (!secret) throw new Error('Webhook secret not prepared')\n return {\n name: input.name,\n description: input.description ?? null,\n url: input.url,\n secret,\n subscribedEvents: input.subscribedEvents,\n httpMethod: input.httpMethod ?? 'POST',\n customHeaders: input.customHeaders ?? null,\n deliveryStrategy: input.deliveryStrategy ?? 'http',\n strategyConfig: input.strategyConfig ?? null,\n maxRetries: input.maxRetries ?? 10,\n timeoutMs: input.timeoutMs ?? 15000,\n rateLimitPerMinute: input.rateLimitPerMinute ?? 0,\n autoDisableThreshold: input.autoDisableThreshold ?? 100,\n integrationId: input.integrationId ?? null,\n organizationId: ctx.auth?.orgId ?? '',\n tenantId: ctx.auth?.tenantId ?? '',\n }\n },\n response: (entity) => ({\n id: entity.id,\n name: entity.name,\n url: entity.url,\n secret: (entity as WebhookEntity & { __revealSecret?: string }).__revealSecret ?? '***',\n subscribedEvents: entity.subscribedEvents,\n isActive: entity.isActive,\n }),\n },\n update: {\n schema: webhookUpdateSchema,\n applyToEntity: (entity, input) => {\n if (input.name !== undefined) entity.name = input.name\n if (input.description !== undefined) entity.description = input.description\n if (input.url !== undefined) entity.url = input.url\n if (input.subscribedEvents !== undefined) entity.subscribedEvents = input.subscribedEvents\n if (input.httpMethod !== undefined) entity.httpMethod = input.httpMethod\n if (input.customHeaders !== undefined) entity.customHeaders = input.customHeaders\n if (input.deliveryStrategy !== undefined) entity.deliveryStrategy = input.deliveryStrategy\n if (input.strategyConfig !== undefined) entity.strategyConfig = input.strategyConfig\n if (input.maxRetries !== undefined) entity.maxRetries = input.maxRetries\n if (input.timeoutMs !== undefined) entity.timeoutMs = input.timeoutMs\n if (input.rateLimitPerMinute !== undefined) entity.rateLimitPerMinute = input.rateLimitPerMinute\n if (input.autoDisableThreshold !== undefined) entity.autoDisableThreshold = input.autoDisableThreshold\n if (input.integrationId !== undefined) entity.integrationId = input.integrationId\n if (input.isActive !== undefined) entity.isActive = input.isActive\n },\n },\n del: { idFrom: 'query' },\n hooks: {\n beforeList: async (query, ctx) => {\n const auth = ctx.auth\n const { translate } = await resolveTranslations()\n if (!auth?.tenantId) throw json({ error: translate('webhooks.errors.tenantRequired', 'Tenant context required') }, { status: 400 })\n\n const page = Math.max(parseInt(query.page ?? '1', 10) || 1, 1)\n const pageSize = Math.min(Math.max(parseInt(query.pageSize ?? '20', 10) || 20, 1), 100)\n const search = (query.search ?? '').trim().toLowerCase()\n\n const organizationIds = Array.isArray(ctx.organizationIds) ? ctx.organizationIds : null\n if (organizationIds && organizationIds.length === 0) {\n throw json({ items: [], total: 0, page, pageSize, totalPages: 0 })\n }\n\n const em = ctx.container.resolve('em') as EntityManager\n const qb = em.createQueryBuilder(WebhookEntity, 'w')\n qb.where({ deletedAt: null })\n qb.andWhere({ tenantId: auth.tenantId })\n\n if (organizationIds && organizationIds.length > 0) {\n qb.andWhere({ organizationId: { $in: organizationIds } })\n } else if (auth.orgId) {\n qb.andWhere({ organizationId: auth.orgId })\n }\n\n if (search) {\n const pattern = `%${escapeLikePattern(search)}%`\n qb.andWhere({\n $or: [\n { name: { $ilike: pattern } },\n { url: { $ilike: pattern } },\n ],\n })\n }\n\n if (query.isActive !== undefined && query.isActive !== '') {\n const active = parseBooleanToken(query.isActive)\n if (active !== null) {\n qb.andWhere({ isActive: active })\n }\n }\n\n qb.orderBy({ createdAt: 'desc' })\n qb.limit(pageSize).offset((page - 1) * pageSize)\n const [items, total] = await qb.getResultAndCount()\n\n const payload = {\n items: items.map((item) => ({\n id: item.id,\n name: item.name,\n description: item.description ?? null,\n url: item.url,\n subscribedEvents: item.subscribedEvents,\n httpMethod: item.httpMethod,\n isActive: item.isActive,\n deliveryStrategy: item.deliveryStrategy,\n maxRetries: item.maxRetries,\n consecutiveFailures: item.consecutiveFailures,\n lastSuccessAt: item.lastSuccessAt?.toISOString() ?? null,\n lastFailureAt: item.lastFailureAt?.toISOString() ?? null,\n createdAt: item.createdAt.toISOString(),\n updatedAt: item.updatedAt.toISOString(),\n })),\n total,\n page,\n pageSize,\n totalPages: Math.ceil(total / pageSize),\n }\n\n throw json(payload)\n },\n beforeCreate: async (input, ctx) => {\n const auth = ctx.auth\n const { translate } = await resolveTranslations()\n if (!auth?.tenantId) throw json({ error: translate('webhooks.errors.tenantRequired', 'Tenant context required') }, { status: 400 })\n if (!auth?.orgId) throw json({ error: translate('webhooks.errors.orgRequired', 'Organization context required') }, { status: 400 })\n\n const scopedCtx = ctx as WebhookCrudCtx\n scopedCtx.__webhookSecret = generateWebhookSecret()\n\n return input\n },\n afterCreate: async (entity, ctx) => {\n const scopedCtx = ctx as WebhookCrudCtx\n if (scopedCtx.__webhookSecret) {\n ;(entity as WebhookEntity & { __revealSecret?: string }).__revealSecret = scopedCtx.__webhookSecret\n }\n await invalidateWebhookSubscriptionCacheFor(ctx.container, resolveTenantIdForInvalidation(entity, ctx))\n },\n afterUpdate: async (entity, ctx) => {\n await invalidateWebhookSubscriptionCacheFor(ctx.container, resolveTenantIdForInvalidation(entity, ctx))\n },\n afterDelete: async (_id, ctx) => {\n await invalidateWebhookSubscriptionCacheFor(ctx.container, ctx.auth?.tenantId ?? null)\n },\n beforeDelete: async (id, ctx) => {\n const auth = ctx.auth\n const { translate } = await resolveTranslations()\n if (!auth?.tenantId) throw json({ error: translate('webhooks.errors.tenantRequired', 'Tenant context required') }, { status: 400 })\n\n const em = ctx.container.resolve('em') as EntityManager\n const record = await em.findOne(WebhookEntity, { id, deletedAt: null, tenantId: auth.tenantId })\n if (!record) throw json({ error: translate('webhooks.errors.notFound', 'Webhook not found') }, { status: 404 })\n\n const allowedIds = ctx.organizationScope?.allowedIds ?? null\n if (record.organizationId && Array.isArray(allowedIds) && allowedIds.length > 0) {\n if (!allowedIds.includes(record.organizationId)) {\n throw json({ error: translate('webhooks.errors.forbidden', 'Forbidden') }, { status: 403 })\n }\n }\n },\n },\n})\n\nexport const metadata = crud.metadata\nexport const GET = crud.GET\nexport const POST = crud.POST\nexport const PUT = crud.PUT\nexport const DELETE = crud.DELETE\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Manage webhooks',\n description: 'CRUD operations for webhook endpoints following the Standard Webhooks specification.',\n methods: {\n GET: {\n summary: 'List webhooks',\n description: 'Returns paginated webhooks for the current tenant and organization.',\n query: webhookListQuerySchema,\n responses: [{ status: 200, description: 'Webhook collection', schema: webhookCollectionResponseSchema }],\n errors: [\n { status: 400, description: 'Tenant context missing', schema: errorSchema },\n { status: 401, description: 'Unauthorized', schema: errorSchema },\n ],\n },\n POST: {\n summary: 'Create webhook',\n description: 'Creates a new webhook endpoint. A signing secret (whsec_ prefixed) is auto-generated and returned once.',\n requestBody: { contentType: 'application/json', schema: webhookCreateSchema, description: 'Webhook configuration.' },\n responses: [{ status: 201, description: 'Webhook created', schema: webhookCreateResponseSchema }],\n errors: [\n { status: 400, description: 'Invalid payload', schema: errorSchema },\n { status: 401, description: 'Unauthorized', schema: errorSchema },\n ],\n },\n PUT: {\n summary: 'Update webhook',\n description: 'Updates an existing webhook configuration.',\n requestBody: { contentType: 'application/json', schema: webhookUpdateSchema, description: 'Fields to update.' },\n responses: [{ status: 200, description: 'Webhook updated', schema: webhookDetailResponseSchema }],\n errors: [\n { status: 400, description: 'Invalid payload', schema: errorSchema },\n { status: 404, description: 'Not found', schema: errorSchema },\n ],\n },\n DELETE: {\n summary: 'Delete webhook',\n description: 'Soft-deletes a webhook endpoint.',\n query: z.object({ id: z.string().uuid().describe('Webhook ID to delete') }),\n responses: [{ status: 200, description: 'Deleted', schema: deleteResponseSchema }],\n errors: [\n { status: 404, description: 'Not found', schema: errorSchema },\n { status: 403, description: 'Forbidden', schema: errorSchema },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -10,6 +10,9 @@ import {
10
10
  safeWebhookFetch,
11
11
  UnsafeWebhookUrlError
12
12
  } from "./url-safety.js";
13
+ import {
14
+ invalidateWebhookSubscriptionCacheFor
15
+ } from "./subscription-cache.js";
13
16
  async function createWebhookDelivery(input) {
14
17
  const bodyPayload = {
15
18
  type: input.eventId,
@@ -161,6 +164,7 @@ async function processWebhookDeliveryJob(em, job, options = {}) {
161
164
  delivery,
162
165
  canRetry: shouldRetryStatus(response.status),
163
166
  scheduleRetries: options.scheduleRetries !== false,
167
+ resolver: options.resolver,
164
168
  fallbackMessage: `HTTP ${response.status}`
165
169
  });
166
170
  await emitWebhooksEvent("webhooks.delivery.failed", {
@@ -197,6 +201,7 @@ async function processWebhookDeliveryJob(em, job, options = {}) {
197
201
  delivery,
198
202
  canRetry: true,
199
203
  scheduleRetries: options.scheduleRetries !== false,
204
+ resolver: options.resolver,
200
205
  fallbackMessage: delivery.errorMessage
201
206
  });
202
207
  await emitWebhooksEvent("webhooks.delivery.failed", {
@@ -272,16 +277,18 @@ async function handleFailedDelivery(input) {
272
277
  errorMessage: delivery.errorMessage ?? input.fallbackMessage
273
278
  });
274
279
  }
280
+ await input.em.flush();
275
281
  if (webhook.autoDisableThreshold > 0 && webhook.consecutiveFailures >= webhook.autoDisableThreshold) {
276
282
  webhook.isActive = false;
283
+ await input.em.flush();
277
284
  await emitWebhooksEvent("webhooks.webhook.disabled", {
278
285
  webhookId: webhook.id,
279
286
  organizationId: webhook.organizationId,
280
287
  tenantId: webhook.tenantId,
281
288
  consecutiveFailures: webhook.consecutiveFailures
282
289
  });
290
+ await invalidateWebhookSubscriptionCacheFor(input.resolver, webhook.tenantId);
283
291
  }
284
- await input.em.flush();
285
292
  }
286
293
  function normalizeWebhookBody(eventType, payload) {
287
294
  const candidateType = typeof payload.type === "string" ? payload.type : null;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/webhooks/lib/delivery.ts"],
4
- "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { buildWebhookHeaders, generateMessageId } from '@open-mercato/shared/lib/webhooks'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { WebhookDeliveryEntity, WebhookEntity } from '../data/entities'\nimport { emitWebhooksEvent } from '../events'\nimport { enqueueWebhookDelivery } from './queue'\nimport { isWebhookIntegrationEnabled, WEBHOOK_INTEGRATION_DISABLED_MESSAGE } from './integration-state'\nimport { sanitizeWebhookCustomHeaders } from './custom-headers'\nimport {\n assertSafeWebhookDeliveryUrl,\n safeWebhookFetch,\n UnsafeWebhookUrlError,\n} from './url-safety'\n\nexport interface WebhookDeliveryJob {\n deliveryId: string\n tenantId: string\n organizationId: string\n}\n\nexport interface CreateWebhookDeliveryInput {\n em: EntityManager\n webhook: WebhookEntity\n eventId: string\n payload: Record<string, unknown>\n}\n\ntype ProcessWebhookDeliveryOptions = {\n scheduleRetries?: boolean\n}\n\ntype WebhookBody = {\n type: string\n timestamp: string\n data: Record<string, unknown>\n}\n\nexport async function createWebhookDelivery(input: CreateWebhookDeliveryInput): Promise<WebhookDeliveryEntity> {\n const bodyPayload: WebhookBody = {\n type: input.eventId,\n timestamp: new Date().toISOString(),\n data: input.payload,\n }\n const now = new Date()\n\n const delivery = input.em.create(WebhookDeliveryEntity, {\n webhookId: input.webhook.id,\n eventType: input.eventId,\n messageId: generateMessageId(),\n payload: bodyPayload,\n status: 'pending',\n attemptNumber: 0,\n maxAttempts: input.webhook.maxRetries,\n targetUrl: input.webhook.url,\n organizationId: input.webhook.organizationId,\n tenantId: input.webhook.tenantId,\n enqueuedAt: now,\n createdAt: now,\n updatedAt: now,\n })\n\n await input.em.flush()\n await emitWebhooksEvent('webhooks.delivery.enqueued', {\n deliveryId: delivery.id,\n webhookId: input.webhook.id,\n eventType: input.eventId,\n organizationId: input.webhook.organizationId,\n tenantId: input.webhook.tenantId,\n })\n return delivery\n}\n\nexport async function processWebhookDeliveryJob(\n em: EntityManager,\n job: WebhookDeliveryJob,\n options: ProcessWebhookDeliveryOptions = {},\n): Promise<{ status: string; deliveryId: string } | null> {\n const delivery = await em.findOne(WebhookDeliveryEntity, {\n id: job.deliveryId,\n tenantId: job.tenantId,\n organizationId: job.organizationId,\n })\n\n if (!delivery) return null\n\n const webhook = await findOneWithDecryption(\n em,\n WebhookEntity,\n {\n id: delivery.webhookId,\n tenantId: job.tenantId,\n organizationId: job.organizationId,\n deletedAt: null,\n },\n {},\n { tenantId: job.tenantId, organizationId: job.organizationId },\n )\n\n if (!webhook) {\n delivery.status = 'failed'\n delivery.errorMessage = 'Webhook not found'\n delivery.nextRetryAt = null\n await em.flush()\n return { status: delivery.status, deliveryId: delivery.id }\n }\n\n if (!webhook.isActive) {\n delivery.status = 'expired'\n delivery.errorMessage = 'Webhook is inactive'\n delivery.nextRetryAt = null\n await em.flush()\n return { status: delivery.status, deliveryId: delivery.id }\n }\n\n const integrationEnabled = await isWebhookIntegrationEnabled(em, {\n tenantId: webhook.tenantId,\n organizationId: webhook.organizationId,\n })\n\n if (!integrationEnabled) {\n delivery.status = 'expired'\n delivery.errorMessage = WEBHOOK_INTEGRATION_DISABLED_MESSAGE\n delivery.nextRetryAt = null\n await em.flush()\n return { status: delivery.status, deliveryId: delivery.id }\n }\n\n try {\n await assertSafeWebhookDeliveryUrl(webhook.url)\n } catch (error) {\n return await recordWebhookSafetyFailure({\n em,\n delivery,\n webhook,\n error,\n })\n }\n\n const bodyPayload = normalizeWebhookBody(delivery.eventType, delivery.payload)\n delivery.payload = bodyPayload\n delivery.status = 'sending'\n delivery.lastAttemptAt = new Date()\n delivery.errorMessage = null\n await em.flush()\n\n const body = JSON.stringify(bodyPayload)\n const timestamp = Math.floor(Date.now() / 1000)\n const headers = buildWebhookHeaders(\n delivery.messageId,\n timestamp,\n body,\n webhook.secret,\n webhook.previousSecret,\n )\n\n const attemptStartedAt = Date.now()\n\n try {\n const controller = new AbortController()\n const timeoutId = setTimeout(() => controller.abort(), webhook.timeoutMs)\n\n let response: Response\n try {\n response = await safeWebhookFetch(webhook.url, {\n method: webhook.httpMethod,\n redirect: 'manual',\n headers: {\n 'content-type': 'application/json',\n ...sanitizeWebhookCustomHeaders(webhook.customHeaders),\n ...headers,\n },\n body,\n signal: controller.signal,\n })\n } finally {\n clearTimeout(timeoutId)\n }\n\n delivery.attemptNumber += 1\n delivery.responseStatus = response.status\n delivery.responseBody = (await response.text()).slice(0, 4096)\n delivery.responseHeaders = Object.fromEntries(response.headers.entries())\n delivery.durationMs = Date.now() - delivery.enqueuedAt.getTime()\n delivery.lastAttemptAt = new Date()\n\n if (response.ok) {\n delivery.status = 'delivered'\n delivery.deliveredAt = new Date()\n delivery.nextRetryAt = null\n webhook.consecutiveFailures = 0\n webhook.lastSuccessAt = new Date()\n\n await emitWebhooksEvent('webhooks.delivery.succeeded', {\n deliveryId: delivery.id,\n webhookId: webhook.id,\n eventType: delivery.eventType,\n organizationId: delivery.organizationId,\n tenantId: delivery.tenantId,\n })\n\n await em.flush()\n return { status: delivery.status, deliveryId: delivery.id }\n }\n\n webhook.consecutiveFailures += 1\n webhook.lastFailureAt = new Date()\n\n await handleFailedDelivery({\n em,\n webhook,\n delivery,\n canRetry: shouldRetryStatus(response.status),\n scheduleRetries: options.scheduleRetries !== false,\n fallbackMessage: `HTTP ${response.status}`,\n })\n\n await emitWebhooksEvent('webhooks.delivery.failed', {\n deliveryId: delivery.id,\n webhookId: webhook.id,\n eventType: delivery.eventType,\n responseStatus: response.status,\n organizationId: delivery.organizationId,\n tenantId: delivery.tenantId,\n willRetry: delivery.status === 'pending',\n })\n\n return { status: delivery.status, deliveryId: delivery.id }\n } catch (error) {\n if (error instanceof UnsafeWebhookUrlError) {\n // DNS rebinding caught between the upfront safety check and the pinned connect:\n // the same attacker-controlled DNS would defeat retries too, so fail terminally.\n const durationMs = Date.now() - delivery.enqueuedAt.getTime()\n delivery.durationMs = durationMs\n return await recordWebhookSafetyFailure({\n em,\n delivery,\n webhook,\n error,\n durationMs,\n })\n }\n\n delivery.attemptNumber += 1\n delivery.durationMs = Date.now() - delivery.enqueuedAt.getTime()\n delivery.lastAttemptAt = new Date()\n delivery.errorMessage = error instanceof Error ? error.message : 'Unknown delivery error'\n\n webhook.consecutiveFailures += 1\n webhook.lastFailureAt = new Date()\n\n await handleFailedDelivery({\n em,\n webhook,\n delivery,\n canRetry: true,\n scheduleRetries: options.scheduleRetries !== false,\n fallbackMessage: delivery.errorMessage,\n })\n\n await emitWebhooksEvent('webhooks.delivery.failed', {\n deliveryId: delivery.id,\n webhookId: webhook.id,\n eventType: delivery.eventType,\n errorMessage: delivery.errorMessage,\n durationMs: Date.now() - attemptStartedAt,\n organizationId: delivery.organizationId,\n tenantId: delivery.tenantId,\n willRetry: delivery.status === 'pending',\n })\n\n return { status: delivery.status, deliveryId: delivery.id }\n }\n}\n\ntype RecordWebhookSafetyFailureInput = {\n em: EntityManager\n delivery: WebhookDeliveryEntity\n webhook: WebhookEntity\n error: unknown\n durationMs?: number | null\n}\n\nasync function recordWebhookSafetyFailure(\n input: RecordWebhookSafetyFailureInput,\n): Promise<{ status: string; deliveryId: string }> {\n const { em, delivery, webhook, error } = input\n const message = error instanceof UnsafeWebhookUrlError\n ? error.message\n : 'Webhook URL rejected by safety check'\n\n delivery.status = 'failed'\n delivery.errorMessage = message\n delivery.nextRetryAt = null\n delivery.lastAttemptAt = new Date()\n delivery.attemptNumber += 1\n webhook.consecutiveFailures += 1\n webhook.lastFailureAt = new Date()\n await em.flush()\n\n await emitWebhooksEvent('webhooks.delivery.failed', {\n deliveryId: delivery.id,\n webhookId: webhook.id,\n eventType: delivery.eventType,\n errorMessage: message,\n durationMs: input.durationMs ?? 0,\n organizationId: delivery.organizationId,\n tenantId: delivery.tenantId,\n willRetry: false,\n })\n\n return { status: delivery.status, deliveryId: delivery.id }\n}\n\ntype HandleFailedDeliveryInput = {\n em: EntityManager\n webhook: WebhookEntity\n delivery: WebhookDeliveryEntity\n canRetry: boolean\n scheduleRetries: boolean\n fallbackMessage: string\n}\n\nasync function handleFailedDelivery(input: HandleFailedDeliveryInput): Promise<void> {\n const { delivery, webhook } = input\n const retriesRemaining = delivery.attemptNumber < Math.max(delivery.maxAttempts, 1)\n const shouldRetry = input.canRetry && retriesRemaining\n\n if (shouldRetry) {\n const nextRetryAt = calculateNextRetry(delivery.attemptNumber)\n delivery.status = 'pending'\n delivery.nextRetryAt = nextRetryAt\n\n await input.em.flush()\n\n if (input.scheduleRetries) {\n try {\n await enqueueWebhookDelivery(\n {\n deliveryId: delivery.id,\n tenantId: delivery.tenantId,\n organizationId: delivery.organizationId,\n },\n Math.max(nextRetryAt.getTime() - Date.now(), 0),\n )\n } catch (error) {\n delivery.status = 'failed'\n delivery.nextRetryAt = null\n delivery.errorMessage = error instanceof Error\n ? `Retry scheduling failed: ${error.message}`\n : 'Retry scheduling failed'\n }\n }\n } else {\n delivery.status = 'expired'\n delivery.nextRetryAt = null\n\n await emitWebhooksEvent('webhooks.delivery.exhausted', {\n deliveryId: delivery.id,\n webhookId: webhook.id,\n eventType: delivery.eventType,\n organizationId: delivery.organizationId,\n tenantId: delivery.tenantId,\n errorMessage: delivery.errorMessage ?? input.fallbackMessage,\n })\n }\n\n if (webhook.autoDisableThreshold > 0 && webhook.consecutiveFailures >= webhook.autoDisableThreshold) {\n webhook.isActive = false\n await emitWebhooksEvent('webhooks.webhook.disabled', {\n webhookId: webhook.id,\n organizationId: webhook.organizationId,\n tenantId: webhook.tenantId,\n consecutiveFailures: webhook.consecutiveFailures,\n })\n }\n\n await input.em.flush()\n}\n\nfunction normalizeWebhookBody(eventType: string, payload: Record<string, unknown>): WebhookBody {\n const candidateType = typeof payload.type === 'string' ? payload.type : null\n const candidateTimestamp = typeof payload.timestamp === 'string' ? payload.timestamp : null\n const candidateData = isRecord(payload.data) ? payload.data : null\n\n if (candidateType && candidateTimestamp && candidateData) {\n return {\n type: candidateType,\n timestamp: candidateTimestamp,\n data: candidateData,\n }\n }\n\n return {\n type: eventType,\n timestamp: new Date().toISOString(),\n data: payload,\n }\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\nfunction shouldRetryStatus(status: number): boolean {\n if (status >= 200 && status < 300) return false\n if (status === 408 || status === 429) return true\n return status >= 500\n}\n\nfunction calculateNextRetry(attemptNumber: number): Date {\n const baseDelayMs = 1000\n const jitterMs = Math.floor(Math.random() * 1000)\n const delayMs = baseDelayMs * Math.pow(2, Math.max(attemptNumber - 1, 0)) + jitterMs\n return new Date(Date.now() + delayMs)\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { buildWebhookHeaders, generateMessageId } from '@open-mercato/shared/lib/webhooks'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { WebhookDeliveryEntity, WebhookEntity } from '../data/entities'\nimport { emitWebhooksEvent } from '../events'\nimport { enqueueWebhookDelivery } from './queue'\nimport { isWebhookIntegrationEnabled, WEBHOOK_INTEGRATION_DISABLED_MESSAGE } from './integration-state'\nimport { sanitizeWebhookCustomHeaders } from './custom-headers'\nimport {\n assertSafeWebhookDeliveryUrl,\n safeWebhookFetch,\n UnsafeWebhookUrlError,\n} from './url-safety'\nimport {\n invalidateWebhookSubscriptionCacheFor,\n type WebhookCacheDependencyResolver,\n} from './subscription-cache'\n\nexport interface WebhookDeliveryJob {\n deliveryId: string\n tenantId: string\n organizationId: string\n}\n\nexport interface CreateWebhookDeliveryInput {\n em: EntityManager\n webhook: WebhookEntity\n eventId: string\n payload: Record<string, unknown>\n}\n\ntype ProcessWebhookDeliveryOptions = {\n scheduleRetries?: boolean\n resolver?: WebhookCacheDependencyResolver\n}\n\ntype WebhookBody = {\n type: string\n timestamp: string\n data: Record<string, unknown>\n}\n\nexport async function createWebhookDelivery(input: CreateWebhookDeliveryInput): Promise<WebhookDeliveryEntity> {\n const bodyPayload: WebhookBody = {\n type: input.eventId,\n timestamp: new Date().toISOString(),\n data: input.payload,\n }\n const now = new Date()\n\n const delivery = input.em.create(WebhookDeliveryEntity, {\n webhookId: input.webhook.id,\n eventType: input.eventId,\n messageId: generateMessageId(),\n payload: bodyPayload,\n status: 'pending',\n attemptNumber: 0,\n maxAttempts: input.webhook.maxRetries,\n targetUrl: input.webhook.url,\n organizationId: input.webhook.organizationId,\n tenantId: input.webhook.tenantId,\n enqueuedAt: now,\n createdAt: now,\n updatedAt: now,\n })\n\n await input.em.flush()\n await emitWebhooksEvent('webhooks.delivery.enqueued', {\n deliveryId: delivery.id,\n webhookId: input.webhook.id,\n eventType: input.eventId,\n organizationId: input.webhook.organizationId,\n tenantId: input.webhook.tenantId,\n })\n return delivery\n}\n\nexport async function processWebhookDeliveryJob(\n em: EntityManager,\n job: WebhookDeliveryJob,\n options: ProcessWebhookDeliveryOptions = {},\n): Promise<{ status: string; deliveryId: string } | null> {\n const delivery = await em.findOne(WebhookDeliveryEntity, {\n id: job.deliveryId,\n tenantId: job.tenantId,\n organizationId: job.organizationId,\n })\n\n if (!delivery) return null\n\n const webhook = await findOneWithDecryption(\n em,\n WebhookEntity,\n {\n id: delivery.webhookId,\n tenantId: job.tenantId,\n organizationId: job.organizationId,\n deletedAt: null,\n },\n {},\n { tenantId: job.tenantId, organizationId: job.organizationId },\n )\n\n if (!webhook) {\n delivery.status = 'failed'\n delivery.errorMessage = 'Webhook not found'\n delivery.nextRetryAt = null\n await em.flush()\n return { status: delivery.status, deliveryId: delivery.id }\n }\n\n if (!webhook.isActive) {\n delivery.status = 'expired'\n delivery.errorMessage = 'Webhook is inactive'\n delivery.nextRetryAt = null\n await em.flush()\n return { status: delivery.status, deliveryId: delivery.id }\n }\n\n const integrationEnabled = await isWebhookIntegrationEnabled(em, {\n tenantId: webhook.tenantId,\n organizationId: webhook.organizationId,\n })\n\n if (!integrationEnabled) {\n delivery.status = 'expired'\n delivery.errorMessage = WEBHOOK_INTEGRATION_DISABLED_MESSAGE\n delivery.nextRetryAt = null\n await em.flush()\n return { status: delivery.status, deliveryId: delivery.id }\n }\n\n try {\n await assertSafeWebhookDeliveryUrl(webhook.url)\n } catch (error) {\n return await recordWebhookSafetyFailure({\n em,\n delivery,\n webhook,\n error,\n })\n }\n\n const bodyPayload = normalizeWebhookBody(delivery.eventType, delivery.payload)\n delivery.payload = bodyPayload\n delivery.status = 'sending'\n delivery.lastAttemptAt = new Date()\n delivery.errorMessage = null\n await em.flush()\n\n const body = JSON.stringify(bodyPayload)\n const timestamp = Math.floor(Date.now() / 1000)\n const headers = buildWebhookHeaders(\n delivery.messageId,\n timestamp,\n body,\n webhook.secret,\n webhook.previousSecret,\n )\n\n const attemptStartedAt = Date.now()\n\n try {\n const controller = new AbortController()\n const timeoutId = setTimeout(() => controller.abort(), webhook.timeoutMs)\n\n let response: Response\n try {\n response = await safeWebhookFetch(webhook.url, {\n method: webhook.httpMethod,\n redirect: 'manual',\n headers: {\n 'content-type': 'application/json',\n ...sanitizeWebhookCustomHeaders(webhook.customHeaders),\n ...headers,\n },\n body,\n signal: controller.signal,\n })\n } finally {\n clearTimeout(timeoutId)\n }\n\n delivery.attemptNumber += 1\n delivery.responseStatus = response.status\n delivery.responseBody = (await response.text()).slice(0, 4096)\n delivery.responseHeaders = Object.fromEntries(response.headers.entries())\n delivery.durationMs = Date.now() - delivery.enqueuedAt.getTime()\n delivery.lastAttemptAt = new Date()\n\n if (response.ok) {\n delivery.status = 'delivered'\n delivery.deliveredAt = new Date()\n delivery.nextRetryAt = null\n webhook.consecutiveFailures = 0\n webhook.lastSuccessAt = new Date()\n\n await emitWebhooksEvent('webhooks.delivery.succeeded', {\n deliveryId: delivery.id,\n webhookId: webhook.id,\n eventType: delivery.eventType,\n organizationId: delivery.organizationId,\n tenantId: delivery.tenantId,\n })\n\n await em.flush()\n return { status: delivery.status, deliveryId: delivery.id }\n }\n\n webhook.consecutiveFailures += 1\n webhook.lastFailureAt = new Date()\n\n await handleFailedDelivery({\n em,\n webhook,\n delivery,\n canRetry: shouldRetryStatus(response.status),\n scheduleRetries: options.scheduleRetries !== false,\n resolver: options.resolver,\n fallbackMessage: `HTTP ${response.status}`,\n })\n\n await emitWebhooksEvent('webhooks.delivery.failed', {\n deliveryId: delivery.id,\n webhookId: webhook.id,\n eventType: delivery.eventType,\n responseStatus: response.status,\n organizationId: delivery.organizationId,\n tenantId: delivery.tenantId,\n willRetry: delivery.status === 'pending',\n })\n\n return { status: delivery.status, deliveryId: delivery.id }\n } catch (error) {\n if (error instanceof UnsafeWebhookUrlError) {\n // DNS rebinding caught between the upfront safety check and the pinned connect:\n // the same attacker-controlled DNS would defeat retries too, so fail terminally.\n const durationMs = Date.now() - delivery.enqueuedAt.getTime()\n delivery.durationMs = durationMs\n return await recordWebhookSafetyFailure({\n em,\n delivery,\n webhook,\n error,\n durationMs,\n })\n }\n\n delivery.attemptNumber += 1\n delivery.durationMs = Date.now() - delivery.enqueuedAt.getTime()\n delivery.lastAttemptAt = new Date()\n delivery.errorMessage = error instanceof Error ? error.message : 'Unknown delivery error'\n\n webhook.consecutiveFailures += 1\n webhook.lastFailureAt = new Date()\n\n await handleFailedDelivery({\n em,\n webhook,\n delivery,\n canRetry: true,\n scheduleRetries: options.scheduleRetries !== false,\n resolver: options.resolver,\n fallbackMessage: delivery.errorMessage,\n })\n\n await emitWebhooksEvent('webhooks.delivery.failed', {\n deliveryId: delivery.id,\n webhookId: webhook.id,\n eventType: delivery.eventType,\n errorMessage: delivery.errorMessage,\n durationMs: Date.now() - attemptStartedAt,\n organizationId: delivery.organizationId,\n tenantId: delivery.tenantId,\n willRetry: delivery.status === 'pending',\n })\n\n return { status: delivery.status, deliveryId: delivery.id }\n }\n}\n\ntype RecordWebhookSafetyFailureInput = {\n em: EntityManager\n delivery: WebhookDeliveryEntity\n webhook: WebhookEntity\n error: unknown\n durationMs?: number | null\n}\n\nasync function recordWebhookSafetyFailure(\n input: RecordWebhookSafetyFailureInput,\n): Promise<{ status: string; deliveryId: string }> {\n const { em, delivery, webhook, error } = input\n const message = error instanceof UnsafeWebhookUrlError\n ? error.message\n : 'Webhook URL rejected by safety check'\n\n delivery.status = 'failed'\n delivery.errorMessage = message\n delivery.nextRetryAt = null\n delivery.lastAttemptAt = new Date()\n delivery.attemptNumber += 1\n webhook.consecutiveFailures += 1\n webhook.lastFailureAt = new Date()\n await em.flush()\n\n await emitWebhooksEvent('webhooks.delivery.failed', {\n deliveryId: delivery.id,\n webhookId: webhook.id,\n eventType: delivery.eventType,\n errorMessage: message,\n durationMs: input.durationMs ?? 0,\n organizationId: delivery.organizationId,\n tenantId: delivery.tenantId,\n willRetry: false,\n })\n\n return { status: delivery.status, deliveryId: delivery.id }\n}\n\ntype HandleFailedDeliveryInput = {\n em: EntityManager\n webhook: WebhookEntity\n delivery: WebhookDeliveryEntity\n canRetry: boolean\n scheduleRetries: boolean\n fallbackMessage: string\n resolver?: WebhookCacheDependencyResolver\n}\n\nasync function handleFailedDelivery(input: HandleFailedDeliveryInput): Promise<void> {\n const { delivery, webhook } = input\n const retriesRemaining = delivery.attemptNumber < Math.max(delivery.maxAttempts, 1)\n const shouldRetry = input.canRetry && retriesRemaining\n\n if (shouldRetry) {\n const nextRetryAt = calculateNextRetry(delivery.attemptNumber)\n delivery.status = 'pending'\n delivery.nextRetryAt = nextRetryAt\n\n await input.em.flush()\n\n if (input.scheduleRetries) {\n try {\n await enqueueWebhookDelivery(\n {\n deliveryId: delivery.id,\n tenantId: delivery.tenantId,\n organizationId: delivery.organizationId,\n },\n Math.max(nextRetryAt.getTime() - Date.now(), 0),\n )\n } catch (error) {\n delivery.status = 'failed'\n delivery.nextRetryAt = null\n delivery.errorMessage = error instanceof Error\n ? `Retry scheduling failed: ${error.message}`\n : 'Retry scheduling failed'\n }\n }\n } else {\n delivery.status = 'expired'\n delivery.nextRetryAt = null\n\n await emitWebhooksEvent('webhooks.delivery.exhausted', {\n deliveryId: delivery.id,\n webhookId: webhook.id,\n eventType: delivery.eventType,\n organizationId: delivery.organizationId,\n tenantId: delivery.tenantId,\n errorMessage: delivery.errorMessage ?? input.fallbackMessage,\n })\n }\n\n await input.em.flush()\n\n if (webhook.autoDisableThreshold > 0 && webhook.consecutiveFailures >= webhook.autoDisableThreshold) {\n webhook.isActive = false\n await input.em.flush()\n await emitWebhooksEvent('webhooks.webhook.disabled', {\n webhookId: webhook.id,\n organizationId: webhook.organizationId,\n tenantId: webhook.tenantId,\n consecutiveFailures: webhook.consecutiveFailures,\n })\n await invalidateWebhookSubscriptionCacheFor(input.resolver, webhook.tenantId)\n }\n}\n\nfunction normalizeWebhookBody(eventType: string, payload: Record<string, unknown>): WebhookBody {\n const candidateType = typeof payload.type === 'string' ? payload.type : null\n const candidateTimestamp = typeof payload.timestamp === 'string' ? payload.timestamp : null\n const candidateData = isRecord(payload.data) ? payload.data : null\n\n if (candidateType && candidateTimestamp && candidateData) {\n return {\n type: candidateType,\n timestamp: candidateTimestamp,\n data: candidateData,\n }\n }\n\n return {\n type: eventType,\n timestamp: new Date().toISOString(),\n data: payload,\n }\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\nfunction shouldRetryStatus(status: number): boolean {\n if (status >= 200 && status < 300) return false\n if (status === 408 || status === 429) return true\n return status >= 500\n}\n\nfunction calculateNextRetry(attemptNumber: number): Date {\n const baseDelayMs = 1000\n const jitterMs = Math.floor(Math.random() * 1000)\n const delayMs = baseDelayMs * Math.pow(2, Math.max(attemptNumber - 1, 0)) + jitterMs\n return new Date(Date.now() + delayMs)\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -31,7 +31,7 @@ async function ensureLocalWebhookQueueWorkerStarted() {
31
31
  ]);
32
32
  const container = await createRequestContainer();
33
33
  const em = container.resolve("em").fork();
34
- await processWebhookDeliveryJob(em, job.payload);
34
+ await processWebhookDeliveryJob(em, job.payload, { resolver: container });
35
35
  });
36
36
  })().catch((error) => {
37
37
  delete globalStore[LOCAL_WORKER_PROMISE_KEY];
@@ -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'\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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",
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, { resolver: container })\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,93 @@
1
+ import { runWithCacheTenant } from "@open-mercato/cache";
2
+ import { createLogger } from "@open-mercato/shared/lib/logger";
3
+ const logger = createLogger("webhooks").child({ component: "subscription-cache" });
4
+ const DEFAULT_TTL_MS = 6e4;
5
+ const GLOBAL_SCOPE_SEGMENT = "global";
6
+ function isWebhookSubscriptionCache(value) {
7
+ if (!value || typeof value !== "object") return false;
8
+ const candidate = value;
9
+ return typeof candidate.get === "function" && typeof candidate.set === "function" && typeof candidate.deleteByTags === "function";
10
+ }
11
+ function resolveWebhookSubscriptionCache(resolve) {
12
+ try {
13
+ const cache = resolve("cache");
14
+ return isWebhookSubscriptionCache(cache) ? cache : null;
15
+ } catch {
16
+ return null;
17
+ }
18
+ }
19
+ function toResolveFunction(source) {
20
+ if (typeof source === "function") return source;
21
+ if (source && typeof source.resolve === "function") return source.resolve.bind(source);
22
+ return null;
23
+ }
24
+ function getWebhookSubscriptionCacheTtlMs() {
25
+ const raw = process.env.OM_WEBHOOKS_SUBSCRIPTION_CACHE_TTL_MS;
26
+ if (raw === void 0 || raw.trim() === "") return DEFAULT_TTL_MS;
27
+ const parsed = Number.parseInt(raw, 10);
28
+ if (!Number.isFinite(parsed) || parsed < 0) return DEFAULT_TTL_MS;
29
+ return parsed;
30
+ }
31
+ function normalizeScopeSegment(organizationId) {
32
+ return organizationId ?? GLOBAL_SCOPE_SEGMENT;
33
+ }
34
+ function getWebhookSubscriptionCacheKey(tenantId, organizationId) {
35
+ return `webhooks:subscriptions:${tenantId}:${normalizeScopeSegment(organizationId)}`;
36
+ }
37
+ function getWebhookSubscriptionCacheTag(tenantId) {
38
+ return `webhooks:subscriptions:tenant:${tenantId}`;
39
+ }
40
+ function isCachedActiveWebhookList(value) {
41
+ return Array.isArray(value) && value.every((entry) => entry !== null && typeof entry === "object" && typeof entry.id === "string" && typeof entry.tenantId === "string" && Array.isArray(entry.subscribedEvents));
42
+ }
43
+ async function getCachedActiveWebhooks(cache, tenantId, organizationId) {
44
+ if (!cache) return null;
45
+ try {
46
+ const value = await runWithCacheTenant(tenantId, () => cache.get(getWebhookSubscriptionCacheKey(tenantId, organizationId)));
47
+ return isCachedActiveWebhookList(value) ? value : null;
48
+ } catch (error) {
49
+ logger.warn("Failed to read webhook subscription cache", { err: error });
50
+ return null;
51
+ }
52
+ }
53
+ async function setCachedActiveWebhooks(cache, tenantId, organizationId, webhooks, ttlMs) {
54
+ if (!cache || ttlMs <= 0) return;
55
+ try {
56
+ await runWithCacheTenant(tenantId, () => cache.set(
57
+ getWebhookSubscriptionCacheKey(tenantId, organizationId),
58
+ webhooks,
59
+ { ttl: ttlMs, tags: [getWebhookSubscriptionCacheTag(tenantId)] }
60
+ ));
61
+ } catch (error) {
62
+ logger.warn("Failed to write webhook subscription cache", { err: error });
63
+ }
64
+ }
65
+ async function invalidateWebhookSubscriptionCache(cache, tenantId) {
66
+ if (!cache) return;
67
+ const normalizedTenantId = tenantId?.trim();
68
+ if (!normalizedTenantId) return;
69
+ try {
70
+ await runWithCacheTenant(normalizedTenantId, () => cache.deleteByTags([getWebhookSubscriptionCacheTag(normalizedTenantId)]));
71
+ } catch (error) {
72
+ logger.warn("Failed to invalidate webhook subscription cache", { err: error });
73
+ }
74
+ }
75
+ async function invalidateWebhookSubscriptionCacheFor(source, tenantId) {
76
+ const normalizedTenantId = tenantId?.trim();
77
+ if (!normalizedTenantId) return;
78
+ const resolve = toResolveFunction(source);
79
+ if (!resolve) return;
80
+ await invalidateWebhookSubscriptionCache(resolveWebhookSubscriptionCache(resolve), normalizedTenantId);
81
+ }
82
+ export {
83
+ getCachedActiveWebhooks,
84
+ getWebhookSubscriptionCacheKey,
85
+ getWebhookSubscriptionCacheTag,
86
+ getWebhookSubscriptionCacheTtlMs,
87
+ invalidateWebhookSubscriptionCache,
88
+ invalidateWebhookSubscriptionCacheFor,
89
+ isWebhookSubscriptionCache,
90
+ resolveWebhookSubscriptionCache,
91
+ setCachedActiveWebhooks
92
+ };
93
+ //# sourceMappingURL=subscription-cache.js.map