@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,5 +1,12 @@
1
+ import type { EntityManager as PostgreSqlEntityManager } from '@mikro-orm/postgresql'
1
2
  import type { EvaluationContext } from './expression-evaluator'
2
3
  import { getNestedValue, resolveSpecialValue } from './value-resolver'
4
+ import {
5
+ findOpenMercatoEndpointOption,
6
+ getCurrentOpenMercatoEndpointOptions,
7
+ resolveOpenMercatoApiKeyProfile,
8
+ } from './openmercato-call-options'
9
+ import type { OpenMercatoEndpointOption } from './openmercato-call-options-types'
3
10
 
4
11
  /**
5
12
  * Action definition
@@ -19,6 +26,11 @@ export interface ActionContext extends EvaluationContext {
19
26
  data?: any
20
27
  ruleId?: string
21
28
  ruleName?: string
29
+ tenantId?: string
30
+ organizationId?: string
31
+ executedBy?: string | null
32
+ em?: PostgreSqlEntityManager
33
+ openMercatoEndpointOptions?: OpenMercatoEndpointOption[]
22
34
  [key: string]: any
23
35
  }
24
36
 
@@ -85,6 +97,19 @@ export interface CallWebhookResult {
85
97
  status: 'pending'
86
98
  }
87
99
 
100
+ export interface CallOpenMercatoResult {
101
+ type: 'CALL_OPEN_MERCATO'
102
+ endpoint: string
103
+ method: string
104
+ apiKeyId: string
105
+ status: number
106
+ statusText: string
107
+ body: any
108
+ authenticated: true
109
+ tenantId: string
110
+ organizationId: string
111
+ }
112
+
88
113
  export interface EmitEventResult {
89
114
  type: 'EMIT_EVENT'
90
115
  event: string
@@ -104,6 +129,7 @@ export type ActionHandlerResult =
104
129
  | NotifyResult
105
130
  | SetFieldResult
106
131
  | CallWebhookResult
132
+ | CallOpenMercatoResult
107
133
  | EmitEventResult
108
134
 
109
135
  /**
@@ -219,6 +245,7 @@ function getActionHandler(actionType: string): ActionHandler {
219
245
  NOTIFY: handleNotify,
220
246
  SET_FIELD: handleSetField,
221
247
  CALL_WEBHOOK: handleCallWebhook,
248
+ CALL_OPEN_MERCATO: handleCallOpenMercato,
222
249
  EMIT_EVENT: handleEmitEvent,
223
250
  }
224
251
 
@@ -397,6 +424,119 @@ async function handleCallWebhook(
397
424
  }
398
425
  }
399
426
 
427
+ /**
428
+ * CALL_OPEN_MERCATO action handler
429
+ */
430
+ async function handleCallOpenMercato(
431
+ action: Action,
432
+ context: ActionContext
433
+ ): Promise<CallOpenMercatoResult> {
434
+ const endpoint = String(action.config?.endpoint ?? '').trim()
435
+ const method = String(action.config?.method ?? '').trim().toUpperCase()
436
+ const apiKeyId = String(action.config?.apiKeyId ?? '').trim()
437
+
438
+ if (!endpoint) {
439
+ throw new Error('CALL_OPEN_MERCATO action requires an endpoint')
440
+ }
441
+ if (!method) {
442
+ throw new Error('CALL_OPEN_MERCATO action requires an HTTP method')
443
+ }
444
+ if (!apiKeyId) {
445
+ throw new Error('CALL_OPEN_MERCATO action requires an API key profile')
446
+ }
447
+
448
+ const endpointOptions = context.openMercatoEndpointOptions ?? await getCurrentOpenMercatoEndpointOptions()
449
+ const endpointOption = findOpenMercatoEndpointOption(
450
+ endpoint,
451
+ method,
452
+ endpointOptions,
453
+ )
454
+ if (!endpointOption) {
455
+ throw new Error('CALL_OPEN_MERCATO action requires a currently available /api/* endpoint')
456
+ }
457
+
458
+ const em = context.em
459
+ if (!em) {
460
+ throw new Error('CALL_OPEN_MERCATO action requires an EntityManager in action context')
461
+ }
462
+
463
+ const tenantId = context.tenantId ?? context.tenant?.id
464
+ const organizationId = context.organizationId ?? context.organization?.id
465
+ if (!tenantId || !organizationId) {
466
+ throw new Error('CALL_OPEN_MERCATO action requires tenant and organization context')
467
+ }
468
+
469
+ const apiKeyProfile = await resolveOpenMercatoApiKeyProfile(em, apiKeyId, {
470
+ tenantId,
471
+ organizationId,
472
+ })
473
+ if (!apiKeyProfile) {
474
+ throw new Error('CALL_OPEN_MERCATO action requires an active API key profile in scope')
475
+ }
476
+
477
+ const roleIds = Array.isArray(apiKeyProfile.rolesJson)
478
+ ? apiKeyProfile.rolesJson.filter((roleId): roleId is string => typeof roleId === 'string' && roleId.length > 0)
479
+ : []
480
+ if (roleIds.length === 0) {
481
+ throw new Error('CALL_OPEN_MERCATO action requires an API key profile with at least one role')
482
+ }
483
+
484
+ const { withOnetimeApiKey } = await import('../../api_keys/services/apiKeyService')
485
+ const fullUrl = buildOpenMercatoApiUrl(endpointOption.path)
486
+ const requestBody = method === 'GET'
487
+ ? undefined
488
+ : normalizeOpenMercatoRequestBody(action.config?.body, context)
489
+
490
+ return await withOnetimeApiKey(
491
+ em,
492
+ {
493
+ name: `__business_rule_${context.ruleId || 'unknown'}__`,
494
+ description: `One-time key for business rule ${context.ruleId || 'unknown'}`,
495
+ tenantId,
496
+ organizationId,
497
+ roles: roleIds,
498
+ createdBy: null,
499
+ expiresAt: null,
500
+ },
501
+ async (apiKeySecret) => {
502
+ const headers: Record<string, string> = {
503
+ 'Content-Type': 'application/json',
504
+ 'Authorization': `apikey ${apiKeySecret}`,
505
+ 'X-Tenant-Id': tenantId,
506
+ 'X-Organization-Id': organizationId,
507
+ }
508
+ if (context.ruleId) headers['X-Business-Rule-Id'] = context.ruleId
509
+ if (context.ruleName) headers['X-Business-Rule-Name'] = context.ruleName
510
+ if (context.entityType) headers['X-Business-Rule-Entity-Type'] = context.entityType
511
+ if (context.entityId) headers['X-Business-Rule-Entity-Id'] = context.entityId
512
+
513
+ const response = await fetch(fullUrl, {
514
+ method,
515
+ headers,
516
+ body: requestBody === undefined ? undefined : JSON.stringify(requestBody),
517
+ })
518
+
519
+ const responseBody = await parseOpenMercatoResponseBody(response)
520
+ if (!response.ok) {
521
+ throwOpenMercatoResponseError(response.status)
522
+ }
523
+
524
+ return {
525
+ type: 'CALL_OPEN_MERCATO',
526
+ endpoint: endpointOption.path,
527
+ method,
528
+ apiKeyId,
529
+ status: response.status,
530
+ statusText: response.statusText,
531
+ body: responseBody,
532
+ authenticated: true,
533
+ tenantId,
534
+ organizationId,
535
+ }
536
+ },
537
+ )
538
+ }
539
+
400
540
  /**
401
541
  * EMIT_EVENT action handler
402
542
  */
