@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.
- package/.turbo/turbo-build.log +1 -1
- package/dist/generated/entities/customer_deal_person_link/index.js +2 -0
- package/dist/generated/entities/customer_deal_person_link/index.js.map +2 -2
- package/dist/generated/entities/customer_user_invitation/index.js +2 -0
- package/dist/generated/entities/customer_user_invitation/index.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +2 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/modules/auth/lib/backendChrome.js +10 -9
- package/dist/modules/auth/lib/backendChrome.js.map +2 -2
- package/dist/modules/auth/lib/tenantAccess.js +7 -0
- package/dist/modules/auth/lib/tenantAccess.js.map +2 -2
- package/dist/modules/business_rules/api/openmercato-call-options/route.js +85 -0
- package/dist/modules/business_rules/api/openmercato-call-options/route.js.map +7 -0
- package/dist/modules/business_rules/api/rules/route.js +71 -4
- package/dist/modules/business_rules/api/rules/route.js.map +2 -2
- package/dist/modules/business_rules/components/ActionBuilder.js +47 -4
- package/dist/modules/business_rules/components/ActionBuilder.js.map +3 -3
- package/dist/modules/business_rules/components/ActionRow.js +106 -16
- package/dist/modules/business_rules/components/ActionRow.js.map +2 -2
- package/dist/modules/business_rules/components/utils/actionValidation.js +6 -0
- package/dist/modules/business_rules/components/utils/actionValidation.js.map +2 -2
- package/dist/modules/business_rules/lib/action-executor.js +148 -0
- package/dist/modules/business_rules/lib/action-executor.js.map +2 -2
- package/dist/modules/business_rules/lib/openmercato-call-options-types.js +5 -0
- package/dist/modules/business_rules/lib/openmercato-call-options-types.js.map +7 -0
- package/dist/modules/business_rules/lib/openmercato-call-options.js +231 -0
- package/dist/modules/business_rules/lib/openmercato-call-options.js.map +7 -0
- package/dist/modules/business_rules/lib/rule-engine.js +5 -1
- package/dist/modules/business_rules/lib/rule-engine.js.map +2 -2
- package/dist/modules/communication_channels/api/post/webhook/[provider]/route.js +11 -1
- package/dist/modules/communication_channels/api/post/webhook/[provider]/route.js.map +2 -2
- package/dist/modules/communication_channels/api/post/webhooks/gmail/route.js +11 -6
- package/dist/modules/communication_channels/api/post/webhooks/gmail/route.js.map +2 -2
- package/dist/modules/configs/lib/upgrade-actions.js +12 -0
- package/dist/modules/configs/lib/upgrade-actions.js.map +2 -2
- package/dist/modules/customer_accounts/api/admin/users/[id].js +10 -1
- package/dist/modules/customer_accounts/api/admin/users/[id].js.map +2 -2
- package/dist/modules/customer_accounts/api/admin/users-invite.js +61 -4
- package/dist/modules/customer_accounts/api/admin/users-invite.js.map +2 -2
- package/dist/modules/customer_accounts/api/portal/users-invite.js +20 -2
- package/dist/modules/customer_accounts/api/portal/users-invite.js.map +2 -2
- package/dist/modules/customer_accounts/data/entities.js +3 -0
- package/dist/modules/customer_accounts/data/entities.js.map +2 -2
- package/dist/modules/customer_accounts/data/validators.js +1 -0
- package/dist/modules/customer_accounts/data/validators.js.map +2 -2
- package/dist/modules/customer_accounts/emails/CustomerInvitationEmail.js +26 -0
- package/dist/modules/customer_accounts/emails/CustomerInvitationEmail.js.map +7 -0
- package/dist/modules/customer_accounts/lib/customerEntityOwnership.js +29 -3
- package/dist/modules/customer_accounts/lib/customerEntityOwnership.js.map +2 -2
- package/dist/modules/customer_accounts/lib/customerUrl.js +19 -10
- package/dist/modules/customer_accounts/lib/customerUrl.js.map +2 -2
- package/dist/modules/customer_accounts/lib/invitationEmail.js +35 -0
- package/dist/modules/customer_accounts/lib/invitationEmail.js.map +7 -0
- package/dist/modules/customer_accounts/migrations/Migration20260723120000_customer_accounts.js +13 -0
- package/dist/modules/customer_accounts/migrations/Migration20260723120000_customer_accounts.js.map +7 -0
- package/dist/modules/customer_accounts/services/customerInvitationService.js +32 -2
- package/dist/modules/customer_accounts/services/customerInvitationService.js.map +2 -2
- package/dist/modules/customer_accounts/subscribers/autoLinkCrm.js +47 -7
- package/dist/modules/customer_accounts/subscribers/autoLinkCrm.js.map +2 -2
- package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js +2 -2
- package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js.map +2 -2
- package/dist/modules/customer_accounts/widgets/injection-table.js +1 -1
- package/dist/modules/customer_accounts/widgets/injection-table.js.map +2 -2
- package/dist/modules/customers/api/deals/[id]/people/route.js +4 -2
- package/dist/modules/customers/api/deals/[id]/people/route.js.map +2 -2
- package/dist/modules/customers/api/deals/[id]/route.js +7 -3
- package/dist/modules/customers/api/deals/[id]/route.js.map +2 -2
- package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js +11 -1
- package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/[id]/page.js +14 -8
- package/dist/modules/customers/backend/customers/deals/[id]/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/people-v2/[id]/page.js +21 -6
- package/dist/modules/customers/backend/customers/people-v2/[id]/page.js.map +2 -2
- package/dist/modules/customers/commands/deals.js +48 -10
- package/dist/modules/customers/commands/deals.js.map +2 -2
- package/dist/modules/customers/commands/people.js +2 -0
- package/dist/modules/customers/commands/people.js.map +2 -2
- package/dist/modules/customers/components/detail/CompanyDetailTabs.js +26 -6
- package/dist/modules/customers/components/detail/CompanyDetailTabs.js.map +2 -2
- package/dist/modules/customers/components/detail/DealDetailTabs.js +30 -8
- package/dist/modules/customers/components/detail/DealDetailTabs.js.map +2 -2
- package/dist/modules/customers/components/detail/PersonDetailTabs.js +30 -8
- package/dist/modules/customers/components/detail/PersonDetailTabs.js.map +2 -2
- package/dist/modules/customers/data/entities.js +8 -0
- package/dist/modules/customers/data/entities.js.map +2 -2
- package/dist/modules/customers/data/validators.js +2 -1
- package/dist/modules/customers/data/validators.js.map +2 -2
- package/dist/modules/customers/migrations/Migration20260723210000_customers.js +15 -0
- package/dist/modules/customers/migrations/Migration20260723210000_customers.js.map +7 -0
- package/dist/modules/directory/api/tenants/route.js +8 -2
- package/dist/modules/directory/api/tenants/route.js.map +2 -2
- package/dist/modules/directory/commands/tenants.js +6 -0
- package/dist/modules/directory/commands/tenants.js.map +2 -2
- package/dist/modules/entities/api/entity-settings.js +59 -9
- package/dist/modules/entities/api/entity-settings.js.map +2 -2
- package/dist/modules/entities/backend/config/settings/page.js +10 -0
- package/dist/modules/entities/backend/config/settings/page.js.map +7 -0
- package/dist/modules/entities/backend/config/settings/page.meta.js +36 -0
- package/dist/modules/entities/backend/config/settings/page.meta.js.map +7 -0
- package/dist/modules/entities/backend/entities/user/create/page.js +16 -7
- package/dist/modules/entities/backend/entities/user/create/page.js.map +2 -2
- package/dist/modules/entities/components/EntitySettingsManager.js +136 -0
- package/dist/modules/entities/components/EntitySettingsManager.js.map +7 -0
- package/dist/modules/inbox_ops/api/webhook/inbound.js +14 -10
- package/dist/modules/inbox_ops/api/webhook/inbound.js.map +2 -2
- package/dist/modules/payment_gateways/api/sessions/route.js +1 -3
- package/dist/modules/payment_gateways/api/sessions/route.js.map +2 -2
- package/dist/modules/payment_gateways/api/webhook/[provider]/route.js +11 -1
- package/dist/modules/payment_gateways/api/webhook/[provider]/route.js.map +2 -2
- package/dist/modules/payment_gateways/data/entities.js +4 -0
- package/dist/modules/payment_gateways/data/entities.js.map +2 -2
- package/dist/modules/payment_gateways/lib/gateway-service.js +15 -6
- package/dist/modules/payment_gateways/lib/gateway-service.js.map +2 -2
- package/dist/modules/payment_gateways/lib/session-idempotency.js +33 -0
- package/dist/modules/payment_gateways/lib/session-idempotency.js.map +2 -2
- package/dist/modules/payment_gateways/migrations/Migration20260803103035_payment_gateways.js +18 -0
- package/dist/modules/payment_gateways/migrations/Migration20260803103035_payment_gateways.js.map +7 -0
- package/dist/modules/payment_gateways/setup.js +47 -0
- package/dist/modules/payment_gateways/setup.js.map +2 -2
- package/dist/modules/payment_gateways/workers/session-initialization-prune.js +50 -0
- package/dist/modules/payment_gateways/workers/session-initialization-prune.js.map +7 -0
- package/dist/modules/portal/frontend/[orgSlug]/portal/invite/page.js +134 -0
- package/dist/modules/portal/frontend/[orgSlug]/portal/invite/page.js.map +7 -0
- package/dist/modules/portal/frontend/[orgSlug]/portal/invite/page.meta.js +11 -0
- package/dist/modules/portal/frontend/[orgSlug]/portal/invite/page.meta.js.map +7 -0
- package/dist/modules/portal/lib/navigation.js +7 -0
- package/dist/modules/portal/lib/navigation.js.map +7 -0
- package/dist/modules/query_index/lib/engine.js +43 -0
- package/dist/modules/query_index/lib/engine.js.map +2 -2
- package/dist/modules/query_index/lib/search-tokens.js +15 -5
- package/dist/modules/query_index/lib/search-tokens.js.map +2 -2
- package/dist/modules/shipping_carriers/api/webhook/[provider]/route.js +11 -1
- package/dist/modules/shipping_carriers/api/webhook/[provider]/route.js.map +2 -2
- package/generated/entities/customer_deal_person_link/index.ts +1 -0
- package/generated/entities/customer_user_invitation/index.ts +1 -0
- package/generated/entity-fields-registry.ts +2 -0
- package/package.json +7 -7
- package/src/modules/auth/lib/backendChrome.tsx +15 -9
- package/src/modules/auth/lib/tenantAccess.ts +8 -0
- package/src/modules/business_rules/api/openmercato-call-options/route.ts +94 -0
- package/src/modules/business_rules/api/rules/route.ts +92 -4
- package/src/modules/business_rules/components/ActionBuilder.tsx +54 -5
- package/src/modules/business_rules/components/ActionRow.tsx +156 -18
- package/src/modules/business_rules/components/utils/actionValidation.ts +6 -0
- package/src/modules/business_rules/i18n/de.json +12 -0
- package/src/modules/business_rules/i18n/en.json +12 -0
- package/src/modules/business_rules/i18n/es.json +12 -0
- package/src/modules/business_rules/i18n/pl.json +12 -0
- package/src/modules/business_rules/lib/action-executor.ts +198 -0
- package/src/modules/business_rules/lib/openmercato-call-options-types.ts +31 -0
- package/src/modules/business_rules/lib/openmercato-call-options.ts +297 -0
- package/src/modules/business_rules/lib/rule-engine.ts +4 -0
- package/src/modules/communication_channels/api/post/webhook/[provider]/route.ts +11 -1
- package/src/modules/communication_channels/api/post/webhooks/gmail/route.ts +17 -11
- package/src/modules/configs/lib/upgrade-actions.ts +14 -0
- package/src/modules/customer_accounts/api/admin/users/[id].ts +16 -1
- package/src/modules/customer_accounts/api/admin/users-invite.ts +78 -3
- package/src/modules/customer_accounts/api/portal/users-invite.ts +22 -1
- package/src/modules/customer_accounts/data/entities.ts +3 -0
- package/src/modules/customer_accounts/data/validators.ts +1 -0
- package/src/modules/customer_accounts/emails/CustomerInvitationEmail.tsx +45 -0
- package/src/modules/customer_accounts/i18n/de.json +6 -0
- package/src/modules/customer_accounts/i18n/en.json +6 -0
- package/src/modules/customer_accounts/i18n/es.json +6 -0
- package/src/modules/customer_accounts/i18n/pl.json +6 -0
- package/src/modules/customer_accounts/lib/customerEntityOwnership.ts +68 -2
- package/src/modules/customer_accounts/lib/customerUrl.ts +27 -11
- package/src/modules/customer_accounts/lib/invitationEmail.ts +42 -0
- package/src/modules/customer_accounts/migrations/.snapshot-open-mercato.json +16 -0
- package/src/modules/customer_accounts/migrations/Migration20260723120000_customer_accounts.ts +13 -0
- package/src/modules/customer_accounts/services/customerInvitationService.ts +56 -3
- package/src/modules/customer_accounts/subscribers/autoLinkCrm.ts +75 -8
- package/src/modules/customer_accounts/widgets/injection/account-status/widget.client.tsx +2 -2
- package/src/modules/customer_accounts/widgets/injection-table.ts +4 -1
- package/src/modules/customers/api/deals/[id]/people/route.ts +3 -0
- package/src/modules/customers/api/deals/[id]/route.ts +10 -2
- package/src/modules/customers/backend/customers/companies-v2/[id]/page.tsx +12 -1
- package/src/modules/customers/backend/customers/deals/[id]/hooks/types.ts +1 -0
- package/src/modules/customers/backend/customers/deals/[id]/page.tsx +15 -9
- package/src/modules/customers/backend/customers/people-v2/[id]/page.tsx +24 -6
- package/src/modules/customers/commands/deals.ts +50 -9
- package/src/modules/customers/commands/people.ts +3 -0
- package/src/modules/customers/components/detail/CompanyDetailTabs.tsx +30 -7
- package/src/modules/customers/components/detail/DealDetailTabs.tsx +34 -9
- package/src/modules/customers/components/detail/PersonDetailTabs.tsx +34 -9
- package/src/modules/customers/data/entities.ts +9 -1
- package/src/modules/customers/data/validators.ts +1 -0
- package/src/modules/customers/i18n/de.json +1 -0
- package/src/modules/customers/i18n/en.json +1 -0
- package/src/modules/customers/i18n/es.json +1 -0
- package/src/modules/customers/i18n/pl.json +1 -0
- package/src/modules/customers/migrations/.snapshot-open-mercato.json +27 -1
- package/src/modules/customers/migrations/Migration20260723210000_customers.ts +15 -0
- package/src/modules/directory/api/tenants/route.ts +9 -1
- package/src/modules/directory/commands/tenants.ts +6 -0
- package/src/modules/entities/api/entity-settings.ts +66 -22
- package/src/modules/entities/backend/config/settings/page.meta.ts +34 -0
- package/src/modules/entities/backend/config/settings/page.tsx +12 -0
- package/src/modules/entities/backend/entities/user/create/page.tsx +16 -7
- package/src/modules/entities/components/EntitySettingsManager.tsx +160 -0
- package/src/modules/entities/i18n/de.json +9 -0
- package/src/modules/entities/i18n/en.json +9 -0
- package/src/modules/entities/i18n/es.json +9 -0
- package/src/modules/entities/i18n/pl.json +9 -0
- package/src/modules/inbox_ops/api/webhook/inbound.ts +14 -12
- package/src/modules/payment_gateways/api/sessions/route.ts +1 -3
- package/src/modules/payment_gateways/api/webhook/[provider]/route.ts +13 -1
- package/src/modules/payment_gateways/data/entities.ts +5 -0
- package/src/modules/payment_gateways/i18n/de.json +5 -1
- package/src/modules/payment_gateways/i18n/en.json +5 -1
- package/src/modules/payment_gateways/i18n/es.json +5 -1
- package/src/modules/payment_gateways/i18n/pl.json +5 -1
- package/src/modules/payment_gateways/lib/gateway-service.ts +17 -8
- package/src/modules/payment_gateways/lib/session-idempotency.ts +36 -0
- package/src/modules/payment_gateways/migrations/.snapshot-open-mercato.json +10 -1
- package/src/modules/payment_gateways/migrations/Migration20260803103035_payment_gateways.ts +17 -0
- package/src/modules/payment_gateways/setup.ts +62 -0
- package/src/modules/payment_gateways/workers/session-initialization-prune.ts +63 -0
- package/src/modules/portal/frontend/[orgSlug]/portal/invite/page.meta.ts +9 -0
- package/src/modules/portal/frontend/[orgSlug]/portal/invite/page.tsx +164 -0
- package/src/modules/portal/i18n/de.json +18 -0
- package/src/modules/portal/i18n/en.json +18 -0
- package/src/modules/portal/i18n/es.json +18 -0
- package/src/modules/portal/i18n/pl.json +18 -0
- package/src/modules/portal/lib/navigation.ts +3 -0
- package/src/modules/query_index/lib/engine.ts +52 -0
- package/src/modules/query_index/lib/search-tokens.ts +16 -4
- 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
|
-
|
|
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.
|
|
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
|
-
*
|
|
30
|
-
*
|
|
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
|
-
*
|
|
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
|
},
|