@open-mercato/core 0.6.8-develop.7023.1.a1960eb208 → 0.6.8-develop.7026.1.23445b3f3e

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/auth/api/roles/acl/route.js +28 -33
  3. package/dist/modules/auth/api/roles/acl/route.js.map +2 -2
  4. package/dist/modules/auth/api/users/acl/route.js +96 -51
  5. package/dist/modules/auth/api/users/acl/route.js.map +2 -2
  6. package/dist/modules/auth/commands/acl.js +254 -0
  7. package/dist/modules/auth/commands/acl.js.map +7 -0
  8. package/dist/modules/auth/lib/grantChecks.js.map +2 -2
  9. package/dist/modules/communication_channels/commands/ingest-inbound-message.js +9 -1
  10. package/dist/modules/communication_channels/commands/ingest-inbound-message.js.map +2 -2
  11. package/dist/modules/inbox_ops/lib/htmlToPlainText.js +1 -12
  12. package/dist/modules/inbox_ops/lib/htmlToPlainText.js.map +2 -2
  13. package/dist/modules/messages/api/[id]/route.js +3 -0
  14. package/dist/modules/messages/api/[id]/route.js.map +2 -2
  15. package/dist/modules/messages/api/openapi.js +3 -0
  16. package/dist/modules/messages/api/openapi.js.map +2 -2
  17. package/dist/modules/messages/components/MessagesInboxPageClient.js +3 -13
  18. package/dist/modules/messages/components/MessagesInboxPageClient.js.map +2 -2
  19. package/dist/modules/messages/components/inboxFilters.js +15 -0
  20. package/dist/modules/messages/components/inboxFilters.js.map +2 -2
  21. package/dist/modules/messages/components/message-detail/hooks/useMessageDetailsConversation.js +11 -1
  22. package/dist/modules/messages/components/message-detail/hooks/useMessageDetailsConversation.js.map +2 -2
  23. package/dist/modules/messages/components/message-detail/panels/MessageHeader.js +3 -1
  24. package/dist/modules/messages/components/message-detail/panels/MessageHeader.js.map +2 -2
  25. package/dist/modules/messages/components/message-detail/panels/thread-panel.js +2 -1
  26. package/dist/modules/messages/components/message-detail/panels/thread-panel.js.map +2 -2
  27. package/dist/modules/messages/components/messageListLabels.js +14 -2
  28. package/dist/modules/messages/components/messageListLabels.js.map +2 -2
  29. package/dist/modules/sales/components/documents/SalesDocumentsTable.js +64 -14
  30. package/dist/modules/sales/components/documents/SalesDocumentsTable.js.map +2 -2
  31. package/package.json +7 -9
  32. package/src/modules/auth/api/roles/acl/route.ts +42 -39
  33. package/src/modules/auth/api/users/acl/route.ts +177 -53
  34. package/src/modules/auth/commands/acl.ts +495 -0
  35. package/src/modules/auth/i18n/de.json +2 -0
  36. package/src/modules/auth/i18n/en.json +2 -0
  37. package/src/modules/auth/i18n/es.json +2 -0
  38. package/src/modules/auth/i18n/ko.json +2 -0
  39. package/src/modules/auth/i18n/pl.json +2 -0
  40. package/src/modules/auth/lib/grantChecks.ts +6 -0
  41. package/src/modules/communication_channels/commands/ingest-inbound-message.ts +33 -1
  42. package/src/modules/inbox_ops/lib/htmlToPlainText.ts +1 -17
  43. package/src/modules/messages/api/[id]/route.ts +3 -0
  44. package/src/modules/messages/api/openapi.ts +3 -0
  45. package/src/modules/messages/components/MessagesInboxPageClient.tsx +5 -16
  46. package/src/modules/messages/components/inboxFilters.ts +36 -0
  47. package/src/modules/messages/components/message-detail/hooks/useMessageDetailsConversation.ts +14 -1
  48. package/src/modules/messages/components/message-detail/panels/MessageHeader.tsx +3 -1
  49. package/src/modules/messages/components/message-detail/panels/thread-panel.tsx +2 -1
  50. package/src/modules/messages/components/message-detail/types.ts +3 -0
  51. package/src/modules/messages/components/messageListLabels.ts +52 -3
  52. package/src/modules/sales/components/documents/SalesDocumentsTable.tsx +101 -14
  53. package/src/modules/sales/i18n/de.json +2 -0
  54. package/src/modules/sales/i18n/en.json +2 -0
  55. package/src/modules/sales/i18n/es.json +2 -0
  56. package/src/modules/sales/i18n/ko.json +2 -0
  57. package/src/modules/sales/i18n/pl.json +2 -0
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/communication_channels/commands/ingest-inbound-message.ts"],
4
- "sourcesContent": ["import { randomUUID } from 'node:crypto'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CommandBus, CommandHandler, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { registerCommand } from '@open-mercato/shared/lib/commands'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { emitCommunicationChannelsEvent } from '../events'\nimport { resolveContact } from '../lib/contact-resolver'\nimport type { ChannelAdapterRegistry } from '../lib/registry'\nimport type { NormalizedInboundMessage } from '../lib/adapter'\nimport { matchThread, type ThreadMatch } from '../lib/thread-matcher'\nimport {\n ChannelThreadMapping,\n CommunicationChannel,\n ExternalConversation,\n ExternalMessage,\n MessageChannelLink,\n} from '../data/entities'\nimport { normalizedInboundMessageSchema } from '../data/validators'\nimport { resolveCommunicationChannelsSystemUserId } from '../lib/system-user'\nimport { isUniqueViolation } from '../lib/pg-errors'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('communication_channels').child({ component: 'ingest-inbound-message' })\n\nconst ingestInputSchema = z.object({\n channelId: z.string().uuid(),\n providerKey: z.string().min(1),\n channelType: z.string().min(1),\n scope: z.object({\n tenantId: z.string().uuid(),\n organizationId: z.string().uuid().nullable(),\n }),\n message: normalizedInboundMessageSchema,\n})\n\nexport type IngestInboundMessageInput = z.infer<typeof ingestInputSchema>\n\nexport type IngestInboundMessageResult = {\n status: 'created' | 'duplicate'\n messageId?: string\n externalConversationId?: string\n externalMessageId?: string\n channelLinkId?: string\n threadMappingId?: string\n contactPersonId?: string | null\n}\n\nexport const COMMUNICATION_CHANNELS_INGEST_INBOUND_COMMAND_ID = 'communication_channels.message.ingest_inbound'\n\n/**\n * Idempotently ingest a normalized inbound channel message.\n *\n * Steps (per SPEC-045d \u00A76):\n * 1. Dedup by `(channel_id, external_message_id)` \u2014 if a MessageChannelLink already\n * exists for that pair, return `{ status: 'duplicate' }` without side effects.\n * 2. Create or load `ExternalConversation` by `(channel_id, external_conversation_id)`.\n * 3. Create or load `ChannelThreadMapping` (1:1 with ExternalConversation).\n * 4. Resolve CRM contact via adapter + QueryEngine (best-effort).\n * 5. Compose the platform `Message` via `messages.messages.compose` (separate transaction).\n * 6. Create `ExternalMessage` + `MessageChannelLink`.\n * 7. Emit `communication_channels.message.received` (and `.conversation.created` / `.contact.resolved` when applicable).\n *\n * The two-transaction model (compose-message-then-record-link) is acceptable for v1;\n * the link's unique-on-message-id constraint is the safety net against orphans. See\n * the pre-implementation analysis for a discussion of single-transaction alternatives.\n */\nconst ingestInboundMessageCommand: CommandHandler<IngestInboundMessageInput, IngestInboundMessageResult> = {\n id: COMMUNICATION_CHANNELS_INGEST_INBOUND_COMMAND_ID,\n async execute(rawInput, ctx) {\n const input = ingestInputSchema.parse(rawInput) as IngestInboundMessageInput\n\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const adapterRegistry = ctx.container.resolve('channelAdapterRegistry') as ChannelAdapterRegistry\n const dscope = {\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n }\n\n // (1) Dedup: short-circuit if we've already processed this provider message.\n // The unique constraint is on `messageId`, not (channel, externalMessageId).\n // We must dedup by joining against ExternalMessage which IS uniquely indexed by\n // (channel_id, external_message_id). Hub-side dedup is the authoritative gate.\n const existingExternal = await findOneWithDecryption(\n em,\n ExternalMessage,\n {\n channelId: input.channelId,\n externalMessageId: input.message.externalMessageId,\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n },\n undefined,\n dscope,\n )\n if (existingExternal) {\n return {\n status: 'duplicate',\n externalConversationId: existingExternal.conversationId,\n externalMessageId: existingExternal.id,\n }\n }\n\n // (1b) Spec B \u00A7 Sent-folder dedup.\n //\n // When an outbound message lands in the user's IMAP Sent folder (or\n // when Gmail's \"send and archive\" deposits it in All Mail), the next\n // poll will re-fetch it from INBOX as if it were inbound. Skip it\n // here using the RFC 5322 `Message-ID` header \u2014 we recorded it on the\n // outbound `MessageChannelLink.channelMetadata.messageId` at send time.\n //\n // We dedup ONLY on outbound links (direction='outbound') for the same\n // channel \u2014 that way an inbound copy of someone ELSE's email that\n // happens to share a Message-ID is still ingested normally.\n const incomingMessageId = (() => {\n const fromMeta = (input.message.channelMetadata as Record<string, unknown> | undefined)?.messageId\n if (typeof fromMeta === 'string' && fromMeta.length > 0) return fromMeta\n return null\n })()\n if (incomingMessageId) {\n // MikroORM v7 dropped the Knex builder in favour of Kysely/raw SQL.\n // We use a positional-placeholder raw query for the JSONB\n // `channel_metadata->>messageId` comparison.\n try {\n const sentFolderHit = await em.getConnection().execute<Array<{ id: string }>>(\n `SELECT link.id FROM message_channel_links AS link\n INNER JOIN external_conversations AS conv\n ON conv.id = link.external_conversation_id\n WHERE link.tenant_id = ?\n AND ((?::uuid IS NULL AND link.organization_id IS NULL) OR link.organization_id = ?::uuid)\n AND conv.tenant_id = ?\n AND ((?::uuid IS NULL AND conv.organization_id IS NULL) OR conv.organization_id = ?::uuid)\n AND conv.channel_id = ?\n AND link.direction = 'outbound'\n AND link.channel_metadata->>'messageId' = ?\n LIMIT 1`,\n [\n input.scope.tenantId,\n input.scope.organizationId ?? null,\n input.scope.organizationId ?? null,\n input.scope.tenantId,\n input.scope.organizationId ?? null,\n input.scope.organizationId ?? null,\n input.channelId,\n incomingMessageId,\n ],\n )\n if (Array.isArray(sentFolderHit) && sentFolderHit.length > 0) {\n return {\n status: 'duplicate',\n externalConversationId: input.message.externalConversationId,\n externalMessageId: input.message.externalMessageId,\n }\n }\n } catch (dedupErr) {\n // Sent-folder dedup is best-effort \u2014 a failure here must not abort\n // ingest (better a possible duplicate than a lost inbound message).\n logger.warn('sent-folder dedup query failed, continuing', { err: dedupErr })\n }\n }\n\n // Channel + adapter lookup (the channel must exist + be active).\n const channel = await findOneWithDecryption(\n em,\n CommunicationChannel,\n {\n id: input.channelId,\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n deletedAt: null,\n },\n undefined,\n dscope,\n )\n if (!channel) {\n throw new Error(\n `[internal] Channel ${input.channelId} not found for tenant ${input.scope.tenantId} (or has been deleted)`,\n )\n }\n if (!channel.isActive) {\n throw new Error(`[internal] Channel ${input.channelId} is inactive; refusing to ingest`)\n }\n\n const adapter = adapterRegistry.get(input.providerKey)\n if (!adapter) {\n throw new Error(\n `[internal] No ChannelAdapter registered for providerKey '${input.providerKey}'. ` +\n 'Check that the provider package is enabled in modules.ts.',\n )\n }\n\n // (2) ExternalConversation upsert by (channel_id, externalConversationId).\n const m = input.message\n let conversation = await findOneWithDecryption(\n em,\n ExternalConversation,\n {\n channelId: input.channelId,\n externalConversationId: m.externalConversationId,\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n },\n undefined,\n dscope,\n )\n let conversationCreated = false\n if (!conversation) {\n conversation = em.create(ExternalConversation, {\n channelId: input.channelId,\n externalConversationId: m.externalConversationId,\n subject: m.subject ?? null,\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n lastMessageAt: m.timestamp ?? new Date(),\n })\n em.persist(conversation)\n conversationCreated = true\n }\n\n // (3) ChannelThreadMapping upsert (1:1 with ExternalConversation per tenant).\n let mapping = await findOneWithDecryption(\n em,\n ChannelThreadMapping,\n {\n externalConversationId: conversation.id,\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n },\n undefined,\n dscope,\n )\n\n // Last-activity bump on an existing conversation. Applied AFTER the mapping\n // lookup, immediately before the flush, so the scalar mutation and its flush\n // stay adjacent with no query in between (core flush-ordering rule \u2014 a query\n // between a scalar mutation and `em.flush()` can drop the change under some\n // flush modes / subscriber configurations).\n if (\n !conversationCreated &&\n m.timestamp &&\n (!conversation.lastMessageAt || m.timestamp > conversation.lastMessageAt)\n ) {\n conversation.lastMessageAt = m.timestamp\n }\n // We'll fill `messageThreadId` after composing the platform Message (since the\n // first inbound message becomes the thread root in the messages module).\n await em.flush()\n\n // (3b) Spec B \u2014 layered thread match.\n //\n // Resolve the inbound message to an existing platform thread using\n // (in priority order):\n // 1. Crypto token in References / In-Reply-To header (high confidence)\n // 2. Crypto token in body hidden span or plain-text marker (high)\n // 3. JWZ on Message-Id \u2194 stored `MessageChannelLink.channelMetadata.messageId` (medium)\n // 4. Subject + participants in last 30 days, same channel (low)\n //\n // The matcher returns `null` when nothing hits \u2014 in that case we fall\n // back to the existing `ChannelThreadMapping`-by-conversation-id lookup\n // (which also returns null on first-ever inbound, in which case the\n // compose command opens a new thread).\n const metaForMatcher = (m.channelMetadata ?? {}) as Record<string, unknown>\n let threadMatch: ThreadMatch | null = null\n try {\n threadMatch = await matchThread(\n {\n channelId: input.channelId,\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n messageId: extractStringFromMeta(metaForMatcher, 'messageId') ?? m.externalMessageId,\n inReplyTo:\n m.replyToExternalId ?? extractStringFromMeta(metaForMatcher, 'inReplyTo'),\n references: extractStringArrayFromMeta(metaForMatcher, 'references'),\n subject: m.subject ?? '',\n fromAddress:\n extractStringFromMeta(metaForMatcher, 'from') ?? m.senderIdentifier,\n toAddresses: extractStringArrayFromMeta(metaForMatcher, 'to'),\n ccAddresses: extractStringArrayFromMeta(metaForMatcher, 'cc'),\n bodyPlain: m.bodyFormat === 'html' ? null : m.body ?? null,\n bodyHtml: m.bodyFormat === 'html' ? m.body ?? null : null,\n receivedAt: m.timestamp ?? new Date(),\n },\n { em },\n )\n } catch (matcherErr) {\n // Matcher failure must not block ingest \u2014 fall back to the existing\n // conversation-based thread mapping so the message still lands.\n logger.warn('thread matcher failed, falling back to conversation mapping', { err: matcherErr })\n }\n\n // (4) Contact resolution (best-effort, advisory).\n let contactHint: {\n matchedPersonId?: string | null\n email?: string\n displayName?: string\n } | null = null\n try {\n contactHint = await resolveContact(\n {\n adapter,\n senderIdentifier: m.senderIdentifier,\n senderDisplayName: m.senderDisplayName,\n channelMetadata: m.channelMetadata,\n credentials: {}, // credentials decrypted at the webhook route; resolver doesn't re-fetch\n scope: {\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? input.scope.tenantId,\n },\n },\n { container: ctx.container },\n )\n } catch (contactErr) {\n // Best-effort: contact resolution is advisory and must not abort ingest.\n // Log like the sibling dedup/matcher catches so a misbehaving resolver is\n // visible in operator logs instead of failing silently.\n logger.warn('contact resolution failed, continuing without a CRM match', { err: contactErr })\n contactHint = null\n }\n const matchedPersonId = contactHint?.matchedPersonId ?? null\n if (matchedPersonId && conversation.contactPersonId !== matchedPersonId) {\n conversation.contactPersonId = matchedPersonId\n // Flush this scalar mutation before the system-user lookup below queries the\n // same EntityManager. SPEC-018: a query between a scalar mutation and its\n // flush can silently discard the pending UPDATE (mirrors the lastMessageAt\n // bump above).\n await em.flush()\n }\n\n // (5) Compose the platform Message via the messages module command.\n //\n // Sanitize against the `messages` module's validators (max 50_000 char body\n // + non-empty subject) so real-world emails don't get rejected mid-ingest:\n // - HTML emails routinely exceed 50k (Gmail signatures, marketing\n // templates, RFC 5322 multipart). Truncate with a marker rather than\n // drop the whole message \u2014 the full raw body is still in\n // ExternalMessage.rawPayload if needed for forensic / forward use.\n // - Some legitimate messages have no subject (notifications, bounce\n // digests). Substitute a placeholder instead of failing ingest.\n const MAX_COMPOSE_BODY = 50_000\n const TRUNCATE_MARKER =\n '\\n\\n[\u2026message truncated by Open Mercato \u2014 full body preserved in ExternalMessage.rawPayload]'\n const rawBody = m.body ?? ''\n const truncatedBody =\n rawBody.length > MAX_COMPOSE_BODY\n ? rawBody.slice(0, MAX_COMPOSE_BODY - TRUNCATE_MARKER.length) + TRUNCATE_MARKER\n : rawBody\n const safeSubject = (m.subject ?? '').trim() || '(no subject)'\n\n const composeInput = {\n type: `channel.${input.providerKey}`,\n visibility: 'public' as const,\n sourceEntityType: 'communication_channels.external_conversation',\n sourceEntityId: conversation.id,\n externalEmail: contactHint?.email ?? undefined,\n externalName: contactHint?.displayName ?? m.senderDisplayName,\n recipients: mapping?.assignedUserId\n ? [{ userId: mapping.assignedUserId, type: 'to' as const }]\n : [],\n subject: safeSubject,\n body: truncatedBody,\n bodyFormat: (m.bodyFormat === 'html' ? 'text' : m.bodyFormat) as 'text' | 'markdown',\n priority: 'normal' as const,\n sendViaEmail: false,\n // Spec B: matcher-resolved thread id takes priority over the existing\n // conversation-based mapping. Falls through to `mapping?.messageThreadId`\n // when the matcher returned null (no token / JWZ / subject hit).\n parentMessageId: threadMatch?.messageThreadId ?? mapping?.messageThreadId,\n isDraft: false,\n // Stable dedup key so a retried ingest (after a transient failure between\n // compose and the ExternalMessage anchor insert) reuses the message\n // composed by the first attempt instead of duplicating it. Mirrors the\n // (channel, externalMessageId) ExternalMessage anchor's natural key.\n idempotencyKey: m.externalMessageId\n ? `cc:${input.channelId}:${m.externalMessageId}`\n : undefined,\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId,\n userId: await resolveCommunicationChannelsSystemUserId(\n em,\n input.scope.tenantId,\n mapping?.assignedUserId ?? null,\n ),\n }\n\n const commandBus = ctx.container.resolve('commandBus') as CommandBus\n const composeResult = await commandBus.execute<typeof composeInput, { id: string; threadId: string | null }>(\n 'messages.messages.compose',\n {\n input: composeInput,\n ctx: passthroughCommandCtx(ctx, input.scope),\n },\n )\n const message = composeResult.result\n if (!message?.id) {\n throw new Error('messages.messages.compose did not return a message id')\n }\n\n // (3 continued) Create or update ChannelThreadMapping now that we have a threadId.\n if (!mapping) {\n mapping = em.create(ChannelThreadMapping, {\n externalConversationId: conversation.id,\n messageThreadId: message.threadId ?? message.id,\n channelId: input.channelId,\n providerKey: input.providerKey,\n externalThreadRef: m.externalConversationId,\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n })\n em.persist(mapping)\n }\n\n // (6) Create ExternalMessage + MessageChannelLink (hub-side records).\n //\n // The PrimaryKey for both uses `defaultRaw: 'gen_random_uuid()'` \u2014 a\n // Postgres-side default that doesn't populate the entity's `id` field\n // until after the INSERT returns. If we let MikroORM generate both, then\n // `em.create(MessageChannelLink, { externalMessageId: externalMessage.id })`\n // reads `undefined` for `externalMessage.id` (it hasn't been flushed yet)\n // and writes NULL to `message_channel_links.external_message_id`,\n // breaking the FK and causing downstream joins to silently return 0 rows.\n //\n // Pre-generating both UUIDs client-side fixes the cross-row reference\n // problem and keeps the single-transaction flush semantics intact.\n const externalMessageRowId = randomUUID()\n const channelLinkRowId = randomUUID()\n const externalMessage = em.create(ExternalMessage, {\n id: externalMessageRowId,\n channelId: input.channelId,\n conversationId: conversation.id,\n externalMessageId: m.externalMessageId,\n direction: 'inbound',\n senderIdentifier: m.senderIdentifier,\n senderDisplayName: m.senderDisplayName ?? null,\n providerTimestamp: m.timestamp,\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n })\n em.persist(externalMessage)\n\n // Spec B: annotate the link with which thread-matcher strategy resolved\n // this message (or `'new-thread'` when matcher returned null and we\n // opened a fresh thread). Surfaced to observability + future UI (\"this\n // thread match is low-confidence \u2014 confirm or move\").\n const matcherAnnotatedMetadata: Record<string, unknown> = {\n ...((m.channelMetadata as Record<string, unknown> | undefined) ?? {}),\n threadMatchStrategy: threadMatch?.matchedBy ?? 'new-thread',\n threadMatchConfidence: threadMatch?.confidence ?? 'low',\n }\n\n const channelLink = em.create(MessageChannelLink, {\n id: channelLinkRowId,\n messageId: message.id,\n externalConversationId: conversation.id,\n externalMessageId: externalMessageRowId,\n providerKey: input.providerKey,\n channelType: input.channelType,\n direction: 'inbound',\n deliveryStatus: 'received',\n channelPayload: m.channelPayload,\n channelContentType: m.channelContentType,\n channelMetadata: matcherAnnotatedMetadata,\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n })\n em.persist(channelLink)\n\n try {\n await em.flush()\n } catch (flushErr) {\n // Concurrency guard: the pre-check at (1) is not atomic with this insert,\n // so a poll re-fetch racing a push notification (or two push deliveries)\n // can both pass the check and reach here. The `(channel_id,\n // external_message_id)` unique index rejects the loser with a 23505. Treat\n // that as a duplicate \u2014 returning here (instead of throwing) prevents the\n // message from being dead-lettered and retried forever. The winning job\n // already recorded the message + link.\n if (isUniqueViolation(flushErr)) {\n return {\n status: 'duplicate',\n externalConversationId: conversation.id,\n externalMessageId: m.externalMessageId,\n }\n }\n throw flushErr\n }\n\n // (7) Emit events \u2014 order matters for downstream subscribers.\n if (conversationCreated) {\n await emitCommunicationChannelsEvent(\n 'communication_channels.conversation.created',\n {\n conversationId: conversation.id,\n channelId: input.channelId,\n providerKey: input.providerKey,\n channelType: input.channelType,\n externalConversationId: m.externalConversationId,\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n },\n { persistent: true },\n )\n }\n if (matchedPersonId) {\n await emitCommunicationChannelsEvent(\n 'communication_channels.contact.resolved',\n {\n conversationId: conversation.id,\n contactPersonId: matchedPersonId,\n providerKey: input.providerKey,\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n },\n { persistent: true },\n )\n }\n await emitCommunicationChannelsEvent(\n 'communication_channels.message.received',\n {\n messageId: message.id,\n externalMessageId: externalMessage.id,\n channelLinkId: channelLink.id,\n conversationId: conversation.id,\n channelId: input.channelId,\n providerKey: input.providerKey,\n channelType: input.channelType,\n direction: 'inbound',\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n },\n { persistent: true },\n )\n\n return {\n status: 'created',\n messageId: message.id,\n externalConversationId: conversation.id,\n externalMessageId: externalMessage.id,\n channelLinkId: channelLink.id,\n threadMappingId: mapping.id,\n contactPersonId: matchedPersonId,\n }\n },\n}\n\n\n/**\n * Build a runtime context for the nested `messages.messages.compose` call.\n *\n * The compose command expects a `CommandRuntimeContext`. For inbound webhook\n * processing there is no platform user; we pass `auth: null` and use the tenant\n * scope from our input.\n */\nfunction passthroughCommandCtx(\n parent: CommandRuntimeContext,\n scope: IngestInboundMessageInput['scope'],\n): CommandRuntimeContext {\n return {\n container: parent.container,\n auth: null,\n organizationScope: null,\n selectedOrganizationId: scope.organizationId ?? null,\n organizationIds: scope.organizationId ? [scope.organizationId] : null,\n }\n}\n\n/**\n * Pull a string value from the provider's `channelMetadata` map. Returns\n * `null` (not `undefined`) when the key is absent or the value isn't a\n * string \u2014 keeps the matcher's input shape predictable.\n */\nfunction extractStringFromMeta(\n meta: Record<string, unknown>,\n key: string,\n): string | null {\n const value = meta[key]\n if (typeof value === 'string' && value.length > 0) return value\n return null\n}\n\n/**\n * Pull a string[] value from the provider's `channelMetadata` map.\n * Filters out non-string entries defensively. Returns an empty array\n * when the key is absent or the value isn't an array.\n */\nfunction extractStringArrayFromMeta(\n meta: Record<string, unknown>,\n key: string,\n): string[] {\n const value = meta[key]\n if (!Array.isArray(value)) return []\n return value.filter((item): item is string => typeof item === 'string')\n}\n\nregisterCommand(ingestInboundMessageCommand)\n\nexport default ingestInboundMessageCommand\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { randomUUID } from 'node:crypto'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CommandBus, CommandHandler, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { registerCommand } from '@open-mercato/shared/lib/commands'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { htmlToPlainText } from '@open-mercato/shared/lib/html/htmlToPlainText'\nimport { emitCommunicationChannelsEvent } from '../events'\nimport { resolveContact } from '../lib/contact-resolver'\nimport type { ChannelAdapterRegistry } from '../lib/registry'\nimport type { NormalizedInboundMessage } from '../lib/adapter'\nimport { matchThread, type ThreadMatch } from '../lib/thread-matcher'\nimport {\n ChannelThreadMapping,\n CommunicationChannel,\n ExternalConversation,\n ExternalMessage,\n MessageChannelLink,\n} from '../data/entities'\nimport { normalizedInboundMessageSchema } from '../data/validators'\nimport { resolveCommunicationChannelsSystemUserId } from '../lib/system-user'\nimport { isUniqueViolation } from '../lib/pg-errors'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('communication_channels').child({ component: 'ingest-inbound-message' })\n\nconst ingestInputSchema = z.object({\n channelId: z.string().uuid(),\n providerKey: z.string().min(1),\n channelType: z.string().min(1),\n scope: z.object({\n tenantId: z.string().uuid(),\n organizationId: z.string().uuid().nullable(),\n }),\n message: normalizedInboundMessageSchema,\n})\n\nexport type IngestInboundMessageInput = z.infer<typeof ingestInputSchema>\n\nexport type IngestInboundMessageResult = {\n status: 'created' | 'duplicate'\n messageId?: string\n externalConversationId?: string\n externalMessageId?: string\n channelLinkId?: string\n threadMappingId?: string\n contactPersonId?: string | null\n}\n\nexport const COMMUNICATION_CHANNELS_INGEST_INBOUND_COMMAND_ID = 'communication_channels.message.ingest_inbound'\n\n/**\n * Idempotently ingest a normalized inbound channel message.\n *\n * Steps (per SPEC-045d \u00A76):\n * 1. Dedup by `(channel_id, external_message_id)` \u2014 if a MessageChannelLink already\n * exists for that pair, return `{ status: 'duplicate' }` without side effects.\n * 2. Create or load `ExternalConversation` by `(channel_id, external_conversation_id)`.\n * 3. Create or load `ChannelThreadMapping` (1:1 with ExternalConversation).\n * 4. Resolve CRM contact via adapter + QueryEngine (best-effort).\n * 5. Compose the platform `Message` via `messages.messages.compose` (separate transaction).\n * 6. Create `ExternalMessage` + `MessageChannelLink`.\n * 7. Emit `communication_channels.message.received` (and `.conversation.created` / `.contact.resolved` when applicable).\n *\n * The two-transaction model (compose-message-then-record-link) is acceptable for v1;\n * the link's unique-on-message-id constraint is the safety net against orphans. See\n * the pre-implementation analysis for a discussion of single-transaction alternatives.\n */\nconst ingestInboundMessageCommand: CommandHandler<IngestInboundMessageInput, IngestInboundMessageResult> = {\n id: COMMUNICATION_CHANNELS_INGEST_INBOUND_COMMAND_ID,\n async execute(rawInput, ctx) {\n const input = ingestInputSchema.parse(rawInput) as IngestInboundMessageInput\n\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const adapterRegistry = ctx.container.resolve('channelAdapterRegistry') as ChannelAdapterRegistry\n const dscope = {\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n }\n\n // (1) Dedup: short-circuit if we've already processed this provider message.\n // The unique constraint is on `messageId`, not (channel, externalMessageId).\n // We must dedup by joining against ExternalMessage which IS uniquely indexed by\n // (channel_id, external_message_id). Hub-side dedup is the authoritative gate.\n const existingExternal = await findOneWithDecryption(\n em,\n ExternalMessage,\n {\n channelId: input.channelId,\n externalMessageId: input.message.externalMessageId,\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n },\n undefined,\n dscope,\n )\n if (existingExternal) {\n return {\n status: 'duplicate',\n externalConversationId: existingExternal.conversationId,\n externalMessageId: existingExternal.id,\n }\n }\n\n // (1b) Spec B \u00A7 Sent-folder dedup.\n //\n // When an outbound message lands in the user's IMAP Sent folder (or\n // when Gmail's \"send and archive\" deposits it in All Mail), the next\n // poll will re-fetch it from INBOX as if it were inbound. Skip it\n // here using the RFC 5322 `Message-ID` header \u2014 we recorded it on the\n // outbound `MessageChannelLink.channelMetadata.messageId` at send time.\n //\n // We dedup ONLY on outbound links (direction='outbound') for the same\n // channel \u2014 that way an inbound copy of someone ELSE's email that\n // happens to share a Message-ID is still ingested normally.\n const incomingMessageId = (() => {\n const fromMeta = (input.message.channelMetadata as Record<string, unknown> | undefined)?.messageId\n if (typeof fromMeta === 'string' && fromMeta.length > 0) return fromMeta\n return null\n })()\n if (incomingMessageId) {\n // MikroORM v7 dropped the Knex builder in favour of Kysely/raw SQL.\n // We use a positional-placeholder raw query for the JSONB\n // `channel_metadata->>messageId` comparison.\n try {\n const sentFolderHit = await em.getConnection().execute<Array<{ id: string }>>(\n `SELECT link.id FROM message_channel_links AS link\n INNER JOIN external_conversations AS conv\n ON conv.id = link.external_conversation_id\n WHERE link.tenant_id = ?\n AND ((?::uuid IS NULL AND link.organization_id IS NULL) OR link.organization_id = ?::uuid)\n AND conv.tenant_id = ?\n AND ((?::uuid IS NULL AND conv.organization_id IS NULL) OR conv.organization_id = ?::uuid)\n AND conv.channel_id = ?\n AND link.direction = 'outbound'\n AND link.channel_metadata->>'messageId' = ?\n LIMIT 1`,\n [\n input.scope.tenantId,\n input.scope.organizationId ?? null,\n input.scope.organizationId ?? null,\n input.scope.tenantId,\n input.scope.organizationId ?? null,\n input.scope.organizationId ?? null,\n input.channelId,\n incomingMessageId,\n ],\n )\n if (Array.isArray(sentFolderHit) && sentFolderHit.length > 0) {\n return {\n status: 'duplicate',\n externalConversationId: input.message.externalConversationId,\n externalMessageId: input.message.externalMessageId,\n }\n }\n } catch (dedupErr) {\n // Sent-folder dedup is best-effort \u2014 a failure here must not abort\n // ingest (better a possible duplicate than a lost inbound message).\n logger.warn('sent-folder dedup query failed, continuing', { err: dedupErr })\n }\n }\n\n // Channel + adapter lookup (the channel must exist + be active).\n const channel = await findOneWithDecryption(\n em,\n CommunicationChannel,\n {\n id: input.channelId,\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n deletedAt: null,\n },\n undefined,\n dscope,\n )\n if (!channel) {\n throw new Error(\n `[internal] Channel ${input.channelId} not found for tenant ${input.scope.tenantId} (or has been deleted)`,\n )\n }\n if (!channel.isActive) {\n throw new Error(`[internal] Channel ${input.channelId} is inactive; refusing to ingest`)\n }\n\n const adapter = adapterRegistry.get(input.providerKey)\n if (!adapter) {\n throw new Error(\n `[internal] No ChannelAdapter registered for providerKey '${input.providerKey}'. ` +\n 'Check that the provider package is enabled in modules.ts.',\n )\n }\n\n // (2) ExternalConversation upsert by (channel_id, externalConversationId).\n const m = input.message\n let conversation = await findOneWithDecryption(\n em,\n ExternalConversation,\n {\n channelId: input.channelId,\n externalConversationId: m.externalConversationId,\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n },\n undefined,\n dscope,\n )\n let conversationCreated = false\n if (!conversation) {\n conversation = em.create(ExternalConversation, {\n channelId: input.channelId,\n externalConversationId: m.externalConversationId,\n subject: m.subject ?? null,\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n lastMessageAt: m.timestamp ?? new Date(),\n })\n em.persist(conversation)\n conversationCreated = true\n }\n\n // (3) ChannelThreadMapping upsert (1:1 with ExternalConversation per tenant).\n let mapping = await findOneWithDecryption(\n em,\n ChannelThreadMapping,\n {\n externalConversationId: conversation.id,\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n },\n undefined,\n dscope,\n )\n\n // Last-activity bump on an existing conversation. Applied AFTER the mapping\n // lookup, immediately before the flush, so the scalar mutation and its flush\n // stay adjacent with no query in between (core flush-ordering rule \u2014 a query\n // between a scalar mutation and `em.flush()` can drop the change under some\n // flush modes / subscriber configurations).\n if (\n !conversationCreated &&\n m.timestamp &&\n (!conversation.lastMessageAt || m.timestamp > conversation.lastMessageAt)\n ) {\n conversation.lastMessageAt = m.timestamp\n }\n // We'll fill `messageThreadId` after composing the platform Message (since the\n // first inbound message becomes the thread root in the messages module).\n await em.flush()\n\n // (3b) Spec B \u2014 layered thread match.\n //\n // Resolve the inbound message to an existing platform thread using\n // (in priority order):\n // 1. Crypto token in References / In-Reply-To header (high confidence)\n // 2. Crypto token in body hidden span or plain-text marker (high)\n // 3. JWZ on Message-Id \u2194 stored `MessageChannelLink.channelMetadata.messageId` (medium)\n // 4. Subject + participants in last 30 days, same channel (low)\n //\n // The matcher returns `null` when nothing hits \u2014 in that case we fall\n // back to the existing `ChannelThreadMapping`-by-conversation-id lookup\n // (which also returns null on first-ever inbound, in which case the\n // compose command opens a new thread).\n const metaForMatcher = (m.channelMetadata ?? {}) as Record<string, unknown>\n let threadMatch: ThreadMatch | null = null\n try {\n threadMatch = await matchThread(\n {\n channelId: input.channelId,\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n messageId: extractStringFromMeta(metaForMatcher, 'messageId') ?? m.externalMessageId,\n inReplyTo:\n m.replyToExternalId ?? extractStringFromMeta(metaForMatcher, 'inReplyTo'),\n references: extractStringArrayFromMeta(metaForMatcher, 'references'),\n subject: m.subject ?? '',\n fromAddress:\n extractStringFromMeta(metaForMatcher, 'from') ?? m.senderIdentifier,\n toAddresses: extractStringArrayFromMeta(metaForMatcher, 'to'),\n ccAddresses: extractStringArrayFromMeta(metaForMatcher, 'cc'),\n bodyPlain: m.bodyFormat === 'html' ? null : m.body ?? null,\n bodyHtml: m.bodyFormat === 'html' ? m.body ?? null : null,\n receivedAt: m.timestamp ?? new Date(),\n },\n { em },\n )\n } catch (matcherErr) {\n // Matcher failure must not block ingest \u2014 fall back to the existing\n // conversation-based thread mapping so the message still lands.\n logger.warn('thread matcher failed, falling back to conversation mapping', { err: matcherErr })\n }\n\n // (4) Contact resolution (best-effort, advisory).\n let contactHint: {\n matchedPersonId?: string | null\n email?: string\n displayName?: string\n } | null = null\n try {\n contactHint = await resolveContact(\n {\n adapter,\n senderIdentifier: m.senderIdentifier,\n senderDisplayName: m.senderDisplayName,\n channelMetadata: m.channelMetadata,\n credentials: {}, // credentials decrypted at the webhook route; resolver doesn't re-fetch\n scope: {\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? input.scope.tenantId,\n },\n },\n { container: ctx.container },\n )\n } catch (contactErr) {\n // Best-effort: contact resolution is advisory and must not abort ingest.\n // Log like the sibling dedup/matcher catches so a misbehaving resolver is\n // visible in operator logs instead of failing silently.\n logger.warn('contact resolution failed, continuing without a CRM match', { err: contactErr })\n contactHint = null\n }\n const matchedPersonId = contactHint?.matchedPersonId ?? null\n if (matchedPersonId && conversation.contactPersonId !== matchedPersonId) {\n conversation.contactPersonId = matchedPersonId\n // Flush this scalar mutation before the system-user lookup below queries the\n // same EntityManager. SPEC-018: a query between a scalar mutation and its\n // flush can silently discard the pending UPDATE (mirrors the lastMessageAt\n // bump above).\n await em.flush()\n }\n\n // (5) Compose the platform Message via the messages module command.\n //\n // Sanitize against the `messages` module's validators (max 50_000 char body\n // + non-empty subject) so real-world emails don't get rejected mid-ingest:\n // - HTML emails routinely exceed 50k (Gmail signatures, marketing\n // templates, RFC 5322 multipart). Truncate with a marker rather than\n // drop the whole message \u2014 the full raw body is still in\n // ExternalMessage.rawPayload if needed for forensic / forward use.\n // - Some legitimate messages have no subject (notifications, bounce\n // digests). Substitute a placeholder instead of failing ingest.\n // - HTML bodies land in the platform body as plain text rather than being\n // merely relabelled as `text`: `MarkdownContent` renders a `text` body\n // verbatim, so unconverted markup shows up as literal doctype, <style>\n // blocks and tags in the inbox preview and the detail body. The source\n // markup is untouched in `channelPayload.html`, which `data/enrichers.ts`\n // sanitizes into `_channelPayload.sanitizedHtml` for the channel-payload\n // renderer widget mounted at `detail:messages:message:body:after`.\n // - The plain-text alternative wins over conversion when the sender's\n // client supplied one (`multipart/alternative`), because that part is\n // what a human actually wrote; `html-to-text` output \u2014 inlined link\n // URLs, flattened tables, synthesized list markers \u2014 is the fallback for\n // HTML-only senders. `lib/email-mime.ts` preferred `bodyHtml` when\n // picking `body`, but preserved the text part at `channelPayload.text`.\n const MAX_COMPOSE_BODY = 50_000\n // Bound the synchronous parse: inbound mail is untrusted and the 5MB body\n // ceiling from `lib/email-capabilities.ts` would otherwise block the ingest\n // worker on a single oversized document. This cap counts markup bytes while\n // MAX_COMPOSE_BODY counts text characters, so it can bite first \u2014 an\n // ordinary marketing template or a long quoted thread is easily over 512KB\n // of markup and still well under 50k characters of text. It therefore\n // carries the same truncation marker, rather than ending mid-sentence with\n // nothing to tell the reader the rest exists.\n const MAX_HTML_PARSE_INPUT = 512 * 1024\n const TRUNCATE_MARKER =\n '\\n\\n[\u2026message truncated by Open Mercato \u2014 full body preserved in ExternalMessage.rawPayload]'\n const sourceBody = m.body ?? ''\n const plainAlternative =\n typeof m.channelPayload?.text === 'string' ? m.channelPayload.text.trim() : ''\n const convertHtmlBody = () => {\n const converted = htmlToPlainText(sourceBody.slice(0, MAX_HTML_PARSE_INPUT))\n return sourceBody.length > MAX_HTML_PARSE_INPUT ? converted + TRUNCATE_MARKER : converted\n }\n const rawBody = m.bodyFormat === 'html'\n ? (plainAlternative || convertHtmlBody())\n : sourceBody\n const truncatedBody =\n rawBody.length > MAX_COMPOSE_BODY\n ? rawBody.slice(0, MAX_COMPOSE_BODY - TRUNCATE_MARKER.length) + TRUNCATE_MARKER\n : rawBody\n const safeSubject = (m.subject ?? '').trim() || '(no subject)'\n\n const composeInput = {\n type: `channel.${input.providerKey}`,\n visibility: 'public' as const,\n sourceEntityType: 'communication_channels.external_conversation',\n sourceEntityId: conversation.id,\n externalEmail: contactHint?.email ?? undefined,\n externalName: contactHint?.displayName ?? m.senderDisplayName,\n recipients: mapping?.assignedUserId\n ? [{ userId: mapping.assignedUserId, type: 'to' as const }]\n : [],\n subject: safeSubject,\n body: truncatedBody,\n bodyFormat: (m.bodyFormat === 'html' ? 'text' : m.bodyFormat) as 'text' | 'markdown',\n priority: 'normal' as const,\n sendViaEmail: false,\n // Spec B: matcher-resolved thread id takes priority over the existing\n // conversation-based mapping. Falls through to `mapping?.messageThreadId`\n // when the matcher returned null (no token / JWZ / subject hit).\n parentMessageId: threadMatch?.messageThreadId ?? mapping?.messageThreadId,\n isDraft: false,\n // Stable dedup key so a retried ingest (after a transient failure between\n // compose and the ExternalMessage anchor insert) reuses the message\n // composed by the first attempt instead of duplicating it. Mirrors the\n // (channel, externalMessageId) ExternalMessage anchor's natural key.\n idempotencyKey: m.externalMessageId\n ? `cc:${input.channelId}:${m.externalMessageId}`\n : undefined,\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId,\n userId: await resolveCommunicationChannelsSystemUserId(\n em,\n input.scope.tenantId,\n mapping?.assignedUserId ?? null,\n ),\n }\n\n const commandBus = ctx.container.resolve('commandBus') as CommandBus\n const composeResult = await commandBus.execute<typeof composeInput, { id: string; threadId: string | null }>(\n 'messages.messages.compose',\n {\n input: composeInput,\n ctx: passthroughCommandCtx(ctx, input.scope),\n },\n )\n const message = composeResult.result\n if (!message?.id) {\n throw new Error('messages.messages.compose did not return a message id')\n }\n\n // (3 continued) Create or update ChannelThreadMapping now that we have a threadId.\n if (!mapping) {\n mapping = em.create(ChannelThreadMapping, {\n externalConversationId: conversation.id,\n messageThreadId: message.threadId ?? message.id,\n channelId: input.channelId,\n providerKey: input.providerKey,\n externalThreadRef: m.externalConversationId,\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n })\n em.persist(mapping)\n }\n\n // (6) Create ExternalMessage + MessageChannelLink (hub-side records).\n //\n // The PrimaryKey for both uses `defaultRaw: 'gen_random_uuid()'` \u2014 a\n // Postgres-side default that doesn't populate the entity's `id` field\n // until after the INSERT returns. If we let MikroORM generate both, then\n // `em.create(MessageChannelLink, { externalMessageId: externalMessage.id })`\n // reads `undefined` for `externalMessage.id` (it hasn't been flushed yet)\n // and writes NULL to `message_channel_links.external_message_id`,\n // breaking the FK and causing downstream joins to silently return 0 rows.\n //\n // Pre-generating both UUIDs client-side fixes the cross-row reference\n // problem and keeps the single-transaction flush semantics intact.\n const externalMessageRowId = randomUUID()\n const channelLinkRowId = randomUUID()\n const externalMessage = em.create(ExternalMessage, {\n id: externalMessageRowId,\n channelId: input.channelId,\n conversationId: conversation.id,\n externalMessageId: m.externalMessageId,\n direction: 'inbound',\n senderIdentifier: m.senderIdentifier,\n senderDisplayName: m.senderDisplayName ?? null,\n providerTimestamp: m.timestamp,\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n })\n em.persist(externalMessage)\n\n // Spec B: annotate the link with which thread-matcher strategy resolved\n // this message (or `'new-thread'` when matcher returned null and we\n // opened a fresh thread). Surfaced to observability + future UI (\"this\n // thread match is low-confidence \u2014 confirm or move\").\n const matcherAnnotatedMetadata: Record<string, unknown> = {\n ...((m.channelMetadata as Record<string, unknown> | undefined) ?? {}),\n threadMatchStrategy: threadMatch?.matchedBy ?? 'new-thread',\n threadMatchConfidence: threadMatch?.confidence ?? 'low',\n }\n\n const channelLink = em.create(MessageChannelLink, {\n id: channelLinkRowId,\n messageId: message.id,\n externalConversationId: conversation.id,\n externalMessageId: externalMessageRowId,\n providerKey: input.providerKey,\n channelType: input.channelType,\n direction: 'inbound',\n deliveryStatus: 'received',\n channelPayload: m.channelPayload,\n channelContentType: m.channelContentType,\n channelMetadata: matcherAnnotatedMetadata,\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n })\n em.persist(channelLink)\n\n try {\n await em.flush()\n } catch (flushErr) {\n // Concurrency guard: the pre-check at (1) is not atomic with this insert,\n // so a poll re-fetch racing a push notification (or two push deliveries)\n // can both pass the check and reach here. The `(channel_id,\n // external_message_id)` unique index rejects the loser with a 23505. Treat\n // that as a duplicate \u2014 returning here (instead of throwing) prevents the\n // message from being dead-lettered and retried forever. The winning job\n // already recorded the message + link.\n if (isUniqueViolation(flushErr)) {\n return {\n status: 'duplicate',\n externalConversationId: conversation.id,\n externalMessageId: m.externalMessageId,\n }\n }\n throw flushErr\n }\n\n // (7) Emit events \u2014 order matters for downstream subscribers.\n if (conversationCreated) {\n await emitCommunicationChannelsEvent(\n 'communication_channels.conversation.created',\n {\n conversationId: conversation.id,\n channelId: input.channelId,\n providerKey: input.providerKey,\n channelType: input.channelType,\n externalConversationId: m.externalConversationId,\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n },\n { persistent: true },\n )\n }\n if (matchedPersonId) {\n await emitCommunicationChannelsEvent(\n 'communication_channels.contact.resolved',\n {\n conversationId: conversation.id,\n contactPersonId: matchedPersonId,\n providerKey: input.providerKey,\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n },\n { persistent: true },\n )\n }\n await emitCommunicationChannelsEvent(\n 'communication_channels.message.received',\n {\n messageId: message.id,\n externalMessageId: externalMessage.id,\n channelLinkId: channelLink.id,\n conversationId: conversation.id,\n channelId: input.channelId,\n providerKey: input.providerKey,\n channelType: input.channelType,\n direction: 'inbound',\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n },\n { persistent: true },\n )\n\n return {\n status: 'created',\n messageId: message.id,\n externalConversationId: conversation.id,\n externalMessageId: externalMessage.id,\n channelLinkId: channelLink.id,\n threadMappingId: mapping.id,\n contactPersonId: matchedPersonId,\n }\n },\n}\n\n\n/**\n * Build a runtime context for the nested `messages.messages.compose` call.\n *\n * The compose command expects a `CommandRuntimeContext`. For inbound webhook\n * processing there is no platform user; we pass `auth: null` and use the tenant\n * scope from our input.\n */\nfunction passthroughCommandCtx(\n parent: CommandRuntimeContext,\n scope: IngestInboundMessageInput['scope'],\n): CommandRuntimeContext {\n return {\n container: parent.container,\n auth: null,\n organizationScope: null,\n selectedOrganizationId: scope.organizationId ?? null,\n organizationIds: scope.organizationId ? [scope.organizationId] : null,\n }\n}\n\n/**\n * Pull a string value from the provider's `channelMetadata` map. Returns\n * `null` (not `undefined`) when the key is absent or the value isn't a\n * string \u2014 keeps the matcher's input shape predictable.\n */\nfunction extractStringFromMeta(\n meta: Record<string, unknown>,\n key: string,\n): string | null {\n const value = meta[key]\n if (typeof value === 'string' && value.length > 0) return value\n return null\n}\n\n/**\n * Pull a string[] value from the provider's `channelMetadata` map.\n * Filters out non-string entries defensively. Returns an empty array\n * when the key is absent or the value isn't an array.\n */\nfunction extractStringArrayFromMeta(\n meta: Record<string, unknown>,\n key: string,\n): string[] {\n const value = meta[key]\n if (!Array.isArray(value)) return []\n return value.filter((item): item is string => typeof item === 'string')\n}\n\nregisterCommand(ingestInboundMessageCommand)\n\nexport default ingestInboundMessageCommand\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,15 +1,4 @@
1
- import { convert } from "html-to-text";
2
- function htmlToPlainText(html) {
3
- if (!html) return "";
4
- return convert(html, {
5
- wordwrap: false,
6
- preserveNewlines: true,
7
- selectors: [
8
- { selector: "script", format: "skip" },
9
- { selector: "style", format: "skip" }
10
- ]
11
- }).replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
12
- }
1
+ import { htmlToPlainText } from "@open-mercato/shared/lib/html/htmlToPlainText";
13
2
  export {
14
3
  htmlToPlainText
15
4
  };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/inbox_ops/lib/htmlToPlainText.ts"],
4
- "sourcesContent": ["import { convert } from 'html-to-text'\n\nexport function htmlToPlainText(html: string): string {\n if (!html) return ''\n return convert(html, {\n wordwrap: false,\n preserveNewlines: true,\n selectors: [\n { selector: 'script', format: 'skip' },\n { selector: 'style', format: 'skip' },\n ],\n })\n .replace(/\\r\\n/g, '\\n')\n .replace(/\\r/g, '\\n')\n .replace(/\\n{3,}/g, '\\n\\n')\n .trim()\n}\n"],
5
- "mappings": "AAAA,SAAS,eAAe;AAEjB,SAAS,gBAAgB,MAAsB;AACpD,MAAI,CAAC,KAAM,QAAO;AAClB,SAAO,QAAQ,MAAM;AAAA,IACnB,UAAU;AAAA,IACV,kBAAkB;AAAA,IAClB,WAAW;AAAA,MACT,EAAE,UAAU,UAAU,QAAQ,OAAO;AAAA,MACrC,EAAE,UAAU,SAAS,QAAQ,OAAO;AAAA,IACtC;AAAA,EACF,CAAC,EACE,QAAQ,SAAS,IAAI,EACrB,QAAQ,OAAO,IAAI,EACnB,QAAQ,WAAW,MAAM,EACzB,KAAK;AACV;",
4
+ "sourcesContent": ["export { htmlToPlainText } from '@open-mercato/shared/lib/html/htmlToPlainText'\n"],
5
+ "mappings": "AAAA,SAAS,uBAAuB;",
6
6
  "names": []
7
7
  }
