@open-mercato/core 0.6.1-develop.3081.21270ec58a → 0.6.1-develop.3102.d6e7e6d57a
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/AGENTS.md +21 -1
- package/dist/modules/audit_logs/services/accessLogService.js +13 -0
- package/dist/modules/audit_logs/services/accessLogService.js.map +3 -3
- package/dist/modules/audit_logs/services/actionLogService.js +6 -5
- package/dist/modules/audit_logs/services/actionLogService.js.map +2 -2
- package/dist/modules/configs/cli.js +11 -0
- package/dist/modules/configs/cli.js.map +2 -2
- package/dist/modules/configs/lib/touchGeneratedBarrels.js +46 -0
- package/dist/modules/configs/lib/touchGeneratedBarrels.js.map +7 -0
- package/dist/modules/customers/api/activities/route.js +1 -52
- package/dist/modules/customers/api/activities/route.js.map +2 -2
- package/dist/modules/customers/api/interactions/counts/route.js +2 -1
- package/dist/modules/customers/api/interactions/counts/route.js.map +2 -2
- package/dist/modules/customers/api/interactions/route.js +21 -1
- package/dist/modules/customers/api/interactions/route.js.map +2 -2
- package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js +7 -3
- package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/[id]/page.js +5 -1
- package/dist/modules/customers/backend/customers/deals/[id]/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/people-v2/[id]/page.js +7 -3
- package/dist/modules/customers/backend/customers/people-v2/[id]/page.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivitiesCard.js +62 -6
- package/dist/modules/customers/components/detail/ActivitiesCard.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivitiesDayStrip.js +21 -6
- package/dist/modules/customers/components/detail/ActivitiesDayStrip.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivitiesSection.js +37 -5
- package/dist/modules/customers/components/detail/ActivitiesSection.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivityCard.js +69 -17
- package/dist/modules/customers/components/detail/ActivityCard.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivityHistorySection.js +94 -34
- package/dist/modules/customers/components/detail/ActivityHistorySection.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivityLogTab.js +3 -1
- package/dist/modules/customers/components/detail/ActivityLogTab.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivityTimeline.js +41 -8
- package/dist/modules/customers/components/detail/ActivityTimeline.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivityTimelineFilters.js +19 -6
- package/dist/modules/customers/components/detail/ActivityTimelineFilters.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivityTypeSelector.js +4 -3
- package/dist/modules/customers/components/detail/ActivityTypeSelector.js.map +2 -2
- package/dist/modules/customers/components/detail/ScheduleActivityDialog.js +80 -12
- package/dist/modules/customers/components/detail/ScheduleActivityDialog.js.map +2 -2
- package/dist/modules/customers/components/detail/schedule/DateTimeFields.js +65 -10
- package/dist/modules/customers/components/detail/schedule/DateTimeFields.js.map +2 -2
- package/dist/modules/customers/components/detail/schedule/useScheduleFormState.js +10 -5
- package/dist/modules/customers/components/detail/schedule/useScheduleFormState.js.map +2 -2
- package/dist/modules/customers/data/validators.js +74 -2
- package/dist/modules/customers/data/validators.js.map +2 -2
- package/dist/modules/customers/lib/legacyActivityBridge.js +61 -0
- package/dist/modules/customers/lib/legacyActivityBridge.js.map +7 -0
- package/dist/modules/integrations/lib/credentials-service.js +12 -1
- package/dist/modules/integrations/lib/credentials-service.js.map +2 -2
- package/dist/modules/messages/commands/actions.js +29 -14
- package/dist/modules/messages/commands/actions.js.map +2 -2
- package/dist/modules/messages/lib/actions.js +24 -4
- package/dist/modules/messages/lib/actions.js.map +2 -2
- package/dist/modules/sales/api/documents/factory.js +49 -36
- package/dist/modules/sales/api/documents/factory.js.map +2 -2
- package/package.json +8 -8
- package/src/modules/audit_logs/services/accessLogService.ts +20 -0
- package/src/modules/audit_logs/services/actionLogService.ts +13 -5
- package/src/modules/configs/cli.ts +11 -0
- package/src/modules/configs/lib/touchGeneratedBarrels.ts +61 -0
- package/src/modules/customers/api/activities/route.ts +1 -76
- package/src/modules/customers/api/interactions/counts/route.ts +2 -1
- package/src/modules/customers/api/interactions/route.ts +28 -1
- package/src/modules/customers/backend/customers/companies-v2/[id]/page.tsx +13 -3
- package/src/modules/customers/backend/customers/deals/[id]/page.tsx +14 -2
- package/src/modules/customers/backend/customers/people-v2/[id]/page.tsx +13 -3
- package/src/modules/customers/components/detail/ActivitiesCard.tsx +92 -5
- package/src/modules/customers/components/detail/ActivitiesDayStrip.tsx +38 -6
- package/src/modules/customers/components/detail/ActivitiesSection.tsx +37 -3
- package/src/modules/customers/components/detail/ActivityCard.tsx +79 -14
- package/src/modules/customers/components/detail/ActivityHistorySection.tsx +102 -33
- package/src/modules/customers/components/detail/ActivityLogTab.tsx +7 -1
- package/src/modules/customers/components/detail/ActivityTimeline.tsx +39 -5
- package/src/modules/customers/components/detail/ActivityTimelineFilters.tsx +29 -7
- package/src/modules/customers/components/detail/ActivityTypeSelector.tsx +3 -2
- package/src/modules/customers/components/detail/ScheduleActivityDialog.tsx +96 -13
- package/src/modules/customers/components/detail/schedule/DateTimeFields.tsx +50 -4
- package/src/modules/customers/components/detail/schedule/useScheduleFormState.ts +21 -5
- package/src/modules/customers/data/validators.ts +85 -2
- package/src/modules/customers/i18n/de.json +11 -0
- package/src/modules/customers/i18n/en.json +11 -0
- package/src/modules/customers/i18n/es.json +11 -0
- package/src/modules/customers/i18n/pl.json +11 -0
- package/src/modules/customers/lib/legacyActivityBridge.ts +106 -0
- package/src/modules/integrations/lib/credentials-service.ts +15 -1
- package/src/modules/messages/commands/actions.ts +28 -13
- package/src/modules/messages/lib/actions.ts +34 -3
- package/src/modules/sales/api/documents/factory.ts +55 -38
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { loadCustomFieldValues } from "@open-mercato/shared/lib/crud/custom-fields";
|
|
2
|
+
import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
3
|
+
import { CustomerActivity, CustomerInteraction } from "../data/entities.js";
|
|
4
|
+
import { CUSTOMER_INTERACTION_ACTIVITY_ADAPTER_SOURCE } from "./interactionCompatibility.js";
|
|
5
|
+
async function loadLegacyActivityCustomValues(em, activity) {
|
|
6
|
+
const values = await loadCustomFieldValues({
|
|
7
|
+
em,
|
|
8
|
+
entityId: "customers:customer_activity",
|
|
9
|
+
recordIds: [activity.id],
|
|
10
|
+
tenantIdByRecord: { [activity.id]: activity.tenantId },
|
|
11
|
+
organizationIdByRecord: { [activity.id]: activity.organizationId },
|
|
12
|
+
tenantFallbacks: [activity.tenantId]
|
|
13
|
+
});
|
|
14
|
+
return values[activity.id] ?? null;
|
|
15
|
+
}
|
|
16
|
+
async function ensureCanonicalActivityBridge(em, commandBus, commandContext, activity) {
|
|
17
|
+
const existing = await em.findOne(CustomerInteraction, { id: activity.id, tenantId: activity.tenantId });
|
|
18
|
+
if (existing) return existing.id;
|
|
19
|
+
const entityId = typeof activity.entity === "string" ? activity.entity : activity.entity.id;
|
|
20
|
+
const dealId = activity.deal ? typeof activity.deal === "string" ? activity.deal : activity.deal.id : null;
|
|
21
|
+
const customValues = await loadLegacyActivityCustomValues(em, activity);
|
|
22
|
+
await commandBus.execute("customers.interactions.create", {
|
|
23
|
+
input: {
|
|
24
|
+
id: activity.id,
|
|
25
|
+
tenantId: activity.tenantId,
|
|
26
|
+
organizationId: activity.organizationId,
|
|
27
|
+
entityId,
|
|
28
|
+
interactionType: activity.activityType,
|
|
29
|
+
title: activity.subject ?? null,
|
|
30
|
+
body: activity.body ?? null,
|
|
31
|
+
occurredAt: activity.occurredAt ?? null,
|
|
32
|
+
status: activity.occurredAt ? "done" : "planned",
|
|
33
|
+
dealId,
|
|
34
|
+
authorUserId: activity.authorUserId ?? null,
|
|
35
|
+
appearanceIcon: activity.appearanceIcon ?? null,
|
|
36
|
+
appearanceColor: activity.appearanceColor ?? null,
|
|
37
|
+
source: CUSTOMER_INTERACTION_ACTIVITY_ADAPTER_SOURCE,
|
|
38
|
+
...customValues ? { customValues } : {}
|
|
39
|
+
},
|
|
40
|
+
ctx: commandContext
|
|
41
|
+
});
|
|
42
|
+
return activity.id;
|
|
43
|
+
}
|
|
44
|
+
async function resolveCanonicalActivityTargetId(em, commandBus, commandContext, targetId, tenantId) {
|
|
45
|
+
const existing = await em.findOne(CustomerInteraction, { id: targetId, tenantId });
|
|
46
|
+
if (existing) return existing.id;
|
|
47
|
+
const legacy = await findOneWithDecryption(
|
|
48
|
+
em,
|
|
49
|
+
CustomerActivity,
|
|
50
|
+
{ id: targetId, tenantId },
|
|
51
|
+
{ populate: ["entity", "deal"] },
|
|
52
|
+
{ tenantId }
|
|
53
|
+
);
|
|
54
|
+
if (!legacy) return targetId;
|
|
55
|
+
return ensureCanonicalActivityBridge(em, commandBus, commandContext, legacy);
|
|
56
|
+
}
|
|
57
|
+
export {
|
|
58
|
+
ensureCanonicalActivityBridge,
|
|
59
|
+
resolveCanonicalActivityTargetId
|
|
60
|
+
};
|
|
61
|
+
//# sourceMappingURL=legacyActivityBridge.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/customers/lib/legacyActivityBridge.ts"],
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CommandBus } from '@open-mercato/shared/lib/commands'\nimport { loadCustomFieldValues } from '@open-mercato/shared/lib/crud/custom-fields'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { CustomerActivity, CustomerInteraction } from '../data/entities'\nimport { CUSTOMER_INTERACTION_ACTIVITY_ADAPTER_SOURCE } from './interactionCompatibility'\n\ntype CommandContext = Parameters<CommandBus['execute']>[1]['ctx']\n\nasync function loadLegacyActivityCustomValues(\n em: EntityManager,\n activity: CustomerActivity,\n): Promise<Record<string, unknown> | null> {\n const values = await loadCustomFieldValues({\n em,\n entityId: 'customers:customer_activity',\n recordIds: [activity.id],\n tenantIdByRecord: { [activity.id]: activity.tenantId },\n organizationIdByRecord: { [activity.id]: activity.organizationId },\n tenantFallbacks: [activity.tenantId],\n })\n return values[activity.id] ?? null\n}\n\n/**\n * If the canonical `customer_interactions` row for `activity` does not yet\n * exist, create it via the `customers.interactions.create` command using the\n * legacy activity's primary key. Returns the canonical id (always equal to\n * `activity.id` in this scheme).\n *\n * Mirrors the bridge in /api/customers/activities so the dialog editing flow\n * can edit historical activities that still live only in `customer_activities`\n * (root cause of #1807 PUT 404 \"Interaction not found\").\n */\nexport async function ensureCanonicalActivityBridge(\n em: EntityManager,\n commandBus: CommandBus,\n commandContext: CommandContext,\n activity: CustomerActivity,\n): Promise<string> {\n const existing = await em.findOne(CustomerInteraction, { id: activity.id, tenantId: activity.tenantId })\n if (existing) return existing.id\n\n const entityId = typeof activity.entity === 'string' ? activity.entity : activity.entity.id\n const dealId = activity.deal\n ? (typeof activity.deal === 'string' ? activity.deal : activity.deal.id)\n : null\n const customValues = await loadLegacyActivityCustomValues(em, activity)\n\n await commandBus.execute('customers.interactions.create', {\n input: {\n id: activity.id,\n tenantId: activity.tenantId,\n organizationId: activity.organizationId,\n entityId,\n interactionType: activity.activityType,\n title: activity.subject ?? null,\n body: activity.body ?? null,\n occurredAt: activity.occurredAt ?? null,\n status: activity.occurredAt ? 'done' : 'planned',\n dealId,\n authorUserId: activity.authorUserId ?? null,\n appearanceIcon: activity.appearanceIcon ?? null,\n appearanceColor: activity.appearanceColor ?? null,\n source: CUSTOMER_INTERACTION_ACTIVITY_ADAPTER_SOURCE,\n ...(customValues ? { customValues } : {}),\n },\n ctx: commandContext,\n })\n\n return activity.id\n}\n\n/**\n * Returns the canonical `customer_interactions.id` for the given target id. If\n * the canonical record already exists, returns it directly. Otherwise looks\n * the id up in the legacy `customer_activities` table and bridges the row into\n * `customer_interactions` via {@link ensureCanonicalActivityBridge}. If\n * neither exists, returns the original id unchanged so downstream lookups can\n * surface a normal 404.\n */\nexport async function resolveCanonicalActivityTargetId(\n em: EntityManager,\n commandBus: CommandBus,\n commandContext: CommandContext,\n targetId: string,\n tenantId: string,\n): Promise<string> {\n const existing = await em.findOne(CustomerInteraction, { id: targetId, tenantId })\n if (existing) return existing.id\n\n // Reads encrypted scalar fields (subject, body, appearanceIcon, appearanceColor)\n // that are forwarded into `customers.interactions.create`. Use the decryption\n // helper so the bridged interaction inherits plaintext values rather than\n // ciphertext when tenant data encryption is enabled.\n const legacy = await findOneWithDecryption(\n em,\n CustomerActivity,\n { id: targetId, tenantId } as any,\n { populate: ['entity', 'deal'] } as any,\n { tenantId },\n )\n if (!legacy) return targetId\n\n return ensureCanonicalActivityBridge(em, commandBus, commandContext, legacy)\n}\n"],
|
|
5
|
+
"mappings": "AAEA,SAAS,6BAA6B;AACtC,SAAS,6BAA6B;AACtC,SAAS,kBAAkB,2BAA2B;AACtD,SAAS,oDAAoD;AAI7D,eAAe,+BACb,IACA,UACyC;AACzC,QAAM,SAAS,MAAM,sBAAsB;AAAA,IACzC;AAAA,IACA,UAAU;AAAA,IACV,WAAW,CAAC,SAAS,EAAE;AAAA,IACvB,kBAAkB,EAAE,CAAC,SAAS,EAAE,GAAG,SAAS,SAAS;AAAA,IACrD,wBAAwB,EAAE,CAAC,SAAS,EAAE,GAAG,SAAS,eAAe;AAAA,IACjE,iBAAiB,CAAC,SAAS,QAAQ;AAAA,EACrC,CAAC;AACD,SAAO,OAAO,SAAS,EAAE,KAAK;AAChC;AAYA,eAAsB,8BACpB,IACA,YACA,gBACA,UACiB;AACjB,QAAM,WAAW,MAAM,GAAG,QAAQ,qBAAqB,EAAE,IAAI,SAAS,IAAI,UAAU,SAAS,SAAS,CAAC;AACvG,MAAI,SAAU,QAAO,SAAS;AAE9B,QAAM,WAAW,OAAO,SAAS,WAAW,WAAW,SAAS,SAAS,SAAS,OAAO;AACzF,QAAM,SAAS,SAAS,OACnB,OAAO,SAAS,SAAS,WAAW,SAAS,OAAO,SAAS,KAAK,KACnE;AACJ,QAAM,eAAe,MAAM,+BAA+B,IAAI,QAAQ;AAEtE,QAAM,WAAW,QAAQ,iCAAiC;AAAA,IACxD,OAAO;AAAA,MACL,IAAI,SAAS;AAAA,MACb,UAAU,SAAS;AAAA,MACnB,gBAAgB,SAAS;AAAA,MACzB;AAAA,MACA,iBAAiB,SAAS;AAAA,MAC1B,OAAO,SAAS,WAAW;AAAA,MAC3B,MAAM,SAAS,QAAQ;AAAA,MACvB,YAAY,SAAS,cAAc;AAAA,MACnC,QAAQ,SAAS,aAAa,SAAS;AAAA,MACvC;AAAA,MACA,cAAc,SAAS,gBAAgB;AAAA,MACvC,gBAAgB,SAAS,kBAAkB;AAAA,MAC3C,iBAAiB,SAAS,mBAAmB;AAAA,MAC7C,QAAQ;AAAA,MACR,GAAI,eAAe,EAAE,aAAa,IAAI,CAAC;AAAA,IACzC;AAAA,IACA,KAAK;AAAA,EACP,CAAC;AAED,SAAO,SAAS;AAClB;AAUA,eAAsB,iCACpB,IACA,YACA,gBACA,UACA,UACiB;AACjB,QAAM,WAAW,MAAM,GAAG,QAAQ,qBAAqB,EAAE,IAAI,UAAU,SAAS,CAAC;AACjF,MAAI,SAAU,QAAO,SAAS;AAM9B,QAAM,SAAS,MAAM;AAAA,IACnB;AAAA,IACA;AAAA,IACA,EAAE,IAAI,UAAU,SAAS;AAAA,IACzB,EAAE,UAAU,CAAC,UAAU,MAAM,EAAE;AAAA,IAC/B,EAAE,SAAS;AAAA,EACb;AACA,MAAI,CAAC,OAAQ,QAAO;AAEpB,SAAO,8BAA8B,IAAI,YAAY,gBAAgB,MAAM;AAC7E;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -2,6 +2,7 @@ import crypto from "node:crypto";
|
|
|
2
2
|
import { decryptWithAesGcm, encryptWithAesGcm } from "@open-mercato/shared/lib/encryption/aes";
|
|
3
3
|
import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
4
4
|
import { createKmsService } from "@open-mercato/shared/lib/encryption/kms";
|
|
5
|
+
import { parseDecryptedFieldValue } from "@open-mercato/shared/lib/encryption/tenantDataEncryptionService";
|
|
5
6
|
import {
|
|
6
7
|
getBundle,
|
|
7
8
|
getIntegration,
|
|
@@ -11,6 +12,15 @@ import { EncryptionMap } from "../../entities/data/entities.js";
|
|
|
11
12
|
import { IntegrationCredentials } from "../data/entities.js";
|
|
12
13
|
const ENCRYPTED_CREDENTIALS_BLOB_KEY = "__om_encrypted_credentials_blob_v1";
|
|
13
14
|
const DERIVED_KEY_CONTEXT = "integrations.credentials";
|
|
15
|
+
function isRecordValue(value) {
|
|
16
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
17
|
+
}
|
|
18
|
+
function normalizeCredentialsRecord(value) {
|
|
19
|
+
if (isRecordValue(value)) return value;
|
|
20
|
+
if (typeof value !== "string") return {};
|
|
21
|
+
const parsed = parseDecryptedFieldValue(value);
|
|
22
|
+
return isRecordValue(parsed) ? parsed : {};
|
|
23
|
+
}
|
|
14
24
|
function resolveFallbackEncryptionSecret() {
|
|
15
25
|
const candidates = [
|
|
16
26
|
process.env.TENANT_DATA_ENCRYPTION_FALLBACK_KEY,
|
|
@@ -75,7 +85,8 @@ function createCredentialsService(em) {
|
|
|
75
85
|
const payload = encryptWithAesGcm(JSON.stringify(credentials), dek);
|
|
76
86
|
return { [ENCRYPTED_CREDENTIALS_BLOB_KEY]: payload.value };
|
|
77
87
|
}
|
|
78
|
-
async function decryptCredentialsBlob(
|
|
88
|
+
async function decryptCredentialsBlob(credentialsInput, scope) {
|
|
89
|
+
const credentials = normalizeCredentialsRecord(credentialsInput);
|
|
79
90
|
const encrypted = credentials[ENCRYPTED_CREDENTIALS_BLOB_KEY];
|
|
80
91
|
if (typeof encrypted !== "string" || !encrypted) return credentials;
|
|
81
92
|
const dek = await resolveCredentialsDek(scope);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/integrations/lib/credentials-service.ts"],
|
|
4
|
-
"sourcesContent": ["import crypto from 'node:crypto'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { decryptWithAesGcm, encryptWithAesGcm } from '@open-mercato/shared/lib/encryption/aes'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { createKmsService } from '@open-mercato/shared/lib/encryption/kms'\nimport {\n getBundle,\n getIntegration,\n resolveIntegrationCredentialsSchema,\n type IntegrationScope,\n} from '@open-mercato/shared/modules/integrations/types'\nimport { EncryptionMap } from '../../entities/data/entities'\nimport { IntegrationCredentials } from '../data/entities'\n\nconst ENCRYPTED_CREDENTIALS_BLOB_KEY = '__om_encrypted_credentials_blob_v1'\nconst DERIVED_KEY_CONTEXT = 'integrations.credentials'\n\nfunction resolveFallbackEncryptionSecret(): string {\n const candidates = [\n process.env.TENANT_DATA_ENCRYPTION_FALLBACK_KEY,\n process.env.TENANT_DATA_ENCRYPTION_KEY,\n process.env.AUTH_SECRET,\n process.env.NEXTAUTH_SECRET,\n ]\n\n for (const value of candidates) {\n const normalized = value?.trim()\n if (normalized) return normalized\n }\n\n if (process.env.NODE_ENV !== 'production') return 'om-dev-tenant-encryption'\n\n console.warn(\n '[integrations.credentials] No encryption secret configured; using emergency fallback secret. Configure TENANT_DATA_ENCRYPTION_FALLBACK_KEY immediately.',\n )\n return 'om-emergency-fallback-rotate-me'\n}\n\nfunction deriveDekFromSecret(secret: string, tenantId: string): string {\n return crypto\n .createHash('sha256')\n .update(`${DERIVED_KEY_CONTEXT}:${tenantId}:${secret}`)\n .digest()\n .toString('base64')\n}\n\nexport function createCredentialsService(em: EntityManager) {\n const credentialsEncryptionSpec = [{ field: 'credentials' }]\n\n async function ensureCredentialsEncryptionMap(scope: IntegrationScope): Promise<void> {\n const existing = await findOneWithDecryption(\n em,\n EncryptionMap,\n {\n entityId: 'integrations:integration_credentials',\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n deletedAt: null,\n },\n undefined,\n scope,\n )\n\n if (!existing) {\n const created = em.create(EncryptionMap, {\n entityId: 'integrations:integration_credentials',\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n fieldsJson: credentialsEncryptionSpec,\n isActive: true,\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n em.persist(created)\n return\n }\n\n existing.fieldsJson = credentialsEncryptionSpec\n existing.isActive = true\n }\n\n async function resolveCredentialsDek(scope: IntegrationScope): Promise<string> {\n const kms = createKmsService()\n const existing = await kms.getTenantDek(scope.tenantId)\n if (existing?.key) return existing.key\n\n const created = await kms.createTenantDek(scope.tenantId)\n if (created?.key) return created.key\n\n return deriveDekFromSecret(resolveFallbackEncryptionSecret(), scope.tenantId)\n }\n\n async function encryptCredentialsBlob(\n credentials: Record<string, unknown>,\n scope: IntegrationScope,\n ): Promise<Record<string, unknown>> {\n const dek = await resolveCredentialsDek(scope)\n const payload = encryptWithAesGcm(JSON.stringify(credentials), dek)\n return { [ENCRYPTED_CREDENTIALS_BLOB_KEY]: payload.value }\n }\n\n async function decryptCredentialsBlob(\n
|
|
5
|
-
"mappings": "AAAA,OAAO,YAAY;AAEnB,SAAS,mBAAmB,yBAAyB;AACrD,SAAS,6BAA6B;AACtC,SAAS,wBAAwB;AACjC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AACP,SAAS,qBAAqB;AAC9B,SAAS,8BAA8B;AAEvC,MAAM,iCAAiC;AACvC,MAAM,sBAAsB;AAE5B,SAAS,kCAA0C;AACjD,QAAM,aAAa;AAAA,IACjB,QAAQ,IAAI;AAAA,IACZ,QAAQ,IAAI;AAAA,IACZ,QAAQ,IAAI;AAAA,IACZ,QAAQ,IAAI;AAAA,EACd;AAEA,aAAW,SAAS,YAAY;AAC9B,UAAM,aAAa,OAAO,KAAK;AAC/B,QAAI,WAAY,QAAO;AAAA,EACzB;AAEA,MAAI,QAAQ,IAAI,aAAa,aAAc,QAAO;AAElD,UAAQ;AAAA,IACN;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,oBAAoB,QAAgB,UAA0B;AACrE,SAAO,OACJ,WAAW,QAAQ,EACnB,OAAO,GAAG,mBAAmB,IAAI,QAAQ,IAAI,MAAM,EAAE,EACrD,OAAO,EACP,SAAS,QAAQ;AACtB;AAEO,SAAS,yBAAyB,IAAmB;AAC1D,QAAM,4BAA4B,CAAC,EAAE,OAAO,cAAc,CAAC;AAE3D,iBAAe,+BAA+B,OAAwC;AACpF,UAAM,WAAW,MAAM;AAAA,MACrB;AAAA,MACA;AAAA,MACA;AAAA,QACE,UAAU;AAAA,QACV,UAAU,MAAM;AAAA,QAChB,gBAAgB,MAAM;AAAA,QACtB,WAAW;AAAA,MACb;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAEA,QAAI,CAAC,UAAU;AACb,YAAM,UAAU,GAAG,OAAO,eAAe;AAAA,QACvC,UAAU;AAAA,QACV,UAAU,MAAM;AAAA,QAChB,gBAAgB,MAAM;AAAA,QACtB,YAAY;AAAA,QACZ,UAAU;AAAA,QACV,WAAW,oBAAI,KAAK;AAAA,QACpB,WAAW,oBAAI,KAAK;AAAA,MACtB,CAAC;AACD,SAAG,QAAQ,OAAO;AAClB;AAAA,IACF;AAEA,aAAS,aAAa;AACtB,aAAS,WAAW;AAAA,EACtB;AAEA,iBAAe,sBAAsB,OAA0C;AAC7E,UAAM,MAAM,iBAAiB;AAC7B,UAAM,WAAW,MAAM,IAAI,aAAa,MAAM,QAAQ;AACtD,QAAI,UAAU,IAAK,QAAO,SAAS;AAEnC,UAAM,UAAU,MAAM,IAAI,gBAAgB,MAAM,QAAQ;AACxD,QAAI,SAAS,IAAK,QAAO,QAAQ;AAEjC,WAAO,oBAAoB,gCAAgC,GAAG,MAAM,QAAQ;AAAA,EAC9E;AAEA,iBAAe,uBACb,aACA,OACkC;AAClC,UAAM,MAAM,MAAM,sBAAsB,KAAK;AAC7C,UAAM,UAAU,kBAAkB,KAAK,UAAU,WAAW,GAAG,GAAG;AAClE,WAAO,EAAE,CAAC,8BAA8B,GAAG,QAAQ,MAAM;AAAA,EAC3D;AAEA,iBAAe,uBACb,
|
|
4
|
+
"sourcesContent": ["import crypto from 'node:crypto'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { decryptWithAesGcm, encryptWithAesGcm } from '@open-mercato/shared/lib/encryption/aes'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { createKmsService } from '@open-mercato/shared/lib/encryption/kms'\nimport { parseDecryptedFieldValue } from '@open-mercato/shared/lib/encryption/tenantDataEncryptionService'\nimport {\n getBundle,\n getIntegration,\n resolveIntegrationCredentialsSchema,\n type IntegrationScope,\n} from '@open-mercato/shared/modules/integrations/types'\nimport { EncryptionMap } from '../../entities/data/entities'\nimport { IntegrationCredentials } from '../data/entities'\n\nconst ENCRYPTED_CREDENTIALS_BLOB_KEY = '__om_encrypted_credentials_blob_v1'\nconst DERIVED_KEY_CONTEXT = 'integrations.credentials'\n\nfunction isRecordValue(value: unknown): value is Record<string, unknown> {\n return !!value && typeof value === 'object' && !Array.isArray(value)\n}\n\nfunction normalizeCredentialsRecord(value: unknown): Record<string, unknown> {\n if (isRecordValue(value)) return value\n if (typeof value !== 'string') return {}\n\n const parsed = parseDecryptedFieldValue(value)\n return isRecordValue(parsed) ? parsed : {}\n}\n\nfunction resolveFallbackEncryptionSecret(): string {\n const candidates = [\n process.env.TENANT_DATA_ENCRYPTION_FALLBACK_KEY,\n process.env.TENANT_DATA_ENCRYPTION_KEY,\n process.env.AUTH_SECRET,\n process.env.NEXTAUTH_SECRET,\n ]\n\n for (const value of candidates) {\n const normalized = value?.trim()\n if (normalized) return normalized\n }\n\n if (process.env.NODE_ENV !== 'production') return 'om-dev-tenant-encryption'\n\n console.warn(\n '[integrations.credentials] No encryption secret configured; using emergency fallback secret. Configure TENANT_DATA_ENCRYPTION_FALLBACK_KEY immediately.',\n )\n return 'om-emergency-fallback-rotate-me'\n}\n\nfunction deriveDekFromSecret(secret: string, tenantId: string): string {\n return crypto\n .createHash('sha256')\n .update(`${DERIVED_KEY_CONTEXT}:${tenantId}:${secret}`)\n .digest()\n .toString('base64')\n}\n\nexport function createCredentialsService(em: EntityManager) {\n const credentialsEncryptionSpec = [{ field: 'credentials' }]\n\n async function ensureCredentialsEncryptionMap(scope: IntegrationScope): Promise<void> {\n const existing = await findOneWithDecryption(\n em,\n EncryptionMap,\n {\n entityId: 'integrations:integration_credentials',\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n deletedAt: null,\n },\n undefined,\n scope,\n )\n\n if (!existing) {\n const created = em.create(EncryptionMap, {\n entityId: 'integrations:integration_credentials',\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n fieldsJson: credentialsEncryptionSpec,\n isActive: true,\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n em.persist(created)\n return\n }\n\n existing.fieldsJson = credentialsEncryptionSpec\n existing.isActive = true\n }\n\n async function resolveCredentialsDek(scope: IntegrationScope): Promise<string> {\n const kms = createKmsService()\n const existing = await kms.getTenantDek(scope.tenantId)\n if (existing?.key) return existing.key\n\n const created = await kms.createTenantDek(scope.tenantId)\n if (created?.key) return created.key\n\n return deriveDekFromSecret(resolveFallbackEncryptionSecret(), scope.tenantId)\n }\n\n async function encryptCredentialsBlob(\n credentials: Record<string, unknown>,\n scope: IntegrationScope,\n ): Promise<Record<string, unknown>> {\n const dek = await resolveCredentialsDek(scope)\n const payload = encryptWithAesGcm(JSON.stringify(credentials), dek)\n return { [ENCRYPTED_CREDENTIALS_BLOB_KEY]: payload.value }\n }\n\n async function decryptCredentialsBlob(\n credentialsInput: unknown,\n scope: IntegrationScope,\n ): Promise<Record<string, unknown>> {\n const credentials = normalizeCredentialsRecord(credentialsInput)\n const encrypted = credentials[ENCRYPTED_CREDENTIALS_BLOB_KEY]\n if (typeof encrypted !== 'string' || !encrypted) return credentials\n\n const dek = await resolveCredentialsDek(scope)\n const decryptedRaw = decryptWithAesGcm(encrypted, dek)\n if (!decryptedRaw) return {}\n\n try {\n const parsed = JSON.parse(decryptedRaw) as unknown\n return parsed && typeof parsed === 'object' && !Array.isArray(parsed)\n ? (parsed as Record<string, unknown>)\n : {}\n } catch {\n return {}\n }\n }\n\n return {\n async getRaw(integrationId: string, scope: IntegrationScope): Promise<Record<string, unknown> | null> {\n const row = await findOneWithDecryption(\n em,\n IntegrationCredentials,\n {\n integrationId,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n },\n undefined,\n scope,\n )\n if (!row) return null\n return decryptCredentialsBlob(row.credentials, scope)\n },\n\n async resolve(integrationId: string, scope: IntegrationScope): Promise<Record<string, unknown> | null> {\n const direct = await this.getRaw(integrationId, scope)\n if (direct) return direct\n\n const definition = getIntegration(integrationId)\n if (!definition?.bundleId) return null\n return this.getRaw(definition.bundleId, scope)\n },\n\n async save(integrationId: string, credentials: Record<string, unknown>, scope: IntegrationScope): Promise<void> {\n await ensureCredentialsEncryptionMap(scope)\n const encryptedCredentials = await encryptCredentialsBlob(credentials, scope)\n\n const row = await findOneWithDecryption(\n em,\n IntegrationCredentials,\n {\n integrationId,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n },\n undefined,\n scope,\n )\n\n if (row) {\n row.credentials = encryptedCredentials\n await em.flush()\n return\n }\n\n const created = em.create(IntegrationCredentials, {\n integrationId,\n credentials: encryptedCredentials,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n })\n await em.persist(created).flush()\n },\n\n async saveField(\n integrationId: string,\n fieldKey: string,\n value: unknown,\n scope: IntegrationScope,\n ): Promise<Record<string, unknown>> {\n const current = (await this.getRaw(integrationId, scope)) ?? {}\n const updated = { ...current, [fieldKey]: value }\n await this.save(integrationId, updated, scope)\n return updated\n },\n\n getSchema(integrationId: string) {\n const definition = getIntegration(integrationId)\n if (!definition) return undefined\n\n if (definition.bundleId) {\n const bundle = getBundle(definition.bundleId)\n return bundle?.credentials ?? resolveIntegrationCredentialsSchema(integrationId)\n }\n\n return definition.credentials ?? resolveIntegrationCredentialsSchema(integrationId)\n },\n }\n}\n\nexport type CredentialsService = ReturnType<typeof createCredentialsService>\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { registerCommand } from "@open-mercato/shared/lib/commands";
|
|
3
3
|
import { extractUndoPayload } from "@open-mercato/shared/lib/commands/undo";
|
|
4
|
+
import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
4
5
|
import { Message, MessageObject, MessageRecipient } from "../data/entities.js";
|
|
5
6
|
import { emitMessagesEvent } from "../events.js";
|
|
6
7
|
import {
|
|
7
8
|
findResolvedMessageActionById,
|
|
8
9
|
isTerminalMessageAction,
|
|
9
10
|
resolveActionCommandInput,
|
|
10
|
-
resolveActionHref
|
|
11
|
+
resolveActionHref,
|
|
12
|
+
resolveMessageActionData
|
|
11
13
|
} from "../lib/actions.js";
|
|
12
14
|
import { getMessageType } from "../lib/message-types-registry.js";
|
|
13
15
|
import { assertOrganizationAccess } from "./shared.js";
|
|
@@ -35,11 +37,17 @@ function toDate(value) {
|
|
|
35
37
|
return new Date(value);
|
|
36
38
|
}
|
|
37
39
|
async function requireActionTarget(em, input) {
|
|
38
|
-
const message = await
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
const message = await findOneWithDecryption(
|
|
41
|
+
em,
|
|
42
|
+
Message,
|
|
43
|
+
{
|
|
44
|
+
id: input.messageId,
|
|
45
|
+
tenantId: input.tenantId,
|
|
46
|
+
deletedAt: null
|
|
47
|
+
},
|
|
48
|
+
void 0,
|
|
49
|
+
{ tenantId: input.tenantId, organizationId: input.organizationId }
|
|
50
|
+
);
|
|
43
51
|
if (!message) throw new Error("Message not found");
|
|
44
52
|
assertOrganizationAccess(input, message);
|
|
45
53
|
const recipient = await em.findOne(MessageRecipient, {
|
|
@@ -51,11 +59,17 @@ async function requireActionTarget(em, input) {
|
|
|
51
59
|
return message;
|
|
52
60
|
}
|
|
53
61
|
async function requireActionMessage(em, input) {
|
|
54
|
-
const message = await
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
62
|
+
const message = await findOneWithDecryption(
|
|
63
|
+
em,
|
|
64
|
+
Message,
|
|
65
|
+
{
|
|
66
|
+
id: input.messageId,
|
|
67
|
+
tenantId: input.tenantId,
|
|
68
|
+
deletedAt: null
|
|
69
|
+
},
|
|
70
|
+
void 0,
|
|
71
|
+
{ tenantId: input.tenantId, organizationId: input.organizationId }
|
|
72
|
+
);
|
|
59
73
|
if (!message) throw new Error("Message not found");
|
|
60
74
|
assertOrganizationAccess(input, message);
|
|
61
75
|
const recipient = await em.findOne(MessageRecipient, {
|
|
@@ -127,7 +141,7 @@ const recordTerminalActionCommand = {
|
|
|
127
141
|
const messageId = logEntry?.resourceId;
|
|
128
142
|
if (!messageId) return;
|
|
129
143
|
const em = ctx.container.resolve("em").fork();
|
|
130
|
-
const message = await em
|
|
144
|
+
const message = await findOneWithDecryption(em, Message, { id: messageId });
|
|
131
145
|
if (!message) return;
|
|
132
146
|
message.actionTaken = before.actionTaken;
|
|
133
147
|
message.actionTakenByUserId = before.actionTakenByUserId;
|
|
@@ -151,8 +165,9 @@ const executeActionCommand = {
|
|
|
151
165
|
throw Object.assign(new Error("Action already taken"), { actionTaken: message.actionTaken });
|
|
152
166
|
}
|
|
153
167
|
const shouldRecordActionTaken = isTerminalMessageAction(action);
|
|
154
|
-
|
|
155
|
-
|
|
168
|
+
const actionData = resolveMessageActionData(message);
|
|
169
|
+
if (actionData?.expiresAt) {
|
|
170
|
+
if (new Date(actionData.expiresAt) < /* @__PURE__ */ new Date()) {
|
|
156
171
|
throw new Error("Actions have expired");
|
|
157
172
|
}
|
|
158
173
|
} else {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/messages/commands/actions.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { z } from 'zod'\nimport { registerCommand, type CommandHandler } from '@open-mercato/shared/lib/commands'\nimport { extractUndoPayload, type UndoPayload } from '@open-mercato/shared/lib/commands/undo'\nimport { Message, MessageObject, MessageRecipient } from '../data/entities'\nimport { emitMessagesEvent } from '../events'\nimport {\n findResolvedMessageActionById,\n isTerminalMessageAction,\n resolveActionCommandInput,\n resolveActionHref,\n} from '../lib/actions'\nimport { getMessageType } from '../lib/message-types-registry'\nimport { assertOrganizationAccess, type MessageScopeInput } from './shared'\n\nconst recordTerminalActionSchema = z.object({\n messageId: z.string().uuid(),\n actionId: z.string().min(1),\n result: z.record(z.string(), z.unknown()),\n tenantId: z.string().uuid(),\n organizationId: z.string().uuid().nullable(),\n userId: z.string().uuid(),\n})\n\ntype RecordTerminalActionInput = z.infer<typeof recordTerminalActionSchema>\n\nconst executeActionSchema = z.object({\n messageId: z.string().uuid(),\n actionId: z.string().min(1),\n payload: z.record(z.string(), z.unknown()).optional(),\n tenantId: z.string().uuid(),\n organizationId: z.string().uuid().nullable(),\n userId: z.string().uuid(),\n})\n\ntype ExecuteActionInput = z.infer<typeof executeActionSchema>\n\ntype ActionStateSnapshot = {\n actionTaken: string | null\n actionTakenByUserId: string | null\n actionTakenAt: string | null\n actionResult: Record<string, unknown> | null\n}\n\nfunction toIso(value: Date | null | undefined): string | null {\n return value ? value.toISOString() : null\n}\n\nfunction toDate(value: string | null | undefined): Date | null {\n if (!value) return null\n return new Date(value)\n}\n\nasync function requireActionTarget(em: EntityManager, input: RecordTerminalActionInput) {\n const message = await em.findOne(Message, {\n id: input.messageId,\n tenantId: input.tenantId,\n deletedAt: null,\n })\n if (!message) throw new Error('Message not found')\n assertOrganizationAccess(input as MessageScopeInput, message)\n\n const recipient = await em.findOne(MessageRecipient, {\n messageId: input.messageId,\n recipientUserId: input.userId,\n deletedAt: null,\n })\n if (!recipient) throw new Error('Access denied')\n return message\n}\n\nasync function requireActionMessage(em: EntityManager, input: ExecuteActionInput) {\n const message = await em.findOne(Message, {\n id: input.messageId,\n tenantId: input.tenantId,\n deletedAt: null,\n })\n if (!message) throw new Error('Message not found')\n assertOrganizationAccess(input as MessageScopeInput, message)\n const recipient = await em.findOne(MessageRecipient, {\n messageId: input.messageId,\n recipientUserId: input.userId,\n deletedAt: null,\n })\n if (!recipient) throw new Error('Access denied')\n return message\n}\n\nfunction snapshotActionState(message: Message): ActionStateSnapshot {\n return {\n actionTaken: message.actionTaken ?? null,\n actionTakenByUserId: message.actionTakenByUserId ?? null,\n actionTakenAt: toIso(message.actionTakenAt),\n actionResult: message.actionResult ?? null,\n }\n}\n\nconst recordTerminalActionCommand: CommandHandler<unknown, { ok: true }> = {\n id: 'messages.actions.record_terminal',\n async prepare(rawInput, ctx) {\n const input = recordTerminalActionSchema.parse(rawInput)\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const message = await requireActionTarget(em, input)\n return { before: snapshotActionState(message) }\n },\n async execute(rawInput, ctx) {\n const input = recordTerminalActionSchema.parse(rawInput)\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const message = await requireActionTarget(em, input)\n if (message.actionTaken) {\n throw new Error('Action already taken')\n }\n message.actionTaken = input.actionId\n message.actionTakenByUserId = input.userId\n message.actionTakenAt = new Date()\n message.actionResult = input.result\n await em.flush()\n return { ok: true }\n },\n async captureAfter(rawInput, _result, ctx) {\n const input = recordTerminalActionSchema.parse(rawInput)\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const message = await requireActionTarget(em, input)\n return snapshotActionState(message)\n },\n buildLog: async ({ input, snapshots }) => {\n const parsed = recordTerminalActionSchema.parse(input)\n return {\n actionLabel: 'Execute message terminal action',\n resourceKind: 'messages.message',\n resourceId: parsed.messageId,\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n payload: {\n undo: {\n before: (snapshots.before as ActionStateSnapshot | undefined) ?? null,\n after: (snapshots.after as ActionStateSnapshot | undefined) ?? null,\n } satisfies UndoPayload<ActionStateSnapshot>,\n },\n snapshotBefore: snapshots.before ?? null,\n snapshotAfter: snapshots.after ?? null,\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const undo = extractUndoPayload<UndoPayload<ActionStateSnapshot>>(logEntry)\n const before = undo?.before\n if (!before) return\n const messageId = logEntry?.resourceId as string | null\n if (!messageId) return\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const message = await em.findOne(Message, { id: messageId })\n if (!message) return\n message.actionTaken = before.actionTaken\n message.actionTakenByUserId = before.actionTakenByUserId\n message.actionTakenAt = toDate(before.actionTakenAt)\n message.actionResult = before.actionResult\n await em.flush()\n },\n}\n\nconst executeActionCommand: CommandHandler<\n unknown,\n { ok: true; actionId: string; result: Record<string, unknown>; operationLogEntry: unknown | null }\n> = {\n id: 'messages.actions.execute',\n async execute(rawInput, ctx) {\n const input = executeActionSchema.parse(rawInput)\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const message = await requireActionMessage(em, input)\n const objects = await em.find(MessageObject, { messageId: message.id })\n const action = findResolvedMessageActionById(message, objects, input.actionId)\n\n if (!action) {\n throw new Error('Action not found')\n }\n\n if (message.actionTaken) {\n throw Object.assign(new Error('Action already taken'), { actionTaken: message.actionTaken })\n }\n\n const shouldRecordActionTaken = isTerminalMessageAction(action)\n\n if (message.actionData?.expiresAt) {\n if (new Date(message.actionData.expiresAt) < new Date()) {\n throw new Error('Actions have expired')\n }\n } else {\n const messageType = getMessageType(message.type)\n if (messageType?.actionsExpireAfterHours && message.sentAt) {\n const expiry = new Date(message.sentAt.getTime() + messageType.actionsExpireAfterHours * 60 * 60 * 1000)\n if (expiry < new Date()) {\n throw new Error('Actions have expired')\n }\n }\n }\n\n const commandBus = ctx.container.resolve('commandBus') as {\n execute: (\n commandId: string,\n options: { input: unknown; ctx: unknown; metadata?: unknown }\n ) => Promise<{ result: unknown; logEntry?: unknown }>\n }\n\n let result: Record<string, unknown> = {}\n let operationLogEntry: unknown | null = null\n\n if (action.commandId) {\n try {\n const actionInput = resolveActionCommandInput(\n action,\n message,\n {\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n userId: input.userId,\n },\n input.payload ?? {},\n )\n\n if (actionInput.id == null) {\n const fallbackId = action.objectRef?.entityId ?? message.sourceEntityId ?? null\n if (typeof fallbackId === 'string' && fallbackId.trim().length > 0) {\n actionInput.id = fallbackId\n }\n }\n\n const commandResult = await commandBus.execute(action.commandId, {\n input: actionInput,\n ctx: {\n container: ctx.container,\n auth: {\n sub: input.userId,\n tenantId: input.tenantId,\n orgId: input.organizationId,\n },\n organizationScope: null,\n selectedOrganizationId: input.organizationId,\n organizationIds: input.organizationId ? [input.organizationId] : null,\n },\n metadata: {\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n resourceKind: 'messages',\n },\n })\n\n result = (commandResult.result as Record<string, unknown>) ?? {}\n operationLogEntry = commandResult.logEntry ?? null\n } catch (err) {\n console.error('[messages] executeActionCommand sub-command failed', err)\n throw new Error('Action failed')\n }\n } else if (action.href) {\n result = {\n redirect: resolveActionHref(action, message, {\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n userId: input.userId,\n }) ?? action.href,\n }\n } else {\n throw new Error('Action has no executable target')\n }\n\n if (shouldRecordActionTaken) {\n const terminalResult = await commandBus.execute('messages.actions.record_terminal', {\n input: {\n messageId: message.id,\n actionId: action.id,\n result,\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n userId: input.userId,\n },\n ctx: {\n container: ctx.container,\n auth: ctx.auth ?? null,\n organizationScope: null,\n selectedOrganizationId: input.organizationId,\n organizationIds: input.organizationId ? [input.organizationId] : null,\n },\n })\n if (!operationLogEntry) {\n operationLogEntry = terminalResult.logEntry ?? null\n }\n await emitMessagesEvent(\n 'messages.message.action_taken',\n {\n messageId: message.id,\n actionId: action.id,\n userId: input.userId,\n recipientUserId: input.userId,\n result,\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n },\n { persistent: true }\n )\n }\n\n return {\n ok: true,\n actionId: action.id,\n result,\n operationLogEntry,\n }\n },\n}\n\nregisterCommand(recordTerminalActionCommand)\nregisterCommand(executeActionCommand)\n"],
|
|
5
|
-
"mappings": "AACA,SAAS,SAAS;AAClB,SAAS,uBAA4C;AACrD,SAAS,0BAA4C;AACrD,SAAS,SAAS,eAAe,wBAAwB;AACzD,SAAS,yBAAyB;AAClC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,sBAAsB;AAC/B,SAAS,gCAAwD;AAEjE,MAAM,6BAA6B,EAAE,OAAO;AAAA,EAC1C,WAAW,EAAE,OAAO,EAAE,KAAK;AAAA,EAC3B,UAAU,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EAC1B,QAAQ,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,QAAQ,CAAC;AAAA,EACxC,UAAU,EAAE,OAAO,EAAE,KAAK;AAAA,EAC1B,gBAAgB,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS;AAAA,EAC3C,QAAQ,EAAE,OAAO,EAAE,KAAK;AAC1B,CAAC;AAID,MAAM,sBAAsB,EAAE,OAAO;AAAA,EACnC,WAAW,EAAE,OAAO,EAAE,KAAK;AAAA,EAC3B,UAAU,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EAC1B,SAAS,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,QAAQ,CAAC,EAAE,SAAS;AAAA,EACpD,UAAU,EAAE,OAAO,EAAE,KAAK;AAAA,EAC1B,gBAAgB,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS;AAAA,EAC3C,QAAQ,EAAE,OAAO,EAAE,KAAK;AAC1B,CAAC;AAWD,SAAS,MAAM,OAA+C;AAC5D,SAAO,QAAQ,MAAM,YAAY,IAAI;AACvC;AAEA,SAAS,OAAO,OAA+C;AAC7D,MAAI,CAAC,MAAO,QAAO;AACnB,SAAO,IAAI,KAAK,KAAK;AACvB;AAEA,eAAe,oBAAoB,IAAmB,OAAkC;AACtF,QAAM,UAAU,MAAM,
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { z } from 'zod'\nimport { registerCommand, type CommandHandler } from '@open-mercato/shared/lib/commands'\nimport { extractUndoPayload, type UndoPayload } from '@open-mercato/shared/lib/commands/undo'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { Message, MessageObject, MessageRecipient } from '../data/entities'\nimport { emitMessagesEvent } from '../events'\nimport {\n findResolvedMessageActionById,\n isTerminalMessageAction,\n resolveActionCommandInput,\n resolveActionHref,\n resolveMessageActionData,\n} from '../lib/actions'\nimport { getMessageType } from '../lib/message-types-registry'\nimport { assertOrganizationAccess, type MessageScopeInput } from './shared'\n\nconst recordTerminalActionSchema = z.object({\n messageId: z.string().uuid(),\n actionId: z.string().min(1),\n result: z.record(z.string(), z.unknown()),\n tenantId: z.string().uuid(),\n organizationId: z.string().uuid().nullable(),\n userId: z.string().uuid(),\n})\n\ntype RecordTerminalActionInput = z.infer<typeof recordTerminalActionSchema>\n\nconst executeActionSchema = z.object({\n messageId: z.string().uuid(),\n actionId: z.string().min(1),\n payload: z.record(z.string(), z.unknown()).optional(),\n tenantId: z.string().uuid(),\n organizationId: z.string().uuid().nullable(),\n userId: z.string().uuid(),\n})\n\ntype ExecuteActionInput = z.infer<typeof executeActionSchema>\n\ntype ActionStateSnapshot = {\n actionTaken: string | null\n actionTakenByUserId: string | null\n actionTakenAt: string | null\n actionResult: Record<string, unknown> | null\n}\n\nfunction toIso(value: Date | null | undefined): string | null {\n return value ? value.toISOString() : null\n}\n\nfunction toDate(value: string | null | undefined): Date | null {\n if (!value) return null\n return new Date(value)\n}\n\nasync function requireActionTarget(em: EntityManager, input: RecordTerminalActionInput) {\n const message = await findOneWithDecryption(\n em,\n Message,\n {\n id: input.messageId,\n tenantId: input.tenantId,\n deletedAt: null,\n },\n undefined,\n { tenantId: input.tenantId, organizationId: input.organizationId },\n )\n if (!message) throw new Error('Message not found')\n assertOrganizationAccess(input as MessageScopeInput, message)\n\n const recipient = await em.findOne(MessageRecipient, {\n messageId: input.messageId,\n recipientUserId: input.userId,\n deletedAt: null,\n })\n if (!recipient) throw new Error('Access denied')\n return message\n}\n\nasync function requireActionMessage(em: EntityManager, input: ExecuteActionInput) {\n const message = await findOneWithDecryption(\n em,\n Message,\n {\n id: input.messageId,\n tenantId: input.tenantId,\n deletedAt: null,\n },\n undefined,\n { tenantId: input.tenantId, organizationId: input.organizationId },\n )\n if (!message) throw new Error('Message not found')\n assertOrganizationAccess(input as MessageScopeInput, message)\n const recipient = await em.findOne(MessageRecipient, {\n messageId: input.messageId,\n recipientUserId: input.userId,\n deletedAt: null,\n })\n if (!recipient) throw new Error('Access denied')\n return message\n}\n\nfunction snapshotActionState(message: Message): ActionStateSnapshot {\n return {\n actionTaken: message.actionTaken ?? null,\n actionTakenByUserId: message.actionTakenByUserId ?? null,\n actionTakenAt: toIso(message.actionTakenAt),\n actionResult: message.actionResult ?? null,\n }\n}\n\nconst recordTerminalActionCommand: CommandHandler<unknown, { ok: true }> = {\n id: 'messages.actions.record_terminal',\n async prepare(rawInput, ctx) {\n const input = recordTerminalActionSchema.parse(rawInput)\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const message = await requireActionTarget(em, input)\n return { before: snapshotActionState(message) }\n },\n async execute(rawInput, ctx) {\n const input = recordTerminalActionSchema.parse(rawInput)\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const message = await requireActionTarget(em, input)\n if (message.actionTaken) {\n throw new Error('Action already taken')\n }\n message.actionTaken = input.actionId\n message.actionTakenByUserId = input.userId\n message.actionTakenAt = new Date()\n message.actionResult = input.result\n await em.flush()\n return { ok: true }\n },\n async captureAfter(rawInput, _result, ctx) {\n const input = recordTerminalActionSchema.parse(rawInput)\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const message = await requireActionTarget(em, input)\n return snapshotActionState(message)\n },\n buildLog: async ({ input, snapshots }) => {\n const parsed = recordTerminalActionSchema.parse(input)\n return {\n actionLabel: 'Execute message terminal action',\n resourceKind: 'messages.message',\n resourceId: parsed.messageId,\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n payload: {\n undo: {\n before: (snapshots.before as ActionStateSnapshot | undefined) ?? null,\n after: (snapshots.after as ActionStateSnapshot | undefined) ?? null,\n } satisfies UndoPayload<ActionStateSnapshot>,\n },\n snapshotBefore: snapshots.before ?? null,\n snapshotAfter: snapshots.after ?? null,\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const undo = extractUndoPayload<UndoPayload<ActionStateSnapshot>>(logEntry)\n const before = undo?.before\n if (!before) return\n const messageId = logEntry?.resourceId as string | null\n if (!messageId) return\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const message = await findOneWithDecryption(em, Message, { id: messageId })\n if (!message) return\n message.actionTaken = before.actionTaken\n message.actionTakenByUserId = before.actionTakenByUserId\n message.actionTakenAt = toDate(before.actionTakenAt)\n message.actionResult = before.actionResult\n await em.flush()\n },\n}\n\nconst executeActionCommand: CommandHandler<\n unknown,\n { ok: true; actionId: string; result: Record<string, unknown>; operationLogEntry: unknown | null }\n> = {\n id: 'messages.actions.execute',\n async execute(rawInput, ctx) {\n const input = executeActionSchema.parse(rawInput)\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const message = await requireActionMessage(em, input)\n const objects = await em.find(MessageObject, { messageId: message.id })\n const action = findResolvedMessageActionById(message, objects, input.actionId)\n\n if (!action) {\n throw new Error('Action not found')\n }\n\n if (message.actionTaken) {\n throw Object.assign(new Error('Action already taken'), { actionTaken: message.actionTaken })\n }\n\n const shouldRecordActionTaken = isTerminalMessageAction(action)\n\n const actionData = resolveMessageActionData(message)\n if (actionData?.expiresAt) {\n if (new Date(actionData.expiresAt) < new Date()) {\n throw new Error('Actions have expired')\n }\n } else {\n const messageType = getMessageType(message.type)\n if (messageType?.actionsExpireAfterHours && message.sentAt) {\n const expiry = new Date(message.sentAt.getTime() + messageType.actionsExpireAfterHours * 60 * 60 * 1000)\n if (expiry < new Date()) {\n throw new Error('Actions have expired')\n }\n }\n }\n\n const commandBus = ctx.container.resolve('commandBus') as {\n execute: (\n commandId: string,\n options: { input: unknown; ctx: unknown; metadata?: unknown }\n ) => Promise<{ result: unknown; logEntry?: unknown }>\n }\n\n let result: Record<string, unknown> = {}\n let operationLogEntry: unknown | null = null\n\n if (action.commandId) {\n try {\n const actionInput = resolveActionCommandInput(\n action,\n message,\n {\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n userId: input.userId,\n },\n input.payload ?? {},\n )\n\n if (actionInput.id == null) {\n const fallbackId = action.objectRef?.entityId ?? message.sourceEntityId ?? null\n if (typeof fallbackId === 'string' && fallbackId.trim().length > 0) {\n actionInput.id = fallbackId\n }\n }\n\n const commandResult = await commandBus.execute(action.commandId, {\n input: actionInput,\n ctx: {\n container: ctx.container,\n auth: {\n sub: input.userId,\n tenantId: input.tenantId,\n orgId: input.organizationId,\n },\n organizationScope: null,\n selectedOrganizationId: input.organizationId,\n organizationIds: input.organizationId ? [input.organizationId] : null,\n },\n metadata: {\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n resourceKind: 'messages',\n },\n })\n\n result = (commandResult.result as Record<string, unknown>) ?? {}\n operationLogEntry = commandResult.logEntry ?? null\n } catch (err) {\n console.error('[messages] executeActionCommand sub-command failed', err)\n throw new Error('Action failed')\n }\n } else if (action.href) {\n result = {\n redirect: resolveActionHref(action, message, {\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n userId: input.userId,\n }) ?? action.href,\n }\n } else {\n throw new Error('Action has no executable target')\n }\n\n if (shouldRecordActionTaken) {\n const terminalResult = await commandBus.execute('messages.actions.record_terminal', {\n input: {\n messageId: message.id,\n actionId: action.id,\n result,\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n userId: input.userId,\n },\n ctx: {\n container: ctx.container,\n auth: ctx.auth ?? null,\n organizationScope: null,\n selectedOrganizationId: input.organizationId,\n organizationIds: input.organizationId ? [input.organizationId] : null,\n },\n })\n if (!operationLogEntry) {\n operationLogEntry = terminalResult.logEntry ?? null\n }\n await emitMessagesEvent(\n 'messages.message.action_taken',\n {\n messageId: message.id,\n actionId: action.id,\n userId: input.userId,\n recipientUserId: input.userId,\n result,\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n },\n { persistent: true }\n )\n }\n\n return {\n ok: true,\n actionId: action.id,\n result,\n operationLogEntry,\n }\n },\n}\n\nregisterCommand(recordTerminalActionCommand)\nregisterCommand(executeActionCommand)\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,5 +1,22 @@
|
|
|
1
|
+
import { parseDecryptedFieldValue } from "@open-mercato/shared/lib/encryption/tenantDataEncryptionService";
|
|
1
2
|
import { getMessageObjectType } from "./message-objects-registry.js";
|
|
2
3
|
import { getMessageType } from "./message-types-registry.js";
|
|
4
|
+
function isRecordValue(value) {
|
|
5
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
6
|
+
}
|
|
7
|
+
function resolveMessageActionData(message) {
|
|
8
|
+
const rawActionData = message.actionData;
|
|
9
|
+
const parsedActionData = typeof rawActionData === "string" ? parseDecryptedFieldValue(rawActionData) : rawActionData;
|
|
10
|
+
if (!isRecordValue(parsedActionData)) return null;
|
|
11
|
+
const actions = Array.isArray(parsedActionData.actions) ? parsedActionData.actions.filter(isRecordValue) : [];
|
|
12
|
+
const primaryActionId = typeof parsedActionData.primaryActionId === "string" ? parsedActionData.primaryActionId : void 0;
|
|
13
|
+
const expiresAt = typeof parsedActionData.expiresAt === "string" ? parsedActionData.expiresAt : void 0;
|
|
14
|
+
return {
|
|
15
|
+
actions,
|
|
16
|
+
...primaryActionId ? { primaryActionId } : {},
|
|
17
|
+
...expiresAt ? { expiresAt } : {}
|
|
18
|
+
};
|
|
19
|
+
}
|
|
3
20
|
function normalizeActionLabel(action, fallback) {
|
|
4
21
|
if (typeof action.label === "string" && action.label.trim().length > 0) {
|
|
5
22
|
return action.label;
|
|
@@ -19,7 +36,8 @@ function buildMessageObjectActionId(objectId, actionId) {
|
|
|
19
36
|
return `object:${objectId}:${actionId}`;
|
|
20
37
|
}
|
|
21
38
|
function readActionDataExpiry(message) {
|
|
22
|
-
|
|
39
|
+
const actionData = resolveMessageActionData(message);
|
|
40
|
+
if (actionData?.expiresAt) return actionData.expiresAt;
|
|
23
41
|
const messageType = getMessageType(message.type);
|
|
24
42
|
if (!messageType?.actionsExpireAfterHours || !message.sentAt) return void 0;
|
|
25
43
|
return new Date(
|
|
@@ -29,6 +47,7 @@ function readActionDataExpiry(message) {
|
|
|
29
47
|
function buildResolvedMessageActions(message, objects) {
|
|
30
48
|
const resolved = [];
|
|
31
49
|
const usedIds = /* @__PURE__ */ new Set();
|
|
50
|
+
const actionData = resolveMessageActionData(message);
|
|
32
51
|
const pushAction = (action, source, objectRef, labelFallback) => {
|
|
33
52
|
const id = typeof action.id === "string" ? action.id.trim() : "";
|
|
34
53
|
if (!id || usedIds.has(id)) return;
|
|
@@ -41,7 +60,7 @@ function buildResolvedMessageActions(message, objects) {
|
|
|
41
60
|
objectRef
|
|
42
61
|
});
|
|
43
62
|
};
|
|
44
|
-
for (const action of
|
|
63
|
+
for (const action of actionData?.actions ?? []) {
|
|
45
64
|
pushAction(action, "message");
|
|
46
65
|
}
|
|
47
66
|
const messageType = getMessageType(message.type);
|
|
@@ -81,7 +100,7 @@ function buildResolvedMessageActions(message, objects) {
|
|
|
81
100
|
if (resolved.length === 0 && !expiresAt) {
|
|
82
101
|
return null;
|
|
83
102
|
}
|
|
84
|
-
const configuredPrimaryActionId =
|
|
103
|
+
const configuredPrimaryActionId = actionData?.primaryActionId;
|
|
85
104
|
const primaryActionId = configuredPrimaryActionId && resolved.some((entry) => entry.id === configuredPrimaryActionId) ? configuredPrimaryActionId : resolved[0]?.id;
|
|
86
105
|
return {
|
|
87
106
|
actions: resolved,
|
|
@@ -136,6 +155,7 @@ export {
|
|
|
136
155
|
findResolvedMessageActionById,
|
|
137
156
|
isTerminalMessageAction,
|
|
138
157
|
resolveActionCommandInput,
|
|
139
|
-
resolveActionHref
|
|
158
|
+
resolveActionHref,
|
|
159
|
+
resolveMessageActionData
|
|
140
160
|
};
|
|
141
161
|
//# sourceMappingURL=actions.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/messages/lib/actions.ts"],
|
|
4
|
-
"sourcesContent": ["import type { Message, MessageAction, MessageActionData, MessageObject } from '../data/entities'\nimport { getMessageObjectType } from './message-objects-registry'\nimport { getMessageType } from './message-types-registry'\n\ntype MessageActionSource = 'message' | 'type_default' | 'object'\n\nexport type MessageActionObjectRef = {\n objectId: string\n entityModule: string\n entityType: string\n entityId: string\n}\n\nexport type ResolvedMessageAction = MessageAction & {\n source: MessageActionSource\n objectRef?: MessageActionObjectRef\n}\n\nexport type MessageActionResolutionContext = {\n tenantId: string\n organizationId?: string | null\n userId: string\n}\n\nfunction normalizeActionLabel(\n action: Pick<MessageAction, 'label' | 'id'>,\n fallback?: string | null,\n): string {\n if (typeof action.label === 'string' && action.label.trim().length > 0) {\n return action.label\n }\n if (typeof fallback === 'string' && fallback.trim().length > 0) {\n return fallback.trim()\n }\n return action.id\n}\n\nexport function isTerminalMessageAction(\n action: Pick<MessageAction, 'isTerminal' | 'commandId' | 'href'>,\n): boolean {\n if (typeof action.isTerminal === 'boolean') return action.isTerminal\n if (typeof action.commandId === 'string' && action.commandId.trim().length > 0) return true\n if (typeof action.href === 'string' && action.href.trim().length > 0) return false\n return true\n}\n\nexport function buildMessageObjectActionId(objectId: string, actionId: string): string {\n return `object:${objectId}:${actionId}`\n}\n\nfunction readActionDataExpiry(message: Message): string | undefined {\n if (
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["import { parseDecryptedFieldValue } from '@open-mercato/shared/lib/encryption/tenantDataEncryptionService'\nimport type { Message, MessageAction, MessageActionData, MessageObject } from '../data/entities'\nimport { getMessageObjectType } from './message-objects-registry'\nimport { getMessageType } from './message-types-registry'\n\ntype MessageActionSource = 'message' | 'type_default' | 'object'\n\nexport type MessageActionObjectRef = {\n objectId: string\n entityModule: string\n entityType: string\n entityId: string\n}\n\nexport type ResolvedMessageAction = MessageAction & {\n source: MessageActionSource\n objectRef?: MessageActionObjectRef\n}\n\nexport type MessageActionResolutionContext = {\n tenantId: string\n organizationId?: string | null\n userId: string\n}\n\nfunction isRecordValue(value: unknown): value is Record<string, unknown> {\n return !!value && typeof value === 'object' && !Array.isArray(value)\n}\n\nexport function resolveMessageActionData(message: Pick<Message, 'actionData'>): MessageActionData | null {\n const rawActionData = message.actionData as unknown\n const parsedActionData = typeof rawActionData === 'string'\n ? parseDecryptedFieldValue(rawActionData)\n : rawActionData\n if (!isRecordValue(parsedActionData)) return null\n\n const actions = Array.isArray(parsedActionData.actions)\n ? parsedActionData.actions.filter(isRecordValue) as MessageAction[]\n : []\n const primaryActionId = typeof parsedActionData.primaryActionId === 'string'\n ? parsedActionData.primaryActionId\n : undefined\n const expiresAt = typeof parsedActionData.expiresAt === 'string'\n ? parsedActionData.expiresAt\n : undefined\n\n return {\n actions,\n ...(primaryActionId ? { primaryActionId } : {}),\n ...(expiresAt ? { expiresAt } : {}),\n }\n}\n\nfunction normalizeActionLabel(\n action: Pick<MessageAction, 'label' | 'id'>,\n fallback?: string | null,\n): string {\n if (typeof action.label === 'string' && action.label.trim().length > 0) {\n return action.label\n }\n if (typeof fallback === 'string' && fallback.trim().length > 0) {\n return fallback.trim()\n }\n return action.id\n}\n\nexport function isTerminalMessageAction(\n action: Pick<MessageAction, 'isTerminal' | 'commandId' | 'href'>,\n): boolean {\n if (typeof action.isTerminal === 'boolean') return action.isTerminal\n if (typeof action.commandId === 'string' && action.commandId.trim().length > 0) return true\n if (typeof action.href === 'string' && action.href.trim().length > 0) return false\n return true\n}\n\nexport function buildMessageObjectActionId(objectId: string, actionId: string): string {\n return `object:${objectId}:${actionId}`\n}\n\nfunction readActionDataExpiry(message: Message): string | undefined {\n const actionData = resolveMessageActionData(message)\n if (actionData?.expiresAt) return actionData.expiresAt\n const messageType = getMessageType(message.type)\n if (!messageType?.actionsExpireAfterHours || !message.sentAt) return undefined\n return new Date(\n message.sentAt.getTime() + messageType.actionsExpireAfterHours * 60 * 60 * 1000,\n ).toISOString()\n}\n\nexport function buildResolvedMessageActions(\n message: Message,\n objects: MessageObject[],\n): MessageActionData | null {\n const resolved: ResolvedMessageAction[] = []\n const usedIds = new Set<string>()\n const actionData = resolveMessageActionData(message)\n\n const pushAction = (\n action: MessageAction,\n source: MessageActionSource,\n objectRef?: MessageActionObjectRef,\n labelFallback?: string | null,\n ) => {\n const id = typeof action.id === 'string' ? action.id.trim() : ''\n if (!id || usedIds.has(id)) return\n usedIds.add(id)\n\n resolved.push({\n ...action,\n id,\n label: normalizeActionLabel(action, labelFallback),\n source,\n objectRef,\n })\n }\n\n for (const action of actionData?.actions ?? []) {\n pushAction(action, 'message')\n }\n\n const messageType = getMessageType(message.type)\n for (const action of messageType?.defaultActions ?? []) {\n pushAction(action, 'type_default')\n }\n\n for (const object of objects) {\n if (!object.actionRequired || !object.actionType) continue\n const objectType = getMessageObjectType(object.entityModule, object.entityType)\n if (!objectType) continue\n const actionDef = objectType.actions.find((entry) => entry.id === object.actionType)\n if (!actionDef) continue\n\n pushAction(\n {\n id: buildMessageObjectActionId(object.id, actionDef.id),\n label: object.actionLabel ?? actionDef.id,\n labelKey: actionDef.labelKey,\n variant: actionDef.variant,\n icon: actionDef.icon,\n commandId: actionDef.commandId,\n href: actionDef.href,\n isTerminal: actionDef.isTerminal,\n confirmRequired: actionDef.confirmRequired,\n confirmMessage: actionDef.confirmMessage,\n },\n 'object',\n {\n objectId: object.id,\n entityModule: object.entityModule,\n entityType: object.entityType,\n entityId: object.entityId,\n },\n object.actionLabel ?? actionDef.id,\n )\n }\n\n const expiresAt = readActionDataExpiry(message)\n if (resolved.length === 0 && !expiresAt) {\n return null\n }\n\n const configuredPrimaryActionId = actionData?.primaryActionId\n const primaryActionId = configuredPrimaryActionId && resolved.some((entry) => entry.id === configuredPrimaryActionId)\n ? configuredPrimaryActionId\n : resolved[0]?.id\n\n return {\n actions: resolved,\n primaryActionId,\n expiresAt,\n }\n}\n\nexport function findResolvedMessageActionById(\n message: Message,\n objects: MessageObject[],\n actionId: string,\n): ResolvedMessageAction | null {\n const actionData = buildResolvedMessageActions(message, objects)\n const match = actionData?.actions.find((entry) => entry.id === actionId)\n return (match as ResolvedMessageAction | undefined) ?? null\n}\n\nfunction buildTemplateContext(\n message: Message,\n resolutionContext: MessageActionResolutionContext,\n objectRef?: MessageActionObjectRef,\n): Record<string, unknown> {\n return {\n messageId: message.id,\n sourceEntityId: message.sourceEntityId ?? message.id,\n sourceEntityType: message.sourceEntityType ?? null,\n threadId: message.threadId ?? null,\n parentMessageId: message.parentMessageId ?? null,\n messageType: message.type,\n tenantId: resolutionContext.tenantId,\n organizationId: resolutionContext.organizationId ?? null,\n userId: resolutionContext.userId,\n objectId: objectRef?.objectId ?? null,\n entityId: objectRef?.entityId ?? null,\n entityType: objectRef?.entityType ?? null,\n entityModule: objectRef?.entityModule ?? null,\n }\n}\n\nfunction resolveTemplateString(template: string, context: Record<string, unknown>): string {\n return template.replace(/\\{([a-zA-Z0-9_]+)\\}/g, (fullMatch, key: string) => {\n const value = context[key]\n if (value == null) return fullMatch\n return String(value)\n })\n}\n\nexport function resolveActionHref(\n action: ResolvedMessageAction,\n message: Message,\n resolutionContext: MessageActionResolutionContext,\n): string | null {\n if (!action.href) return null\n const context = buildTemplateContext(message, resolutionContext, action.objectRef)\n return resolveTemplateString(action.href, context)\n}\n\nexport function resolveActionCommandInput(\n action: ResolvedMessageAction,\n message: Message,\n _resolutionContext: MessageActionResolutionContext,\n requestInput: Record<string, unknown>,\n): Record<string, unknown> {\n return {\n ...requestInput,\n messageId: message.id,\n actionId: action.id,\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -18,7 +18,16 @@ import { documentUpdateSchema } from "../../commands/documents.js";
|
|
|
18
18
|
import { escapeLikePattern } from "@open-mercato/shared/lib/db/escapeLikePattern";
|
|
19
19
|
import { parseBooleanToken } from "@open-mercato/shared/lib/boolean";
|
|
20
20
|
import { recalculateOrderTotalsForDisplay } from "../../commands/returns.js";
|
|
21
|
+
import { parseDecryptedFieldValue } from "@open-mercato/shared/lib/encryption/tenantDataEncryptionService";
|
|
21
22
|
const rawBodySchema = z.object({}).passthrough();
|
|
23
|
+
const normalizeJsonRecord = (value) => {
|
|
24
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
if (typeof value !== "string") return null;
|
|
28
|
+
const parsed = parseDecryptedFieldValue(value);
|
|
29
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
30
|
+
};
|
|
22
31
|
const resolveCustomerName = (snapshot, fallback) => {
|
|
23
32
|
if (!snapshot) return fallback ?? null;
|
|
24
33
|
const customer = snapshot.customer;
|
|
@@ -124,36 +133,40 @@ function buildSortMap(numberColumn) {
|
|
|
124
133
|
updatedAt: "updated_at"
|
|
125
134
|
};
|
|
126
135
|
}
|
|
127
|
-
const mapUpdateResponse = (entity) =>
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
136
|
+
const mapUpdateResponse = (entity) => {
|
|
137
|
+
const customerSnapshot = normalizeJsonRecord(entity?.customerSnapshot);
|
|
138
|
+
const metadata = normalizeJsonRecord(entity?.metadata);
|
|
139
|
+
return {
|
|
140
|
+
id: entity?.id ?? null,
|
|
141
|
+
orderNumber: entity?.orderNumber ?? null,
|
|
142
|
+
quoteNumber: entity?.quoteNumber ?? null,
|
|
143
|
+
customerEntityId: entity?.customerEntityId ?? null,
|
|
144
|
+
customerContactId: entity?.customerContactId ?? null,
|
|
145
|
+
customerSnapshot,
|
|
146
|
+
metadata,
|
|
147
|
+
externalReference: entity?.externalReference ?? null,
|
|
148
|
+
customerReference: entity?.customerReference ?? null,
|
|
149
|
+
comment: entity?.comments ?? null,
|
|
150
|
+
statusEntryId: entity?.statusEntryId ?? null,
|
|
151
|
+
status: entity?.status ?? null,
|
|
152
|
+
channelId: entity?.channelId ?? null,
|
|
153
|
+
customerName: resolveCustomerName(customerSnapshot, entity?.customerEntityId ?? null),
|
|
154
|
+
contactEmail: resolveCustomerEmail(customerSnapshot) ?? (typeof metadata?.customerEmail === "string" ? metadata.customerEmail : null),
|
|
155
|
+
currencyCode: entity?.currencyCode ?? null,
|
|
156
|
+
placedAt: entity?.placedAt ? entity.placedAt.toISOString() : null,
|
|
157
|
+
expectedDeliveryAt: entity?.expectedDeliveryAt ? entity.expectedDeliveryAt.toISOString() : null,
|
|
158
|
+
shippingAddressId: entity?.shippingAddressId ?? null,
|
|
159
|
+
billingAddressId: entity?.billingAddressId ?? null,
|
|
160
|
+
shippingAddressSnapshot: normalizeJsonRecord(entity?.shippingAddressSnapshot),
|
|
161
|
+
billingAddressSnapshot: normalizeJsonRecord(entity?.billingAddressSnapshot),
|
|
162
|
+
shippingMethodId: entity?.shippingMethodId ?? null,
|
|
163
|
+
shippingMethodCode: entity?.shippingMethodCode ?? null,
|
|
164
|
+
shippingMethodSnapshot: normalizeJsonRecord(entity?.shippingMethodSnapshot),
|
|
165
|
+
paymentMethodId: entity?.paymentMethodId ?? null,
|
|
166
|
+
paymentMethodCode: entity?.paymentMethodCode ?? null,
|
|
167
|
+
paymentMethodSnapshot: normalizeJsonRecord(entity?.paymentMethodSnapshot)
|
|
168
|
+
};
|
|
169
|
+
};
|
|
157
170
|
const attachTags = async (payload, ctx) => {
|
|
158
171
|
const items = Array.isArray(payload?.items) ? payload.items : [];
|
|
159
172
|
if (!items.length) return;
|
|
@@ -317,10 +330,10 @@ function buildDocumentCrudOptions(binding) {
|
|
|
317
330
|
shippingAddressId: item.shipping_address_id ?? null,
|
|
318
331
|
shippingMethodId: item.shipping_method_id ?? null,
|
|
319
332
|
shippingMethodCode: item.shipping_method_code ?? null,
|
|
320
|
-
shippingMethodSnapshot: item.shipping_method_snapshot
|
|
333
|
+
shippingMethodSnapshot: normalizeJsonRecord(item.shipping_method_snapshot),
|
|
321
334
|
paymentMethodId: item.payment_method_id ?? null,
|
|
322
335
|
paymentMethodCode: item.payment_method_code ?? null,
|
|
323
|
-
paymentMethodSnapshot: item.payment_method_snapshot
|
|
336
|
+
paymentMethodSnapshot: normalizeJsonRecord(item.payment_method_snapshot),
|
|
324
337
|
currencyCode: item.currency_code ?? null,
|
|
325
338
|
channelId: item.channel_id ?? null,
|
|
326
339
|
externalReference: item.external_reference ?? null,
|
|
@@ -343,10 +356,10 @@ function buildDocumentCrudOptions(binding) {
|
|
|
343
356
|
paidTotalAmount: toNumber(item.paid_total_amount),
|
|
344
357
|
refundedTotalAmount: toNumber(item.refunded_total_amount),
|
|
345
358
|
outstandingAmount: toNumber(item.outstanding_amount),
|
|
346
|
-
customerSnapshot: item.customer_snapshot
|
|
347
|
-
billingAddressSnapshot: item.billing_address_snapshot
|
|
348
|
-
shippingAddressSnapshot: item.shipping_address_snapshot
|
|
349
|
-
metadata: item.metadata
|
|
359
|
+
customerSnapshot: normalizeJsonRecord(item.customer_snapshot),
|
|
360
|
+
billingAddressSnapshot: normalizeJsonRecord(item.billing_address_snapshot),
|
|
361
|
+
shippingAddressSnapshot: normalizeJsonRecord(item.shipping_address_snapshot),
|
|
362
|
+
metadata: normalizeJsonRecord(item.metadata),
|
|
350
363
|
organizationId: item.organization_id ?? null,
|
|
351
364
|
tenantId: item.tenant_id ?? null,
|
|
352
365
|
createdAt: item.created_at,
|