@open-mercato/core 0.6.7-develop.6795.1.8a3f27921c → 0.6.7-develop.6824.1.21fff90682

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 (228) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/generated/entities/customer_deal_person_link/index.js +2 -0
  3. package/dist/generated/entities/customer_deal_person_link/index.js.map +2 -2
  4. package/dist/generated/entities/customer_user_invitation/index.js +2 -0
  5. package/dist/generated/entities/customer_user_invitation/index.js.map +2 -2
  6. package/dist/generated/entity-fields-registry.js +2 -0
  7. package/dist/generated/entity-fields-registry.js.map +2 -2
  8. package/dist/modules/auth/lib/backendChrome.js +10 -9
  9. package/dist/modules/auth/lib/backendChrome.js.map +2 -2
  10. package/dist/modules/auth/lib/tenantAccess.js +7 -0
  11. package/dist/modules/auth/lib/tenantAccess.js.map +2 -2
  12. package/dist/modules/business_rules/api/openmercato-call-options/route.js +85 -0
  13. package/dist/modules/business_rules/api/openmercato-call-options/route.js.map +7 -0
  14. package/dist/modules/business_rules/api/rules/route.js +71 -4
  15. package/dist/modules/business_rules/api/rules/route.js.map +2 -2
  16. package/dist/modules/business_rules/components/ActionBuilder.js +47 -4
  17. package/dist/modules/business_rules/components/ActionBuilder.js.map +3 -3
  18. package/dist/modules/business_rules/components/ActionRow.js +106 -16
  19. package/dist/modules/business_rules/components/ActionRow.js.map +2 -2
  20. package/dist/modules/business_rules/components/utils/actionValidation.js +6 -0
  21. package/dist/modules/business_rules/components/utils/actionValidation.js.map +2 -2
  22. package/dist/modules/business_rules/lib/action-executor.js +148 -0
  23. package/dist/modules/business_rules/lib/action-executor.js.map +2 -2
  24. package/dist/modules/business_rules/lib/openmercato-call-options-types.js +5 -0
  25. package/dist/modules/business_rules/lib/openmercato-call-options-types.js.map +7 -0
  26. package/dist/modules/business_rules/lib/openmercato-call-options.js +231 -0
  27. package/dist/modules/business_rules/lib/openmercato-call-options.js.map +7 -0
  28. package/dist/modules/business_rules/lib/rule-engine.js +5 -1
  29. package/dist/modules/business_rules/lib/rule-engine.js.map +2 -2
  30. package/dist/modules/communication_channels/api/post/webhook/[provider]/route.js +11 -1
  31. package/dist/modules/communication_channels/api/post/webhook/[provider]/route.js.map +2 -2
  32. package/dist/modules/communication_channels/api/post/webhooks/gmail/route.js +11 -6
  33. package/dist/modules/communication_channels/api/post/webhooks/gmail/route.js.map +2 -2
  34. package/dist/modules/configs/lib/upgrade-actions.js +12 -0
  35. package/dist/modules/configs/lib/upgrade-actions.js.map +2 -2
  36. package/dist/modules/customer_accounts/api/admin/users/[id].js +10 -1
  37. package/dist/modules/customer_accounts/api/admin/users/[id].js.map +2 -2
  38. package/dist/modules/customer_accounts/api/admin/users-invite.js +61 -4
  39. package/dist/modules/customer_accounts/api/admin/users-invite.js.map +2 -2
  40. package/dist/modules/customer_accounts/api/portal/users-invite.js +20 -2
  41. package/dist/modules/customer_accounts/api/portal/users-invite.js.map +2 -2
  42. package/dist/modules/customer_accounts/data/entities.js +3 -0
  43. package/dist/modules/customer_accounts/data/entities.js.map +2 -2
  44. package/dist/modules/customer_accounts/data/validators.js +1 -0
  45. package/dist/modules/customer_accounts/data/validators.js.map +2 -2
  46. package/dist/modules/customer_accounts/emails/CustomerInvitationEmail.js +26 -0
  47. package/dist/modules/customer_accounts/emails/CustomerInvitationEmail.js.map +7 -0
  48. package/dist/modules/customer_accounts/lib/customerEntityOwnership.js +29 -3
  49. package/dist/modules/customer_accounts/lib/customerEntityOwnership.js.map +2 -2
  50. package/dist/modules/customer_accounts/lib/customerUrl.js +19 -10
  51. package/dist/modules/customer_accounts/lib/customerUrl.js.map +2 -2
  52. package/dist/modules/customer_accounts/lib/invitationEmail.js +35 -0
  53. package/dist/modules/customer_accounts/lib/invitationEmail.js.map +7 -0
  54. package/dist/modules/customer_accounts/migrations/Migration20260723120000_customer_accounts.js +13 -0
  55. package/dist/modules/customer_accounts/migrations/Migration20260723120000_customer_accounts.js.map +7 -0
  56. package/dist/modules/customer_accounts/services/customerInvitationService.js +32 -2
  57. package/dist/modules/customer_accounts/services/customerInvitationService.js.map +2 -2
  58. package/dist/modules/customer_accounts/subscribers/autoLinkCrm.js +47 -7
  59. package/dist/modules/customer_accounts/subscribers/autoLinkCrm.js.map +2 -2
  60. package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js +2 -2
  61. package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js.map +2 -2
  62. package/dist/modules/customer_accounts/widgets/injection-table.js +1 -1
  63. package/dist/modules/customer_accounts/widgets/injection-table.js.map +2 -2
  64. package/dist/modules/customers/api/deals/[id]/people/route.js +4 -2
  65. package/dist/modules/customers/api/deals/[id]/people/route.js.map +2 -2
  66. package/dist/modules/customers/api/deals/[id]/route.js +7 -3
  67. package/dist/modules/customers/api/deals/[id]/route.js.map +2 -2
  68. package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js +11 -1
  69. package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js.map +2 -2
  70. package/dist/modules/customers/backend/customers/deals/[id]/page.js +14 -8
  71. package/dist/modules/customers/backend/customers/deals/[id]/page.js.map +2 -2
  72. package/dist/modules/customers/backend/customers/people-v2/[id]/page.js +21 -6
  73. package/dist/modules/customers/backend/customers/people-v2/[id]/page.js.map +2 -2
  74. package/dist/modules/customers/commands/deals.js +48 -10
  75. package/dist/modules/customers/commands/deals.js.map +2 -2
  76. package/dist/modules/customers/commands/people.js +2 -0
  77. package/dist/modules/customers/commands/people.js.map +2 -2
  78. package/dist/modules/customers/components/detail/CompanyDetailTabs.js +26 -6
  79. package/dist/modules/customers/components/detail/CompanyDetailTabs.js.map +2 -2
  80. package/dist/modules/customers/components/detail/DealDetailTabs.js +30 -8
  81. package/dist/modules/customers/components/detail/DealDetailTabs.js.map +2 -2
  82. package/dist/modules/customers/components/detail/PersonDetailTabs.js +30 -8
  83. package/dist/modules/customers/components/detail/PersonDetailTabs.js.map +2 -2
  84. package/dist/modules/customers/data/entities.js +8 -0
  85. package/dist/modules/customers/data/entities.js.map +2 -2
  86. package/dist/modules/customers/data/validators.js +2 -1
  87. package/dist/modules/customers/data/validators.js.map +2 -2
  88. package/dist/modules/customers/migrations/Migration20260723210000_customers.js +15 -0
  89. package/dist/modules/customers/migrations/Migration20260723210000_customers.js.map +7 -0
  90. package/dist/modules/directory/api/tenants/route.js +8 -2
  91. package/dist/modules/directory/api/tenants/route.js.map +2 -2
  92. package/dist/modules/directory/commands/tenants.js +6 -0
  93. package/dist/modules/directory/commands/tenants.js.map +2 -2
  94. package/dist/modules/entities/api/entity-settings.js +59 -9
  95. package/dist/modules/entities/api/entity-settings.js.map +2 -2
  96. package/dist/modules/entities/backend/config/settings/page.js +10 -0
  97. package/dist/modules/entities/backend/config/settings/page.js.map +7 -0
  98. package/dist/modules/entities/backend/config/settings/page.meta.js +36 -0
  99. package/dist/modules/entities/backend/config/settings/page.meta.js.map +7 -0
  100. package/dist/modules/entities/backend/entities/user/create/page.js +16 -7
  101. package/dist/modules/entities/backend/entities/user/create/page.js.map +2 -2
  102. package/dist/modules/entities/components/EntitySettingsManager.js +136 -0
  103. package/dist/modules/entities/components/EntitySettingsManager.js.map +7 -0
  104. package/dist/modules/inbox_ops/api/webhook/inbound.js +14 -10
  105. package/dist/modules/inbox_ops/api/webhook/inbound.js.map +2 -2
  106. package/dist/modules/payment_gateways/api/sessions/route.js +1 -3
  107. package/dist/modules/payment_gateways/api/sessions/route.js.map +2 -2
  108. package/dist/modules/payment_gateways/api/webhook/[provider]/route.js +11 -1
  109. package/dist/modules/payment_gateways/api/webhook/[provider]/route.js.map +2 -2
  110. package/dist/modules/payment_gateways/data/entities.js +4 -0
  111. package/dist/modules/payment_gateways/data/entities.js.map +2 -2
  112. package/dist/modules/payment_gateways/lib/gateway-service.js +15 -6
  113. package/dist/modules/payment_gateways/lib/gateway-service.js.map +2 -2
  114. package/dist/modules/payment_gateways/lib/session-idempotency.js +33 -0
  115. package/dist/modules/payment_gateways/lib/session-idempotency.js.map +2 -2
  116. package/dist/modules/payment_gateways/migrations/Migration20260803103035_payment_gateways.js +18 -0
  117. package/dist/modules/payment_gateways/migrations/Migration20260803103035_payment_gateways.js.map +7 -0
  118. package/dist/modules/payment_gateways/setup.js +47 -0
  119. package/dist/modules/payment_gateways/setup.js.map +2 -2
  120. package/dist/modules/payment_gateways/workers/session-initialization-prune.js +50 -0
  121. package/dist/modules/payment_gateways/workers/session-initialization-prune.js.map +7 -0
  122. package/dist/modules/portal/frontend/[orgSlug]/portal/invite/page.js +134 -0
  123. package/dist/modules/portal/frontend/[orgSlug]/portal/invite/page.js.map +7 -0
  124. package/dist/modules/portal/frontend/[orgSlug]/portal/invite/page.meta.js +11 -0
  125. package/dist/modules/portal/frontend/[orgSlug]/portal/invite/page.meta.js.map +7 -0
  126. package/dist/modules/portal/lib/navigation.js +7 -0
  127. package/dist/modules/portal/lib/navigation.js.map +7 -0
  128. package/dist/modules/query_index/lib/engine.js +43 -0
  129. package/dist/modules/query_index/lib/engine.js.map +2 -2
  130. package/dist/modules/query_index/lib/search-tokens.js +15 -5
  131. package/dist/modules/query_index/lib/search-tokens.js.map +2 -2
  132. package/dist/modules/shipping_carriers/api/webhook/[provider]/route.js +11 -1
  133. package/dist/modules/shipping_carriers/api/webhook/[provider]/route.js.map +2 -2
  134. package/generated/entities/customer_deal_person_link/index.ts +1 -0
  135. package/generated/entities/customer_user_invitation/index.ts +1 -0
  136. package/generated/entity-fields-registry.ts +2 -0
  137. package/package.json +7 -7
  138. package/src/modules/auth/lib/backendChrome.tsx +15 -9
  139. package/src/modules/auth/lib/tenantAccess.ts +8 -0
  140. package/src/modules/business_rules/api/openmercato-call-options/route.ts +94 -0
  141. package/src/modules/business_rules/api/rules/route.ts +92 -4
  142. package/src/modules/business_rules/components/ActionBuilder.tsx +54 -5
  143. package/src/modules/business_rules/components/ActionRow.tsx +156 -18
  144. package/src/modules/business_rules/components/utils/actionValidation.ts +6 -0
  145. package/src/modules/business_rules/i18n/de.json +12 -0
  146. package/src/modules/business_rules/i18n/en.json +12 -0
  147. package/src/modules/business_rules/i18n/es.json +12 -0
  148. package/src/modules/business_rules/i18n/pl.json +12 -0
  149. package/src/modules/business_rules/lib/action-executor.ts +198 -0
  150. package/src/modules/business_rules/lib/openmercato-call-options-types.ts +31 -0
  151. package/src/modules/business_rules/lib/openmercato-call-options.ts +297 -0
  152. package/src/modules/business_rules/lib/rule-engine.ts +4 -0
  153. package/src/modules/communication_channels/api/post/webhook/[provider]/route.ts +11 -1
  154. package/src/modules/communication_channels/api/post/webhooks/gmail/route.ts +17 -11
  155. package/src/modules/configs/lib/upgrade-actions.ts +14 -0
  156. package/src/modules/customer_accounts/api/admin/users/[id].ts +16 -1
  157. package/src/modules/customer_accounts/api/admin/users-invite.ts +78 -3
  158. package/src/modules/customer_accounts/api/portal/users-invite.ts +22 -1
  159. package/src/modules/customer_accounts/data/entities.ts +3 -0
  160. package/src/modules/customer_accounts/data/validators.ts +1 -0
  161. package/src/modules/customer_accounts/emails/CustomerInvitationEmail.tsx +45 -0
  162. package/src/modules/customer_accounts/i18n/de.json +6 -0
  163. package/src/modules/customer_accounts/i18n/en.json +6 -0
  164. package/src/modules/customer_accounts/i18n/es.json +6 -0
  165. package/src/modules/customer_accounts/i18n/pl.json +6 -0
  166. package/src/modules/customer_accounts/lib/customerEntityOwnership.ts +68 -2
  167. package/src/modules/customer_accounts/lib/customerUrl.ts +27 -11
  168. package/src/modules/customer_accounts/lib/invitationEmail.ts +42 -0
  169. package/src/modules/customer_accounts/migrations/.snapshot-open-mercato.json +16 -0
  170. package/src/modules/customer_accounts/migrations/Migration20260723120000_customer_accounts.ts +13 -0
  171. package/src/modules/customer_accounts/services/customerInvitationService.ts +56 -3
  172. package/src/modules/customer_accounts/subscribers/autoLinkCrm.ts +75 -8
  173. package/src/modules/customer_accounts/widgets/injection/account-status/widget.client.tsx +2 -2
  174. package/src/modules/customer_accounts/widgets/injection-table.ts +4 -1
  175. package/src/modules/customers/api/deals/[id]/people/route.ts +3 -0
  176. package/src/modules/customers/api/deals/[id]/route.ts +10 -2
  177. package/src/modules/customers/backend/customers/companies-v2/[id]/page.tsx +12 -1
  178. package/src/modules/customers/backend/customers/deals/[id]/hooks/types.ts +1 -0
  179. package/src/modules/customers/backend/customers/deals/[id]/page.tsx +15 -9
  180. package/src/modules/customers/backend/customers/people-v2/[id]/page.tsx +24 -6
  181. package/src/modules/customers/commands/deals.ts +50 -9
  182. package/src/modules/customers/commands/people.ts +3 -0
  183. package/src/modules/customers/components/detail/CompanyDetailTabs.tsx +30 -7
  184. package/src/modules/customers/components/detail/DealDetailTabs.tsx +34 -9
  185. package/src/modules/customers/components/detail/PersonDetailTabs.tsx +34 -9
  186. package/src/modules/customers/data/entities.ts +9 -1
  187. package/src/modules/customers/data/validators.ts +1 -0
  188. package/src/modules/customers/i18n/de.json +1 -0
  189. package/src/modules/customers/i18n/en.json +1 -0
  190. package/src/modules/customers/i18n/es.json +1 -0
  191. package/src/modules/customers/i18n/pl.json +1 -0
  192. package/src/modules/customers/migrations/.snapshot-open-mercato.json +27 -1
  193. package/src/modules/customers/migrations/Migration20260723210000_customers.ts +15 -0
  194. package/src/modules/directory/api/tenants/route.ts +9 -1
  195. package/src/modules/directory/commands/tenants.ts +6 -0
  196. package/src/modules/entities/api/entity-settings.ts +66 -22
  197. package/src/modules/entities/backend/config/settings/page.meta.ts +34 -0
  198. package/src/modules/entities/backend/config/settings/page.tsx +12 -0
  199. package/src/modules/entities/backend/entities/user/create/page.tsx +16 -7
  200. package/src/modules/entities/components/EntitySettingsManager.tsx +160 -0
  201. package/src/modules/entities/i18n/de.json +9 -0
  202. package/src/modules/entities/i18n/en.json +9 -0
  203. package/src/modules/entities/i18n/es.json +9 -0
  204. package/src/modules/entities/i18n/pl.json +9 -0
  205. package/src/modules/inbox_ops/api/webhook/inbound.ts +14 -12
  206. package/src/modules/payment_gateways/api/sessions/route.ts +1 -3
  207. package/src/modules/payment_gateways/api/webhook/[provider]/route.ts +13 -1
  208. package/src/modules/payment_gateways/data/entities.ts +5 -0
  209. package/src/modules/payment_gateways/i18n/de.json +5 -1
  210. package/src/modules/payment_gateways/i18n/en.json +5 -1
  211. package/src/modules/payment_gateways/i18n/es.json +5 -1
  212. package/src/modules/payment_gateways/i18n/pl.json +5 -1
  213. package/src/modules/payment_gateways/lib/gateway-service.ts +17 -8
  214. package/src/modules/payment_gateways/lib/session-idempotency.ts +36 -0
  215. package/src/modules/payment_gateways/migrations/.snapshot-open-mercato.json +10 -1
  216. package/src/modules/payment_gateways/migrations/Migration20260803103035_payment_gateways.ts +17 -0
  217. package/src/modules/payment_gateways/setup.ts +62 -0
  218. package/src/modules/payment_gateways/workers/session-initialization-prune.ts +63 -0
  219. package/src/modules/portal/frontend/[orgSlug]/portal/invite/page.meta.ts +9 -0
  220. package/src/modules/portal/frontend/[orgSlug]/portal/invite/page.tsx +164 -0
  221. package/src/modules/portal/i18n/de.json +18 -0
  222. package/src/modules/portal/i18n/en.json +18 -0
  223. package/src/modules/portal/i18n/es.json +18 -0
  224. package/src/modules/portal/i18n/pl.json +18 -0
  225. package/src/modules/portal/lib/navigation.ts +3 -0
  226. package/src/modules/query_index/lib/engine.ts +52 -0
  227. package/src/modules/query_index/lib/search-tokens.ts +16 -4
  228. package/src/modules/shipping_carriers/api/webhook/[provider]/route.ts +11 -1
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/inbox_ops/api/webhook/inbound.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { createHmac, timingSafeEqual } from 'node:crypto'\nimport { z } from 'zod'\nimport { Webhook } from 'svix'\nimport { Resend } from 'resend'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CacheStrategy } from '@open-mercato/cache'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { InboxSettings, InboxEmail } from '../../data/entities'\nimport { parseInboundEmail } from '../../lib/emailParser'\nimport { checkRateLimit } from '../../lib/rateLimiter'\nimport { emitInboxOpsEvent } from '../../events'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('inbox_ops').child({ component: 'webhook' })\n\nexport const metadata = {\n POST: { requireAuth: false },\n}\n\nconst MAX_PAYLOAD_SIZE = 2 * 1024 * 1024 // 2MB\nconst REPLAY_WINDOW_MS = 5 * 60 * 1000 // 5 minutes\n\nfunction isTimestampFresh(timestamp: string): boolean {\n if (!timestamp) return false\n const timestampMs = parseInt(timestamp, 10) * 1000\n if (isNaN(timestampMs)) return false\n return Math.abs(Date.now() - timestampMs) <= REPLAY_WINDOW_MS\n}\n\nfunction verifyHmacSignature(\n payload: string,\n signature: string,\n timestamp: string,\n secret: string,\n): boolean {\n if (!signature || !timestamp || !secret) return false\n\n if (!isTimestampFresh(timestamp)) return false\n\n const expected = createHmac('sha256', secret)\n .update(`${timestamp}.${payload}`)\n .digest('hex')\n const received = signature.startsWith('sha256=')\n ? signature.slice('sha256='.length)\n : signature\n\n try {\n if (received.length !== expected.length) return false\n return timingSafeEqual(\n Buffer.from(received, 'hex'),\n Buffer.from(expected, 'hex'),\n )\n } catch {\n return false\n }\n}\n\n// True when the signature verifies against ANY of the candidate secrets. Used\n// to bind the inbound signature to the resolved tenant: once the target inbox\n// declares its own per-tenant secret, the global INBOX_OPS_WEBHOOK_SECRET is no\n// longer accepted for that inbox, so a holder of the global key alone cannot\n// forge mail into a tenant that opted into a dedicated secret.\nfunction verifyHmacAgainstAny(\n payload: string,\n signature: string,\n timestamp: string,\n secrets: string[],\n): boolean {\n for (const secret of secrets) {\n if (secret && verifyHmacSignature(payload, signature, timestamp, secret)) return true\n }\n return false\n}\n\n// Stable, non-reversible identifier for the pre-DB global rate-limit bucket.\n// Keying on the configured signing secret (rather than the attacker-set `to` or\n// the per-request signature) means the bucket is request-invariant for a given\n// secret, so rotating recipients or mutating the body cannot dilute the limit.\nfunction secretFingerprint(secret: string): string {\n return createHmac('sha256', 'inbox_ops:rate_limit:salt').update(secret).digest('hex').slice(0, 32)\n}\n\nfunction verifySvixSignature(\n rawBody: string,\n headers: Record<string, string>,\n secret: string,\n): boolean {\n try {\n const wh = new Webhook(secret)\n wh.verify(rawBody, {\n 'svix-id': headers['svix-id'],\n 'svix-timestamp': headers['svix-timestamp'],\n 'svix-signature': headers['svix-signature'],\n })\n return true\n } catch {\n return false\n }\n}\n\nasync function fetchResendEmail(emailId: string): Promise<{\n from?: string\n to?: string[]\n subject?: string\n text?: string\n html?: string\n messageId?: string\n replyTo?: string\n inReplyTo?: string\n references?: string[]\n}> {\n const apiKey = process.env.RESEND_API_KEY\n if (!apiKey) throw new Error('RESEND_API_KEY is required to fetch Resend inbound emails')\n\n const resend = new Resend(apiKey)\n const { data, error } = await resend.emails.receiving.get(emailId)\n if (error || !data) {\n throw new Error(`Failed to fetch Resend email ${emailId}: ${error?.message || 'unknown error'}`)\n }\n\n return {\n from: data.from,\n to: data.to,\n subject: data.subject,\n text: data.text ?? undefined,\n html: data.html ?? undefined,\n messageId: data.message_id,\n replyTo: data.reply_to?.[0] ?? undefined,\n inReplyTo: data.headers?.['in-reply-to'] ?? undefined,\n references: data.headers?.['references']?.split(/\\s+/).filter(Boolean),\n }\n}\n\ntype VerifiedPayload =\n | { kind: 'custom'; payload: Record<string, unknown>; signature: string; timestamp: string }\n | { kind: 'resend'; emailFields: Awaited<ReturnType<typeof fetchResendEmail>>; to: string[] }\n\nasync function verifyAndParse(req: Request, rawBody: string): Promise<\n | { ok: true; data: VerifiedPayload }\n | { ok: false; response: NextResponse }\n> {\n const customSig = req.headers.get('x-webhook-signature')\n const svixId = req.headers.get('svix-id')\n\n if (customSig) {\n const webhookSecret = process.env.INBOX_OPS_WEBHOOK_SECRET\n if (!webhookSecret) {\n logger.error('INBOX_OPS_WEBHOOK_SECRET not configured')\n return { ok: false, response: NextResponse.json({ error: 'Service unavailable' }, { status: 503 }) }\n }\n const timestamp = req.headers.get('x-webhook-timestamp') || ''\n // Cheap, secret-independent gate first: reject replayed/stale requests\n // before parsing. The HMAC itself is verified later, against the secret\n // bound to the resolved inbox (see the POST handler), so a holder of the\n // global secret cannot inject into a tenant that owns its own secret.\n if (!isTimestampFresh(timestamp)) {\n return { ok: false, response: NextResponse.json({ error: 'Invalid signature' }, { status: 400 }) }\n }\n let payload: Record<string, unknown>\n try {\n payload = JSON.parse(rawBody)\n } catch {\n return { ok: false, response: NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }) }\n }\n return { ok: true, data: { kind: 'custom', payload, signature: customSig, timestamp } }\n }\n\n if (svixId) {\n const signingSecret = process.env.RESEND_WEBHOOK_SIGNING_SECRET\n if (!signingSecret) {\n logger.error('RESEND_WEBHOOK_SIGNING_SECRET not configured')\n return { ok: false, response: NextResponse.json({ error: 'Service unavailable' }, { status: 503 }) }\n }\n const headers: Record<string, string> = {\n 'svix-id': svixId,\n 'svix-timestamp': req.headers.get('svix-timestamp') || '',\n 'svix-signature': req.headers.get('svix-signature') || '',\n }\n if (!verifySvixSignature(rawBody, headers, signingSecret)) {\n return { ok: false, response: NextResponse.json({ error: 'Invalid signature' }, { status: 400 }) }\n }\n let envelope: { type?: string; data?: { email_id?: string; from?: string; to?: string[] } }\n try {\n envelope = JSON.parse(rawBody)\n } catch {\n return { ok: false, response: NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }) }\n }\n if (envelope.type !== 'email.received') {\n return { ok: false, response: NextResponse.json({ ok: true }) }\n }\n const emailId = envelope.data?.email_id\n if (!emailId) {\n return { ok: false, response: NextResponse.json({ error: 'Missing email_id in Resend payload' }, { status: 400 }) }\n }\n const emailFields = await fetchResendEmail(emailId)\n const to = emailFields.to || envelope.data?.to || []\n return { ok: true, data: { kind: 'resend', emailFields, to } }\n }\n\n return { ok: false, response: NextResponse.json({ error: 'Missing signature headers' }, { status: 400 }) }\n}\n\nexport async function POST(req: Request) {\n const hasCustomSecret = Boolean(process.env.INBOX_OPS_WEBHOOK_SECRET)\n const hasResendSecret = Boolean(process.env.RESEND_WEBHOOK_SIGNING_SECRET)\n if (!hasCustomSecret && !hasResendSecret) {\n logger.error('Neither INBOX_OPS_WEBHOOK_SECRET nor RESEND_WEBHOOK_SIGNING_SECRET is configured')\n return NextResponse.json({ error: 'Service unavailable' }, { status: 503 })\n }\n\n const contentLength = parseInt(req.headers.get('content-length') || '0', 10)\n if (contentLength > MAX_PAYLOAD_SIZE) {\n return NextResponse.json({ error: 'Payload too large' }, { status: 413 })\n }\n\n let rawBody: string\n try {\n rawBody = await req.text()\n } catch {\n return NextResponse.json({ error: 'Bad request' }, { status: 400 })\n }\n\n if (rawBody.length > MAX_PAYLOAD_SIZE) {\n return NextResponse.json({ error: 'Payload too large' }, { status: 413 })\n }\n\n const verified = await verifyAndParse(req, rawBody)\n if (!verified.ok) {\n return verified.response\n }\n\n let toAddress: string | null\n let emailInput: Parameters<typeof parseInboundEmail>[0]\n\n if (verified.data.kind === 'custom') {\n const payload = verified.data.payload\n toAddress = extractToAddress(payload)\n emailInput = {\n from: payload.from as string | undefined,\n to: payload.to as string | string[] | undefined,\n subject: payload.subject as string | undefined,\n text: payload.text as string | undefined,\n html: payload.html as string | undefined,\n messageId: payload.messageId as string | undefined,\n replyTo: payload.replyTo as string | undefined,\n inReplyTo: payload.inReplyTo as string | undefined,\n references: payload.references as string | string[] | undefined,\n }\n } else {\n const { emailFields, to } = verified.data\n toAddress = to.length > 0 ? to[0].toLowerCase() : null\n emailInput = {\n from: emailFields.from,\n to: emailFields.to,\n subject: emailFields.subject,\n text: emailFields.text,\n html: emailFields.html,\n messageId: emailFields.messageId,\n replyTo: emailFields.replyTo,\n inReplyTo: emailFields.inReplyTo,\n references: emailFields.references,\n }\n }\n\n if (!toAddress) {\n return NextResponse.json({ error: 'Missing recipient address' }, { status: 400 })\n }\n\n const container = await createRequestContainer()\n\n let cache: CacheStrategy | null = null\n try {\n cache = container.resolve('cache') as CacheStrategy\n } catch {\n // Cache not available \u2014 the limiter now fails closed with a process-local\n // bucket instead of waving the request through.\n }\n\n // Pre-DB global bucket keyed on the configured signing secret (custom) or the\n // static Resend path, NOT on the attacker-controllable `to` and NOT on the\n // per-request signature (which mutates with timestamp/body, so it would mint a\n // fresh bucket every request and never throttle). The per-tenant secret lives\n // behind the encrypted-column lookup, so the only secret available here without\n // a DB hit is the global one \u2014 keying on it caps total inbound volume for the\n // custom path and runs BEFORE the lookup so a flood cannot amplify DB work.\n const globalBucketKey = verified.data.kind === 'custom'\n ? `webhook:secret:${secretFingerprint(process.env.INBOX_OPS_WEBHOOK_SECRET || '')}`\n : 'webhook:resend'\n const globalRateCheck = await checkRateLimit(cache, globalBucketKey)\n if (!globalRateCheck.allowed) {\n return rateLimitedResponse(globalRateCheck.retryAfterSeconds)\n }\n\n const em = (container.resolve('em') as EntityManager).fork()\n\n // No explicit scope: decryptEntitiesWithFallbackScope reads tenant/org off the\n // resolved row, which is exactly what binds webhook_secret to its own tenant.\n const settings = await findOneWithDecryption(\n em,\n InboxSettings,\n {\n inboxAddress: toAddress.toLowerCase(),\n isActive: true,\n deletedAt: null,\n },\n )\n\n // Final HMAC verification for the custom provider path: bind the signature to\n // the resolved tenant. When the inbox declares its own secret, ONLY that\n // secret is accepted; otherwise the global secret is accepted (back-compat).\n // For an unknown `to` we still require the global secret so the endpoint never\n // behaves as an unauthenticated probe oracle.\n if (verified.data.kind === 'custom') {\n const globalSecret = process.env.INBOX_OPS_WEBHOOK_SECRET || ''\n const inboxSecret = settings?.webhookSecret || ''\n const acceptableSecrets = inboxSecret ? [inboxSecret] : [globalSecret]\n const signatureValid = verifyHmacAgainstAny(\n rawBody,\n verified.data.signature,\n verified.data.timestamp,\n acceptableSecrets,\n )\n if (!signatureValid) {\n return NextResponse.json({ error: 'Invalid signature' }, { status: 400 })\n }\n }\n\n if (!settings) {\n return NextResponse.json({ ok: true })\n }\n\n // Per-tenant bucket keyed on the RESOLVED tenant (not the raw `to`), so it\n // cannot be diluted by alias addresses that map to the same inbox.\n const tenantRateCheck = await checkRateLimit(\n cache,\n `webhook:tenant:${settings.tenantId}`,\n settings.tenantId,\n )\n if (!tenantRateCheck.allowed) {\n return rateLimitedResponse(tenantRateCheck.retryAfterSeconds)\n }\n\n const parsed = parseInboundEmail(emailInput)\n\n const isDuplicate = await checkDuplicate(em, settings, parsed.messageId, parsed.contentHash)\n if (isDuplicate) {\n try {\n await emitInboxOpsEvent('inbox_ops.email.deduplicated', {\n tenantId: settings.tenantId,\n organizationId: settings.organizationId,\n toAddress,\n })\n } catch (eventError) {\n logger.error('Failed to emit deduplicated event', { err: eventError })\n }\n return NextResponse.json({ ok: true })\n }\n\n const maxTextSize = parseInt(process.env.INBOX_OPS_MAX_TEXT_SIZE || '204800', 10)\n const cleanedText = parsed.cleanedText.slice(0, maxTextSize)\n\n const email = em.create(InboxEmail, {\n messageId: parsed.messageId,\n contentHash: parsed.contentHash,\n forwardedByAddress: parsed.from.email,\n forwardedByName: parsed.from.name || null,\n toAddress,\n subject: parsed.subject,\n replyTo: parsed.replyTo,\n inReplyTo: parsed.inReplyTo,\n emailReferences: parsed.references,\n rawText: parsed.rawText,\n rawHtml: parsed.rawHtml,\n cleanedText,\n threadMessages: parsed.threadMessages,\n detectedLanguage: parsed.detectedLanguage,\n receivedAt: new Date(),\n status: 'received' as const,\n isActive: true,\n organizationId: settings.organizationId,\n tenantId: settings.tenantId,\n })\n\n em.persist(email)\n await em.flush()\n\n try {\n await emitInboxOpsEvent('inbox_ops.email.received', {\n emailId: email.id,\n tenantId: settings.tenantId,\n organizationId: settings.organizationId,\n forwardedByAddress: parsed.from.email,\n subject: parsed.subject,\n })\n } catch (eventError) {\n logger.error('Failed to emit email.received event', { err: eventError })\n }\n\n return NextResponse.json({ ok: true })\n}\n\nfunction rateLimitedResponse(retryAfterSeconds?: number): NextResponse {\n return NextResponse.json(\n { error: 'Rate limit exceeded' },\n {\n status: 429,\n headers: retryAfterSeconds ? { 'Retry-After': String(retryAfterSeconds) } : undefined,\n },\n )\n}\n\nfunction extractToAddress(payload: Record<string, unknown>): string | null {\n const to = payload.to\n if (typeof to === 'string') {\n const match = to.match(/<([^>]+)>/)\n return match ? match[1].toLowerCase() : to.trim().toLowerCase()\n }\n if (Array.isArray(to) && to.length > 0) {\n const first = to[0]\n if (typeof first === 'string') {\n const match = first.match(/<([^>]+)>/)\n return match ? match[1].toLowerCase() : first.trim().toLowerCase()\n }\n }\n return null\n}\n\nasync function checkDuplicate(\n em: EntityManager,\n settings: InboxSettings,\n messageId: string | null | undefined,\n contentHash: string | null | undefined,\n): Promise<boolean> {\n const encScope = {\n tenantId: settings.tenantId,\n organizationId: settings.organizationId,\n }\n const whereBase = {\n organizationId: settings.organizationId,\n tenantId: settings.tenantId,\n deletedAt: null,\n }\n\n if (messageId) {\n const byMessageId = await findOneWithDecryption(em, InboxEmail, { ...whereBase, messageId }, undefined, encScope)\n if (byMessageId) return true\n }\n\n if (contentHash) {\n const byHash = await findOneWithDecryption(em, InboxEmail, { ...whereBase, contentHash }, undefined, encScope)\n if (byHash) return true\n }\n\n return false\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'InboxOps',\n summary: 'Inbound email webhook',\n methods: {\n POST: {\n summary: 'Receive forwarded email from provider webhook',\n description: 'Public endpoint \u2014 validated by provider HMAC signature or Resend/Svix signature. Rate limited per tenant.',\n requestBody: {\n contentType: 'application/json',\n schema: z.object({\n from: z.string().optional(),\n to: z.union([z.string(), z.array(z.string())]).optional(),\n subject: z.string().optional(),\n text: z.string().optional(),\n html: z.string().optional(),\n messageId: z.string().optional(),\n replyTo: z.string().optional(),\n inReplyTo: z.string().optional(),\n references: z.union([z.string(), z.array(z.string())]).optional(),\n }),\n },\n responses: [\n { status: 200, description: 'Email received and queued for processing' },\n { status: 400, description: 'Invalid payload or signature' },\n { status: 413, description: 'Payload too large' },\n { status: 429, description: 'Rate limit exceeded' },\n { status: 503, description: 'Webhook secret not configured' },\n ],\n },\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { createHmac, timingSafeEqual } from 'node:crypto'\nimport { z } from 'zod'\nimport { Webhook } from 'svix'\nimport { Resend } from 'resend'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CacheStrategy } from '@open-mercato/cache'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { InboxSettings, InboxEmail } from '../../data/entities'\nimport { parseInboundEmail } from '../../lib/emailParser'\nimport { checkRateLimit } from '../../lib/rateLimiter'\nimport { emitInboxOpsEvent } from '../../events'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport {\n readBoundedRequestBody,\n resolveWebhookBodyLimitBytes,\n WebhookBodyTooLargeError,\n} from '@open-mercato/shared/lib/webhooks'\n\nconst logger = createLogger('inbox_ops').child({ component: 'webhook' })\n\nexport const metadata = {\n POST: { requireAuth: false },\n}\n\nconst MAX_PAYLOAD_SIZE = resolveWebhookBodyLimitBytes(\n process.env.INBOX_OPS_WEBHOOK_MAX_BODY_BYTES ?? process.env.OM_WEBHOOK_MAX_BODY_BYTES,\n 2 * 1024 * 1024,\n)\nconst REPLAY_WINDOW_MS = 5 * 60 * 1000 // 5 minutes\n\nfunction isTimestampFresh(timestamp: string): boolean {\n if (!timestamp) return false\n const timestampMs = parseInt(timestamp, 10) * 1000\n if (isNaN(timestampMs)) return false\n return Math.abs(Date.now() - timestampMs) <= REPLAY_WINDOW_MS\n}\n\nfunction verifyHmacSignature(\n payload: string,\n signature: string,\n timestamp: string,\n secret: string,\n): boolean {\n if (!signature || !timestamp || !secret) return false\n\n if (!isTimestampFresh(timestamp)) return false\n\n const expected = createHmac('sha256', secret)\n .update(`${timestamp}.${payload}`)\n .digest('hex')\n const received = signature.startsWith('sha256=')\n ? signature.slice('sha256='.length)\n : signature\n\n try {\n if (received.length !== expected.length) return false\n return timingSafeEqual(\n Buffer.from(received, 'hex'),\n Buffer.from(expected, 'hex'),\n )\n } catch {\n return false\n }\n}\n\n// True when the signature verifies against ANY of the candidate secrets. Used\n// to bind the inbound signature to the resolved tenant: once the target inbox\n// declares its own per-tenant secret, the global INBOX_OPS_WEBHOOK_SECRET is no\n// longer accepted for that inbox, so a holder of the global key alone cannot\n// forge mail into a tenant that opted into a dedicated secret.\nfunction verifyHmacAgainstAny(\n payload: string,\n signature: string,\n timestamp: string,\n secrets: string[],\n): boolean {\n for (const secret of secrets) {\n if (secret && verifyHmacSignature(payload, signature, timestamp, secret)) return true\n }\n return false\n}\n\n// Stable, non-reversible identifier for the pre-DB global rate-limit bucket.\n// Keying on the configured signing secret (rather than the attacker-set `to` or\n// the per-request signature) means the bucket is request-invariant for a given\n// secret, so rotating recipients or mutating the body cannot dilute the limit.\nfunction secretFingerprint(secret: string): string {\n return createHmac('sha256', 'inbox_ops:rate_limit:salt').update(secret).digest('hex').slice(0, 32)\n}\n\nfunction verifySvixSignature(\n rawBody: string,\n headers: Record<string, string>,\n secret: string,\n): boolean {\n try {\n const wh = new Webhook(secret)\n wh.verify(rawBody, {\n 'svix-id': headers['svix-id'],\n 'svix-timestamp': headers['svix-timestamp'],\n 'svix-signature': headers['svix-signature'],\n })\n return true\n } catch {\n return false\n }\n}\n\nasync function fetchResendEmail(emailId: string): Promise<{\n from?: string\n to?: string[]\n subject?: string\n text?: string\n html?: string\n messageId?: string\n replyTo?: string\n inReplyTo?: string\n references?: string[]\n}> {\n const apiKey = process.env.RESEND_API_KEY\n if (!apiKey) throw new Error('RESEND_API_KEY is required to fetch Resend inbound emails')\n\n const resend = new Resend(apiKey)\n const { data, error } = await resend.emails.receiving.get(emailId)\n if (error || !data) {\n throw new Error(`Failed to fetch Resend email ${emailId}: ${error?.message || 'unknown error'}`)\n }\n\n return {\n from: data.from,\n to: data.to,\n subject: data.subject,\n text: data.text ?? undefined,\n html: data.html ?? undefined,\n messageId: data.message_id,\n replyTo: data.reply_to?.[0] ?? undefined,\n inReplyTo: data.headers?.['in-reply-to'] ?? undefined,\n references: data.headers?.['references']?.split(/\\s+/).filter(Boolean),\n }\n}\n\ntype VerifiedPayload =\n | { kind: 'custom'; payload: Record<string, unknown>; signature: string; timestamp: string }\n | { kind: 'resend'; emailFields: Awaited<ReturnType<typeof fetchResendEmail>>; to: string[] }\n\nasync function verifyAndParse(req: Request, rawBody: string): Promise<\n | { ok: true; data: VerifiedPayload }\n | { ok: false; response: NextResponse }\n> {\n const customSig = req.headers.get('x-webhook-signature')\n const svixId = req.headers.get('svix-id')\n\n if (customSig) {\n const webhookSecret = process.env.INBOX_OPS_WEBHOOK_SECRET\n if (!webhookSecret) {\n logger.error('INBOX_OPS_WEBHOOK_SECRET not configured')\n return { ok: false, response: NextResponse.json({ error: 'Service unavailable' }, { status: 503 }) }\n }\n const timestamp = req.headers.get('x-webhook-timestamp') || ''\n // Cheap, secret-independent gate first: reject replayed/stale requests\n // before parsing. The HMAC itself is verified later, against the secret\n // bound to the resolved inbox (see the POST handler), so a holder of the\n // global secret cannot inject into a tenant that owns its own secret.\n if (!isTimestampFresh(timestamp)) {\n return { ok: false, response: NextResponse.json({ error: 'Invalid signature' }, { status: 400 }) }\n }\n let payload: Record<string, unknown>\n try {\n payload = JSON.parse(rawBody)\n } catch {\n return { ok: false, response: NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }) }\n }\n return { ok: true, data: { kind: 'custom', payload, signature: customSig, timestamp } }\n }\n\n if (svixId) {\n const signingSecret = process.env.RESEND_WEBHOOK_SIGNING_SECRET\n if (!signingSecret) {\n logger.error('RESEND_WEBHOOK_SIGNING_SECRET not configured')\n return { ok: false, response: NextResponse.json({ error: 'Service unavailable' }, { status: 503 }) }\n }\n const headers: Record<string, string> = {\n 'svix-id': svixId,\n 'svix-timestamp': req.headers.get('svix-timestamp') || '',\n 'svix-signature': req.headers.get('svix-signature') || '',\n }\n if (!verifySvixSignature(rawBody, headers, signingSecret)) {\n return { ok: false, response: NextResponse.json({ error: 'Invalid signature' }, { status: 400 }) }\n }\n let envelope: { type?: string; data?: { email_id?: string; from?: string; to?: string[] } }\n try {\n envelope = JSON.parse(rawBody)\n } catch {\n return { ok: false, response: NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }) }\n }\n if (envelope.type !== 'email.received') {\n return { ok: false, response: NextResponse.json({ ok: true }) }\n }\n const emailId = envelope.data?.email_id\n if (!emailId) {\n return { ok: false, response: NextResponse.json({ error: 'Missing email_id in Resend payload' }, { status: 400 }) }\n }\n const emailFields = await fetchResendEmail(emailId)\n const to = emailFields.to || envelope.data?.to || []\n return { ok: true, data: { kind: 'resend', emailFields, to } }\n }\n\n return { ok: false, response: NextResponse.json({ error: 'Missing signature headers' }, { status: 400 }) }\n}\n\nexport async function POST(req: Request) {\n const hasCustomSecret = Boolean(process.env.INBOX_OPS_WEBHOOK_SECRET)\n const hasResendSecret = Boolean(process.env.RESEND_WEBHOOK_SIGNING_SECRET)\n if (!hasCustomSecret && !hasResendSecret) {\n logger.error('Neither INBOX_OPS_WEBHOOK_SECRET nor RESEND_WEBHOOK_SIGNING_SECRET is configured')\n return NextResponse.json({ error: 'Service unavailable' }, { status: 503 })\n }\n\n let rawBody: string\n try {\n rawBody = await readBoundedRequestBody(req, { maxBytes: MAX_PAYLOAD_SIZE })\n } catch (error) {\n if (error instanceof WebhookBodyTooLargeError) {\n return NextResponse.json({ error: 'Payload too large' }, { status: 413 })\n }\n return NextResponse.json({ error: 'Bad request' }, { status: 400 })\n }\n\n const verified = await verifyAndParse(req, rawBody)\n if (!verified.ok) {\n return verified.response\n }\n\n let toAddress: string | null\n let emailInput: Parameters<typeof parseInboundEmail>[0]\n\n if (verified.data.kind === 'custom') {\n const payload = verified.data.payload\n toAddress = extractToAddress(payload)\n emailInput = {\n from: payload.from as string | undefined,\n to: payload.to as string | string[] | undefined,\n subject: payload.subject as string | undefined,\n text: payload.text as string | undefined,\n html: payload.html as string | undefined,\n messageId: payload.messageId as string | undefined,\n replyTo: payload.replyTo as string | undefined,\n inReplyTo: payload.inReplyTo as string | undefined,\n references: payload.references as string | string[] | undefined,\n }\n } else {\n const { emailFields, to } = verified.data\n toAddress = to.length > 0 ? to[0].toLowerCase() : null\n emailInput = {\n from: emailFields.from,\n to: emailFields.to,\n subject: emailFields.subject,\n text: emailFields.text,\n html: emailFields.html,\n messageId: emailFields.messageId,\n replyTo: emailFields.replyTo,\n inReplyTo: emailFields.inReplyTo,\n references: emailFields.references,\n }\n }\n\n if (!toAddress) {\n return NextResponse.json({ error: 'Missing recipient address' }, { status: 400 })\n }\n\n const container = await createRequestContainer()\n\n let cache: CacheStrategy | null = null\n try {\n cache = container.resolve('cache') as CacheStrategy\n } catch {\n // Cache not available \u2014 the limiter now fails closed with a process-local\n // bucket instead of waving the request through.\n }\n\n // Pre-DB global bucket keyed on the configured signing secret (custom) or the\n // static Resend path, NOT on the attacker-controllable `to` and NOT on the\n // per-request signature (which mutates with timestamp/body, so it would mint a\n // fresh bucket every request and never throttle). The per-tenant secret lives\n // behind the encrypted-column lookup, so the only secret available here without\n // a DB hit is the global one \u2014 keying on it caps total inbound volume for the\n // custom path and runs BEFORE the lookup so a flood cannot amplify DB work.\n const globalBucketKey = verified.data.kind === 'custom'\n ? `webhook:secret:${secretFingerprint(process.env.INBOX_OPS_WEBHOOK_SECRET || '')}`\n : 'webhook:resend'\n const globalRateCheck = await checkRateLimit(cache, globalBucketKey)\n if (!globalRateCheck.allowed) {\n return rateLimitedResponse(globalRateCheck.retryAfterSeconds)\n }\n\n const em = (container.resolve('em') as EntityManager).fork()\n\n // No explicit scope: decryptEntitiesWithFallbackScope reads tenant/org off the\n // resolved row, which is exactly what binds webhook_secret to its own tenant.\n const settings = await findOneWithDecryption(\n em,\n InboxSettings,\n {\n inboxAddress: toAddress.toLowerCase(),\n isActive: true,\n deletedAt: null,\n },\n )\n\n // Final HMAC verification for the custom provider path: bind the signature to\n // the resolved tenant. When the inbox declares its own secret, ONLY that\n // secret is accepted; otherwise the global secret is accepted (back-compat).\n // For an unknown `to` we still require the global secret so the endpoint never\n // behaves as an unauthenticated probe oracle.\n if (verified.data.kind === 'custom') {\n const globalSecret = process.env.INBOX_OPS_WEBHOOK_SECRET || ''\n const inboxSecret = settings?.webhookSecret || ''\n const acceptableSecrets = inboxSecret ? [inboxSecret] : [globalSecret]\n const signatureValid = verifyHmacAgainstAny(\n rawBody,\n verified.data.signature,\n verified.data.timestamp,\n acceptableSecrets,\n )\n if (!signatureValid) {\n return NextResponse.json({ error: 'Invalid signature' }, { status: 400 })\n }\n }\n\n if (!settings) {\n return NextResponse.json({ ok: true })\n }\n\n // Per-tenant bucket keyed on the RESOLVED tenant (not the raw `to`), so it\n // cannot be diluted by alias addresses that map to the same inbox.\n const tenantRateCheck = await checkRateLimit(\n cache,\n `webhook:tenant:${settings.tenantId}`,\n settings.tenantId,\n )\n if (!tenantRateCheck.allowed) {\n return rateLimitedResponse(tenantRateCheck.retryAfterSeconds)\n }\n\n const parsed = parseInboundEmail(emailInput)\n\n const isDuplicate = await checkDuplicate(em, settings, parsed.messageId, parsed.contentHash)\n if (isDuplicate) {\n try {\n await emitInboxOpsEvent('inbox_ops.email.deduplicated', {\n tenantId: settings.tenantId,\n organizationId: settings.organizationId,\n toAddress,\n })\n } catch (eventError) {\n logger.error('Failed to emit deduplicated event', { err: eventError })\n }\n return NextResponse.json({ ok: true })\n }\n\n const maxTextSize = parseInt(process.env.INBOX_OPS_MAX_TEXT_SIZE || '204800', 10)\n const cleanedText = parsed.cleanedText.slice(0, maxTextSize)\n\n const email = em.create(InboxEmail, {\n messageId: parsed.messageId,\n contentHash: parsed.contentHash,\n forwardedByAddress: parsed.from.email,\n forwardedByName: parsed.from.name || null,\n toAddress,\n subject: parsed.subject,\n replyTo: parsed.replyTo,\n inReplyTo: parsed.inReplyTo,\n emailReferences: parsed.references,\n rawText: parsed.rawText,\n rawHtml: parsed.rawHtml,\n cleanedText,\n threadMessages: parsed.threadMessages,\n detectedLanguage: parsed.detectedLanguage,\n receivedAt: new Date(),\n status: 'received' as const,\n isActive: true,\n organizationId: settings.organizationId,\n tenantId: settings.tenantId,\n })\n\n em.persist(email)\n await em.flush()\n\n try {\n await emitInboxOpsEvent('inbox_ops.email.received', {\n emailId: email.id,\n tenantId: settings.tenantId,\n organizationId: settings.organizationId,\n forwardedByAddress: parsed.from.email,\n subject: parsed.subject,\n })\n } catch (eventError) {\n logger.error('Failed to emit email.received event', { err: eventError })\n }\n\n return NextResponse.json({ ok: true })\n}\n\nfunction rateLimitedResponse(retryAfterSeconds?: number): NextResponse {\n return NextResponse.json(\n { error: 'Rate limit exceeded' },\n {\n status: 429,\n headers: retryAfterSeconds ? { 'Retry-After': String(retryAfterSeconds) } : undefined,\n },\n )\n}\n\nfunction extractToAddress(payload: Record<string, unknown>): string | null {\n const to = payload.to\n if (typeof to === 'string') {\n const match = to.match(/<([^>]+)>/)\n return match ? match[1].toLowerCase() : to.trim().toLowerCase()\n }\n if (Array.isArray(to) && to.length > 0) {\n const first = to[0]\n if (typeof first === 'string') {\n const match = first.match(/<([^>]+)>/)\n return match ? match[1].toLowerCase() : first.trim().toLowerCase()\n }\n }\n return null\n}\n\nasync function checkDuplicate(\n em: EntityManager,\n settings: InboxSettings,\n messageId: string | null | undefined,\n contentHash: string | null | undefined,\n): Promise<boolean> {\n const encScope = {\n tenantId: settings.tenantId,\n organizationId: settings.organizationId,\n }\n const whereBase = {\n organizationId: settings.organizationId,\n tenantId: settings.tenantId,\n deletedAt: null,\n }\n\n if (messageId) {\n const byMessageId = await findOneWithDecryption(em, InboxEmail, { ...whereBase, messageId }, undefined, encScope)\n if (byMessageId) return true\n }\n\n if (contentHash) {\n const byHash = await findOneWithDecryption(em, InboxEmail, { ...whereBase, contentHash }, undefined, encScope)\n if (byHash) return true\n }\n\n return false\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'InboxOps',\n summary: 'Inbound email webhook',\n methods: {\n POST: {\n summary: 'Receive forwarded email from provider webhook',\n description: 'Public endpoint \u2014 validated by provider HMAC signature or Resend/Svix signature. Rate limited per tenant.',\n requestBody: {\n contentType: 'application/json',\n schema: z.object({\n from: z.string().optional(),\n to: z.union([z.string(), z.array(z.string())]).optional(),\n subject: z.string().optional(),\n text: z.string().optional(),\n html: z.string().optional(),\n messageId: z.string().optional(),\n replyTo: z.string().optional(),\n inReplyTo: z.string().optional(),\n references: z.union([z.string(), z.array(z.string())]).optional(),\n }),\n },\n responses: [\n { status: 200, description: 'Email received and queued for processing' },\n { status: 400, description: 'Invalid payload or signature' },\n { status: 413, description: 'Payload too large' },\n { status: 429, description: 'Rate limit exceeded' },\n { status: 503, description: 'Webhook secret not configured' },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -90,9 +90,7 @@ async function POST(req) {
90
90
  if (isCrudHttpError(err)) {
91
91
  return NextResponse.json(err.body, { status: err.status });
92
92
  }