@@ -223,6 +223,9 @@ async function GET(req, { params }) {
223
223
  senderUserId: threadMessage.senderUserId,
224
224
  senderName: threadSenderName,
225
225
  senderEmail: sender?.email ?? null,
226
+ externalName: threadMessage.externalName ?? null,
227
+ externalEmail: threadMessage.externalEmail ?? null,
228
+ sourceEntityType: threadMessage.sourceEntityType ?? null,
226
229
  body: threadMessage.body,
227
230
  bodyFormat: threadMessage.bodyFormat,
228
231
  sentAt: threadMessage.sentAt
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/messages/api/%5Bid%5D/route.ts"],
4
- "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CommandBus } from '@open-mercato/shared/lib/commands/command-bus'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi/types'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { applyResponseEnricherToRecord } from '@open-mercato/shared/lib/crud/enricher-runner'\nimport { enforceCommandOptimisticLock } from '@open-mercato/shared/lib/crud/optimistic-lock-command'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { User } from '../../../auth/data/entities'\nimport { Message, MessageObject, MessageRecipient } from '../../data/entities'\nimport { updateDraftSchema } from '../../data/validators'\nimport { buildResolvedMessageActions } from '../../lib/actions'\nimport { MESSAGE_OPTIMISTIC_LOCK_RESOURCE_KIND } from '../../lib/constants'\nimport { getMessageObjectType } from '../../lib/message-objects-registry'\nimport { getMessageTypeOrDefault } from '../../lib/message-types-registry'\nimport { attachOperationMetadataHeader } from '../../lib/operationMetadata'\nimport { hasOrganizationAccess, resolveMessageContext } from '../../lib/routeHelpers'\nimport { resolveUserFeatures, runMessageMutationGuardAfterSuccess, runMessageMutationGuards } from '../guards'\nimport {\n errorResponseSchema,\n messageDetailResponseSchema,\n okResponseSchema,\n updateDraftSchema as updateDraftOpenApiSchema,\n} from '../openapi'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('messages').child({ component: 'api' })\n\nexport const metadata = {\n GET: { requireAuth: true },\n PATCH: { requireAuth: true, requireFeatures: ['messages.compose'] },\n DELETE: { requireAuth: true, requireFeatures: ['messages.view'] },\n}\n\ntype MessageObjectPreviewPayload = {\n title: string\n subtitle?: string\n status?: string\n statusColor?: string\n metadata?: Record<string, string>\n} | null\n\ntype RbacServiceLike = {\n getGrantedFeatures?: (\n userId: string,\n scope: { tenantId: string | null; organizationId: string | null },\n ) => Promise<string[]>\n}\n\n/**\n * Feature list for the enricher ACL gate, resolved from RBAC.\n *\n * Mirrors `customers/api/interactions/route.ts`. Returning `undefined` on\n * failure is deliberate \u2014 the runner then treats every feature-gated enricher\n * as denied, which is the safe direction.\n */\nasync function resolveGrantedFeatures(\n container: { resolve: (name: string) => unknown },\n scope: { tenantId: string; organizationId: string | null; userId: string },\n): Promise<string[] | undefined> {\n try {\n const rbac = container.resolve('rbacService') as RbacServiceLike | undefined\n if (!rbac?.getGrantedFeatures) return undefined\n return await rbac.getGrantedFeatures(scope.userId, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n })\n } catch {\n return undefined\n }\n}\n\nexport async function GET(req: Request, { params }: { params: { id: string } }) {\n const { ctx, scope } = await resolveMessageContext(req)\n const em = ctx.container.resolve('em') as EntityManager\n const url = new URL(req.url)\n const skipMarkReadParam = url.searchParams.get('skipMarkRead')\n const skipMarkRead = skipMarkReadParam === '1'\n\n const message = await findOneWithDecryption(\n em,\n Message,\n {\n id: params.id,\n tenantId: scope.tenantId,\n deletedAt: null,\n },\n undefined,\n { tenantId: scope.tenantId, organizationId: scope.organizationId },\n )\n\n if (!message) {\n return Response.json({ error: 'Message not found' }, { status: 404 })\n }\n\n if (!hasOrganizationAccess(scope.organizationId, message.organizationId)) {\n return Response.json({ error: 'Access denied' }, { status: 403 })\n }\n\n const recipient = await em.findOne(MessageRecipient, {\n messageId: params.id,\n recipientUserId: scope.userId,\n deletedAt: null,\n })\n\n const isSender = message.senderUserId === scope.userId\n const isRecipient = Boolean(recipient)\n\n if (!isSender && !isRecipient) {\n return Response.json({ error: 'Access denied' }, { status: 403 })\n }\n\n const autoMarkRead = !skipMarkRead && recipient?.status === 'unread'\n const readAt = autoMarkRead ? new Date() : recipient?.readAt ?? null\n\n const objects = await em.find(MessageObject, { messageId: params.id })\n const objectPreviews = await Promise.all(\n objects.map(async (item): Promise<MessageObjectPreviewPayload> => {\n const objectType = getMessageObjectType(item.entityModule, item.entityType)\n if (!objectType?.loadPreview) return null\n try {\n return await objectType.loadPreview(item.entityId, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n })\n } catch (error) {\n logger.error('Failed to load preview', { entityModule: item.entityModule, entityType: item.entityType, entityId: item.entityId, err: error })\n return null\n }\n }),\n )\n const allRecipients = await em.find(MessageRecipient, { messageId: params.id, deletedAt: null })\n\n const threadMessages = await findWithDecryption(\n em,\n Message,\n {\n threadId: message.threadId ?? message.id,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n deletedAt: null,\n isDraft: false,\n },\n { orderBy: { sentAt: 'ASC' } },\n { tenantId: scope.tenantId, organizationId: scope.organizationId },\n )\n const threadMessageIds = threadMessages.map((item) => item.id)\n const visibleRecipientRows = threadMessageIds.length > 0\n ? await em.find(MessageRecipient, {\n messageId: { $in: threadMessageIds },\n recipientUserId: scope.userId,\n deletedAt: null,\n })\n : []\n const visibleRecipientMessageIds = new Set(visibleRecipientRows.map((item) => item.messageId))\n const actorVisibleThreadMessages = threadMessages.filter((threadMessage) => (\n threadMessage.senderUserId === scope.userId || visibleRecipientMessageIds.has(threadMessage.id)\n ))\n\n const actorRecipientStatusByMessageId = new Map<string, string>()\n for (const row of visibleRecipientRows) {\n actorRecipientStatusByMessageId.set(row.messageId, row.status)\n }\n if (recipient) {\n actorRecipientStatusByMessageId.set(params.id, autoMarkRead ? 'read' : recipient.status)\n }\n const actorRecipientStatuses = Array.from(actorRecipientStatusByMessageId.values())\n const conversationArchived = actorRecipientStatuses.length > 0\n && actorRecipientStatuses.every((status) => status === 'archived')\n const conversationAllUnread = actorRecipientStatuses.length > 0\n && actorRecipientStatuses.every((status) => status === 'unread')\n\n const threadSenderIds = actorVisibleThreadMessages\n .map((threadMessage) => threadMessage.senderUserId)\n .filter((value): value is string => typeof value === 'string' && value.length > 0)\n\n const threadSenders = threadSenderIds.length > 0\n ? await findWithDecryption(\n em,\n User,\n { id: { $in: Array.from(new Set(threadSenderIds)) } },\n undefined,\n { tenantId: scope.tenantId, organizationId: scope.organizationId }\n )\n : []\n\n const threadSenderMap = new Map(threadSenders.map((user) => [user.id, user]))\n\n const senderUser = await findOneWithDecryption(\n em,\n User,\n { id: message.senderUserId },\n undefined,\n { tenantId: scope.tenantId, organizationId: scope.organizationId }\n )\n\n const senderName = typeof senderUser?.name === 'string' && senderUser.name.trim().length\n ? senderUser.name.trim()\n : null\n const senderEmail = senderUser?.email ?? null\n\n const messageType = getMessageTypeOrDefault(message.type)\n const resolvedActionData = buildResolvedMessageActions(message, objects)\n\n if (autoMarkRead) {\n const commandBus = ctx.container.resolve('commandBus') as CommandBus\n await commandBus.execute('messages.recipients.mark_read', {\n input: {\n messageId: params.id,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n userId: scope.userId,\n },\n ctx: {\n container: ctx.container,\n auth: ctx.auth ?? null,\n organizationScope: null,\n selectedOrganizationId: scope.organizationId,\n organizationIds: scope.organizationId ? [scope.organizationId] : null,\n request: req,\n },\n })\n }\n\n const detail: Record<string, unknown> = {\n id: message.id,\n updatedAt: message.updatedAt ?? null,\n type: message.type,\n isDraft: message.isDraft,\n canEditDraft: message.isDraft && message.senderUserId === scope.userId,\n canArchive: Boolean(recipient),\n isArchived: recipient?.status === 'archived',\n visibility: message.visibility,\n sourceEntityType: message.sourceEntityType,\n sourceEntityId: message.sourceEntityId,\n externalEmail: message.externalEmail,\n externalName: message.externalName,\n typeDefinition: {\n labelKey: messageType.labelKey,\n icon: messageType.icon,\n color: messageType.color,\n allowReply: messageType.allowReply ?? true,\n allowForward: messageType.allowForward ?? true,\n ui: {\n listItemComponent: messageType.ui?.listItemComponent ?? null,\n contentComponent: messageType.ui?.contentComponent ?? null,\n actionsComponent: messageType.ui?.actionsComponent ?? null,\n },\n },\n threadId: message.threadId,\n parentMessageId: message.parentMessageId,\n senderUserId: message.senderUserId,\n senderName,\n senderEmail,\n subject: message.subject,\n body: message.body,\n bodyFormat: message.bodyFormat,\n priority: message.priority,\n sentAt: message.sentAt,\n actionData: resolvedActionData,\n actionTaken: message.actionTaken,\n actionTakenAt: message.actionTakenAt,\n actionTakenByUserId: message.actionTakenByUserId,\n recipients: allRecipients.map((item) => ({\n userId: item.recipientUserId,\n type: item.recipientType,\n status: autoMarkRead && item.recipientUserId === scope.userId ? 'read' : item.status,\n readAt: autoMarkRead && item.recipientUserId === scope.userId ? readAt : item.readAt,\n })),\n objects: objects.map((item, index) => ({\n id: item.id,\n entityModule: item.entityModule,\n entityType: item.entityType,\n entityId: item.entityId,\n actionRequired: item.actionRequired,\n actionType: item.actionType,\n actionLabel: item.actionLabel,\n snapshot: item.entitySnapshot,\n preview: objectPreviews[index] ?? null,\n })),\n thread: actorVisibleThreadMessages.map((threadMessage) => {\n const sender = threadSenderMap.get(threadMessage.senderUserId)\n const threadSenderName = typeof sender?.name === 'string' && sender.name.trim().length\n ? sender.name.trim()\n : null\n\n return {\n id: threadMessage.id,\n senderUserId: threadMessage.senderUserId,\n senderName: threadSenderName,\n senderEmail: sender?.email ?? null,\n body: threadMessage.body,\n bodyFormat: threadMessage.bodyFormat,\n sentAt: threadMessage.sentAt,\n }\n }),\n isRead: recipient ? (autoMarkRead || recipient.status !== 'unread') : true,\n conversationArchived,\n conversationAllUnread,\n }\n\n // Mirrors `enrichSingleRecord` in `shared/lib/crud/factory.ts`, which every\n // `makeCrudRoute` host gets for free: same `_meta` envelope, and no blanket\n // catch so a `critical: true` enricher still surfaces as an HTTP error.\n // Non-critical failures never escape the runner \u2014 it merges `fallback` and\n // records the id in `_meta.enricherErrors`.\n const enriched = await applyResponseEnricherToRecord(detail, 'messages.message', {\n organizationId: scope.organizationId ?? '',\n tenantId: scope.tenantId,\n userId: scope.userId,\n em,\n container: ctx.container,\n userFeatures: await resolveGrantedFeatures(ctx.container, scope),\n })\n\n if (enriched._meta.enrichedBy.length > 0 || enriched._meta.enricherErrors?.length) {\n return Response.json({ ...enriched.record, _meta: enriched._meta })\n }\n\n return Response.json(enriched.record)\n}\n\nexport async function PATCH(req: Request, { params }: { params: { id: string } }) {\n const { ctx, scope } = await resolveMessageContext(req)\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const commandBus = ctx.container.resolve('commandBus') as CommandBus\n const body = await req.json().catch(() => ({}))\n const input = updateDraftSchema.parse(body)\n\n const message = await em.findOne(Message, {\n id: params.id,\n tenantId: scope.tenantId,\n deletedAt: null,\n })\n\n if (!message) {\n return Response.json({ error: 'Message not found' }, { status: 404 })\n }\n\n if (!hasOrganizationAccess(scope.organizationId, message.organizationId)) {\n return Response.json({ error: 'Access denied' }, { status: 403 })\n }\n\n if (message.senderUserId !== scope.userId) {\n return Response.json({ error: 'Access denied' }, { status: 403 })\n }\n\n if (!message.isDraft) {\n return Response.json({ error: 'Only draft messages can be edited' }, { status: 409 })\n }\n\n const guardResult = await runMessageMutationGuards(\n ctx.container,\n {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n userId: scope.userId,\n resourceKind: 'messages.message',\n resourceId: message.id,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: input as Record<string, unknown>,\n },\n resolveUserFeatures(ctx.auth),\n )\n if (!guardResult.ok) {\n return Response.json(\n guardResult.errorBody ?? { error: 'Operation blocked by guard' },\n { status: guardResult.errorStatus ?? 422 },\n )\n }\n\n try {\n // Reject stale draft edits/sends before mutating: compare the client's\n // expected version (extension header) against the loaded draft.\n enforceCommandOptimisticLock({\n resourceKind: MESSAGE_OPTIMISTIC_LOCK_RESOURCE_KIND,\n resourceId: message.id,\n current: message.updatedAt,\n request: req,\n })\n\n const { logEntry } = await commandBus.execute('messages.messages.update_draft', {\n input: {\n ...input,\n messageId: message.id,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n userId: scope.userId,\n },\n ctx: {\n container: ctx.container,\n auth: ctx.auth ?? null,\n organizationScope: null,\n selectedOrganizationId: scope.organizationId,\n organizationIds: scope.organizationId ? [scope.organizationId] : null,\n request: req,\n },\n })\n\n const response = Response.json({ ok: true, id: message.id })\n attachOperationMetadataHeader(response, logEntry, {\n resourceKind: 'messages.message',\n resourceId: message.id,\n })\n await runMessageMutationGuardAfterSuccess(guardResult.afterSuccessCallbacks, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n userId: scope.userId,\n resourceKind: 'messages.message',\n resourceId: message.id,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n })\n return response\n } catch (error) {\n if (isCrudHttpError(error)) {\n return Response.json(error.body, { status: error.status })\n }\n if (error instanceof Error) {\n if (error.message === 'Message type cannot be created by users') {\n return Response.json({ error: error.message }, { status: 400 })\n }\n if (error.message === 'Only draft messages can be edited') {\n return Response.json({ error: error.message }, { status: 409 })\n }\n if (error.message === 'Access denied') {\n return Response.json({ error: error.message }, { status: 403 })\n }\n }\n throw error\n }\n\n}\n\nexport async function DELETE(req: Request, { params }: { params: { id: string } }) {\n const { ctx, scope } = await resolveMessageContext(req)\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const commandBus = ctx.container.resolve('commandBus') as CommandBus\n\n const message = await em.findOne(Message, {\n id: params.id,\n tenantId: scope.tenantId,\n deletedAt: null,\n })\n\n if (!message) {\n return Response.json({ error: 'Message not found' }, { status: 404 })\n }\n\n if (!hasOrganizationAccess(scope.organizationId, message.organizationId)) {\n return Response.json({ error: 'Access denied' }, { status: 403 })\n }\n\n const isSender = message.senderUserId === scope.userId\n // Only look the recipient row up when the actor is not the sender: deleting one's own\n // message is the common path, and the lookup is a decryption-aware query.\n const isRecipient = isSender\n ? false\n : Boolean(await findOneWithDecryption(\n em,\n MessageRecipient,\n {\n messageId: params.id,\n recipientUserId: scope.userId,\n deletedAt: null,\n },\n undefined,\n { tenantId: scope.tenantId, organizationId: scope.organizationId },\n ))\n\n if (!isSender && !isRecipient) {\n return Response.json({ error: 'Access denied' }, { status: 403 })\n }\n\n const guardResult = await runMessageMutationGuards(\n ctx.container,\n {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n userId: scope.userId,\n resourceKind: 'messages.message',\n resourceId: params.id,\n operation: 'delete',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: null,\n },\n resolveUserFeatures(ctx.auth),\n )\n if (!guardResult.ok) {\n return Response.json(\n guardResult.errorBody ?? { error: 'Operation blocked by guard' },\n { status: guardResult.errorStatus ?? 422 },\n )\n }\n\n try {\n // Reject a stale delete before mutating: a tab that loaded an older version\n // of the message must refresh rather than silently delete a changed record.\n enforceCommandOptimisticLock({\n resourceKind: MESSAGE_OPTIMISTIC_LOCK_RESOURCE_KIND,\n resourceId: message.id,\n current: message.updatedAt,\n request: req,\n })\n\n const { logEntry } = await commandBus.execute('messages.messages.delete_for_actor', {\n input: {\n messageId: params.id,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n userId: scope.userId,\n },\n ctx: {\n container: ctx.container,\n auth: ctx.auth ?? null,\n organizationScope: null,\n selectedOrganizationId: scope.organizationId,\n organizationIds: scope.organizationId ? [scope.organizationId] : null,\n request: req,\n },\n })\n\n const response = Response.json({ ok: true })\n attachOperationMetadataHeader(response, logEntry, {\n resourceKind: 'messages.message',\n resourceId: params.id,\n })\n await runMessageMutationGuardAfterSuccess(guardResult.afterSuccessCallbacks, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n userId: scope.userId,\n resourceKind: 'messages.message',\n resourceId: params.id,\n operation: 'delete',\n requestMethod: req.method,\n requestHeaders: req.headers,\n })\n return response\n } catch (error) {\n if (isCrudHttpError(error)) {\n return Response.json(error.body, { status: error.status })\n }\n if (error instanceof Error && error.message === 'Access denied') {\n return Response.json({ error: 'Access denied' }, { status: 403 })\n }\n throw error\n }\n\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Messages',\n methods: {\n GET: {\n summary: 'Get message detail',\n responses: [\n {\n status: 200,\n description: 'Message detail with actor-visible thread items only',\n schema: messageDetailResponseSchema,\n },\n ],\n errors: [\n { status: 403, description: 'Access denied', schema: errorResponseSchema },\n { status: 404, description: 'Message not found', schema: errorResponseSchema },\n ],\n },\n PATCH: {\n summary: 'Update draft message',\n requestBody: { schema: updateDraftOpenApiSchema },\n responses: [\n { status: 200, description: 'Draft updated', schema: okResponseSchema },\n ],\n errors: [\n { status: 403, description: 'Access denied', schema: errorResponseSchema },\n { status: 404, description: 'Message not found', schema: errorResponseSchema },\n { status: 409, description: 'Only drafts can be edited, or the draft was modified concurrently (optimistic lock conflict)', schema: errorResponseSchema },\n ],\n },\n DELETE: {\n summary: 'Delete message for current sender/recipient context',\n responses: [\n { status: 200, description: 'Message deleted', schema: okResponseSchema },\n ],\n errors: [\n { status: 403, description: 'Access denied', schema: errorResponseSchema },\n { status: 404, description: 'Message not found', schema: errorResponseSchema },\n { status: 409, description: 'Message was modified concurrently (optimistic lock conflict)', schema: errorResponseSchema },\n ],\n },\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CommandBus } from '@open-mercato/shared/lib/commands/command-bus'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi/types'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { applyResponseEnricherToRecord } from '@open-mercato/shared/lib/crud/enricher-runner'\nimport { enforceCommandOptimisticLock } from '@open-mercato/shared/lib/crud/optimistic-lock-command'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { User } from '../../../auth/data/entities'\nimport { Message, MessageObject, MessageRecipient } from '../../data/entities'\nimport { updateDraftSchema } from '../../data/validators'\nimport { buildResolvedMessageActions } from '../../lib/actions'\nimport { MESSAGE_OPTIMISTIC_LOCK_RESOURCE_KIND } from '../../lib/constants'\nimport { getMessageObjectType } from '../../lib/message-objects-registry'\nimport { getMessageTypeOrDefault } from '../../lib/message-types-registry'\nimport { attachOperationMetadataHeader } from '../../lib/operationMetadata'\nimport { hasOrganizationAccess, resolveMessageContext } from '../../lib/routeHelpers'\nimport { resolveUserFeatures, runMessageMutationGuardAfterSuccess, runMessageMutationGuards } from '../guards'\nimport {\n errorResponseSchema,\n messageDetailResponseSchema,\n okResponseSchema,\n updateDraftSchema as updateDraftOpenApiSchema,\n} from '../openapi'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('messages').child({ component: 'api' })\n\nexport const metadata = {\n GET: { requireAuth: true },\n PATCH: { requireAuth: true, requireFeatures: ['messages.compose'] },\n DELETE: { requireAuth: true, requireFeatures: ['messages.view'] },\n}\n\ntype MessageObjectPreviewPayload = {\n title: string\n subtitle?: string\n status?: string\n statusColor?: string\n metadata?: Record<string, string>\n} | null\n\ntype RbacServiceLike = {\n getGrantedFeatures?: (\n userId: string,\n scope: { tenantId: string | null; organizationId: string | null },\n ) => Promise<string[]>\n}\n\n/**\n * Feature list for the enricher ACL gate, resolved from RBAC.\n *\n * Mirrors `customers/api/interactions/route.ts`. Returning `undefined` on\n * failure is deliberate \u2014 the runner then treats every feature-gated enricher\n * as denied, which is the safe direction.\n */\nasync function resolveGrantedFeatures(\n container: { resolve: (name: string) => unknown },\n scope: { tenantId: string; organizationId: string | null; userId: string },\n): Promise<string[] | undefined> {\n try {\n const rbac = container.resolve('rbacService') as RbacServiceLike | undefined\n if (!rbac?.getGrantedFeatures) return undefined\n return await rbac.getGrantedFeatures(scope.userId, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n })\n } catch {\n return undefined\n }\n}\n\nexport async function GET(req: Request, { params }: { params: { id: string } }) {\n const { ctx, scope } = await resolveMessageContext(req)\n const em = ctx.container.resolve('em') as EntityManager\n const url = new URL(req.url)\n const skipMarkReadParam = url.searchParams.get('skipMarkRead')\n const skipMarkRead = skipMarkReadParam === '1'\n\n const message = await findOneWithDecryption(\n em,\n Message,\n {\n id: params.id,\n tenantId: scope.tenantId,\n deletedAt: null,\n },\n undefined,\n { tenantId: scope.tenantId, organizationId: scope.organizationId },\n )\n\n if (!message) {\n return Response.json({ error: 'Message not found' }, { status: 404 })\n }\n\n if (!hasOrganizationAccess(scope.organizationId, message.organizationId)) {\n return Response.json({ error: 'Access denied' }, { status: 403 })\n }\n\n const recipient = await em.findOne(MessageRecipient, {\n messageId: params.id,\n recipientUserId: scope.userId,\n deletedAt: null,\n })\n\n const isSender = message.senderUserId === scope.userId\n const isRecipient = Boolean(recipient)\n\n if (!isSender && !isRecipient) {\n return Response.json({ error: 'Access denied' }, { status: 403 })\n }\n\n const autoMarkRead = !skipMarkRead && recipient?.status === 'unread'\n const readAt = autoMarkRead ? new Date() : recipient?.readAt ?? null\n\n const objects = await em.find(MessageObject, { messageId: params.id })\n const objectPreviews = await Promise.all(\n objects.map(async (item): Promise<MessageObjectPreviewPayload> => {\n const objectType = getMessageObjectType(item.entityModule, item.entityType)\n if (!objectType?.loadPreview) return null\n try {\n return await objectType.loadPreview(item.entityId, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n })\n } catch (error) {\n logger.error('Failed to load preview', { entityModule: item.entityModule, entityType: item.entityType, entityId: item.entityId, err: error })\n return null\n }\n }),\n )\n const allRecipients = await em.find(MessageRecipient, { messageId: params.id, deletedAt: null })\n\n const threadMessages = await findWithDecryption(\n em,\n Message,\n {\n threadId: message.threadId ?? message.id,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n deletedAt: null,\n isDraft: false,\n },\n { orderBy: { sentAt: 'ASC' } },\n { tenantId: scope.tenantId, organizationId: scope.organizationId },\n )\n const threadMessageIds = threadMessages.map((item) => item.id)\n const visibleRecipientRows = threadMessageIds.length > 0\n ? await em.find(MessageRecipient, {\n messageId: { $in: threadMessageIds },\n recipientUserId: scope.userId,\n deletedAt: null,\n })\n : []\n const visibleRecipientMessageIds = new Set(visibleRecipientRows.map((item) => item.messageId))\n const actorVisibleThreadMessages = threadMessages.filter((threadMessage) => (\n threadMessage.senderUserId === scope.userId || visibleRecipientMessageIds.has(threadMessage.id)\n ))\n\n const actorRecipientStatusByMessageId = new Map<string, string>()\n for (const row of visibleRecipientRows) {\n actorRecipientStatusByMessageId.set(row.messageId, row.status)\n }\n if (recipient) {\n actorRecipientStatusByMessageId.set(params.id, autoMarkRead ? 'read' : recipient.status)\n }\n const actorRecipientStatuses = Array.from(actorRecipientStatusByMessageId.values())\n const conversationArchived = actorRecipientStatuses.length > 0\n && actorRecipientStatuses.every((status) => status === 'archived')\n const conversationAllUnread = actorRecipientStatuses.length > 0\n && actorRecipientStatuses.every((status) => status === 'unread')\n\n const threadSenderIds = actorVisibleThreadMessages\n .map((threadMessage) => threadMessage.senderUserId)\n .filter((value): value is string => typeof value === 'string' && value.length > 0)\n\n const threadSenders = threadSenderIds.length > 0\n ? await findWithDecryption(\n em,\n User,\n { id: { $in: Array.from(new Set(threadSenderIds)) } },\n undefined,\n { tenantId: scope.tenantId, organizationId: scope.organizationId }\n )\n : []\n\n const threadSenderMap = new Map(threadSenders.map((user) => [user.id, user]))\n\n const senderUser = await findOneWithDecryption(\n em,\n User,\n { id: message.senderUserId },\n undefined,\n { tenantId: scope.tenantId, organizationId: scope.organizationId }\n )\n\n const senderName = typeof senderUser?.name === 'string' && senderUser.name.trim().length\n ? senderUser.name.trim()\n : null\n const senderEmail = senderUser?.email ?? null\n\n const messageType = getMessageTypeOrDefault(message.type)\n const resolvedActionData = buildResolvedMessageActions(message, objects)\n\n if (autoMarkRead) {\n const commandBus = ctx.container.resolve('commandBus') as CommandBus\n await commandBus.execute('messages.recipients.mark_read', {\n input: {\n messageId: params.id,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n userId: scope.userId,\n },\n ctx: {\n container: ctx.container,\n auth: ctx.auth ?? null,\n organizationScope: null,\n selectedOrganizationId: scope.organizationId,\n organizationIds: scope.organizationId ? [scope.organizationId] : null,\n request: req,\n },\n })\n }\n\n const detail: Record<string, unknown> = {\n id: message.id,\n updatedAt: message.updatedAt ?? null,\n type: message.type,\n isDraft: message.isDraft,\n canEditDraft: message.isDraft && message.senderUserId === scope.userId,\n canArchive: Boolean(recipient),\n isArchived: recipient?.status === 'archived',\n visibility: message.visibility,\n sourceEntityType: message.sourceEntityType,\n sourceEntityId: message.sourceEntityId,\n externalEmail: message.externalEmail,\n externalName: message.externalName,\n typeDefinition: {\n labelKey: messageType.labelKey,\n icon: messageType.icon,\n color: messageType.color,\n allowReply: messageType.allowReply ?? true,\n allowForward: messageType.allowForward ?? true,\n ui: {\n listItemComponent: messageType.ui?.listItemComponent ?? null,\n contentComponent: messageType.ui?.contentComponent ?? null,\n actionsComponent: messageType.ui?.actionsComponent ?? null,\n },\n },\n threadId: message.threadId,\n parentMessageId: message.parentMessageId,\n senderUserId: message.senderUserId,\n senderName,\n senderEmail,\n subject: message.subject,\n body: message.body,\n bodyFormat: message.bodyFormat,\n priority: message.priority,\n sentAt: message.sentAt,\n actionData: resolvedActionData,\n actionTaken: message.actionTaken,\n actionTakenAt: message.actionTakenAt,\n actionTakenByUserId: message.actionTakenByUserId,\n recipients: allRecipients.map((item) => ({\n userId: item.recipientUserId,\n type: item.recipientType,\n status: autoMarkRead && item.recipientUserId === scope.userId ? 'read' : item.status,\n readAt: autoMarkRead && item.recipientUserId === scope.userId ? readAt : item.readAt,\n })),\n objects: objects.map((item, index) => ({\n id: item.id,\n entityModule: item.entityModule,\n entityType: item.entityType,\n entityId: item.entityId,\n actionRequired: item.actionRequired,\n actionType: item.actionType,\n actionLabel: item.actionLabel,\n snapshot: item.entitySnapshot,\n preview: objectPreviews[index] ?? null,\n })),\n thread: actorVisibleThreadMessages.map((threadMessage) => {\n const sender = threadSenderMap.get(threadMessage.senderUserId)\n const threadSenderName = typeof sender?.name === 'string' && sender.name.trim().length\n ? sender.name.trim()\n : null\n\n return {\n id: threadMessage.id,\n senderUserId: threadMessage.senderUserId,\n senderName: threadSenderName,\n senderEmail: sender?.email ?? null,\n externalName: threadMessage.externalName ?? null,\n externalEmail: threadMessage.externalEmail ?? null,\n sourceEntityType: threadMessage.sourceEntityType ?? null,\n body: threadMessage.body,\n bodyFormat: threadMessage.bodyFormat,\n sentAt: threadMessage.sentAt,\n }\n }),\n isRead: recipient ? (autoMarkRead || recipient.status !== 'unread') : true,\n conversationArchived,\n conversationAllUnread,\n }\n\n // Mirrors `enrichSingleRecord` in `shared/lib/crud/factory.ts`, which every\n // `makeCrudRoute` host gets for free: same `_meta` envelope, and no blanket\n // catch so a `critical: true` enricher still surfaces as an HTTP error.\n // Non-critical failures never escape the runner \u2014 it merges `fallback` and\n // records the id in `_meta.enricherErrors`.\n const enriched = await applyResponseEnricherToRecord(detail, 'messages.message', {\n organizationId: scope.organizationId ?? '',\n tenantId: scope.tenantId,\n userId: scope.userId,\n em,\n container: ctx.container,\n userFeatures: await resolveGrantedFeatures(ctx.container, scope),\n })\n\n if (enriched._meta.enrichedBy.length > 0 || enriched._meta.enricherErrors?.length) {\n return Response.json({ ...enriched.record, _meta: enriched._meta })\n }\n\n return Response.json(enriched.record)\n}\n\nexport async function PATCH(req: Request, { params }: { params: { id: string } }) {\n const { ctx, scope } = await resolveMessageContext(req)\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const commandBus = ctx.container.resolve('commandBus') as CommandBus\n const body = await req.json().catch(() => ({}))\n const input = updateDraftSchema.parse(body)\n\n const message = await em.findOne(Message, {\n id: params.id,\n tenantId: scope.tenantId,\n deletedAt: null,\n })\n\n if (!message) {\n return Response.json({ error: 'Message not found' }, { status: 404 })\n }\n\n if (!hasOrganizationAccess(scope.organizationId, message.organizationId)) {\n return Response.json({ error: 'Access denied' }, { status: 403 })\n }\n\n if (message.senderUserId !== scope.userId) {\n return Response.json({ error: 'Access denied' }, { status: 403 })\n }\n\n if (!message.isDraft) {\n return Response.json({ error: 'Only draft messages can be edited' }, { status: 409 })\n }\n\n const guardResult = await runMessageMutationGuards(\n ctx.container,\n {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n userId: scope.userId,\n resourceKind: 'messages.message',\n resourceId: message.id,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: input as Record<string, unknown>,\n },\n resolveUserFeatures(ctx.auth),\n )\n if (!guardResult.ok) {\n return Response.json(\n guardResult.errorBody ?? { error: 'Operation blocked by guard' },\n { status: guardResult.errorStatus ?? 422 },\n )\n }\n\n try {\n // Reject stale draft edits/sends before mutating: compare the client's\n // expected version (extension header) against the loaded draft.\n enforceCommandOptimisticLock({\n resourceKind: MESSAGE_OPTIMISTIC_LOCK_RESOURCE_KIND,\n resourceId: message.id,\n current: message.updatedAt,\n request: req,\n })\n\n const { logEntry } = await commandBus.execute('messages.messages.update_draft', {\n input: {\n ...input,\n messageId: message.id,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n userId: scope.userId,\n },\n ctx: {\n container: ctx.container,\n auth: ctx.auth ?? null,\n organizationScope: null,\n selectedOrganizationId: scope.organizationId,\n organizationIds: scope.organizationId ? [scope.organizationId] : null,\n request: req,\n },\n })\n\n const response = Response.json({ ok: true, id: message.id })\n attachOperationMetadataHeader(response, logEntry, {\n resourceKind: 'messages.message',\n resourceId: message.id,\n })\n await runMessageMutationGuardAfterSuccess(guardResult.afterSuccessCallbacks, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n userId: scope.userId,\n resourceKind: 'messages.message',\n resourceId: message.id,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n })\n return response\n } catch (error) {\n if (isCrudHttpError(error)) {\n return Response.json(error.body, { status: error.status })\n }\n if (error instanceof Error) {\n if (error.message === 'Message type cannot be created by users') {\n return Response.json({ error: error.message }, { status: 400 })\n }\n if (error.message === 'Only draft messages can be edited') {\n return Response.json({ error: error.message }, { status: 409 })\n }\n if (error.message === 'Access denied') {\n return Response.json({ error: error.message }, { status: 403 })\n }\n }\n throw error\n }\n\n}\n\nexport async function DELETE(req: Request, { params }: { params: { id: string } }) {\n const { ctx, scope } = await resolveMessageContext(req)\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const commandBus = ctx.container.resolve('commandBus') as CommandBus\n\n const message = await em.findOne(Message, {\n id: params.id,\n tenantId: scope.tenantId,\n deletedAt: null,\n })\n\n if (!message) {\n return Response.json({ error: 'Message not found' }, { status: 404 })\n }\n\n if (!hasOrganizationAccess(scope.organizationId, message.organizationId)) {\n return Response.json({ error: 'Access denied' }, { status: 403 })\n }\n\n const isSender = message.senderUserId === scope.userId\n // Only look the recipient row up when the actor is not the sender: deleting one's own\n // message is the common path, and the lookup is a decryption-aware query.\n const isRecipient = isSender\n ? false\n : Boolean(await findOneWithDecryption(\n em,\n MessageRecipient,\n {\n messageId: params.id,\n recipientUserId: scope.userId,\n deletedAt: null,\n },\n undefined,\n { tenantId: scope.tenantId, organizationId: scope.organizationId },\n ))\n\n if (!isSender && !isRecipient) {\n return Response.json({ error: 'Access denied' }, { status: 403 })\n }\n\n const guardResult = await runMessageMutationGuards(\n ctx.container,\n {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n userId: scope.userId,\n resourceKind: 'messages.message',\n resourceId: params.id,\n operation: 'delete',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: null,\n },\n resolveUserFeatures(ctx.auth),\n )\n if (!guardResult.ok) {\n return Response.json(\n guardResult.errorBody ?? { error: 'Operation blocked by guard' },\n { status: guardResult.errorStatus ?? 422 },\n )\n }\n\n try {\n // Reject a stale delete before mutating: a tab that loaded an older version\n // of the message must refresh rather than silently delete a changed record.\n enforceCommandOptimisticLock({\n resourceKind: MESSAGE_OPTIMISTIC_LOCK_RESOURCE_KIND,\n resourceId: message.id,\n current: message.updatedAt,\n request: req,\n })\n\n const { logEntry } = await commandBus.execute('messages.messages.delete_for_actor', {\n input: {\n messageId: params.id,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n userId: scope.userId,\n },\n ctx: {\n container: ctx.container,\n auth: ctx.auth ?? null,\n organizationScope: null,\n selectedOrganizationId: scope.organizationId,\n organizationIds: scope.organizationId ? [scope.organizationId] : null,\n request: req,\n },\n })\n\n const response = Response.json({ ok: true })\n attachOperationMetadataHeader(response, logEntry, {\n resourceKind: 'messages.message',\n resourceId: params.id,\n })\n await runMessageMutationGuardAfterSuccess(guardResult.afterSuccessCallbacks, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n userId: scope.userId,\n resourceKind: 'messages.message',\n resourceId: params.id,\n operation: 'delete',\n requestMethod: req.method,\n requestHeaders: req.headers,\n })\n return response\n } catch (error) {\n if (isCrudHttpError(error)) {\n return Response.json(error.body, { status: error.status })\n }\n if (error instanceof Error && error.message === 'Access denied') {\n return Response.json({ error: 'Access denied' }, { status: 403 })\n }\n throw error\n }\n\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Messages',\n methods: {\n GET: {\n summary: 'Get message detail',\n responses: [\n {\n status: 200,\n description: 'Message detail with actor-visible thread items only',\n schema: messageDetailResponseSchema,\n },\n ],\n errors: [\n { status: 403, description: 'Access denied', schema: errorResponseSchema },\n { status: 404, description: 'Message not found', schema: errorResponseSchema },\n ],\n },\n PATCH: {\n summary: 'Update draft message',\n requestBody: { schema: updateDraftOpenApiSchema },\n responses: [\n { status: 200, description: 'Draft updated', schema: okResponseSchema },\n ],\n errors: [\n { status: 403, description: 'Access denied', schema: errorResponseSchema },\n { status: 404, description: 'Message not found', schema: errorResponseSchema },\n { status: 409, description: 'Only drafts can be edited, or the draft was modified concurrently (optimistic lock conflict)', schema: errorResponseSchema },\n ],\n },\n DELETE: {\n summary: 'Delete message for current sender/recipient context',\n responses: [\n { status: 200, description: 'Message deleted', schema: okResponseSchema },\n ],\n errors: [\n { status: 403, description: 'Access denied', schema: errorResponseSchema },\n { status: 404, description: 'Message not found', schema: errorResponseSchema },\n { status: 409, description: 'Message was modified concurrently (optimistic lock conflict)', schema: errorResponseSchema },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -56,6 +56,9 @@ const messageThreadItemSchema = z.object({
56
56
  senderUserId: z.string().uuid(),
57
57
  senderName: z.string().nullable().optional(),
58
58
  senderEmail: z.string().nullable().optional(),
59
+ externalName: z.string().nullable().optional(),
60
+ externalEmail: z.string().nullable().optional(),
61
+ sourceEntityType: z.string().nullable().optional(),
59
62
  body: z.string(),
60
63
  bodyFormat: z.enum(["text", "markdown"]).optional(),
61
64
  sentAt: z.string().nullable().optional()
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/messages/api/openapi.ts"],
4
- "sourcesContent": ["import { z } from 'zod'\nimport {\n attachmentIdsPayloadSchema,\n composeMessageSchema,\n executeActionSchema,\n forwardMessageSchema,\n listMessagesSchema,\n messageObjectTypesQuerySchema,\n messageActionDataSchema,\n messageActionSchema,\n messageObjectSchema,\n messageRecipientSchema,\n replyMessageSchema,\n unlinkAttachmentPayloadSchema,\n updateDraftSchema,\n} from '../data/validators'\n\nexport const messageAttachmentSchema = z.object({\n id: z.string().uuid(),\n fileName: z.string(),\n fileSize: z.number(),\n mimeType: z.string(),\n url: z.string(),\n})\n\nexport const messageListItemSchema = z.object({\n id: z.string().uuid(),\n type: z.string(),\n visibility: z.enum(['public', 'internal']).nullable().optional(),\n sourceEntityType: z.string().nullable().optional(),\n sourceEntityId: z.string().uuid().nullable().optional(),\n externalEmail: z.string().nullable().optional(),\n externalName: z.string().nullable().optional(),\n subject: z.string(),\n bodyPreview: z.string(),\n senderUserId: z.string().uuid(),\n senderName: z.string().nullable().optional(),\n senderEmail: z.string().nullable().optional(),\n priority: z.string(),\n status: z.string(),\n hasObjects: z.boolean(),\n objectCount: z.number(),\n hasAttachments: z.boolean(),\n attachmentCount: z.number(),\n recipientCount: z.number(),\n hasActions: z.boolean(),\n actionTaken: z.string().nullable().optional(),\n sentAt: z.string().nullable().optional(),\n readAt: z.string().nullable().optional(),\n threadId: z.string().uuid().nullable().optional(),\n})\n\nexport const enrichmentMetaSchema = z.object({\n enrichedBy: z.array(z.string()),\n enricherErrors: z.array(z.string()).optional(),\n})\n\nexport const messageThreadItemSchema = z.object({\n id: z.string().uuid(),\n senderUserId: z.string().uuid(),\n senderName: z.string().nullable().optional(),\n senderEmail: z.string().nullable().optional(),\n body: z.string(),\n bodyFormat: z.enum(['text', 'markdown']).optional(),\n sentAt: z.string().nullable().optional(),\n})\n\nexport const messageDetailResponseSchema = z.object({\n id: z.string().uuid(),\n updatedAt: z.string().nullable().optional(),\n type: z.string(),\n isDraft: z.boolean(),\n canEditDraft: z.boolean(),\n canArchive: z.boolean(),\n isArchived: z.boolean(),\n visibility: z.enum(['public', 'internal']).nullable().optional(),\n sourceEntityType: z.string().nullable().optional(),\n sourceEntityId: z.string().uuid().nullable().optional(),\n externalEmail: z.string().nullable().optional(),\n externalName: z.string().nullable().optional(),\n typeDefinition: z.object({\n labelKey: z.string(),\n icon: z.string(),\n color: z.string().optional().nullable(),\n allowReply: z.boolean(),\n allowForward: z.boolean(),\n ui: z.object({\n listItemComponent: z.string().nullable().optional(),\n contentComponent: z.string().nullable().optional(),\n actionsComponent: z.string().nullable().optional(),\n }).nullable().optional(),\n }),\n threadId: z.string().uuid().nullable().optional(),\n parentMessageId: z.string().uuid().nullable().optional(),\n senderUserId: z.string().uuid(),\n senderName: z.string().nullable().optional(),\n senderEmail: z.string().nullable().optional(),\n subject: z.string(),\n body: z.string(),\n bodyFormat: z.enum(['text', 'markdown']),\n priority: z.string(),\n sentAt: z.string().nullable().optional(),\n actionData: messageActionDataSchema.optional().nullable(),\n actionTaken: z.string().nullable().optional(),\n actionTakenAt: z.string().nullable().optional(),\n actionTakenByUserId: z.string().uuid().nullable().optional(),\n recipients: z.array(z.object({\n userId: z.string().uuid(),\n type: z.enum(['to', 'cc', 'bcc']),\n status: z.string(),\n readAt: z.string().nullable().optional(),\n })),\n objects: z.array(z.object({\n id: z.string().uuid(),\n entityModule: z.string(),\n entityType: z.string(),\n entityId: z.string().uuid(),\n actionRequired: z.boolean(),\n actionType: z.string().nullable().optional(),\n actionLabel: z.string().nullable().optional(),\n snapshot: z.record(z.string(), z.unknown()).nullable().optional(),\n preview: z.object({\n title: z.string(),\n subtitle: z.string().optional(),\n status: z.string().optional(),\n statusColor: z.string().optional(),\n metadata: z.record(z.string(), z.string()).optional(),\n }).nullable().optional(),\n })),\n thread: z.array(messageThreadItemSchema),\n isRead: z.boolean(),\n conversationArchived: z.boolean(),\n conversationAllUnread: z.boolean(),\n _channel: z.record(z.string(), z.unknown()).nullable().optional(),\n _channelPayload: z.record(z.string(), z.unknown()).nullable().optional(),\n _channelContact: z.record(z.string(), z.unknown()).nullable().optional(),\n _reactions: z.array(z.record(z.string(), z.unknown())).optional(),\n _meta: enrichmentMetaSchema.optional(),\n})\n // The detail route runs response enrichers for `messages.message`, so any\n // module registering one may contribute further `_`-prefixed keys. Without\n // this the generator emits `additionalProperties: false` and the published\n // spec forbids the very fields the route returns.\n .passthrough()\n\nexport const messageTokenDetailResponseSchema = z.object({\n id: z.string().uuid(),\n type: z.string(),\n subject: z.string(),\n body: z.string(),\n bodyFormat: z.enum(['text', 'markdown']),\n priority: z.enum(['low', 'normal', 'high', 'urgent']),\n senderUserId: z.string().uuid(),\n sentAt: z.string().nullable().optional(),\n actionData: messageActionDataSchema.optional().nullable(),\n actionTaken: z.string().nullable().optional(),\n actionTakenAt: z.string().nullable().optional(),\n actionTakenByUserId: z.string().uuid().nullable().optional(),\n objects: z.array(z.object({\n id: z.string().uuid(),\n entityModule: z.string(),\n entityType: z.string(),\n entityId: z.string().uuid(),\n actionRequired: z.boolean(),\n actionType: z.string().nullable().optional(),\n actionLabel: z.string().nullable().optional(),\n snapshot: z.record(z.string(), z.unknown()).nullable().optional(),\n })),\n requiresAuth: z.boolean(),\n recipientUserId: z.string().uuid(),\n})\n\nexport const messageTokenPreflightResponseSchema = z.object({\n requiresAuth: z.literal(true),\n})\n\nexport const messageTokenResponseSchema = z.union([\n messageTokenDetailResponseSchema,\n messageTokenPreflightResponseSchema,\n])\n\nexport const unreadCountResponseSchema = z.object({\n unreadCount: z.number(),\n})\n\nexport const messageConfirmationResponseSchema = z.object({\n messageId: z.string().uuid(),\n confirmed: z.boolean(),\n confirmedAt: z.string().nullable(),\n confirmedByUserId: z.string().uuid().nullable(),\n})\n\nexport const actionResultResponseSchema = z.object({\n ok: z.boolean(),\n actionId: z.string(),\n result: z.record(z.string(), z.unknown()).optional(),\n})\n\nexport const forwardResponseSchema = z.object({\n id: z.string().uuid(),\n})\n\nexport const forwardPreviewResponseSchema = z.object({\n subject: z.string(),\n body: z.string(),\n})\n\nexport const conversationMutationResponseSchema = z.object({\n ok: z.boolean(),\n affectedCount: z.number().int().min(0),\n})\n\nexport const composeResponseSchema = z.object({\n id: z.string().uuid(),\n threadId: z.string().uuid().nullable().optional(),\n})\n\nexport const messageAttachmentResponseSchema = z.object({\n attachments: z.array(messageAttachmentSchema),\n})\n\nexport const okResponseSchema = z.object({\n ok: z.boolean(),\n id: z.string().uuid().optional(),\n message: z.string().optional(),\n})\n\nexport const errorResponseSchema = z.object({\n code: z.string().optional(),\n error: z.string(),\n})\n\nexport const messageTypeResponseItemSchema = z.object({\n type: z.string(),\n module: z.string(),\n labelKey: z.string(),\n icon: z.string(),\n color: z.string().nullable().optional(),\n allowReply: z.boolean(),\n allowForward: z.boolean(),\n actionsExpireAfterHours: z.number().int().nullable().optional(),\n ui: z.object({\n listItemComponent: z.string().nullable().optional(),\n contentComponent: z.string().nullable().optional(),\n actionsComponent: z.string().nullable().optional(),\n }).nullable().optional(),\n})\n\nexport const messageTypeListResponseSchema = z.object({\n items: z.array(messageTypeResponseItemSchema),\n})\n\nexport const messageObjectTypeActionResponseSchema = z.object({\n id: z.string(),\n labelKey: z.string(),\n variant: z.enum(['default', 'secondary', 'destructive', 'outline']).optional(),\n icon: z.string().optional(),\n commandId: z.string().optional(),\n href: z.string().optional(),\n isTerminal: z.boolean().optional(),\n confirmRequired: z.boolean().optional(),\n confirmMessage: z.string().optional(),\n})\n\nexport const messageObjectTypeResponseItemSchema = z.object({\n module: z.string(),\n entityType: z.string(),\n labelKey: z.string(),\n icon: z.string(),\n actions: z.array(messageObjectTypeActionResponseSchema),\n})\n\nexport const messageObjectTypeListResponseSchema = z.object({\n items: z.array(messageObjectTypeResponseItemSchema),\n})\n\nexport {\n messageActionDataSchema,\n messageActionSchema,\n messageObjectSchema,\n messageRecipientSchema,\n composeMessageSchema,\n listMessagesSchema,\n forwardMessageSchema,\n replyMessageSchema,\n updateDraftSchema,\n executeActionSchema,\n messageObjectTypesQuerySchema,\n attachmentIdsPayloadSchema,\n unlinkAttachmentPayloadSchema,\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { z } from 'zod'\nimport {\n attachmentIdsPayloadSchema,\n composeMessageSchema,\n executeActionSchema,\n forwardMessageSchema,\n listMessagesSchema,\n messageObjectTypesQuerySchema,\n messageActionDataSchema,\n messageActionSchema,\n messageObjectSchema,\n messageRecipientSchema,\n replyMessageSchema,\n unlinkAttachmentPayloadSchema,\n updateDraftSchema,\n} from '../data/validators'\n\nexport const messageAttachmentSchema = z.object({\n id: z.string().uuid(),\n fileName: z.string(),\n fileSize: z.number(),\n mimeType: z.string(),\n url: z.string(),\n})\n\nexport const messageListItemSchema = z.object({\n id: z.string().uuid(),\n type: z.string(),\n visibility: z.enum(['public', 'internal']).nullable().optional(),\n sourceEntityType: z.string().nullable().optional(),\n sourceEntityId: z.string().uuid().nullable().optional(),\n externalEmail: z.string().nullable().optional(),\n externalName: z.string().nullable().optional(),\n subject: z.string(),\n bodyPreview: z.string(),\n senderUserId: z.string().uuid(),\n senderName: z.string().nullable().optional(),\n senderEmail: z.string().nullable().optional(),\n priority: z.string(),\n status: z.string(),\n hasObjects: z.boolean(),\n objectCount: z.number(),\n hasAttachments: z.boolean(),\n attachmentCount: z.number(),\n recipientCount: z.number(),\n hasActions: z.boolean(),\n actionTaken: z.string().nullable().optional(),\n sentAt: z.string().nullable().optional(),\n readAt: z.string().nullable().optional(),\n threadId: z.string().uuid().nullable().optional(),\n})\n\nexport const enrichmentMetaSchema = z.object({\n enrichedBy: z.array(z.string()),\n enricherErrors: z.array(z.string()).optional(),\n})\n\nexport const messageThreadItemSchema = z.object({\n id: z.string().uuid(),\n senderUserId: z.string().uuid(),\n senderName: z.string().nullable().optional(),\n senderEmail: z.string().nullable().optional(),\n externalName: z.string().nullable().optional(),\n externalEmail: z.string().nullable().optional(),\n sourceEntityType: z.string().nullable().optional(),\n body: z.string(),\n bodyFormat: z.enum(['text', 'markdown']).optional(),\n sentAt: z.string().nullable().optional(),\n})\n\nexport const messageDetailResponseSchema = z.object({\n id: z.string().uuid(),\n updatedAt: z.string().nullable().optional(),\n type: z.string(),\n isDraft: z.boolean(),\n canEditDraft: z.boolean(),\n canArchive: z.boolean(),\n isArchived: z.boolean(),\n visibility: z.enum(['public', 'internal']).nullable().optional(),\n sourceEntityType: z.string().nullable().optional(),\n sourceEntityId: z.string().uuid().nullable().optional(),\n externalEmail: z.string().nullable().optional(),\n externalName: z.string().nullable().optional(),\n typeDefinition: z.object({\n labelKey: z.string(),\n icon: z.string(),\n color: z.string().optional().nullable(),\n allowReply: z.boolean(),\n allowForward: z.boolean(),\n ui: z.object({\n listItemComponent: z.string().nullable().optional(),\n contentComponent: z.string().nullable().optional(),\n actionsComponent: z.string().nullable().optional(),\n }).nullable().optional(),\n }),\n threadId: z.string().uuid().nullable().optional(),\n parentMessageId: z.string().uuid().nullable().optional(),\n senderUserId: z.string().uuid(),\n senderName: z.string().nullable().optional(),\n senderEmail: z.string().nullable().optional(),\n subject: z.string(),\n body: z.string(),\n bodyFormat: z.enum(['text', 'markdown']),\n priority: z.string(),\n sentAt: z.string().nullable().optional(),\n actionData: messageActionDataSchema.optional().nullable(),\n actionTaken: z.string().nullable().optional(),\n actionTakenAt: z.string().nullable().optional(),\n actionTakenByUserId: z.string().uuid().nullable().optional(),\n recipients: z.array(z.object({\n userId: z.string().uuid(),\n type: z.enum(['to', 'cc', 'bcc']),\n status: z.string(),\n readAt: z.string().nullable().optional(),\n })),\n objects: z.array(z.object({\n id: z.string().uuid(),\n entityModule: z.string(),\n entityType: z.string(),\n entityId: z.string().uuid(),\n actionRequired: z.boolean(),\n actionType: z.string().nullable().optional(),\n actionLabel: z.string().nullable().optional(),\n snapshot: z.record(z.string(), z.unknown()).nullable().optional(),\n preview: z.object({\n title: z.string(),\n subtitle: z.string().optional(),\n status: z.string().optional(),\n statusColor: z.string().optional(),\n metadata: z.record(z.string(), z.string()).optional(),\n }).nullable().optional(),\n })),\n thread: z.array(messageThreadItemSchema),\n isRead: z.boolean(),\n conversationArchived: z.boolean(),\n conversationAllUnread: z.boolean(),\n _channel: z.record(z.string(), z.unknown()).nullable().optional(),\n _channelPayload: z.record(z.string(), z.unknown()).nullable().optional(),\n _channelContact: z.record(z.string(), z.unknown()).nullable().optional(),\n _reactions: z.array(z.record(z.string(), z.unknown())).optional(),\n _meta: enrichmentMetaSchema.optional(),\n})\n // The detail route runs response enrichers for `messages.message`, so any\n // module registering one may contribute further `_`-prefixed keys. Without\n // this the generator emits `additionalProperties: false` and the published\n // spec forbids the very fields the route returns.\n .passthrough()\n\nexport const messageTokenDetailResponseSchema = z.object({\n id: z.string().uuid(),\n type: z.string(),\n subject: z.string(),\n body: z.string(),\n bodyFormat: z.enum(['text', 'markdown']),\n priority: z.enum(['low', 'normal', 'high', 'urgent']),\n senderUserId: z.string().uuid(),\n sentAt: z.string().nullable().optional(),\n actionData: messageActionDataSchema.optional().nullable(),\n actionTaken: z.string().nullable().optional(),\n actionTakenAt: z.string().nullable().optional(),\n actionTakenByUserId: z.string().uuid().nullable().optional(),\n objects: z.array(z.object({\n id: z.string().uuid(),\n entityModule: z.string(),\n entityType: z.string(),\n entityId: z.string().uuid(),\n actionRequired: z.boolean(),\n actionType: z.string().nullable().optional(),\n actionLabel: z.string().nullable().optional(),\n snapshot: z.record(z.string(), z.unknown()).nullable().optional(),\n })),\n requiresAuth: z.boolean(),\n recipientUserId: z.string().uuid(),\n})\n\nexport const messageTokenPreflightResponseSchema = z.object({\n requiresAuth: z.literal(true),\n})\n\nexport const messageTokenResponseSchema = z.union([\n messageTokenDetailResponseSchema,\n messageTokenPreflightResponseSchema,\n])\n\nexport const unreadCountResponseSchema = z.object({\n unreadCount: z.number(),\n})\n\nexport const messageConfirmationResponseSchema = z.object({\n messageId: z.string().uuid(),\n confirmed: z.boolean(),\n confirmedAt: z.string().nullable(),\n confirmedByUserId: z.string().uuid().nullable(),\n})\n\nexport const actionResultResponseSchema = z.object({\n ok: z.boolean(),\n actionId: z.string(),\n result: z.record(z.string(), z.unknown()).optional(),\n})\n\nexport const forwardResponseSchema = z.object({\n id: z.string().uuid(),\n})\n\nexport const forwardPreviewResponseSchema = z.object({\n subject: z.string(),\n body: z.string(),\n})\n\nexport const conversationMutationResponseSchema = z.object({\n ok: z.boolean(),\n affectedCount: z.number().int().min(0),\n})\n\nexport const composeResponseSchema = z.object({\n id: z.string().uuid(),\n threadId: z.string().uuid().nullable().optional(),\n})\n\nexport const messageAttachmentResponseSchema = z.object({\n attachments: z.array(messageAttachmentSchema),\n})\n\nexport const okResponseSchema = z.object({\n ok: z.boolean(),\n id: z.string().uuid().optional(),\n message: z.string().optional(),\n})\n\nexport const errorResponseSchema = z.object({\n code: z.string().optional(),\n error: z.string(),\n})\n\nexport const messageTypeResponseItemSchema = z.object({\n type: z.string(),\n module: z.string(),\n labelKey: z.string(),\n icon: z.string(),\n color: z.string().nullable().optional(),\n allowReply: z.boolean(),\n allowForward: z.boolean(),\n actionsExpireAfterHours: z.number().int().nullable().optional(),\n ui: z.object({\n listItemComponent: z.string().nullable().optional(),\n contentComponent: z.string().nullable().optional(),\n actionsComponent: z.string().nullable().optional(),\n }).nullable().optional(),\n})\n\nexport const messageTypeListResponseSchema = z.object({\n items: z.array(messageTypeResponseItemSchema),\n})\n\nexport const messageObjectTypeActionResponseSchema = z.object({\n id: z.string(),\n labelKey: z.string(),\n variant: z.enum(['default', 'secondary', 'destructive', 'outline']).optional(),\n icon: z.string().optional(),\n commandId: z.string().optional(),\n href: z.string().optional(),\n isTerminal: z.boolean().optional(),\n confirmRequired: z.boolean().optional(),\n confirmMessage: z.string().optional(),\n})\n\nexport const messageObjectTypeResponseItemSchema = z.object({\n module: z.string(),\n entityType: z.string(),\n labelKey: z.string(),\n icon: z.string(),\n actions: z.array(messageObjectTypeActionResponseSchema),\n})\n\nexport const messageObjectTypeListResponseSchema = z.object({\n items: z.array(messageObjectTypeResponseItemSchema),\n})\n\nexport {\n messageActionDataSchema,\n messageActionSchema,\n messageObjectSchema,\n messageRecipientSchema,\n composeMessageSchema,\n listMessagesSchema,\n forwardMessageSchema,\n replyMessageSchema,\n updateDraftSchema,\n executeActionSchema,\n messageObjectTypesQuerySchema,\n attachmentIdsPayloadSchema,\n unlinkAttachmentPayloadSchema,\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -21,7 +21,8 @@ import { toErrorMessage } from "./message-detail/utils.js";
21
21
  import { useMessagesInboxBulkActions } from "./useMessagesInboxBulkActions.js";
22
22
  import {
23
23
  buildMessagesInboxFilters,
24
- buildMessagesListParams
24
+ buildMessagesListParams,
25
+ buildSenderOptionsFromMessages
25
26
  } from "./inboxFilters.js";
26
27
  import { createLogger } from "@open-mercato/shared/lib/logger";
27
28
  const logger = createLogger("messages").child({ component: "MessagesInboxPageClient" });
@@ -163,18 +164,7 @@ function MessagesInboxPageClient() {
163
164
  }, [mergeSenderOptions, scopeVersion]);
164
165
  React.useEffect(() => {
165
166
  const items = listQuery.data?.items ?? [];
166
- const next = items.flatMap((item) => {
167
- if (typeof item.senderUserId !== "string" || item.senderUserId.trim().length === 0) return [];
168
- const name = typeof item.senderName === "string" && item.senderName.trim().length > 0 ? item.senderName.trim() : null;
169
- const email = typeof item.senderEmail === "string" && item.senderEmail.trim().length > 0 ? item.senderEmail.trim() : null;
170
- const label = name ?? email ?? item.senderUserId;
171
- return [{
172
- value: item.senderUserId,
173
- label,
174
- description: email && email !== label ? email : null
175
- }];
176
- });
177
- mergeSenderOptions(next);
167
+ mergeSenderOptions(buildSenderOptionsFromMessages(items));
178
168
  }, [listQuery.data?.items, mergeSenderOptions]);
179
169
  React.useEffect(() => {
180
170
  senderOptionsScopeRef.current = scopeVersion;