@@ -454,3 +594,61 @@ function resolveValue(value: any, context: ActionContext): any {
454
594
 
455
595
  return value
456
596
  }
597
+
598
+ function buildOpenMercatoApiUrl(endpoint: string): string {
599
+ if (!endpoint.startsWith('/api/')) {
600
+ throw new Error(`CALL_OPEN_MERCATO only supports /api/* paths, got: ${endpoint}`)
601
+ }
602
+ const appUrl = (process.env.APP_URL || 'http://localhost:3000').replace(/\/$/, '')
603
+ return `${appUrl}${endpoint}`
604
+ }
605
+
606
+ function normalizeOpenMercatoRequestBody(value: any, context: ActionContext): any {
607
+ if (value === undefined || value === null || value === '') return undefined
608
+ const interpolated = interpolateActionValue(value, context)
609
+ if (typeof interpolated !== 'string') return interpolated
610
+
611
+ const trimmed = interpolated.trim()
612
+ if (!trimmed) return undefined
613
+
614
+ try {
615
+ return JSON.parse(trimmed)
616
+ } catch {
617
+ return interpolated
618
+ }
619
+ }
620
+
621
+ function interpolateActionValue(value: any, context: ActionContext): any {
622
+ if (typeof value === 'string') {
623
+ return interpolateMessage(value, context)
624
+ }
625
+ if (Array.isArray(value)) {
626
+ return value.map((item) => interpolateActionValue(item, context))
627
+ }
628
+ if (value && typeof value === 'object') {
629
+ return Object.fromEntries(
630
+ Object.entries(value).map(([key, entry]) => [key, interpolateActionValue(entry, context)]),
631
+ )
632
+ }
633
+ return value
634
+ }
635
+
636
+ async function parseOpenMercatoResponseBody(response: Response): Promise<any> {
637
+ const contentType = response.headers.get('content-type')
638
+ try {
639
+ if (contentType && contentType.includes('application/json')) {
640
+ return await response.json()
641
+ }
642
+ return await response.text()
643
+ } catch {
644
+ return null
645
+ }
646
+ }
647
+
648
+ function throwOpenMercatoResponseError(status: number): never {
649
+ const error: Error & { retriable?: boolean } = new Error(`CALL_OPEN_MERCATO request failed with status ${status}`)
650
+ if (status >= 500) {
651
+ error.retriable = true
652
+ }
653
+ throw error
654
+ }
@@ -0,0 +1,31 @@
1
+ export const OPENMERCATO_CALL_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'] as const
2
+
3
+ export type OpenMercatoCallMethod = (typeof OPENMERCATO_CALL_METHODS)[number]
4
+
5
+ export type OpenMercatoEndpointOption = {
6
+ id: string
7
+ path: string
8
+ method: OpenMercatoCallMethod
9
+ label: string
10
+ summary: string | null
11
+ operationId: string | null
12
+ }
13
+
14
+ export type OpenMercatoApiKeyRoleOption = {
15
+ id: string
16
+ name: string | null
17
+ }
18
+
19
+ export type OpenMercatoApiKeyOption = {
20
+ id: string
21
+ name: string
22
+ keyPrefix: string
23
+ organizationId: string | null
24
+ organizationName: string | null
25
+ roles: OpenMercatoApiKeyRoleOption[]
26
+ }
27
+
28
+ export type OpenMercatoCallOptionsResponse = {
29
+ endpoints: OpenMercatoEndpointOption[]
30
+ apiKeys: OpenMercatoApiKeyOption[]
31
+ }
@@ -0,0 +1,297 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql'
2
+ import {
3
+ getApiRouteManifests,
4
+ type ApiRouteManifestEntry,
5
+ type Module,
6
+ } from '@open-mercato/shared/modules/registry'
7
+ import { getModules } from '@open-mercato/shared/lib/modules/registry'
8
+ import { buildOpenApiDocument } from '@open-mercato/shared/lib/openapi'
9
+ import type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
10
+ import { ApiKey } from '../../api_keys/data/entities'
11
+ import { Role } from '../../auth/data/entities'
12
+ import { Organization } from '../../directory/data/entities'
13
+ import {
14
+ OPENMERCATO_CALL_METHODS,
15
+ type OpenMercatoApiKeyOption,
16
+ type OpenMercatoCallMethod,
17
+ type OpenMercatoEndpointOption,
18
+ } from './openmercato-call-options-types'
19
+
20
+ export type OpenMercatoCallScope = {
21
+ tenantId: string
22
+ organizationId?: string | null
23
+ }
24
+
25
+ const METHOD_ORDER = new Map<string, number>(
26
+ OPENMERCATO_CALL_METHODS.map((method, index) => [method, index]),
27
+ )
28
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
29
+
30
+ function isOpenMercatoCallMethod(value: string): value is OpenMercatoCallMethod {
31
+ return OPENMERCATO_CALL_METHODS.includes(value as OpenMercatoCallMethod)
32
+ }
33
+
34
+ function hasPathSegment(path: string, segment: string): boolean {
35
+ return path.split('/').some((part) => part === segment)
36
+ }
37
+
38
+ function shouldExposeEndpoint(path: string, method: string, operation: Record<string, any>): boolean {
39
+ if (!isOpenMercatoCallMethod(method.toUpperCase())) return false
40
+ if (!path.startsWith('/api/')) return false
41
+ if (path.includes('{')) return false
42
+ if (path.includes('[')) return false
43
+ if (path.startsWith('/api/docs')) return false
44
+ if (hasPathSegment(path, 'options')) return false
45
+ if (path === '/api/business_rules/openmercato-call-options') return false
46
+ if (operation.deprecated === true) return false
47
+ return true
48
+ }
49
+
50
+ export function dedupeOpenMercatoEndpointOptions(
51
+ options: OpenMercatoEndpointOption[],
52
+ ): OpenMercatoEndpointOption[] {
53
+ const deduped = new Map<string, OpenMercatoEndpointOption>()
54
+
55
+ for (const option of options) {
56
+ const existing = deduped.get(option.id)
57
+ if (!existing) {
58
+ deduped.set(option.id, option)
59
+ continue
60
+ }
61
+
62
+ if ((!existing.summary && option.summary) || (!existing.operationId && option.operationId)) {
63
+ deduped.set(option.id, {
64
+ ...existing,
65
+ label: option.label,
66
+ summary: option.summary ?? existing.summary,
67
+ operationId: option.operationId ?? existing.operationId,
68
+ })
69
+ }
70
+ }
71
+
72
+ return Array.from(deduped.values())
73
+ }
74
+
75
+ function sortEndpointOptions(options: OpenMercatoEndpointOption[]): OpenMercatoEndpointOption[] {
76
+ return dedupeOpenMercatoEndpointOptions(options).sort((a, b) => {
77
+ const pathCompare = a.path.localeCompare(b.path)
78
+ if (pathCompare !== 0) return pathCompare
79
+ return (METHOD_ORDER.get(a.method) ?? 99) - (METHOD_ORDER.get(b.method) ?? 99)
80
+ })
81
+ }
82
+
83
+ export function collectOpenMercatoEndpointOptionsFromDocument(doc: { paths?: Record<string, any> }): OpenMercatoEndpointOption[] {
84
+ const options: OpenMercatoEndpointOption[] = []
85
+
86
+ for (const [path, methods] of Object.entries(doc.paths ?? {})) {
87
+ for (const [rawMethod, operation] of Object.entries(methods ?? {})) {
88
+ const method = rawMethod.toUpperCase()
89
+ if (!operation || typeof operation !== 'object') continue
90
+ if (!shouldExposeEndpoint(path, method, operation as Record<string, any>)) continue
91
+
92
+ const typedMethod = method as OpenMercatoCallMethod
93
+ const summary = typeof (operation as any).summary === 'string' ? (operation as any).summary : null
94
+ const operationId = typeof (operation as any).operationId === 'string' ? (operation as any).operationId : null
95
+
96
+ options.push({
97
+ id: `${typedMethod} ${path}`,
98
+ path,
99
+ method: typedMethod,
100
+ label: summary ? `${typedMethod} ${path} - ${summary}` : `${typedMethod} ${path}`,
101
+ summary,
102
+ operationId,
103
+ })
104
+ }
105
+ }
106
+
107
+ return sortEndpointOptions(options)
108
+ }
109
+
110
+ export function collectOpenMercatoEndpointOptions(modules: Module[]): OpenMercatoEndpointOption[] {
111
+ return collectOpenMercatoEndpointOptionsFromDocument(buildOpenApiDocument(modules))
112
+ }
113
+
114
+ function normalizeManifestPath(path: string): string {
115
+ const prefixed = path.startsWith('/api/') ? path : `/api${path.startsWith('/') ? path : `/${path}`}`
116
+ return prefixed.replace(/\/+$/, '') || '/api'
117
+ }
118
+
119
+ function getRouteMethodDoc(routeDoc: OpenApiRouteDoc | undefined, method: OpenMercatoCallMethod): OpenApiMethodDoc | undefined {
120
+ return routeDoc?.methods?.[method]
121
+ }
122
+
123
+ async function collectOpenMercatoEndpointOptionsFromApiRouteManifests(
124
+ routes: ApiRouteManifestEntry[],
125
+ ): Promise<OpenMercatoEndpointOption[]> {
126
+ const options: OpenMercatoEndpointOption[] = []
127
+
128
+ for (const route of routes) {
129
+ const path = normalizeManifestPath(route.path)
130
+ const candidateMethods = route.methods
131
+ .map((method) => method.toUpperCase())
132
+ .filter(isOpenMercatoCallMethod)
133
+ if (candidateMethods.length === 0) continue
134
+ if (!path.startsWith('/api/')) continue
135
+ if (path.includes('[')) continue
136
+ if (path.startsWith('/api/docs')) continue
137
+ if (hasPathSegment(path, 'options')) continue
138
+ if (path === '/api/business_rules/openmercato-call-options') continue
139
+
140
+ let routeDoc: OpenApiRouteDoc | undefined
141
+ try {
142
+ const routeModule = await route.load()
143
+ routeDoc = routeModule.openApi as OpenApiRouteDoc | undefined
144
+ } catch {
145
+ routeDoc = undefined
146
+ }
147
+
148
+ for (const typedMethod of candidateMethods) {
149
+ const methodDoc = getRouteMethodDoc(routeDoc, typedMethod)
150
+ const operation = methodDoc ?? {}
151
+ if (!shouldExposeEndpoint(path, typedMethod, operation as Record<string, any>)) continue
152
+
153
+ const summary = typeof methodDoc?.summary === 'string' ? methodDoc.summary : null
154
+ const operationId = typeof methodDoc?.operationId === 'string' ? methodDoc.operationId : null
155
+ options.push({
156
+ id: `${typedMethod} ${path}`,
157
+ path,
158
+ method: typedMethod,
159
+ label: summary ? `${typedMethod} ${path} - ${summary}` : `${typedMethod} ${path}`,
160
+ summary,
161
+ operationId,
162
+ })
163
+ }
164
+ }
165
+
166
+ return sortEndpointOptions(options)
167
+ }
168
+
169
+ export async function getCurrentOpenMercatoEndpointOptions(): Promise<OpenMercatoEndpointOption[]> {
170
+ try {
171
+ const moduleOptions = collectOpenMercatoEndpointOptions(getModules())
172
+ if (moduleOptions.length > 0) return moduleOptions
173
+ } catch {
174
+ // The runtime app route registry is the source of truth when modules were not
175
+ // registered in this package instance.
176
+ }
177
+
178
+ const apiRoutes = getApiRouteManifests()
179
+ if (apiRoutes.length === 0) return []
180
+ return collectOpenMercatoEndpointOptionsFromApiRouteManifests(apiRoutes)
181
+ }
182
+
183
+ export function findOpenMercatoEndpointOption(
184
+ endpoint: string,
185
+ method: string,
186
+ options: OpenMercatoEndpointOption[],
187
+ ): OpenMercatoEndpointOption | null {
188
+ const normalizedMethod = method.trim().toUpperCase()
189
+ if (!endpoint.startsWith('/api/')) return null
190
+ if (!isOpenMercatoCallMethod(normalizedMethod)) return null
191
+ return options.find((option) => option.path === endpoint && option.method === normalizedMethod) ?? null
192
+ }
193
+
194
+ export async function listOpenMercatoApiKeyOptions(
195
+ em: EntityManager,
196
+ scope: OpenMercatoCallScope,
197
+ ): Promise<OpenMercatoApiKeyOption[]> {
198
+ const filters: Record<string, any> = {
199
+ tenantId: scope.tenantId,
200
+ deletedAt: null,
201
+ }
202
+ if (scope.organizationId) {
203
+ filters.organizationId = scope.organizationId
204
+ }
205
+
206
+ const keys = (await em.find(ApiKey, filters, { orderBy: { name: 'asc' } })) as ApiKey[]
207
+ const now = Date.now()
208
+ const activeKeys = keys.filter((key) => !key.expiresAt || key.expiresAt.getTime() > now)
209
+
210
+ const roleIds = new Set<string>()
211
+ const organizationIds = new Set<string>()
212
+ for (const key of activeKeys) {
213
+ if (key.organizationId) organizationIds.add(String(key.organizationId))
214
+ if (Array.isArray(key.rolesJson)) {
215
+ for (const roleId of key.rolesJson) roleIds.add(String(roleId))
216
+ }
217
+ }
218
+
219
+ const [roles, organizations] = await Promise.all([
220
+ roleIds.size > 0 ? em.find(Role, { id: { $in: Array.from(roleIds) }, deletedAt: null }) : [],
221
+ organizationIds.size > 0 ? em.find(Organization, { id: { $in: Array.from(organizationIds) } }) : [],
222
+ ])
223
+ const roleMap = new Map((roles as Role[]).map((role) => [String(role.id), role.name ?? null]))
224
+ const orgMap = new Map((organizations as Organization[]).map((org) => [String(org.id), org.name ?? null]))
225
+
226
+ return activeKeys.map((key) => ({
227
+ id: key.id,
228
+ name: key.name,
229
+ keyPrefix: key.keyPrefix,
230
+ organizationId: key.organizationId ?? null,
231
+ organizationName: key.organizationId ? orgMap.get(String(key.organizationId)) ?? null : null,
232
+ roles: Array.isArray(key.rolesJson)
233
+ ? key.rolesJson.map((roleId) => ({
234
+ id: String(roleId),
235
+ name: roleMap.get(String(roleId)) ?? null,
236
+ }))
237
+ : [],
238
+ }))
239
+ }
240
+
241
+ export async function resolveOpenMercatoApiKeyProfile(
242
+ em: EntityManager,
243
+ apiKeyId: string,
244
+ scope: OpenMercatoCallScope,
245
+ ): Promise<ApiKey | null> {
246
+ if (!apiKeyId || typeof apiKeyId !== 'string') return null
247
+ if (!UUID_RE.test(apiKeyId)) return null
248
+ const filters: Record<string, any> = {
249
+ id: apiKeyId,
250
+ tenantId: scope.tenantId,
251
+ deletedAt: null,
252
+ }
253
+ if (scope.organizationId) {
254
+ filters.organizationId = scope.organizationId
255
+ }
256
+
257
+ const key = await em.findOne(ApiKey, filters)
258
+ if (!key) return null
259
+ if (key.expiresAt && key.expiresAt.getTime() <= Date.now()) return null
260
+ return key
261
+ }
262
+
263
+ export async function validateOpenMercatoCallActions(
264
+ em: EntityManager,
265
+ actions: unknown,
266
+ scope: OpenMercatoCallScope,
267
+ endpointOptions?: OpenMercatoEndpointOption[],
268
+ ): Promise<string[]> {
269
+ if (!Array.isArray(actions) || actions.length === 0) return []
270
+
271
+ const errors: string[] = []
272
+ let availableEndpointOptions = endpointOptions
273
+ for (const [index, action] of actions.entries()) {
274
+ if (!action || typeof action !== 'object') continue
275
+ const typedAction = action as { type?: string; config?: Record<string, any> }
276
+ if (typedAction.type !== 'CALL_OPEN_MERCATO') continue
277
+ availableEndpointOptions ??= await getCurrentOpenMercatoEndpointOptions()
278
+
279
+ const config = typedAction.config ?? {}
280
+ const endpoint = typeof config.endpoint === 'string' ? config.endpoint : ''
281
+ const method = typeof config.method === 'string' ? config.method : ''
282
+ const apiKeyId = typeof config.apiKeyId === 'string' ? config.apiKeyId : ''
283
+
284
+ if (!findOpenMercatoEndpointOption(endpoint, method, availableEndpointOptions)) {
285
+ errors.push(`Action ${index + 1}: selected OpenMercato endpoint is not available`)
286
+ }
287
+
288
+ const apiKey = await resolveOpenMercatoApiKeyProfile(em, apiKeyId, scope)
289
+ if (!apiKey) {
290
+ errors.push(`Action ${index + 1}: selected API key profile is not available`)
291
+ } else if (!Array.isArray(apiKey.rolesJson) || apiKey.rolesJson.length === 0) {
292
+ errors.push(`Action ${index + 1}: selected API key profile has no roles`)
293
+ }
294
+ }
295
+
296
+ return errors
297
+ }
@@ -523,6 +523,10 @@ export async function executeSingleRule(
523
523
  data: context.data,
524
524
  ruleId: rule.ruleId,
525
525
  ruleName: rule.ruleName,
526
+ tenantId: context.tenantId,
527
+ organizationId: context.organizationId,
528
+ executedBy: context.executedBy ?? context.user?.id ?? null,
529
+ em: em as any,
526
530
  }