93
- const message = err instanceof Error ? err.message : "Failed to create payment session";
94
- const status = message.includes("No gateway adapter") ? 422 : 502;
95
- return NextResponse.json({ error: message }, { status });
93
+ return NextResponse.json({ error: "Failed to create payment session" }, { status: 502 });
96
94
  }
97
95
  }
98
96
  const openApi = {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/payment_gateways/api/sessions/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { createSessionSchema } from '../../data/validators'\nimport type { PaymentGatewayService } from '../../lib/gateway-service'\nimport { paymentGatewaysTag } from '../openapi'\nimport {\n resolveUserFeatures,\n runPaymentGatewayMutationGuardAfterSuccess,\n runPaymentGatewayMutationGuards,\n} from '../guards'\n\nconst gatewayTransactionResourceKind = 'payment_gateways.gateway_transaction'\n\nexport const metadata = {\n path: '/payment_gateways/sessions',\n POST: { requireAuth: true, requireFeatures: ['payment_gateways.manage'] },\n}\n\nexport async function POST(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId || !auth.orgId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const payload = await readJsonSafe<unknown>(req)\n const parsed = createSessionSchema.safeParse(payload)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid payload', details: parsed.error.flatten() }, { status: 422 })\n }\n\n const container = await createRequestContainer()\n const guardResult = await runPaymentGatewayMutationGuards(\n container,\n {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n userId: auth.sub ?? '',\n resourceKind: gatewayTransactionResourceKind,\n resourceId: null,\n operation: 'create',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: parsed.data as Record<string, unknown>,\n },\n resolveUserFeatures(auth),\n )\n if (!guardResult.ok) {\n return NextResponse.json(\n guardResult.errorBody ?? { error: 'Operation blocked by guard' },\n { status: guardResult.errorStatus ?? 422 },\n )\n }\n\n const service = container.resolve('paymentGatewayService') as PaymentGatewayService\n\n try {\n const { transaction, session } = await service.createPaymentSession({\n providerKey: parsed.data.providerKey,\n paymentId: crypto.randomUUID(),\n orderId: parsed.data.orderId,\n amount: parsed.data.amount,\n currencyCode: parsed.data.currencyCode,\n captureMethod: parsed.data.captureMethod,\n description: parsed.data.description,\n successUrl: parsed.data.successUrl,\n cancelUrl: parsed.data.cancelUrl,\n metadata: parsed.data.metadata,\n presentation: parsed.data.presentation,\n organizationId: auth.orgId as string,\n tenantId: auth.tenantId,\n })\n\n const redirectUrl = session.redirectUrl\n ?? (session.clientSession?.type === 'redirect' ? session.clientSession.redirectUrl : null)\n\n await runPaymentGatewayMutationGuardAfterSuccess(guardResult.afterSuccessCallbacks, {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n userId: auth.sub ?? '',\n resourceKind: gatewayTransactionResourceKind,\n resourceId: transaction.id,\n operation: 'create',\n requestMethod: req.method,\n requestHeaders: req.headers,\n })\n\n return NextResponse.json({\n transactionId: transaction.id,\n sessionId: session.sessionId,\n providerKey: transaction.providerKey,\n clientSecret: session.clientSecret,\n redirectUrl,\n providerData: session.providerData ?? null,\n clientSession: session.clientSession ?? null,\n status: session.status,\n paymentId: transaction.paymentId,\n }, { status: 201 })\n } catch (err: unknown) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n const message = err instanceof Error ? err.message : 'Failed to create payment session'\n const status = message.includes('No gateway adapter') ? 422 : 502\n return NextResponse.json({ error: message }, { status })\n }\n}\n\nexport const openApi = {\n tags: [paymentGatewaysTag],\n summary: 'Create a payment session via a gateway provider',\n methods: {\n POST: {\n summary: 'Create payment session',\n tags: [paymentGatewaysTag],\n responses: [\n { status: 201, description: 'Payment session created' },\n { status: 409, description: 'Amount or currency does not match the referenced order' },\n { status: 422, description: 'Invalid payload or unknown provider' },\n { status: 502, description: 'Gateway provider error' },\n ],\n },\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { createSessionSchema } from '../../data/validators'\nimport type { PaymentGatewayService } from '../../lib/gateway-service'\nimport { paymentGatewaysTag } from '../openapi'\nimport {\n resolveUserFeatures,\n runPaymentGatewayMutationGuardAfterSuccess,\n runPaymentGatewayMutationGuards,\n} from '../guards'\n\nconst gatewayTransactionResourceKind = 'payment_gateways.gateway_transaction'\n\nexport const metadata = {\n path: '/payment_gateways/sessions',\n POST: { requireAuth: true, requireFeatures: ['payment_gateways.manage'] },\n}\n\nexport async function POST(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId || !auth.orgId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const payload = await readJsonSafe<unknown>(req)\n const parsed = createSessionSchema.safeParse(payload)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid payload', details: parsed.error.flatten() }, { status: 422 })\n }\n\n const container = await createRequestContainer()\n const guardResult = await runPaymentGatewayMutationGuards(\n container,\n {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n userId: auth.sub ?? '',\n resourceKind: gatewayTransactionResourceKind,\n resourceId: null,\n operation: 'create',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: parsed.data as Record<string, unknown>,\n },\n resolveUserFeatures(auth),\n )\n if (!guardResult.ok) {\n return NextResponse.json(\n guardResult.errorBody ?? { error: 'Operation blocked by guard' },\n { status: guardResult.errorStatus ?? 422 },\n )\n }\n\n const service = container.resolve('paymentGatewayService') as PaymentGatewayService\n\n try {\n const { transaction, session } = await service.createPaymentSession({\n providerKey: parsed.data.providerKey,\n paymentId: crypto.randomUUID(),\n orderId: parsed.data.orderId,\n amount: parsed.data.amount,\n currencyCode: parsed.data.currencyCode,\n captureMethod: parsed.data.captureMethod,\n description: parsed.data.description,\n successUrl: parsed.data.successUrl,\n cancelUrl: parsed.data.cancelUrl,\n metadata: parsed.data.metadata,\n presentation: parsed.data.presentation,\n organizationId: auth.orgId as string,\n tenantId: auth.tenantId,\n })\n\n const redirectUrl = session.redirectUrl\n ?? (session.clientSession?.type === 'redirect' ? session.clientSession.redirectUrl : null)\n\n await runPaymentGatewayMutationGuardAfterSuccess(guardResult.afterSuccessCallbacks, {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n userId: auth.sub ?? '',\n resourceKind: gatewayTransactionResourceKind,\n resourceId: transaction.id,\n operation: 'create',\n requestMethod: req.method,\n requestHeaders: req.headers,\n })\n\n return NextResponse.json({\n transactionId: transaction.id,\n sessionId: session.sessionId,\n providerKey: transaction.providerKey,\n clientSecret: session.clientSecret,\n redirectUrl,\n providerData: session.providerData ?? null,\n clientSession: session.clientSession ?? null,\n status: session.status,\n paymentId: transaction.paymentId,\n }, { status: 201 })\n } catch (err: unknown) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n return NextResponse.json({ error: 'Failed to create payment session' }, { status: 502 })\n }\n}\n\nexport const openApi = {\n tags: [paymentGatewaysTag],\n summary: 'Create a payment session via a gateway provider',\n methods: {\n POST: {\n summary: 'Create payment session',\n tags: [paymentGatewaysTag],\n responses: [\n { status: 201, description: 'Payment session created' },\n { status: 409, description: 'Amount or currency does not match the referenced order' },\n { status: 422, description: 'Invalid payload or unknown provider' },\n { status: 502, description: 'Gateway provider error' },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -9,6 +9,7 @@ import { getPaymentGatewayQueue } from "../../../lib/queue.js";
9
9
  import { processPaymentGatewayWebhookJob } from "../../../lib/webhook-processor.js";
10
10
  import { paymentGatewaysTag } from "../../openapi.js";
11
11
  import { createLogger } from "@open-mercato/shared/lib/logger";
12
+ import { readBoundedRequestBody, WebhookBodyTooLargeError } from "@open-mercato/shared/lib/webhooks";
12
13
  const logger = createLogger("payment_gateways").child({ component: "webhook" });
13
14
  const metadata = {
14
15
  path: "/payment_gateways/webhook/[provider]",
@@ -30,7 +31,15 @@ async function POST(req, { params }) {
30
31
  }
31
32
  const rateLimitResponse = await checkProviderWebhookRateLimit(container, req, providerKey);
32
33
  if (rateLimitResponse) return rateLimitResponse;
33
- const rawBody = await req.text();
34
+ let rawBody;
35
+ try {
36
+ rawBody = registration.maxBodyBytes === void 0 ? await req.text() : await readBoundedRequestBody(req, { maxBytes: registration.maxBodyBytes });
37
+ } catch (error) {
38
+ if (error instanceof WebhookBodyTooLargeError) {
39
+ return NextResponse.json({ error: "Webhook payload too large" }, { status: 413 });
40
+ }
41
+ throw error;
42
+ }
34
43
  const headers = {};
35
44
  req.headers.forEach((value, key) => {
36
45
  headers[key] = value;
@@ -126,6 +135,7 @@ const openApi = {
126
135
  responses: [
127
136
  { status: 202, description: "Webhook accepted for async processing" },
128
137
  { status: 401, description: "Signature verification failed" },
138
+ { status: 413, description: "Webhook payload too large" },
129
139
  { status: 404, description: "Unknown provider" }
130
140
  ]
131
141
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/payment_gateways/api/webhook/%5Bprovider%5D/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport { checkRateLimit, getClientIp, RATE_LIMIT_ERROR_FALLBACK } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport type { RateLimiterService } from '@open-mercato/shared/lib/ratelimit/service'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { getWebhookHandler } from '@open-mercato/shared/modules/payment_gateways/types'\nimport type { IntegrationLogService } from '../../../../integrations/lib/log-service'\nimport type { PaymentGatewayService } from '../../../lib/gateway-service'\nimport type { CredentialsService } from '../../../../integrations/lib/credentials-service'\nimport { GatewayTransaction } from '../../../data/entities'\nimport { getPaymentGatewayQueue } from '../../../lib/queue'\nimport { processPaymentGatewayWebhookJob } from '../../../lib/webhook-processor'\nimport { paymentGatewaysTag } from '../../openapi'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('payment_gateways').child({ component: 'webhook' })\n\nexport const metadata = {\n path: '/payment_gateways/webhook/[provider]',\n POST: { requireAuth: false },\n}\n\nconst WEBHOOK_VERIFICATION_FAILED = 'Webhook verification failed'\n\nconst paymentGatewayWebhookRateLimitConfig = {\n points: 60,\n duration: 60,\n keyPrefix: 'payment_gateways:webhook',\n}\n\nexport async function POST(req: Request, { params }: { params: Promise<{ provider: string }> | { provider: string } }) {\n const resolvedParams = await params\n const providerKey = resolvedParams.provider\n const container = await createRequestContainer()\n const registration = getWebhookHandler(providerKey)\n if (!registration) {\n return NextResponse.json({ error: `No webhook handler for provider: ${providerKey}` }, { status: 404 })\n }\n\n const rateLimitResponse = await checkProviderWebhookRateLimit(container, req, providerKey)\n if (rateLimitResponse) return rateLimitResponse\n\n const rawBody = await req.text()\n const headers: Record<string, string> = {}\n req.headers.forEach((value, key) => {\n headers[key] = value\n })\n\n const service = container.resolve('paymentGatewayService') as PaymentGatewayService\n const em = container.resolve('em') as EntityManager\n const integrationCredentialsService = container.resolve('integrationCredentialsService') as CredentialsService\n const queue = getPaymentGatewayQueue(registration.queue ?? 'payment-gateways-webhook')\n const payload = await readJsonSafe<Record<string, unknown>>(rawBody)\n const sessionIdHint = registration.readSessionIdHint?.(payload) ?? null\n\n try {\n // The webhook endpoint is unauthenticated. Tenant/organization scope MUST come from a\n // GatewayTransaction whose per-tenant credentials successfully verify the inbound\n // signature \u2014 NEVER from attacker-controlled payload metadata. If no candidate\n // transaction can be located by the provider-reported session id, or no candidate's\n // credentials can verify the signature, we fail closed with 401. This prevents\n // forged webhooks (e.g. mock gateway PoC) from mutating another tenant's payment\n // state via `event.data.metadata.{organizationId,tenantId}`.\n const candidates = sessionIdHint\n ? await findWithDecryption(\n em,\n GatewayTransaction,\n {\n providerKey,\n providerSessionId: sessionIdHint,\n deletedAt: null,\n },\n { limit: 10, orderBy: { createdAt: 'desc' } },\n )\n : []\n\n let transaction: GatewayTransaction | null = null\n let matchedScope: { organizationId: string; tenantId: string } | null = null\n let event: Awaited<ReturnType<typeof registration.handler>> | null = null\n let lastVerificationError: unknown = null\n\n for (const candidate of candidates) {\n const candidateScope = { organizationId: candidate.organizationId, tenantId: candidate.tenantId }\n const credentials = await integrationCredentialsService.resolve(`gateway_${providerKey}`, candidateScope) ?? {}\n try {\n event = await registration.handler({ rawBody, headers, credentials })\n transaction = candidate\n matchedScope = candidateScope\n break\n } catch (error: unknown) {\n lastVerificationError = error\n }\n }\n\n if (!event || !transaction || !matchedScope) {\n throw lastVerificationError ?? new Error('Webhook verification failed: no matching transaction')\n }\n\n const scope = matchedScope\n\n const jobPayload = {\n providerKey,\n event,\n transactionId: transaction.id,\n scope,\n }\n\n if (process.env.QUEUE_STRATEGY === 'async') {\n await queue.enqueue({\n name: 'payment-gateway-webhook',\n payload: jobPayload,\n })\n } else {\n await processPaymentGatewayWebhookJob(\n {\n em: container.resolve('em') as EntityManager,\n paymentGatewayService: service,\n integrationLogService: container.resolve('integrationLogService') as IntegrationLogService,\n },\n jobPayload,\n )\n }\n\n return NextResponse.json({ received: true, queued: true }, { status: 202 })\n } catch (err: unknown) {\n logger.warn('Webhook verification failed', { providerKey, err })\n return NextResponse.json({ error: WEBHOOK_VERIFICATION_FAILED }, { status: 401 })\n }\n}\n\nasync function checkProviderWebhookRateLimit(\n container: { resolve: (name: string) => unknown },\n req: Request,\n providerKey: string,\n): Promise<NextResponse | null> {\n const rateLimiterService = tryResolve<RateLimiterService>(container, 'rateLimiterService')\n if (!rateLimiterService) return null\n\n return checkRateLimit(\n rateLimiterService,\n paymentGatewayWebhookRateLimitConfig,\n `${providerKey}:${getClientIp(req, rateLimiterService.trustProxyDepth) ?? 'unknown'}`,\n RATE_LIMIT_ERROR_FALLBACK,\n )\n}\n\nfunction tryResolve<T>(container: { resolve: (name: string) => unknown }, name: string): T | null {\n try {\n return container.resolve(name) as T\n } catch {\n return null\n }\n}\n\nexport const openApi = {\n tags: [paymentGatewaysTag],\n summary: 'Receive payment gateway webhook',\n methods: {\n POST: {\n summary: 'Process inbound webhook from payment provider',\n tags: [paymentGatewaysTag],\n responses: [\n { status: 202, description: 'Webhook accepted for async processing' },\n { status: 401, description: 'Signature verification failed' },\n { status: 404, description: 'Unknown provider' },\n ],\n },\n },\n}\n\nexport default POST\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport { checkRateLimit, getClientIp, RATE_LIMIT_ERROR_FALLBACK } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport type { RateLimiterService } from '@open-mercato/shared/lib/ratelimit/service'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { getWebhookHandler } from '@open-mercato/shared/modules/payment_gateways/types'\nimport type { IntegrationLogService } from '../../../../integrations/lib/log-service'\nimport type { PaymentGatewayService } from '../../../lib/gateway-service'\nimport type { CredentialsService } from '../../../../integrations/lib/credentials-service'\nimport { GatewayTransaction } from '../../../data/entities'\nimport { getPaymentGatewayQueue } from '../../../lib/queue'\nimport { processPaymentGatewayWebhookJob } from '../../../lib/webhook-processor'\nimport { paymentGatewaysTag } from '../../openapi'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { readBoundedRequestBody, WebhookBodyTooLargeError } from '@open-mercato/shared/lib/webhooks'\n\nconst logger = createLogger('payment_gateways').child({ component: 'webhook' })\n\nexport const metadata = {\n path: '/payment_gateways/webhook/[provider]',\n POST: { requireAuth: false },\n}\n\nconst WEBHOOK_VERIFICATION_FAILED = 'Webhook verification failed'\n\nconst paymentGatewayWebhookRateLimitConfig = {\n points: 60,\n duration: 60,\n keyPrefix: 'payment_gateways:webhook',\n}\n\nexport async function POST(req: Request, { params }: { params: Promise<{ provider: string }> | { provider: string } }) {\n const resolvedParams = await params\n const providerKey = resolvedParams.provider\n const container = await createRequestContainer()\n const registration = getWebhookHandler(providerKey)\n if (!registration) {\n return NextResponse.json({ error: `No webhook handler for provider: ${providerKey}` }, { status: 404 })\n }\n\n const rateLimitResponse = await checkProviderWebhookRateLimit(container, req, providerKey)\n if (rateLimitResponse) return rateLimitResponse\n\n let rawBody: string\n try {\n rawBody = registration.maxBodyBytes === undefined\n ? await req.text()\n : await readBoundedRequestBody(req, { maxBytes: registration.maxBodyBytes })\n } catch (error) {\n if (error instanceof WebhookBodyTooLargeError) {\n return NextResponse.json({ error: 'Webhook payload too large' }, { status: 413 })\n }\n throw error\n }\n const headers: Record<string, string> = {}\n req.headers.forEach((value, key) => {\n headers[key] = value\n })\n\n const service = container.resolve('paymentGatewayService') as PaymentGatewayService\n const em = container.resolve('em') as EntityManager\n const integrationCredentialsService = container.resolve('integrationCredentialsService') as CredentialsService\n const queue = getPaymentGatewayQueue(registration.queue ?? 'payment-gateways-webhook')\n const payload = await readJsonSafe<Record<string, unknown>>(rawBody)\n const sessionIdHint = registration.readSessionIdHint?.(payload) ?? null\n\n try {\n // The webhook endpoint is unauthenticated. Tenant/organization scope MUST come from a\n // GatewayTransaction whose per-tenant credentials successfully verify the inbound\n // signature \u2014 NEVER from attacker-controlled payload metadata. If no candidate\n // transaction can be located by the provider-reported session id, or no candidate's\n // credentials can verify the signature, we fail closed with 401. This prevents\n // forged webhooks (e.g. mock gateway PoC) from mutating another tenant's payment\n // state via `event.data.metadata.{organizationId,tenantId}`.\n const candidates = sessionIdHint\n ? await findWithDecryption(\n em,\n GatewayTransaction,\n {\n providerKey,\n providerSessionId: sessionIdHint,\n deletedAt: null,\n },\n { limit: 10, orderBy: { createdAt: 'desc' } },\n )\n : []\n\n let transaction: GatewayTransaction | null = null\n let matchedScope: { organizationId: string; tenantId: string } | null = null\n let event: Awaited<ReturnType<typeof registration.handler>> | null = null\n let lastVerificationError: unknown = null\n\n for (const candidate of candidates) {\n const candidateScope = { organizationId: candidate.organizationId, tenantId: candidate.tenantId }\n const credentials = await integrationCredentialsService.resolve(`gateway_${providerKey}`, candidateScope) ?? {}\n try {\n event = await registration.handler({ rawBody, headers, credentials })\n transaction = candidate\n matchedScope = candidateScope\n break\n } catch (error: unknown) {\n lastVerificationError = error\n }\n }\n\n if (!event || !transaction || !matchedScope) {\n throw lastVerificationError ?? new Error('Webhook verification failed: no matching transaction')\n }\n\n const scope = matchedScope\n\n const jobPayload = {\n providerKey,\n event,\n transactionId: transaction.id,\n scope,\n }\n\n if (process.env.QUEUE_STRATEGY === 'async') {\n await queue.enqueue({\n name: 'payment-gateway-webhook',\n payload: jobPayload,\n })\n } else {\n await processPaymentGatewayWebhookJob(\n {\n em: container.resolve('em') as EntityManager,\n paymentGatewayService: service,\n integrationLogService: container.resolve('integrationLogService') as IntegrationLogService,\n },\n jobPayload,\n )\n }\n\n return NextResponse.json({ received: true, queued: true }, { status: 202 })\n } catch (err: unknown) {\n logger.warn('Webhook verification failed', { providerKey, err })\n return NextResponse.json({ error: WEBHOOK_VERIFICATION_FAILED }, { status: 401 })\n }\n}\n\nasync function checkProviderWebhookRateLimit(\n container: { resolve: (name: string) => unknown },\n req: Request,\n providerKey: string,\n): Promise<NextResponse | null> {\n const rateLimiterService = tryResolve<RateLimiterService>(container, 'rateLimiterService')\n if (!rateLimiterService) return null\n\n return checkRateLimit(\n rateLimiterService,\n paymentGatewayWebhookRateLimitConfig,\n `${providerKey}:${getClientIp(req, rateLimiterService.trustProxyDepth) ?? 'unknown'}`,\n RATE_LIMIT_ERROR_FALLBACK,\n )\n}\n\nfunction tryResolve<T>(container: { resolve: (name: string) => unknown }, name: string): T | null {\n try {\n return container.resolve(name) as T\n } catch {\n return null\n }\n}\n\nexport const openApi = {\n tags: [paymentGatewaysTag],\n summary: 'Receive payment gateway webhook',\n methods: {\n POST: {\n summary: 'Process inbound webhook from payment provider',\n tags: [paymentGatewaysTag],\n responses: [\n { status: 202, description: 'Webhook accepted for async processing' },\n { status: 401, description: 'Signature verification failed' },\n { status: 413, description: 'Webhook payload too large' },\n { status: 404, description: 'Unknown provider' },\n ],\n },\n },\n}\n\nexport default POST\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -205,6 +205,10 @@ GatewaySessionInitialization = __decorateClass([
205
205
  Unique({
206
206
  name: "gateway_session_initializations_scope_operation_unique",
207
207
  properties: ["operationKey", "providerKey", "organizationId", "tenantId"]
208
+ }),
209
+ Index({
210
+ name: "gateway_session_initializations_prune_idx",
211
+ expression: 'create index "gateway_session_initializations_prune_idx" on "gateway_session_initializations" ("tenant_id", "organization_id", "updated_at") where "gateway_transaction_id" is not null'
208
212
  })
209
213
  ], GatewaySessionInitialization);
210
214
  OptionalProps;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/payment_gateways/data/entities.ts"],
4
- "sourcesContent": ["import { OptionalProps } from '@mikro-orm/core'\nimport { Entity, Index, PrimaryKey, Property, Unique } from '@mikro-orm/decorators/legacy'\n\n@Entity({ tableName: 'gateway_transactions' })\n@Index({ properties: ['paymentId', 'organizationId', 'tenantId'] })\n@Index({ properties: ['providerKey', 'providerSessionId', 'organizationId'] })\n@Index({ properties: ['organizationId', 'tenantId', 'unifiedStatus'] })\nexport class GatewayTransaction {\n [OptionalProps]?: 'unifiedStatus' | 'gatewayStatus' | 'providerSessionId' | 'gatewayPaymentId' | 'gatewayRefundId' | 'redirectUrl' | 'clientSecret' | 'capturedAmount' | 'gatewayMetadata' | 'webhookLog' | 'lastWebhookAt' | 'lastPolledAt' | 'expiresAt' | 'createdAt' | 'updatedAt' | 'deletedAt'\n\n @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })\n id!: string\n\n @Property({ name: 'payment_id', type: 'uuid' })\n paymentId!: string\n\n @Property({ name: 'provider_key', type: 'text' })\n providerKey!: string\n\n @Property({ name: 'provider_session_id', type: 'text', nullable: true })\n providerSessionId?: string | null\n\n @Property({ name: 'gateway_payment_id', type: 'text', nullable: true })\n gatewayPaymentId?: string | null\n\n @Property({ name: 'gateway_refund_id', type: 'text', nullable: true })\n gatewayRefundId?: string | null\n\n @Property({ name: 'unified_status', type: 'text' })\n unifiedStatus: string = 'pending'\n\n @Property({ name: 'gateway_status', type: 'text', nullable: true })\n gatewayStatus?: string | null\n\n @Property({ name: 'redirect_url', type: 'text', nullable: true })\n redirectUrl?: string | null\n\n @Property({ name: 'client_secret', type: 'text', nullable: true })\n clientSecret?: string | null\n\n @Property({ name: 'amount', type: 'numeric', precision: 18, scale: 4 })\n amount!: string\n\n @Property({ name: 'captured_amount', type: 'numeric', precision: 18, scale: 4, default: '0' })\n capturedAmount: string = '0'\n\n @Property({ name: 'currency_code', type: 'text' })\n currencyCode!: string\n\n @Property({ name: 'gateway_metadata', type: 'jsonb', nullable: true })\n gatewayMetadata?: Record<string, unknown> | null\n\n @Property({ name: 'webhook_log', type: 'jsonb', nullable: true })\n webhookLog?: Array<{ eventType: string; receivedAt: string; idempotencyKey: string; unifiedStatus: string; processed: boolean }> | null\n\n @Property({ name: 'last_webhook_at', type: Date, nullable: true })\n lastWebhookAt?: Date | null\n\n @Property({ name: 'last_polled_at', type: Date, nullable: true })\n lastPolledAt?: Date | null\n\n @Property({ name: 'expires_at', type: Date, nullable: true })\n expiresAt?: Date | null\n\n @Property({ name: 'organization_id', type: 'uuid' })\n organizationId!: string\n\n @Property({ name: 'tenant_id', type: 'uuid' })\n tenantId!: string\n\n @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })\n createdAt: Date = new Date()\n\n @Property({ name: 'updated_at', type: Date, onUpdate: () => new Date() })\n updatedAt: Date = new Date()\n\n @Property({ name: 'deleted_at', type: Date, nullable: true })\n deletedAt?: Date | null\n}\n\n@Entity({ tableName: 'gateway_payment_operations' })\n@Unique({\n name: 'gateway_payment_operations_scope_operation_unique',\n properties: ['operationId', 'organizationId', 'tenantId'],\n})\n@Index({ properties: ['transactionId', 'operationType', 'organizationId', 'tenantId'] })\n@Index({ properties: ['status', 'leaseExpiresAt'] })\nexport class GatewayPaymentOperation {\n [OptionalProps]?: 'status' | 'attemptCount' | 'result' | 'reservedAmount' | 'leaseExpiresAt' | 'createdAt' | 'updatedAt'\n\n @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })\n id!: string\n\n @Property({ name: 'operation_id', type: 'text' })\n operationId!: string\n\n @Property({ name: 'transaction_id', type: 'uuid' })\n transactionId!: string\n\n @Property({ name: 'operation_type', type: 'text' })\n operationType!: string\n\n @Property({ name: 'provider_key', type: 'text' })\n providerKey!: string\n\n @Property({ name: 'request_hash', type: 'text' })\n requestHash!: string\n\n @Property({ name: 'provider_idempotency_key', type: 'text' })\n providerIdempotencyKey!: string\n\n @Property({ name: 'status', type: 'text' })\n status: string = 'in_progress'\n\n @Property({ name: 'attempt_token', type: 'text' })\n attemptToken!: string\n\n @Property({ name: 'attempt_count', type: 'integer' })\n attemptCount: number = 1\n\n @Property({ name: 'result', type: 'jsonb', nullable: true })\n result?: Record<string, unknown> | null\n\n @Property({ name: 'reserved_amount', type: 'numeric', precision: 18, scale: 4, nullable: true })\n reservedAmount?: string | null\n\n @Property({ name: 'lease_expires_at', type: Date, nullable: true })\n leaseExpiresAt?: Date | null\n\n @Property({ name: 'organization_id', type: 'uuid' })\n organizationId!: string\n\n @Property({ name: 'tenant_id', type: 'uuid' })\n tenantId!: string\n\n @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })\n createdAt: Date = new Date()\n\n @Property({ name: 'updated_at', type: Date, onCreate: () => new Date(), onUpdate: () => new Date() })\n updatedAt: Date = new Date()\n}\n\n@Entity({ tableName: 'gateway_session_initializations' })\n@Unique({\n name: 'gateway_session_initializations_scope_operation_unique',\n properties: ['operationKey', 'providerKey', 'organizationId', 'tenantId'],\n})\nexport class GatewaySessionInitialization {\n [OptionalProps]?: 'claimToken' | 'claimedAt' | 'gatewayTransactionId' | 'createdAt' | 'updatedAt'\n\n @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })\n id!: string\n\n @Property({ name: 'operation_key', type: 'text' })\n operationKey!: string\n\n @Property({ name: 'provider_key', type: 'text' })\n providerKey!: string\n\n @Property({ name: 'claim_token', type: 'uuid', nullable: true })\n claimToken?: string | null\n\n @Property({ name: 'claimed_at', type: Date, nullable: true })\n claimedAt?: Date | null\n\n @Property({ name: 'gateway_transaction_id', type: 'uuid', nullable: true })\n gatewayTransactionId?: string | null\n\n @Property({ name: 'organization_id', type: 'uuid' })\n organizationId!: string\n\n @Property({ name: 'tenant_id', type: 'uuid' })\n tenantId!: string\n\n @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })\n createdAt: Date = new Date()\n\n @Property({ name: 'updated_at', type: Date, onCreate: () => new Date(), onUpdate: () => new Date() })\n updatedAt: Date = new Date()\n}\n\n@Entity({ tableName: 'gateway_webhook_events' })\n@Unique({\n name: 'gateway_webhook_events_idempotency_unique',\n properties: ['idempotencyKey', 'providerKey', 'organizationId', 'tenantId'],\n})\nexport class WebhookProcessedEvent {\n [OptionalProps]?: 'processedAt'\n\n @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })\n id!: string\n\n @Property({ name: 'provider_key', type: 'text' })\n providerKey!: string\n\n @Property({ name: 'idempotency_key', type: 'text' })\n idempotencyKey!: string\n\n @Property({ name: 'event_type', type: 'text' })\n eventType!: string\n\n @Property({ name: 'organization_id', type: 'uuid' })\n organizationId!: string\n\n @Property({ name: 'tenant_id', type: 'uuid' })\n tenantId!: string\n\n @Property({ name: 'processed_at', type: Date, onCreate: () => new Date() })\n processedAt: Date = new Date()\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { OptionalProps } from '@mikro-orm/core'\nimport { Entity, Index, PrimaryKey, Property, Unique } from '@mikro-orm/decorators/legacy'\n\n@Entity({ tableName: 'gateway_transactions' })\n@Index({ properties: ['paymentId', 'organizationId', 'tenantId'] })\n@Index({ properties: ['providerKey', 'providerSessionId', 'organizationId'] })\n@Index({ properties: ['organizationId', 'tenantId', 'unifiedStatus'] })\nexport class GatewayTransaction {\n [OptionalProps]?: 'unifiedStatus' | 'gatewayStatus' | 'providerSessionId' | 'gatewayPaymentId' | 'gatewayRefundId' | 'redirectUrl' | 'clientSecret' | 'capturedAmount' | 'gatewayMetadata' | 'webhookLog' | 'lastWebhookAt' | 'lastPolledAt' | 'expiresAt' | 'createdAt' | 'updatedAt' | 'deletedAt'\n\n @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })\n id!: string\n\n @Property({ name: 'payment_id', type: 'uuid' })\n paymentId!: string\n\n @Property({ name: 'provider_key', type: 'text' })\n providerKey!: string\n\n @Property({ name: 'provider_session_id', type: 'text', nullable: true })\n providerSessionId?: string | null\n\n @Property({ name: 'gateway_payment_id', type: 'text', nullable: true })\n gatewayPaymentId?: string | null\n\n @Property({ name: 'gateway_refund_id', type: 'text', nullable: true })\n gatewayRefundId?: string | null\n\n @Property({ name: 'unified_status', type: 'text' })\n unifiedStatus: string = 'pending'\n\n @Property({ name: 'gateway_status', type: 'text', nullable: true })\n gatewayStatus?: string | null\n\n @Property({ name: 'redirect_url', type: 'text', nullable: true })\n redirectUrl?: string | null\n\n @Property({ name: 'client_secret', type: 'text', nullable: true })\n clientSecret?: string | null\n\n @Property({ name: 'amount', type: 'numeric', precision: 18, scale: 4 })\n amount!: string\n\n @Property({ name: 'captured_amount', type: 'numeric', precision: 18, scale: 4, default: '0' })\n capturedAmount: string = '0'\n\n @Property({ name: 'currency_code', type: 'text' })\n currencyCode!: string\n\n @Property({ name: 'gateway_metadata', type: 'jsonb', nullable: true })\n gatewayMetadata?: Record<string, unknown> | null\n\n @Property({ name: 'webhook_log', type: 'jsonb', nullable: true })\n webhookLog?: Array<{ eventType: string; receivedAt: string; idempotencyKey: string; unifiedStatus: string; processed: boolean }> | null\n\n @Property({ name: 'last_webhook_at', type: Date, nullable: true })\n lastWebhookAt?: Date | null\n\n @Property({ name: 'last_polled_at', type: Date, nullable: true })\n lastPolledAt?: Date | null\n\n @Property({ name: 'expires_at', type: Date, nullable: true })\n expiresAt?: Date | null\n\n @Property({ name: 'organization_id', type: 'uuid' })\n organizationId!: string\n\n @Property({ name: 'tenant_id', type: 'uuid' })\n tenantId!: string\n\n @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })\n createdAt: Date = new Date()\n\n @Property({ name: 'updated_at', type: Date, onUpdate: () => new Date() })\n updatedAt: Date = new Date()\n\n @Property({ name: 'deleted_at', type: Date, nullable: true })\n deletedAt?: Date | null\n}\n\n@Entity({ tableName: 'gateway_payment_operations' })\n@Unique({\n name: 'gateway_payment_operations_scope_operation_unique',\n properties: ['operationId', 'organizationId', 'tenantId'],\n})\n@Index({ properties: ['transactionId', 'operationType', 'organizationId', 'tenantId'] })\n@Index({ properties: ['status', 'leaseExpiresAt'] })\nexport class GatewayPaymentOperation {\n [OptionalProps]?: 'status' | 'attemptCount' | 'result' | 'reservedAmount' | 'leaseExpiresAt' | 'createdAt' | 'updatedAt'\n\n @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })\n id!: string\n\n @Property({ name: 'operation_id', type: 'text' })\n operationId!: string\n\n @Property({ name: 'transaction_id', type: 'uuid' })\n transactionId!: string\n\n @Property({ name: 'operation_type', type: 'text' })\n operationType!: string\n\n @Property({ name: 'provider_key', type: 'text' })\n providerKey!: string\n\n @Property({ name: 'request_hash', type: 'text' })\n requestHash!: string\n\n @Property({ name: 'provider_idempotency_key', type: 'text' })\n providerIdempotencyKey!: string\n\n @Property({ name: 'status', type: 'text' })\n status: string = 'in_progress'\n\n @Property({ name: 'attempt_token', type: 'text' })\n attemptToken!: string\n\n @Property({ name: 'attempt_count', type: 'integer' })\n attemptCount: number = 1\n\n @Property({ name: 'result', type: 'jsonb', nullable: true })\n result?: Record<string, unknown> | null\n\n @Property({ name: 'reserved_amount', type: 'numeric', precision: 18, scale: 4, nullable: true })\n reservedAmount?: string | null\n\n @Property({ name: 'lease_expires_at', type: Date, nullable: true })\n leaseExpiresAt?: Date | null\n\n @Property({ name: 'organization_id', type: 'uuid' })\n organizationId!: string\n\n @Property({ name: 'tenant_id', type: 'uuid' })\n tenantId!: string\n\n @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })\n createdAt: Date = new Date()\n\n @Property({ name: 'updated_at', type: Date, onCreate: () => new Date(), onUpdate: () => new Date() })\n updatedAt: Date = new Date()\n}\n\n@Entity({ tableName: 'gateway_session_initializations' })\n@Unique({\n name: 'gateway_session_initializations_scope_operation_unique',\n properties: ['operationKey', 'providerKey', 'organizationId', 'tenantId'],\n})\n@Index({\n name: 'gateway_session_initializations_prune_idx',\n expression:\n 'create index \"gateway_session_initializations_prune_idx\" on \"gateway_session_initializations\" (\"tenant_id\", \"organization_id\", \"updated_at\") where \"gateway_transaction_id\" is not null',\n})\nexport class GatewaySessionInitialization {\n [OptionalProps]?: 'claimToken' | 'claimedAt' | 'gatewayTransactionId' | 'createdAt' | 'updatedAt'\n\n @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })\n id!: string\n\n @Property({ name: 'operation_key', type: 'text' })\n operationKey!: string\n\n @Property({ name: 'provider_key', type: 'text' })\n providerKey!: string\n\n @Property({ name: 'claim_token', type: 'uuid', nullable: true })\n claimToken?: string | null\n\n @Property({ name: 'claimed_at', type: Date, nullable: true })\n claimedAt?: Date | null\n\n @Property({ name: 'gateway_transaction_id', type: 'uuid', nullable: true })\n gatewayTransactionId?: string | null\n\n @Property({ name: 'organization_id', type: 'uuid' })\n organizationId!: string\n\n @Property({ name: 'tenant_id', type: 'uuid' })\n tenantId!: string\n\n @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })\n createdAt: Date = new Date()\n\n @Property({ name: 'updated_at', type: Date, onCreate: () => new Date(), onUpdate: () => new Date() })\n updatedAt: Date = new Date()\n}\n\n@Entity({ tableName: 'gateway_webhook_events' })\n@Unique({\n name: 'gateway_webhook_events_idempotency_unique',\n properties: ['idempotencyKey', 'providerKey', 'organizationId', 'tenantId'],\n})\nexport class WebhookProcessedEvent {\n [OptionalProps]?: 'processedAt'\n\n @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })\n id!: string\n\n @Property({ name: 'provider_key', type: 'text' })\n providerKey!: string\n\n @Property({ name: 'idempotency_key', type: 'text' })\n idempotencyKey!: string\n\n @Property({ name: 'event_type', type: 'text' })\n eventType!: string\n\n @Property({ name: 'organization_id', type: 'uuid' })\n organizationId!: string\n\n @Property({ name: 'tenant_id', type: 'uuid' })\n tenantId!: string\n\n @Property({ name: 'processed_at', type: Date, onCreate: () => new Date() })\n processedAt: Date = new Date()\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,9 +1,10 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { findOneWithDecryption, findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
3
+ import { createLogger } from "@open-mercato/shared/lib/logger";
3
4
  import {
4
5
  getGatewayAdapter
5
6
  } from "@open-mercato/shared/modules/payment_gateways/types";
6
- import { conflict } from "@open-mercato/shared/lib/crud/errors";
7
+ import { conflict, CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
7
8
  import { GatewaySessionInitialization, GatewayTransaction } from "../data/entities.js";
8
9
  import { canApplyManualAction, isValidTransition } from "./status-machine.js";
9
10
  import { emitPaymentGatewayEvent } from "../events.js";
@@ -33,6 +34,7 @@ import {
33
34
  } from "./session-idempotency.js";
34
35
  const PAYMENT_SESSION_CLAIM_STALE_MS = 3e4;
35
36
  const PAYMENT_SESSION_WAIT_INTERVAL_MS = 25;
37
+ const logger = createLogger("payment_gateways").child({ component: "gateway-service" });
36
38
  function assertManualActionAllowed(action, transaction) {
37
39
  const current = transaction.unifiedStatus;
38
40
  if (!canApplyManualAction(action, current)) {
@@ -112,9 +114,8 @@ function createPaymentGatewayService(deps) {
112
114
  const selectedVersion = deps.integrationStateService ? await deps.integrationStateService.resolveApiVersion(integrationId, scope) : void 0;
113
115
  const adapter = getGatewayAdapter(providerKey, selectedVersion);
114
116
  if (!adapter) {
115
- throw new Error(
116
- selectedVersion ? `No gateway adapter registered for provider: ${providerKey} (version: ${selectedVersion})` : `No gateway adapter registered for provider: ${providerKey}`
117
- );
117
+ const message = selectedVersion ? `No gateway adapter registered for provider: ${providerKey} (version: ${selectedVersion})` : `No gateway adapter registered for provider: ${providerKey}`;
118
+ throw new CrudHttpError(422, { error: message });
118
119
  }
119
120
  const credentials = await integrationCredentialsService.resolve(integrationId, scope) ?? {};
120
121
  return { adapter, credentials };
@@ -330,7 +331,7 @@ function createPaymentGatewayService(deps) {
330
331
  scope
331
332
  );
332
333
  if (!transaction2) {
333
- throw new Error("Completed payment session is missing its gateway transaction");
334
+ throw new Error("[internal] Completed payment session is missing its gateway transaction");
334
335
  }
335
336
  return { transaction: transaction2, session: restoreSession(transaction2) };
336
337
  }
@@ -346,7 +347,15 @@ function createPaymentGatewayService(deps) {
346
347
  try {
347
348
  session = await adapter.createSession({ ...sessionInput, idempotencyKey: operationKey });
348
349
  } catch (error) {
349
- await releasePaymentSessionInitialization(em, ownership, scope).catch(() => void 0);
350
+ try {
351
+ await releasePaymentSessionInitialization(em, ownership, scope);
352
+ } catch (releaseError) {
353
+ logger.warn("Failed to release payment session initialization claim", {
354
+ claimId: ownership.id,
355
+ providerKey: input.providerKey,
356
+ err: releaseError
357
+ });
358
+ }
350
359
  throw error;
351
360
  } finally {
352
361
  await stopHeartbeat();