@open-mercato/core 0.6.7-develop.6795.1.8a3f27921c → 0.6.7-develop.6814.1.0627c7e9f1
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/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/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/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 +12 -2
- 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/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/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/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 +12 -2
- 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
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Button } from "@open-mercato/ui/primitives/button";
|
|
5
|
+
import { Checkbox } from "@open-mercato/ui/primitives/checkbox";
|
|
6
|
+
import { Label } from "@open-mercato/ui/primitives/label";
|
|
7
|
+
import { Spinner } from "@open-mercato/ui/primitives/spinner";
|
|
8
|
+
import { flash } from "@open-mercato/ui/backend/FlashMessages";
|
|
9
|
+
import { apiCallOrThrow, readApiResultOrThrow } from "@open-mercato/ui/backend/utils/apiCall";
|
|
10
|
+
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
11
|
+
import { ErrorMessage } from "@open-mercato/ui/backend/detail";
|
|
12
|
+
import { useGuardedMutation } from "@open-mercato/ui/backend/injection/useGuardedMutation";
|
|
13
|
+
import { buildOptimisticLockHeader, extractOptimisticLockConflict } from "@open-mercato/ui/backend/utils/optimisticLock";
|
|
14
|
+
import { withScopedApiRequestHeaders } from "@open-mercato/ui/backend/utils/apiCall";
|
|
15
|
+
function EntitySettingsManager() {
|
|
16
|
+
const t = useT();
|
|
17
|
+
const [loading, setLoading] = React.useState(true);
|
|
18
|
+
const [loadError, setLoadError] = React.useState(false);
|
|
19
|
+
const [saving, setSaving] = React.useState(false);
|
|
20
|
+
const [restricted, setRestricted] = React.useState(false);
|
|
21
|
+
const [updatedAt, setUpdatedAt] = React.useState(null);
|
|
22
|
+
const { runMutation } = useGuardedMutation({
|
|
23
|
+
contextId: "custom-entities-settings"
|
|
24
|
+
});
|
|
25
|
+
const loadSettings = React.useCallback(async () => {
|
|
26
|
+
setLoading(true);
|
|
27
|
+
setLoadError(false);
|
|
28
|
+
try {
|
|
29
|
+
const data = await readApiResultOrThrow(
|
|
30
|
+
"/api/entities/entity-settings",
|
|
31
|
+
void 0,
|
|
32
|
+
{
|
|
33
|
+
errorMessage: t("entities.settings.errors.loadFailed", "Failed to load settings")
|
|
34
|
+
}
|
|
35
|
+
);
|
|
36
|
+
setRestricted(data.newEntitiesRestrictedByDefault === true);
|
|
37
|
+
setUpdatedAt(data.updatedAt ?? null);
|
|
38
|
+
} catch {
|
|
39
|
+
setLoadError(true);
|
|
40
|
+
} finally {
|
|
41
|
+
setLoading(false);
|
|
42
|
+
}
|
|
43
|
+
}, [t]);
|
|
44
|
+
React.useEffect(() => {
|
|
45
|
+
void loadSettings();
|
|
46
|
+
}, [loadSettings]);
|
|
47
|
+
const handleSubmit = async (e) => {
|
|
48
|
+
e.preventDefault();
|
|
49
|
+
setSaving(true);
|
|
50
|
+
try {
|
|
51
|
+
const headers = buildOptimisticLockHeader(updatedAt);
|
|
52
|
+
await runMutation({
|
|
53
|
+
operation: () => withScopedApiRequestHeaders(headers, async () => {
|
|
54
|
+
const res = await apiCallOrThrow(
|
|
55
|
+
"/api/entities/entity-settings",
|
|
56
|
+
{
|
|
57
|
+
method: "PUT",
|
|
58
|
+
headers: { "content-type": "application/json" },
|
|
59
|
+
body: JSON.stringify({
|
|
60
|
+
newEntitiesRestrictedByDefault: restricted,
|
|
61
|
+
expectedUpdatedAt: updatedAt ?? void 0
|
|
62
|
+
})
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
errorMessage: t("entities.settings.errors.saveFailed", "Failed to save settings")
|
|
66
|
+
}
|
|
67
|
+
);
|
|
68
|
+
if (res.ok && res.result) {
|
|
69
|
+
setUpdatedAt(res.result.updatedAt);
|
|
70
|
+
flash(t("entities.settings.flash.saved", "Settings saved"), "success");
|
|
71
|
+
}
|
|
72
|
+
return res;
|
|
73
|
+
}),
|
|
74
|
+
context: {},
|
|
75
|
+
mutationPayload: {
|
|
76
|
+
newEntitiesRestrictedByDefault: restricted
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
} catch (err) {
|
|
80
|
+
const conflict = extractOptimisticLockConflict(err);
|
|
81
|
+
if (conflict) {
|
|
82
|
+
setLoading(true);
|
|
83
|
+
await loadSettings();
|
|
84
|
+
} else {
|
|
85
|
+
flash(t("entities.settings.errors.saveFailed", "Failed to save settings"), "error");
|
|
86
|
+
}
|
|
87
|
+
} finally {
|
|
88
|
+
setSaving(false);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
if (loading) {
|
|
92
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-sm text-muted-foreground", children: [
|
|
93
|
+
/* @__PURE__ */ jsx(Spinner, { className: "h-4 w-4" }),
|
|
94
|
+
/* @__PURE__ */ jsx("span", { children: t("entities.userEntities.form.loading", "Loading\u2026") })
|
|
95
|
+
] });
|
|
96
|
+
}
|
|
97
|
+
if (loadError) {
|
|
98
|
+
return /* @__PURE__ */ jsx(
|
|
99
|
+
ErrorMessage,
|
|
100
|
+
{
|
|
101
|
+
label: t("entities.settings.errors.loadFailed", "Failed to load settings"),
|
|
102
|
+
action: /* @__PURE__ */ jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: () => void loadSettings(), children: t("common.retry", "Retry") })
|
|
103
|
+
}
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
return /* @__PURE__ */ jsxs("form", { onSubmit: (e) => void handleSubmit(e), className: "space-y-6 max-w-2xl", children: [
|
|
107
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
|
|
108
|
+
/* @__PURE__ */ jsx("h3", { className: "text-base font-semibold", children: t("entities.settings.title", "Custom Entities") }),
|
|
109
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t("entities.settings.description", "Manage global custom entity configurations.") })
|
|
110
|
+
] }),
|
|
111
|
+
/* @__PURE__ */ jsx("div", { className: "rounded-lg border bg-card p-4 space-y-4", children: /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3", children: [
|
|
112
|
+
/* @__PURE__ */ jsx(
|
|
113
|
+
Checkbox,
|
|
114
|
+
{
|
|
115
|
+
id: "newEntitiesRestrictedByDefault",
|
|
116
|
+
checked: restricted,
|
|
117
|
+
onCheckedChange: (checked) => setRestricted(checked === true),
|
|
118
|
+
disabled: saving,
|
|
119
|
+
className: "mt-1"
|
|
120
|
+
}
|
|
121
|
+
),
|
|
122
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
|
|
123
|
+
/* @__PURE__ */ jsx(Label, { htmlFor: "newEntitiesRestrictedByDefault", className: "font-medium cursor-pointer", children: t("entities.settings.newEntitiesRestrictedByDefault.label", "Restrict record access by default") }),
|
|
124
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t("entities.settings.newEntitiesRestrictedByDefault.description", "Require explicit per-entity permissions for records of newly created custom entities by default.") })
|
|
125
|
+
] })
|
|
126
|
+
] }) }),
|
|
127
|
+
/* @__PURE__ */ jsx("div", { className: "flex items-center justify-end gap-3 border-t pt-4", children: /* @__PURE__ */ jsxs(Button, { type: "submit", disabled: saving, children: [
|
|
128
|
+
saving ? /* @__PURE__ */ jsx(Spinner, { className: "mr-2 h-4 w-4" }) : null,
|
|
129
|
+
t("common.save", "Save")
|
|
130
|
+
] }) })
|
|
131
|
+
] });
|
|
132
|
+
}
|
|
133
|
+
export {
|
|
134
|
+
EntitySettingsManager
|
|
135
|
+
};
|
|
136
|
+
//# sourceMappingURL=EntitySettingsManager.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/entities/components/EntitySettingsManager.tsx"],
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Checkbox } from '@open-mercato/ui/primitives/checkbox'\nimport { Label } from '@open-mercato/ui/primitives/label'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { apiCall, apiCallOrThrow, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { ErrorMessage } from '@open-mercato/ui/backend/detail'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { buildOptimisticLockHeader, extractOptimisticLockConflict } from '@open-mercato/ui/backend/utils/optimisticLock'\nimport { withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'\n\nexport function EntitySettingsManager(): React.ReactElement {\n const t = useT()\n const [loading, setLoading] = React.useState(true)\n const [loadError, setLoadError] = React.useState(false)\n const [saving, setSaving] = React.useState(false)\n const [restricted, setRestricted] = React.useState(false)\n const [updatedAt, setUpdatedAt] = React.useState<string | null>(null)\n\n const { runMutation } = useGuardedMutation({\n contextId: 'custom-entities-settings',\n })\n\n const loadSettings = React.useCallback(async () => {\n setLoading(true)\n setLoadError(false)\n try {\n const data = await readApiResultOrThrow<{ newEntitiesRestrictedByDefault?: boolean; updatedAt?: string | null }>(\n '/api/entities/entity-settings',\n undefined,\n {\n errorMessage: t('entities.settings.errors.loadFailed', 'Failed to load settings'),\n }\n )\n setRestricted(data.newEntitiesRestrictedByDefault === true)\n setUpdatedAt(data.updatedAt ?? null)\n } catch {\n setLoadError(true)\n } finally {\n setLoading(false)\n }\n }, [t])\n\n React.useEffect(() => {\n void loadSettings()\n }, [loadSettings])\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault()\n setSaving(true)\n try {\n const headers = buildOptimisticLockHeader(updatedAt)\n await runMutation({\n operation: () =>\n withScopedApiRequestHeaders(headers, async () => {\n const res = await apiCallOrThrow<{ ok: boolean; newEntitiesRestrictedByDefault: boolean; updatedAt: string | null }>(\n '/api/entities/entity-settings',\n {\n method: 'PUT',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n newEntitiesRestrictedByDefault: restricted,\n expectedUpdatedAt: updatedAt ?? undefined,\n }),\n },\n {\n errorMessage: t('entities.settings.errors.saveFailed', 'Failed to save settings'),\n }\n )\n\n if (res.ok && res.result) {\n setUpdatedAt(res.result.updatedAt)\n flash(t('entities.settings.flash.saved', 'Settings saved'), 'success')\n }\n return res\n }),\n context: {},\n mutationPayload: {\n newEntitiesRestrictedByDefault: restricted,\n },\n })\n } catch (err) {\n const conflict = extractOptimisticLockConflict(err)\n if (conflict) {\n setLoading(true)\n await loadSettings()\n } else {\n flash(t('entities.settings.errors.saveFailed', 'Failed to save settings'), 'error')\n }\n } finally {\n setSaving(false)\n }\n }\n\n if (loading) {\n return (\n <div className=\"flex items-center gap-2 text-sm text-muted-foreground\">\n <Spinner className=\"h-4 w-4\" />\n <span>{t('entities.userEntities.form.loading', 'Loading\u2026')}</span>\n </div>\n )\n }\n\n if (loadError) {\n return (\n <ErrorMessage\n label={t('entities.settings.errors.loadFailed', 'Failed to load settings')}\n action={(\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={() => void loadSettings()}>\n {t('common.retry', 'Retry')}\n </Button>\n )}\n />\n )\n }\n\n return (\n <form onSubmit={(e) => void handleSubmit(e)} className=\"space-y-6 max-w-2xl\">\n <div className=\"space-y-1\">\n <h3 className=\"text-base font-semibold\">\n {t('entities.settings.title', 'Custom Entities')}\n </h3>\n <p className=\"text-sm text-muted-foreground\">\n {t('entities.settings.description', 'Manage global custom entity configurations.')}\n </p>\n </div>\n\n <div className=\"rounded-lg border bg-card p-4 space-y-4\">\n <div className=\"flex items-start gap-3\">\n <Checkbox\n id=\"newEntitiesRestrictedByDefault\"\n checked={restricted}\n onCheckedChange={(checked) => setRestricted(checked === true)}\n disabled={saving}\n className=\"mt-1\"\n />\n <div className=\"space-y-1\">\n <Label htmlFor=\"newEntitiesRestrictedByDefault\" className=\"font-medium cursor-pointer\">\n {t('entities.settings.newEntitiesRestrictedByDefault.label', 'Restrict record access by default')}\n </Label>\n <p className=\"text-sm text-muted-foreground\">\n {t('entities.settings.newEntitiesRestrictedByDefault.description', 'Require explicit per-entity permissions for records of newly created custom entities by default.')}\n </p>\n </div>\n </div>\n </div>\n\n <div className=\"flex items-center justify-end gap-3 border-t pt-4\">\n <Button type=\"submit\" disabled={saving}>\n {saving ? <Spinner className=\"mr-2 h-4 w-4\" /> : null}\n {t('common.save', 'Save')}\n </Button>\n </div>\n </form>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -10,11 +10,19 @@ import { parseInboundEmail } from "../../lib/emailParser.js";
|
|
|
10
10
|
import { checkRateLimit } from "../../lib/rateLimiter.js";
|
|
11
11
|
import { emitInboxOpsEvent } from "../../events.js";
|
|
12
12
|
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
13
|
+
import {
|
|
14
|
+
readBoundedRequestBody,
|
|
15
|
+
resolveWebhookBodyLimitBytes,
|
|
16
|
+
WebhookBodyTooLargeError
|
|
17
|
+
} from "@open-mercato/shared/lib/webhooks";
|
|
13
18
|
const logger = createLogger("inbox_ops").child({ component: "webhook" });
|
|
14
19
|
const metadata = {
|
|
15
20
|
POST: { requireAuth: false }
|
|
16
21
|
};
|
|
17
|
-
const MAX_PAYLOAD_SIZE =
|
|
22
|
+
const MAX_PAYLOAD_SIZE = resolveWebhookBodyLimitBytes(
|
|
23
|
+
process.env.INBOX_OPS_WEBHOOK_MAX_BODY_BYTES ?? process.env.OM_WEBHOOK_MAX_BODY_BYTES,
|
|
24
|
+
2 * 1024 * 1024
|
|
25
|
+
);
|
|
18
26
|
const REPLAY_WINDOW_MS = 5 * 60 * 1e3;
|
|
19
27
|
function isTimestampFresh(timestamp) {
|
|
20
28
|
if (!timestamp) return false;
|
|
@@ -140,19 +148,15 @@ async function POST(req) {
|
|
|
140
148
|
logger.error("Neither INBOX_OPS_WEBHOOK_SECRET nor RESEND_WEBHOOK_SIGNING_SECRET is configured");
|
|
141
149
|
return NextResponse.json({ error: "Service unavailable" }, { status: 503 });
|
|
142
150
|
}
|
|
143
|
-
const contentLength = parseInt(req.headers.get("content-length") || "0", 10);
|
|
144
|
-
if (contentLength > MAX_PAYLOAD_SIZE) {
|
|
145
|
-
return NextResponse.json({ error: "Payload too large" }, { status: 413 });
|
|
146
|
-
}
|
|
147
151
|
let rawBody;
|
|
148
152
|
try {
|
|
149
|
-
rawBody = await req
|
|
150
|
-
} catch {
|
|
153
|
+
rawBody = await readBoundedRequestBody(req, { maxBytes: MAX_PAYLOAD_SIZE });
|
|
154
|
+
} catch (error) {
|
|
155
|
+
if (error instanceof WebhookBodyTooLargeError) {
|
|
156
|
+
return NextResponse.json({ error: "Payload too large" }, { status: 413 });
|
|
157
|
+
}
|
|
151
158
|
return NextResponse.json({ error: "Bad request" }, { status: 400 });
|
|
152
159
|
}
|
|
153
|
-
if (rawBody.length > MAX_PAYLOAD_SIZE) {
|
|
154
|
-
return NextResponse.json({ error: "Payload too large" }, { status: 413 });
|
|
155
|
-
}
|
|
156
160
|
const verified = await verifyAndParse(req, rawBody);
|
|
157
161
|
if (!verified.ok) {
|
|
158
162
|
return verified.response;
|
|
@@ -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": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,YAAY,uBAAuB;AAC5C,SAAS,SAAS;AAClB,SAAS,eAAe;AACxB,SAAS,cAAc;AACvB,SAAS,8BAA8B;AAIvC,SAAS,6BAA6B;AACtC,SAAS,eAAe,kBAAkB;AAC1C,SAAS,yBAAyB;AAClC,SAAS,sBAAsB;AAC/B,SAAS,yBAAyB;AAClC,SAAS,oBAAoB;
|
|
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
|
}
|
|
@@ -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
|
-
|
|
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
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,8BAA8B;AACvC,SAAS,0BAA0B;AACnC,SAAS,oBAAoB;AAC7B,SAAS,gBAAgB,aAAa,iCAAiC;AAGvE,SAAS,yBAAyB;AAIlC,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AACvC,SAAS,uCAAuC;AAChD,SAAS,0BAA0B;AACnC,SAAS,oBAAoB;
|
|
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": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,8BAA8B;AACvC,SAAS,0BAA0B;AACnC,SAAS,oBAAoB;AAC7B,SAAS,gBAAgB,aAAa,iCAAiC;AAGvE,SAAS,yBAAyB;AAIlC,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AACvC,SAAS,uCAAuC;AAChD,SAAS,0BAA0B;AACnC,SAAS,oBAAoB;AAC7B,SAAS,wBAAwB,gCAAgC;AAEjE,MAAM,SAAS,aAAa,kBAAkB,EAAE,MAAM,EAAE,WAAW,UAAU,CAAC;AAEvE,MAAM,WAAW;AAAA,EACtB,MAAM;AAAA,EACN,MAAM,EAAE,aAAa,MAAM;AAC7B;AAEA,MAAM,8BAA8B;AAEpC,MAAM,uCAAuC;AAAA,EAC3C,QAAQ;AAAA,EACR,UAAU;AAAA,EACV,WAAW;AACb;AAEA,eAAsB,KAAK,KAAc,EAAE,OAAO,GAAqE;AACrH,QAAM,iBAAiB,MAAM;AAC7B,QAAM,cAAc,eAAe;AACnC,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,eAAe,kBAAkB,WAAW;AAClD,MAAI,CAAC,cAAc;AACjB,WAAO,aAAa,KAAK,EAAE,OAAO,oCAAoC,WAAW,GAAG,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACxG;AAEA,QAAM,oBAAoB,MAAM,8BAA8B,WAAW,KAAK,WAAW;AACzF,MAAI,kBAAmB,QAAO;AAE9B,MAAI;AACJ,MAAI;AACF,cAAU,aAAa,iBAAiB,SACpC,MAAM,IAAI,KAAK,IACf,MAAM,uBAAuB,KAAK,EAAE,UAAU,aAAa,aAAa,CAAC;AAAA,EAC/E,SAAS,OAAO;AACd,QAAI,iBAAiB,0BAA0B;AAC7C,aAAO,aAAa,KAAK,EAAE,OAAO,4BAA4B,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IAClF;AACA,UAAM;AAAA,EACR;AACA,QAAM,UAAkC,CAAC;AACzC,MAAI,QAAQ,QAAQ,CAAC,OAAO,QAAQ;AAClC,YAAQ,GAAG,IAAI;AAAA,EACjB,CAAC;AAED,QAAM,UAAU,UAAU,QAAQ,uBAAuB;AACzD,QAAM,KAAK,UAAU,QAAQ,IAAI;AACjC,QAAM,gCAAgC,UAAU,QAAQ,+BAA+B;AACvF,QAAM,QAAQ,uBAAuB,aAAa,SAAS,0BAA0B;AACrF,QAAM,UAAU,MAAM,aAAsC,OAAO;AACnE,QAAM,gBAAgB,aAAa,oBAAoB,OAAO,KAAK;AAEnE,MAAI;AAQF,UAAM,aAAa,gBACf,MAAM;AAAA,MACN;AAAA,MACA;AAAA,MACA;AAAA,QACE;AAAA,QACA,mBAAmB;AAAA,QACnB,WAAW;AAAA,MACb;AAAA,MACA,EAAE,OAAO,IAAI,SAAS,EAAE,WAAW,OAAO,EAAE;AAAA,IAC9C,IACE,CAAC;AAEL,QAAI,cAAyC;AAC7C,QAAI,eAAoE;AACxE,QAAI,QAAiE;AACrE,QAAI,wBAAiC;AAErC,eAAW,aAAa,YAAY;AAClC,YAAM,iBAAiB,EAAE,gBAAgB,UAAU,gBAAgB,UAAU,UAAU,SAAS;AAChG,YAAM,cAAc,MAAM,8BAA8B,QAAQ,WAAW,WAAW,IAAI,cAAc,KAAK,CAAC;AAC9G,UAAI;AACF,gBAAQ,MAAM,aAAa,QAAQ,EAAE,SAAS,SAAS,YAAY,CAAC;AACpE,sBAAc;AACd,uBAAe;AACf;AAAA,MACF,SAAS,OAAgB;AACvB,gCAAwB;AAAA,MAC1B;AAAA,IACF;AAEA,QAAI,CAAC,SAAS,CAAC,eAAe,CAAC,cAAc;AAC3C,YAAM,yBAAyB,IAAI,MAAM,sDAAsD;AAAA,IACjG;AAEA,UAAM,QAAQ;AAEd,UAAM,aAAa;AAAA,MACjB;AAAA,MACA;AAAA,MACA,eAAe,YAAY;AAAA,MAC3B;AAAA,IACF;AAEA,QAAI,QAAQ,IAAI,mBAAmB,SAAS;AAC1C,YAAM,MAAM,QAAQ;AAAA,QAClB,MAAM;AAAA,QACN,SAAS;AAAA,MACX,CAAC;AAAA,IACH,OAAO;AACL,YAAM;AAAA,QACJ;AAAA,UACE,IAAI,UAAU,QAAQ,IAAI;AAAA,UAC1B,uBAAuB;AAAA,UACvB,uBAAuB,UAAU,QAAQ,uBAAuB;AAAA,QAClE;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAEA,WAAO,aAAa,KAAK,EAAE,UAAU,MAAM,QAAQ,KAAK,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC5E,SAAS,KAAc;AACrB,WAAO,KAAK,+BAA+B,EAAE,aAAa,IAAI,CAAC;AAC/D,WAAO,aAAa,KAAK,EAAE,OAAO,4BAA4B,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAClF;AACF;AAEA,eAAe,8BACb,WACA,KACA,aAC8B;AAC9B,QAAM,qBAAqB,WAA+B,WAAW,oBAAoB;AACzF,MAAI,CAAC,mBAAoB,QAAO;AAEhC,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,GAAG,WAAW,IAAI,YAAY,KAAK,mBAAmB,eAAe,KAAK,SAAS;AAAA,IACnF;AAAA,EACF;AACF;AAEA,SAAS,WAAc,WAAmD,MAAwB;AAChG,MAAI;AACF,WAAO,UAAU,QAAQ,IAAI;AAAA,EAC/B,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEO,MAAM,UAAU;AAAA,EACrB,MAAM,CAAC,kBAAkB;AAAA,EACzB,SAAS;AAAA,EACT,SAAS;AAAA,IACP,MAAM;AAAA,MACJ,SAAS;AAAA,MACT,MAAM,CAAC,kBAAkB;AAAA,MACzB,WAAW;AAAA,QACT,EAAE,QAAQ,KAAK,aAAa,wCAAwC;AAAA,QACpE,EAAE,QAAQ,KAAK,aAAa,gCAAgC;AAAA,QAC5D,EAAE,QAAQ,KAAK,aAAa,4BAA4B;AAAA,QACxD,EAAE,QAAQ,KAAK,aAAa,mBAAmB;AAAA,MACjD;AAAA,IACF;AAAA,EACF;AACF;AAEA,IAAO,gBAAQ;",
|
|
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,5 +1,6 @@
|
|
|
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";
|
|
@@ -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)) {
|
|
@@ -330,7 +332,7 @@ function createPaymentGatewayService(deps) {
|
|
|
330
332
|
scope
|
|
331
333
|
);
|
|
332
334
|
if (!transaction2) {
|
|
333
|
-
throw new Error("Completed payment session is missing its gateway transaction");
|
|
335
|
+
throw new Error("[internal] Completed payment session is missing its gateway transaction");
|
|
334
336
|
}
|
|
335
337
|
return { transaction: transaction2, session: restoreSession(transaction2) };
|
|
336
338
|
}
|
|
@@ -346,7 +348,15 @@ function createPaymentGatewayService(deps) {
|
|
|
346
348
|
try {
|
|
347
349
|
session = await adapter.createSession({ ...sessionInput, idempotencyKey: operationKey });
|
|
348
350
|
} catch (error) {
|
|
349
|
-
|
|
351
|
+
try {
|
|
352
|
+
await releasePaymentSessionInitialization(em, ownership, scope);
|
|
353
|
+
} catch (releaseError) {
|
|
354
|
+
logger.warn("Failed to release payment session initialization claim", {
|
|
355
|
+
claimId: ownership.id,
|
|
356
|
+
providerKey: input.providerKey,
|
|
357
|
+
err: releaseError
|
|
358
|
+
});
|
|
359
|
+
}
|
|
350
360
|
throw error;
|
|
351
361
|
} finally {
|
|
352
362
|
await stopHeartbeat();
|