527
531
 
528
532
  // Execute actions
@@ -12,6 +12,7 @@ import type { InboundMessage } from '../../../../lib/adapter'
12
12
  import type { InboundProcessorPayload } from '../../../../workers/inbound-processor'
13
13
  import type { ReactionInboundJob } from '../../../../lib/reaction-processor-types'
14
14
  import { createLogger } from '@open-mercato/shared/lib/logger'
15
+ import { readBoundedRequestBody, WebhookBodyTooLargeError } from '@open-mercato/shared/lib/webhooks'
15
16
 
16
17
  const logger = createLogger('communication_channels').child({ component: 'inbound-webhook' })
17
18
 
@@ -60,7 +61,15 @@ export async function POST(req: Request, { params }: RouteContext): Promise<Resp
60
61
  )
61
62
  }
62
63
 
63
- const rawBody = await req.text()
64
+ let rawBody: string
65
+ try {
66
+ rawBody = await readBoundedRequestBody(req)
67
+ } catch (error) {
68
+ if (error instanceof WebhookBodyTooLargeError) {
69
+ return NextResponse.json({ error: 'Webhook payload too large' }, { status: 413 })
70
+ }
71
+ throw error
72
+ }
64
73
  const headers: Record<string, string> = {}
65
74
  req.headers.forEach((value, key) => {
66
75
  headers[key] = value
@@ -222,6 +231,7 @@ export const openApi = {
222
231
  responses: [
223
232
  { status: 202, description: 'Webhook accepted for async processing' },
224
233
  { status: 401, description: 'Signature verification failed against every candidate channel' },
234
+ { status: 413, description: 'Webhook payload too large' },
225
235
  { status: 404, description: 'Unknown provider' },
226
236
  ],
227
237
  },
@@ -13,6 +13,7 @@ import {
13
13
  GmailPubSubJwtError,
14
14
  } from '../../../../lib/gmail-pubsub-jwt'
15
15
  import { createLogger } from '@open-mercato/shared/lib/logger'
16
+ import { readBoundedRequestBody, WebhookBodyTooLargeError } from '@open-mercato/shared/lib/webhooks'
16
17
 
17
18
  const logger = createLogger('communication_channels').child({ component: 'gmail-webhook' })
18
19
 
@@ -24,13 +25,14 @@ const logger = createLogger('communication_channels').child({ component: 'gmail-
24
25
  * header, which we verify against Google's public certs.
25
26
  *
26
27
  * Validation pipeline:
27
- * 1. Verify JWT signature, audience (`OM_GMAIL_PUBSUB_AUDIENCE`), and
28
+ * 1. Bound the raw request body before cryptographic work.
29
+ * 2. Verify JWT signature, audience (`OM_GMAIL_PUBSUB_AUDIENCE`), and
28
30
  * email claim (`OM_GMAIL_PUBSUB_SERVICE_ACCOUNT_EMAIL`).
29
- * 2. Decode the Pub/Sub envelope → `{ emailAddress, historyId }`.
30
- * 3. Look up every active Gmail channel matching `emailAddress`. Multiple
31
+ * 3. Decode the Pub/Sub envelope → `{ emailAddress, historyId }`.
32
+ * 4. Look up every active Gmail channel matching `emailAddress`. Multiple
31
33
  * tenants may have the same mailbox connected — we enqueue one sync
32
34
  * job per matching channel (each tenant sees their own data).
33
- * 4. Return `204 No Content` even when no channels match — returning 4xx
35
+ * 5. Return `204 No Content` even when no channels match — returning 4xx
34
36
  * would cause Pub/Sub to retry forever on a permanently-orphaned
35
37
  * registration.
36
38
  */
@@ -58,6 +60,16 @@ export async function POST(req: Request): Promise<Response> {
58
60
  return NextResponse.json({ error: 'webhook not configured' }, { status: 503 })
59
61
  }
60
62
 
63
+ let rawBody: string
64
+ try {
65
+ rawBody = await readBoundedRequestBody(req)
66
+ } catch (error) {
67
+ if (error instanceof WebhookBodyTooLargeError) {
68
+ return NextResponse.json({ error: 'Webhook payload too large' }, { status: 413 })
69
+ }
70
+ return NextResponse.json({ error: 'unreadable_body' }, { status: 400 })
71
+ }
72
+
61
73
  const verifier = getGmailPubSubVerifier()
62
74
  try {
63
75
  await verifier.verify({
@@ -76,13 +88,6 @@ export async function POST(req: Request): Promise<Response> {
76
88
  throw err
77
89
  }
78
90
 
79
- let rawBody: string
80
- try {
81
- rawBody = await req.text()
82
- } catch {
83
- return NextResponse.json({ error: 'unreadable_body' }, { status: 400 })
84
- }
85
-
86
91
  let payload: { emailAddress: string; historyId: string | number }
87
92
  try {
88
93
  payload = decodeGmailPubSubBody(rawBody)
@@ -153,6 +158,7 @@ export const openApi = {
153
158
  { status: 400, description: 'Body not a valid Pub/Sub envelope' },
154
159
  { status: 401, description: 'Invalid JWT or email claim' },
155
160
  { status: 403, description: 'Wrong audience' },
161
+ { status: 413, description: 'Webhook payload too large' },
156
162
  { status: 503, description: 'Webhook not configured / Google certs unreachable' },
157
163
  ],
158
164
  },