@open-mercato/core 0.7.1-develop.7150.1.c1941e0c22 → 0.7.1-develop.7151.1.00d0391847
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/dist/helpers/integration/communicationChannelsFixtures.js +77 -1
- package/dist/helpers/integration/communicationChannelsFixtures.js.map +2 -2
- package/dist/helpers/integration/crmFixtures.js +3 -0
- package/dist/helpers/integration/crmFixtures.js.map +2 -2
- package/dist/modules/auth/api/reset.js +7 -1
- package/dist/modules/auth/api/reset.js.map +2 -2
- package/dist/modules/auth/api/users/resend-invite/route.js +7 -1
- package/dist/modules/auth/api/users/resend-invite/route.js.map +2 -2
- package/dist/modules/auth/commands/users.js +7 -1
- package/dist/modules/auth/commands/users.js.map +2 -2
- package/dist/modules/communication_channels/api/post/test-seed/route.js +101 -26
- package/dist/modules/communication_channels/api/post/test-seed/route.js.map +3 -3
- package/dist/modules/communication_channels/di.js +8 -0
- package/dist/modules/communication_channels/di.js.map +2 -2
- package/dist/modules/communication_channels/lib/ensure-system-email-channel.js +57 -0
- package/dist/modules/communication_channels/lib/ensure-system-email-channel.js.map +7 -0
- package/dist/modules/communication_channels/lib/system-email-provider-config.js +32 -0
- package/dist/modules/communication_channels/lib/system-email-provider-config.js.map +7 -0
- package/dist/modules/communication_channels/lib/system-email.js +190 -0
- package/dist/modules/communication_channels/lib/system-email.js.map +7 -0
- package/dist/modules/communication_channels/lib/test-seed.js +130 -1
- package/dist/modules/communication_channels/lib/test-seed.js.map +2 -2
- package/dist/modules/communication_channels/setup.js +56 -1
- package/dist/modules/communication_channels/setup.js.map +2 -2
- package/dist/modules/customer_accounts/api/admin/users-invite.js +1 -0
- package/dist/modules/customer_accounts/api/admin/users-invite.js.map +2 -2
- package/dist/modules/customer_accounts/api/portal/users-invite.js +1 -0
- package/dist/modules/customer_accounts/api/portal/users-invite.js.map +2 -2
- package/dist/modules/customer_accounts/api/signup.js +8 -4
- package/dist/modules/customer_accounts/api/signup.js.map +2 -2
- package/dist/modules/customer_accounts/lib/invitationEmail.js +3 -1
- package/dist/modules/customer_accounts/lib/invitationEmail.js.map +2 -2
- package/dist/modules/customers/api/interactions/route.js +3 -0
- package/dist/modules/customers/api/interactions/route.js.map +2 -2
- package/dist/modules/messages/lib/email-sender.js +16 -14
- package/dist/modules/messages/lib/email-sender.js.map +2 -2
- package/dist/modules/notifications/lib/strategies/email-delivery-strategy.js +3 -1
- package/dist/modules/notifications/lib/strategies/email-delivery-strategy.js.map +2 -2
- package/dist/modules/sales/api/quotes/accept/route.js +3 -1
- package/dist/modules/sales/api/quotes/accept/route.js.map +2 -2
- package/dist/modules/sales/api/quotes/send/route.js +13 -4
- package/dist/modules/sales/api/quotes/send/route.js.map +3 -3
- package/package.json +7 -7
- package/src/helpers/integration/communicationChannelsFixtures.ts +129 -0
- package/src/helpers/integration/crmFixtures.ts +4 -1
- package/src/modules/auth/api/reset.ts +7 -1
- package/src/modules/auth/api/users/resend-invite/route.ts +7 -1
- package/src/modules/auth/commands/users.ts +7 -1
- package/src/modules/auth/i18n/de.json +4 -0
- package/src/modules/auth/i18n/en.json +4 -0
- package/src/modules/auth/i18n/es.json +4 -0
- package/src/modules/auth/i18n/ko.json +4 -0
- package/src/modules/auth/i18n/pl.json +4 -0
- package/src/modules/communication_channels/api/post/test-seed/route.ts +121 -29
- package/src/modules/communication_channels/di.ts +8 -0
- package/src/modules/communication_channels/lib/ensure-system-email-channel.ts +85 -0
- package/src/modules/communication_channels/lib/system-email-provider-config.ts +62 -0
- package/src/modules/communication_channels/lib/system-email.ts +316 -0
- package/src/modules/communication_channels/lib/test-seed.ts +198 -0
- package/src/modules/communication_channels/setup.ts +91 -1
- package/src/modules/customer_accounts/api/admin/users-invite.ts +1 -0
- package/src/modules/customer_accounts/api/portal/users-invite.ts +1 -0
- package/src/modules/customer_accounts/api/signup.ts +6 -2
- package/src/modules/customer_accounts/lib/invitationEmail.ts +3 -0
- package/src/modules/customers/api/interactions/route.ts +4 -0
- package/src/modules/messages/lib/email-sender.ts +18 -16
- package/src/modules/notifications/lib/strategies/email-delivery-strategy.ts +3 -1
- package/src/modules/sales/api/quotes/accept/route.ts +2 -0
- package/src/modules/sales/api/quotes/send/route.ts +16 -3
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/communication_channels/api/post/test-seed/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { CommandBus } from '@open-mercato/shared/lib/commands'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n ChannelThreadMapping,\n CommunicationChannel,\n ExternalConversation,\n MessageChannelLink,\n} from '../../../data/entities'\nimport { ChannelAccessDeniedError, assertCanManageChannel } from '../../../lib/access-control'\nimport {\n COMMUNICATION_CHANNELS_CONNECT_CREDENTIAL_CHANNEL_COMMAND_ID,\n type ConnectCredentialChannelInput,\n type ConnectCredentialChannelResult,\n} from '../../../commands/connect-credential-channel'\nimport { emitCommunicationChannelsEvent } from '../../../events'\nimport {\n TEST_SEED_CHAT_PROVIDER_KEY,\n TEST_SEED_PROVIDER_KEY,\n ensureTestSeedAdapterRegistered,\n isTestChannelSeedingEnabled,\n} from '../../../lib/test-seed'\nimport {\n COMMUNICATION_CHANNELS_INGEST_INBOUND_COMMAND_ID,\n type IngestInboundMessageInput,\n type IngestInboundMessageResult,\n} from '../../../commands/ingest-inbound-message'\n\n/**\n * TEST-ONLY channel seeding endpoint.\n *\n * Gated by `OM_ENABLE_TEST_CHANNEL_SEEDING` \u2014 when the flag is unset (the\n * production default) every request returns 404, so this route is invisible and\n * inert in production. See `lib/test-seed.ts` for the full rationale.\n *\n * Three actions, all scoped to the caller's tenant/org:\n * - `connect-channel`: connect a network-free stub channel owned by the caller\n * (delegates to the real connect-credential command so the channel persists\n * credentials + lands in `status='connected'`). Enables the outbound\n * compose \u2192 deliver \u2192 `.sent` chain to complete in CI. `providerFlavor: 'chat'`\n * connects the non-email stub instead, for tests about sender identity, and\n * `labelAsProviderKey` relabels the connected row so a provider-scoped\n * listing has something to list without a live credential probe.\n * - `ingest-inbound`: run the REAL `ingest_inbound_message` command over an\n * adapter-normalized chat frame, so the platform compose path \u2014 and every\n * validation rule on it \u2014 actually executes. Use this for anything that\n * claims inbound works.\n * - `emit-inbound`: insert an inbound `MessageChannelLink` (+ a `messages.message`\n * row for threading) and emit `communication_channels.message.received` so the\n * customers link-channel-message subscriber runs against real Postgres. Enables\n * the inbound auto-link tests (TC-CRM-EMAIL-002..005). NOTE: this action\n * deliberately bypasses `messages.messages.compose` \u2014 it can never prove that\n * the hub accepts a message, only that downstream subscribers fire.\n */\ntype RbacServiceLike = {\n loadAcl: (\n userId: string,\n scope: { tenantId: string | null; organizationId: string | null },\n ) => Promise<{ isSuperAdmin: boolean; features: string[]; organizations: string[] | null }>\n}\n\nexport const metadata = {\n path: '/communication_channels/test-seed',\n POST: {\n requireAuth: true,\n requireFeatures: ['communication_channels.connect_user_channel'],\n },\n}\n\nconst addressObjectSchema = z.object({ address: z.string(), name: z.string().optional() })\nconst addressFieldSchema = z.union([\n z.string(),\n addressObjectSchema,\n z.array(z.union([z.string(), addressObjectSchema])),\n])\n\nconst connectChannelSchema = z.object({\n action: z.literal('connect-channel'),\n displayName: z.string().min(1).max(255).optional(),\n externalIdentifier: z.string().min(1).max(255).optional(),\n /**\n * Which stub provider to connect. `email` (default) keeps the historical\n * email-shaped channel; `chat` connects a channel whose senders have no\n * address, so a test can exercise the hub's non-email identity contract\n * without inventing one (#4975).\n */\n providerFlavor: z.enum(['email', 'chat']).optional(),\n /**\n * TEST-ONLY: rewrite the connected stub channel's `provider_key` to this value.\n *\n * Routes that a provider package owns filter on their own `provider_key`, and\n * a real channel for those providers cannot be connected in CI \u2014 the Discord\n * adapter's `validateCredentials` performs a live API call against a bot token\n * no test environment has. Without this, a provider-scoped listing has nothing\n * to list and can only be asserted against an empty result, which is exactly\n * the assertion that would have stayed green through #5602.\n *\n * It relabels a row; it registers nothing. The stub adapter is still the only\n * adapter present, so the channel remains network-free \u2014 but no test should\n * drive an outbound send through a relabelled channel, because delivery would\n * then resolve the real provider's adapter.\n */\n labelAsProviderKey: z\n .string()\n .min(1)\n .max(64)\n .regex(/^[a-z0-9_-]+$/)\n .optional(),\n})\n\n/**\n * Drive the REAL `ingest_inbound_message` command with a chat-shaped frame.\n *\n * `emit-inbound` below deliberately bypasses the platform compose path (it\n * inserts the `messages` row with raw SQL) because it only ever needed a\n * landing zone for the CRM-link subscribers. That bypass is why an inbound test\n * could pass while `composeMessageSchema` rejected every real message (#4975).\n * This action takes the opposite approach: it hands the frame to the adapter and\n * the ingest command and asserts nothing on the way, so whatever the hub's\n * contract really is, the test feels it.\n */\nconst ingestInboundSchema = z.object({\n action: z.literal('ingest-inbound'),\n channelId: z.string().uuid(),\n /** Opaque sender handle \u2014 a Discord snowflake, a Slack member id, etc. */\n senderIdentifier: z.string().min(1).max(255),\n senderDisplayName: z.string().max(255).optional(),\n body: z.string().max(50_000).optional(),\n externalMessageId: z.string().min(1).max(255),\n externalConversationId: z.string().min(1).max(255),\n})\n\nconst emitInboundSchema = z.object({\n action: z.literal('emit-inbound'),\n /** Channel that owns the inbound message; controls authorUserId + default visibility. */\n channelId: z.string().uuid(),\n /** Provider key persisted on the link (defaults to the stub provider). */\n providerKey: z.string().min(1).max(64).optional(),\n /** Normalized inbound addresses (stored under channelPayload). */\n from: addressFieldSchema.optional(),\n to: addressFieldSchema.optional(),\n cc: addressFieldSchema.optional(),\n subject: z.string().max(500).optional(),\n bodyText: z.string().max(200_000).optional(),\n /** RFC2822 Message-ID of this inbound message (for In-Reply-To matching). */\n messageId: z.string().max(500).optional(),\n /** RFC2822 In-Reply-To header (threading-inheritance fallback). */\n inReplyTo: z.string().max(500).optional(),\n references: z.array(z.string().max(500)).max(50).optional(),\n /**\n * Open Mercato `messages.message` thread id this inbound message belongs to.\n * When set, a `messages.message` row is created with this `threadId` so the\n * hub-thread inheritance join can resolve a Person from a sibling message.\n */\n messageThreadId: z.string().uuid().optional(),\n /**\n * Test-only: also create a `ChannelThreadMapping` for the seeded thread. The\n * reaction (`/messages/[id]/reactions`) and thread-assign\n * (`/threads/[id]/assign`) routes resolve the owning channel through this\n * mapping and return 409/404 without it. Opt-in so the existing CRM-link\n * seeds (which don't need a mapping) keep their current mapping-free shape.\n */\n createThreadMapping: z.boolean().optional(),\n})\n\nconst bodySchema = z.discriminatedUnion('action', [\n connectChannelSchema,\n ingestInboundSchema,\n emitInboundSchema,\n])\n\nexport async function POST(req: Request): Promise<Response> {\n // Fail-closed: invisible in production. Mirrors an unknown route (404) rather\n // than 403 so the surface leaks nothing when the flag is off.\n if (!isTestChannelSeedingEnabled()) {\n return NextResponse.json({ error: 'Not found' }, { status: 404 })\n }\n\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub || !auth?.tenantId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n let body: z.infer<typeof bodySchema>\n try {\n body = bodySchema.parse(await readJsonSafe(req, null))\n } catch (err) {\n return NextResponse.json(\n { error: err instanceof Error ? err.message : 'Invalid request body' },\n { status: 422 },\n )\n }\n\n const container = await createRequestContainer()\n // Defensive: make sure the stub adapter is registered for this process even if\n // a worker-only node skipped module di registration.\n ensureTestSeedAdapterRegistered()\n\n const tenantId = auth.tenantId as string\n const organizationId = (auth as { orgId?: string | null }).orgId ?? null\n const userId = auth.sub as string\n\n if (body.action === 'connect-channel') {\n const stamp = Date.now()\n const commandBus = container.resolve('commandBus') as CommandBus\n const isChatFlavor = body.providerFlavor === 'chat'\n // A chat channel is deliberately connected WITHOUT an email-ish credential\n // key, so `connect-credential-channel` derives no `externalIdentifier` and\n // the row is shaped exactly like a real Discord channel (identifier NULL \u2014\n // the condition #4977 describes). Inventing one here would recreate the\n // fixture dishonesty that hid #4975.\n const credentials = isChatFlavor\n ? { handle: body.externalIdentifier ?? `test-seed-chat-${stamp}` }\n : {\n username: body.externalIdentifier ?? `test-seed-${stamp}@test-seed.local`,\n fromAddress: body.externalIdentifier ?? `test-seed-${stamp}@test-seed.local`,\n }\n const input: ConnectCredentialChannelInput = {\n providerKey: isChatFlavor ? TEST_SEED_CHAT_PROVIDER_KEY : TEST_SEED_PROVIDER_KEY,\n displayName:\n body.displayName ?? `Test Seed ${isChatFlavor ? 'Chat ' : ''}Channel ${stamp}`,\n credentials,\n userId,\n scope: { tenantId, organizationId },\n }\n const { result } = await commandBus.execute<\n ConnectCredentialChannelInput,\n ConnectCredentialChannelResult\n >(COMMUNICATION_CHANNELS_CONNECT_CREDENTIAL_CHANNEL_COMMAND_ID, {\n input,\n ctx: {\n container,\n auth: auth as never,\n organizationScope: null,\n selectedOrganizationId: organizationId,\n organizationIds: organizationId ? [organizationId] : null,\n },\n })\n if (result.status !== 'connected') {\n return NextResponse.json(\n { error: '[internal] test-seed connect failed', detail: result },\n { status: 500 },\n )\n }\n if (body.labelAsProviderKey && body.labelAsProviderKey !== input.providerKey) {\n const seedEm = (container.resolve('em') as EntityManager).fork()\n const connected = await findOneWithDecryption(\n seedEm,\n CommunicationChannel,\n { id: result.channelId, tenantId },\n undefined,\n { tenantId, organizationId },\n )\n if (!connected) {\n return NextResponse.json(\n { error: '[internal] test-seed could not reload the channel it just connected' },\n { status: 500 },\n )\n }\n connected.providerKey = body.labelAsProviderKey\n await seedEm.flush()\n }\n return NextResponse.json(\n {\n channelId: result.channelId,\n externalIdentifier: result.externalIdentifier,\n providerKey: body.labelAsProviderKey ?? input.providerKey,\n },\n { status: 201 },\n )\n }\n\n // action === 'ingest-inbound' | 'emit-inbound' \u2014 both address an existing channel.\n const em = (container.resolve('em') as EntityManager).fork()\n // Only the `emit-inbound` branch stamps a caller-chosen provider key onto the\n // rows it seeds; `ingest-inbound` takes the channel's own (see below).\n const providerKey =\n body.action === 'emit-inbound' ? body.providerKey ?? TEST_SEED_PROVIDER_KEY : TEST_SEED_PROVIDER_KEY\n\n // `channelId` is caller-supplied, so confirm it names a channel this tenant/org\n // actually owns before seeding rows that reference it. Mirrors the ownership\n // lookup every other per-channel route performs (see channels/[id]/test-send).\n const ownedChannel = await findOneWithDecryption(\n em,\n CommunicationChannel,\n {\n id: body.channelId,\n tenantId,\n organizationId,\n deletedAt: null,\n },\n undefined,\n { tenantId, organizationId },\n )\n if (!ownedChannel) {\n return NextResponse.json({ error: 'Channel not found' }, { status: 404 })\n }\n\n // Tenant/org scope alone does not authorize: `connect_user_channel` is granted\n // broadly, so a same-tenant caller could otherwise seed against a colleague's\n // personal mailbox. Enforce the module's owner-only contract \u2014 personal\n // channels are owner-restricted, shared channels need `manage`.\n let userFeatures: string[] = []\n try {\n const rbac = container.resolve('rbacService') as RbacServiceLike\n const acl = await rbac.loadAcl(userId, { tenantId, organizationId })\n userFeatures = acl?.isSuperAdmin ? ['*'] : Array.isArray(acl?.features) ? acl.features : []\n } catch {\n userFeatures = []\n }\n try {\n assertCanManageChannel(\n { userId: ownedChannel.userId },\n userId,\n userFeatures,\n 'communication_channels.manage',\n )\n } catch (err) {\n if (err instanceof ChannelAccessDeniedError) {\n return NextResponse.json({ error: 'Channel not found' }, { status: 404 })\n }\n const status = (err as { statusCode?: number }).statusCode ?? 403\n return NextResponse.json(\n { error: err instanceof Error ? err.message : 'Access denied' },\n { status },\n )\n }\n\n if (body.action === 'ingest-inbound') {\n // Everything below this point is the real path: the adapter normalizes the\n // frame and `ingest_inbound_message` composes the platform message through\n // `messages.messages.compose`. Nothing is short-circuited, so a hub contract\n // the provider cannot satisfy surfaces here as a failure instead of hiding\n // behind seeded rows (#4975).\n // The channel's own provider key, never a hardcoded one: ingest does not\n // check that `providerKey` matches the channel it names, so passing a\n // different one would silently stamp the wrong provider onto the link.\n const channelProviderKey = ownedChannel.providerKey\n if (channelProviderKey !== TEST_SEED_CHAT_PROVIDER_KEY) {\n return NextResponse.json(\n {\n error:\n 'ingest-inbound requires a channel connected with providerFlavor: \"chat\"; ' +\n `channel ${body.channelId} is '${channelProviderKey}'`,\n },\n { status: 422 },\n )\n }\n\n const commandBus = container.resolve('commandBus') as CommandBus\n const adapterRegistry = container.resolve('channelAdapterRegistry') as {\n get: (key: string) => { normalizeInbound: (raw: unknown) => Promise<unknown> } | undefined\n }\n const adapter = adapterRegistry.get(channelProviderKey)\n if (!adapter) {\n return NextResponse.json(\n { error: '[internal] test-seed chat adapter is not registered' },\n { status: 500 },\n )\n }\n\n const normalized = await adapter.normalizeInbound({\n raw: {\n externalMessageId: body.externalMessageId,\n externalConversationId: body.externalConversationId,\n senderIdentifier: body.senderIdentifier,\n senderDisplayName: body.senderDisplayName,\n body: body.body ?? '',\n },\n eventType: 'message',\n metadata: {},\n })\n\n const ingestInput = {\n channelId: body.channelId,\n providerKey: channelProviderKey,\n channelType: ownedChannel.channelType,\n scope: { tenantId, organizationId },\n message: normalized,\n } as IngestInboundMessageInput\n\n const { result } = await commandBus.execute<\n IngestInboundMessageInput,\n IngestInboundMessageResult\n >(COMMUNICATION_CHANNELS_INGEST_INBOUND_COMMAND_ID, {\n input: ingestInput,\n ctx: {\n container,\n auth: auth as never,\n organizationScope: null,\n selectedOrganizationId: organizationId,\n organizationIds: organizationId ? [organizationId] : null,\n },\n })\n\n return NextResponse.json(\n {\n status: result.status,\n messageId: result.messageId ?? null,\n conversationId: result.externalConversationId ?? null,\n channelLinkId: result.channelLinkId ?? null,\n channelType: ownedChannel.channelType,\n },\n { status: 201 },\n )\n }\n\n // A MessageChannelLink requires a non-null external_conversation_id (FK) and\n // message_id. Create a synthetic conversation + (optionally threaded) message\n // so the link is shaped like a real inbound row the subscriber can consume.\n const conversation = em.create(ExternalConversation, {\n channelId: body.channelId,\n externalConversationId: `inbound-seed:${Date.now()}:${Math.random().toString(16).slice(2, 8)}`,\n subject: body.subject ?? null,\n tenantId,\n organizationId,\n lastMessageAt: new Date(),\n })\n em.persist(conversation)\n await em.flush()\n\n // Insert the platform `messages.message` row via raw SQL rather than importing\n // the messages module's entity class (cross-module ORM coupling rule). Only\n // `thread_id` matters for the hub-thread inheritance join (TC-CRM-EMAIL-005);\n // the rest satisfy NOT NULL constraints.\n const messageRows = (await em.getConnection().execute(\n `INSERT INTO messages\n (type, thread_id, sender_user_id, subject, body, body_format, priority, status,\n is_draft, sent_at, visibility, source_entity_type, source_entity_id,\n tenant_id, organization_id, created_at, updated_at)\n VALUES\n (?, ?, ?, ?, ?, 'text', 'normal', 'sent',\n false, now(), 'public', 'communication_channels.test_seed_inbound', ?,\n ?, ?, now(), now())\n RETURNING id`,\n [\n `channel.${providerKey}`,\n body.messageThreadId ?? null,\n userId,\n body.subject ?? '(no subject)',\n body.bodyText ?? '',\n body.channelId,\n tenantId,\n organizationId,\n ],\n )) as Array<{ id: string }>\n const messageId = messageRows[0]?.id\n if (!messageId) {\n return NextResponse.json({ error: '[internal] failed to seed message row' }, { status: 500 })\n }\n\n const link = em.create(MessageChannelLink, {\n messageId,\n externalConversationId: conversation.id,\n providerKey,\n channelType: 'email',\n direction: 'inbound',\n deliveryStatus: 'delivered',\n channelPayload: {\n ...(body.from !== undefined ? { from: body.from } : {}),\n ...(body.to !== undefined ? { to: body.to } : {}),\n ...(body.cc !== undefined ? { cc: body.cc } : {}),\n ...(body.subject !== undefined ? { subject: body.subject } : {}),\n ...(body.bodyText !== undefined ? { text: body.bodyText } : {}),\n ...(body.inReplyTo !== undefined ? { inReplyTo: body.inReplyTo } : {}),\n ...(body.references !== undefined ? { references: body.references } : {}),\n },\n channelContentType: 'text/plain',\n channelMetadata: {\n ...(body.messageId !== undefined ? { messageId: body.messageId } : {}),\n },\n tenantId,\n organizationId,\n })\n em.persist(link)\n await em.flush()\n\n // Optionally mirror `ingest-inbound-message`: a real inbound message always\n // lands a ChannelThreadMapping that the reaction + thread-assign routes use to\n // resolve the owning channel. Seeded inbound messages skip it by default; opt\n // in for tests that exercise those routes. Keyed by `messageThreadId ?? messageId`\n // to match how those commands resolve the mapping (`message.threadId ?? message.id`).\n if (body.createThreadMapping) {\n const mapping = em.create(ChannelThreadMapping, {\n externalConversationId: conversation.id,\n messageThreadId: body.messageThreadId ?? messageId,\n channelId: body.channelId,\n providerKey,\n externalThreadRef: conversation.externalConversationId,\n tenantId,\n organizationId,\n })\n em.persist(mapping)\n await em.flush()\n }\n\n // Emit the hub event through the real event bus so the persistent customers\n // link-channel-message-received subscriber is enqueued to the `events` queue.\n await emitCommunicationChannelsEvent(\n 'communication_channels.message.received',\n {\n channelLinkId: link.id,\n channelId: body.channelId,\n providerKey,\n direction: 'inbound',\n tenantId,\n organizationId,\n },\n { persistent: true },\n )\n\n return NextResponse.json(\n { channelLinkId: link.id, messageId, conversationId: conversation.id },\n { status: 201 },\n )\n}\n\nexport const openApi = {\n tags: ['CommunicationChannels'],\n methods: {\n POST: {\n summary:\n 'Test-only: seed a connected channel, ingest a real inbound message, or emit a seeded inbound link (env-gated)',\n tags: ['CommunicationChannels'],\n responses: [\n { status: 201, description: 'Channel seeded / inbound message emitted' },\n { status: 401, description: 'Unauthorized' },\n {\n status: 404,\n description:\n 'Test channel seeding disabled (production default), or the requested channel does not belong to the caller',\n },\n { status: 422, description: 'Invalid request body' },\n { status: 500, description: 'Seed failed' },\n ],\n },\n },\n}\n\nexport default POST\n"],
|
|
5
|
-
"mappings": "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",
|
|
6
|
-
"names": []
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { CommandBus } from '@open-mercato/shared/lib/commands'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n ChannelThreadMapping,\n CommunicationChannel,\n ExternalConversation,\n MessageChannelLink,\n} from '../../../data/entities'\nimport { ChannelAccessDeniedError, assertCanManageChannel } from '../../../lib/access-control'\nimport {\n COMMUNICATION_CHANNELS_CONNECT_CREDENTIAL_CHANNEL_COMMAND_ID,\n type ConnectCredentialChannelInput,\n type ConnectCredentialChannelResult,\n} from '../../../commands/connect-credential-channel'\nimport { emitCommunicationChannelsEvent } from '../../../events'\nimport {\n TEST_SEED_CHAT_PROVIDER_KEY,\n TEST_SEED_PROVIDER_KEY,\n clearTestSeedCapturedMessages,\n createTestSeedPlatformMessage,\n ensureTestSeedAdapterRegistered,\n isTestEmailCaptureAccessAuthorized,\n isTestChannelSeedingEnabled,\n listTestSeedCapturedMessages,\n} from '../../../lib/test-seed'\nimport {\n COMMUNICATION_CHANNELS_INGEST_INBOUND_COMMAND_ID,\n type IngestInboundMessageInput,\n type IngestInboundMessageResult,\n} from '../../../commands/ingest-inbound-message'\n\n/**\n * TEST-ONLY channel seeding endpoint.\n *\n * Gated by `OM_ENABLE_TEST_CHANNEL_SEEDING` \u2014 when the flag is unset (the\n * production default) every request returns 404, so this route is invisible and\n * inert in production. See `lib/test-seed.ts` for the full rationale.\n *\n * Three actions, all scoped to the caller's tenant/org:\n * - `connect-channel`: connect a network-free stub channel owned by the caller\n * (delegates to the real connect-credential command so the channel persists\n * credentials + lands in `status='connected'`). Enables the outbound\n * compose \u2192 deliver \u2192 `.sent` chain to complete in CI. `providerFlavor: 'chat'`\n * connects the non-email stub instead, for tests about sender identity, and\n * `labelAsProviderKey` relabels the connected row so a provider-scoped\n * listing has something to list without a live credential probe.\n * - `ingest-inbound`: run the REAL `ingest_inbound_message` command over an\n * adapter-normalized chat frame, so the platform compose path \u2014 and every\n * validation rule on it \u2014 actually executes. Use this for anything that\n * claims inbound works.\n * - `emit-inbound`: insert an inbound `MessageChannelLink` (+ a `messages.message`\n * row for threading) and emit `communication_channels.message.received` so the\n * customers link-channel-message subscriber runs against real Postgres. Enables\n * the inbound auto-link tests (TC-CRM-EMAIL-002..005). NOTE: this action\n * deliberately bypasses `messages.messages.compose` \u2014 it can never prove that\n * the hub accepts a message, only that downstream subscribers fire.\n */\ntype RbacServiceLike = {\n loadAcl: (\n userId: string,\n scope: { tenantId: string | null; organizationId: string | null },\n ) => Promise<{ isSuperAdmin: boolean; features: string[]; organizations: string[] | null }>\n}\n\nexport const metadata = {\n path: '/communication_channels/test-seed',\n POST: {\n requireAuth: true,\n requireFeatures: ['communication_channels.connect_user_channel'],\n },\n}\n\nconst addressObjectSchema = z.object({ address: z.string(), name: z.string().optional() })\nconst addressFieldSchema = z.union([\n z.string(),\n addressObjectSchema,\n z.array(z.union([z.string(), addressObjectSchema])),\n])\n\nconst connectChannelSchema = z.object({\n action: z.literal('connect-channel'),\n displayName: z.string().min(1).max(255).optional(),\n externalIdentifier: z.string().min(1).max(255).optional(),\n /**\n * Which stub provider to connect. `email` (default) keeps the historical\n * email-shaped channel; `chat` connects a channel whose senders have no\n * address, so a test can exercise the hub's non-email identity contract\n * without inventing one (#4975).\n */\n providerFlavor: z.enum(['email', 'chat']).optional(),\n /**\n * TEST-ONLY: rewrite the connected stub channel's `provider_key` to this value.\n *\n * Routes that a provider package owns filter on their own `provider_key`, and\n * a real channel for those providers cannot be connected in CI \u2014 the Discord\n * adapter's `validateCredentials` performs a live API call against a bot token\n * no test environment has. Without this, a provider-scoped listing has nothing\n * to list and can only be asserted against an empty result, which is exactly\n * the assertion that would have stayed green through #5602.\n *\n * It relabels a row; it registers nothing. The stub adapter is still the only\n * adapter present, so the channel remains network-free \u2014 but no test should\n * drive an outbound send through a relabelled channel, because delivery would\n * then resolve the real provider's adapter.\n */\n labelAsProviderKey: z\n .string()\n .min(1)\n .max(64)\n .regex(/^[a-z0-9_-]+$/)\n .optional(),\n})\n\n/**\n * Drive the REAL `ingest_inbound_message` command with a chat-shaped frame.\n *\n * `emit-inbound` below deliberately bypasses the platform compose path (it\n * inserts the `messages` row with raw SQL) because it only ever needed a\n * landing zone for the CRM-link subscribers. That bypass is why an inbound test\n * could pass while `composeMessageSchema` rejected every real message (#4975).\n * This action takes the opposite approach: it hands the frame to the adapter and\n * the ingest command and asserts nothing on the way, so whatever the hub's\n * contract really is, the test feels it.\n */\nconst ingestInboundSchema = z.object({\n action: z.literal('ingest-inbound'),\n channelId: z.string().uuid(),\n /** Opaque sender handle \u2014 a Discord snowflake, a Slack member id, etc. */\n senderIdentifier: z.string().min(1).max(255),\n senderDisplayName: z.string().max(255).optional(),\n body: z.string().max(50_000).optional(),\n externalMessageId: z.string().min(1).max(255),\n externalConversationId: z.string().min(1).max(255),\n})\n\nconst seedSystemChannelSchema = z.object({\n action: z.literal('seed-system-channel'),\n displayName: z.string().min(1).max(255).optional(),\n externalIdentifier: z.string().min(1).max(255).optional(),\n})\n\nconst clearCaptureSchema = z.object({\n action: z.literal('clear-capture'),\n systemRecipient: z.string().email().max(320).optional(),\n captureCorrelationToken: z.string().min(32).max(512).optional(),\n})\n\nconst listCaptureSchema = z.object({\n action: z.literal('list-capture'),\n systemRecipient: z.string().email().max(320).optional(),\n captureCorrelationToken: z.string().min(32).max(512).optional(),\n})\n\nconst emitInboundSchema = z.object({\n action: z.literal('emit-inbound'),\n /** Channel that owns the inbound message; controls authorUserId + default visibility. */\n channelId: z.string().uuid(),\n /** Provider key persisted on the link (defaults to the stub provider). */\n providerKey: z.string().min(1).max(64).optional(),\n /** Normalized inbound addresses (stored under channelPayload). */\n from: addressFieldSchema.optional(),\n to: addressFieldSchema.optional(),\n cc: addressFieldSchema.optional(),\n subject: z.string().max(500).optional(),\n bodyText: z.string().max(200_000).optional(),\n /** RFC2822 Message-ID of this inbound message (for In-Reply-To matching). */\n messageId: z.string().max(500).optional(),\n /** RFC2822 In-Reply-To header (threading-inheritance fallback). */\n inReplyTo: z.string().max(500).optional(),\n references: z.array(z.string().max(500)).max(50).optional(),\n /**\n * Open Mercato `messages.message` thread id this inbound message belongs to.\n * When set, a `messages.message` row is created with this `threadId` so the\n * hub-thread inheritance join can resolve a Person from a sibling message.\n */\n messageThreadId: z.string().uuid().optional(),\n /**\n * Test-only: also create a `ChannelThreadMapping` for the seeded thread. The\n * reaction (`/messages/[id]/reactions`) and thread-assign\n * (`/threads/[id]/assign`) routes resolve the owning channel through this\n * mapping and return 409/404 without it. Opt-in so the existing CRM-link\n * seeds (which don't need a mapping) keep their current mapping-free shape.\n */\n createThreadMapping: z.boolean().optional(),\n})\n\nconst bodySchema = z.discriminatedUnion('action', [\n connectChannelSchema,\n ingestInboundSchema,\n emitInboundSchema,\n seedSystemChannelSchema,\n clearCaptureSchema,\n listCaptureSchema,\n])\n\nexport async function POST(req: Request): Promise<Response> {\n // Fail-closed: invisible in production. Mirrors an unknown route (404) rather\n // than 403 so the surface leaks nothing when the flag is off.\n if (!isTestChannelSeedingEnabled()) {\n return NextResponse.json({ error: 'Not found' }, { status: 404 })\n }\n\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub || !auth?.tenantId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n let body: z.infer<typeof bodySchema>\n try {\n body = bodySchema.parse(await readJsonSafe(req, null))\n } catch (err) {\n return NextResponse.json(\n { error: err instanceof Error ? err.message : 'Invalid request body' },\n { status: 422 },\n )\n }\n\n const tenantId = auth.tenantId as string\n const organizationId = (auth as { orgId?: string | null }).orgId ?? null\n const userId = auth.sub as string\n const captureScope = { tenantId, organizationId }\n\n if (\n (body.action === 'clear-capture' || body.action === 'list-capture')\n && body.systemRecipient\n && (\n !body.captureCorrelationToken\n || !isTestEmailCaptureAccessAuthorized(req.headers.get('x-om-test-email-capture-access-token'))\n )\n ) {\n return NextResponse.json({ error: 'Forbidden' }, { status: 403 })\n }\n\n if (body.action === 'clear-capture') {\n await clearTestSeedCapturedMessages(captureScope, {\n systemRecipient: body.systemRecipient,\n captureCorrelationToken: body.captureCorrelationToken,\n })\n return NextResponse.json({ ok: true })\n }\n\n if (body.action === 'list-capture') {\n return NextResponse.json({\n items: (await listTestSeedCapturedMessages(captureScope, {\n systemRecipient: body.systemRecipient,\n captureCorrelationToken: body.captureCorrelationToken,\n })).map(({ captureCorrelationToken: _captureCorrelationToken, ...message }) => message),\n })\n }\n\n const container = await createRequestContainer()\n // Defensive: make sure the stub adapter is registered for this process even if\n // a worker-only node skipped module di registration.\n ensureTestSeedAdapterRegistered()\n\n if (body.action === 'seed-system-channel') {\n const em = (container.resolve('em') as EntityManager).fork()\n const credentialsService = container.resolve('integrationCredentialsService') as {\n save: (\n integrationId: string,\n credentials: Record<string, unknown>,\n scope: { organizationId: string; tenantId: string; userId?: string | null },\n ) => Promise<void>\n }\n await credentialsService.save(\n `channel_${TEST_SEED_PROVIDER_KEY}`,\n { testSeed: true },\n { tenantId, organizationId: organizationId ?? tenantId, userId: null },\n )\n let channel = await findOneWithDecryption(\n em,\n CommunicationChannel,\n {\n providerKey: TEST_SEED_PROVIDER_KEY,\n channelType: 'email',\n tenantId,\n organizationId,\n userId: null,\n deletedAt: null,\n },\n undefined,\n { tenantId, organizationId },\n )\n if (!channel) {\n const stamp = Date.now()\n channel = em.create(CommunicationChannel, {\n providerKey: TEST_SEED_PROVIDER_KEY,\n channelType: 'email',\n displayName: body.displayName ?? `Test Seed System Email ${stamp}`,\n externalIdentifier: body.externalIdentifier ?? `system-${stamp}@test-seed.local`,\n userId: null,\n isPrimary: false,\n isActive: true,\n status: 'connected',\n tenantId,\n organizationId,\n })\n em.persist(channel)\n await em.flush()\n } else if (!channel.isActive || channel.status !== 'connected') {\n channel.isActive = true\n channel.status = 'connected'\n channel.lastError = null\n await em.flush()\n }\n\n return NextResponse.json({ channelId: channel.id }, { status: 201 })\n }\n\n if (body.action === 'connect-channel') {\n const stamp = Date.now()\n const commandBus = container.resolve('commandBus') as CommandBus\n const isChatFlavor = body.providerFlavor === 'chat'\n // A chat channel is deliberately connected WITHOUT an email-ish credential\n // key, so `connect-credential-channel` derives no `externalIdentifier` and\n // the row is shaped exactly like a real Discord channel (identifier NULL \u2014\n // the condition #4977 describes). Inventing one here would recreate the\n // fixture dishonesty that hid #4975.\n const credentials = isChatFlavor\n ? { handle: body.externalIdentifier ?? `test-seed-chat-${stamp}` }\n : {\n username: body.externalIdentifier ?? `test-seed-${stamp}@test-seed.local`,\n fromAddress: body.externalIdentifier ?? `test-seed-${stamp}@test-seed.local`,\n }\n const input: ConnectCredentialChannelInput = {\n providerKey: isChatFlavor ? TEST_SEED_CHAT_PROVIDER_KEY : TEST_SEED_PROVIDER_KEY,\n displayName:\n body.displayName ?? `Test Seed ${isChatFlavor ? 'Chat ' : ''}Channel ${stamp}`,\n credentials,\n userId,\n scope: { tenantId, organizationId },\n }\n const { result } = await commandBus.execute<\n ConnectCredentialChannelInput,\n ConnectCredentialChannelResult\n >(COMMUNICATION_CHANNELS_CONNECT_CREDENTIAL_CHANNEL_COMMAND_ID, {\n input,\n ctx: {\n container,\n auth: auth as never,\n organizationScope: null,\n selectedOrganizationId: organizationId,\n organizationIds: organizationId ? [organizationId] : null,\n },\n })\n if (result.status !== 'connected') {\n return NextResponse.json(\n { error: '[internal] test-seed connect failed', detail: result },\n { status: 500 },\n )\n }\n if (body.labelAsProviderKey && body.labelAsProviderKey !== input.providerKey) {\n const seedEm = (container.resolve('em') as EntityManager).fork()\n const connected = await findOneWithDecryption(\n seedEm,\n CommunicationChannel,\n { id: result.channelId, tenantId },\n undefined,\n { tenantId, organizationId },\n )\n if (!connected) {\n return NextResponse.json(\n { error: '[internal] test-seed could not reload the channel it just connected' },\n { status: 500 },\n )\n }\n connected.providerKey = body.labelAsProviderKey\n await seedEm.flush()\n }\n return NextResponse.json(\n {\n channelId: result.channelId,\n externalIdentifier: result.externalIdentifier,\n providerKey: body.labelAsProviderKey ?? input.providerKey,\n },\n { status: 201 },\n )\n }\n\n // action === 'ingest-inbound' | 'emit-inbound' \u2014 both address an existing channel.\n const em = (container.resolve('em') as EntityManager).fork()\n // Only the `emit-inbound` branch stamps a caller-chosen provider key onto the\n // rows it seeds; `ingest-inbound` takes the channel's own (see below).\n const providerKey =\n body.action === 'emit-inbound' ? body.providerKey ?? TEST_SEED_PROVIDER_KEY : TEST_SEED_PROVIDER_KEY\n\n // `channelId` is caller-supplied, so confirm it names a channel this tenant/org\n // actually owns before seeding rows that reference it. Mirrors the ownership\n // lookup every other per-channel route performs (see channels/[id]/test-send).\n const ownedChannel = await findOneWithDecryption(\n em,\n CommunicationChannel,\n {\n id: body.channelId,\n tenantId,\n organizationId,\n deletedAt: null,\n },\n undefined,\n { tenantId, organizationId },\n )\n if (!ownedChannel) {\n return NextResponse.json({ error: 'Channel not found' }, { status: 404 })\n }\n\n // Tenant/org scope alone does not authorize: `connect_user_channel` is granted\n // broadly, so a same-tenant caller could otherwise seed against a colleague's\n // personal mailbox. Enforce the module's owner-only contract \u2014 personal\n // channels are owner-restricted, shared channels need `manage`.\n let userFeatures: string[] = []\n try {\n const rbac = container.resolve('rbacService') as RbacServiceLike\n const acl = await rbac.loadAcl(userId, { tenantId, organizationId })\n userFeatures = acl?.isSuperAdmin ? ['*'] : Array.isArray(acl?.features) ? acl.features : []\n } catch {\n userFeatures = []\n }\n try {\n assertCanManageChannel(\n { userId: ownedChannel.userId },\n userId,\n userFeatures,\n 'communication_channels.manage',\n )\n } catch (err) {\n if (err instanceof ChannelAccessDeniedError) {\n return NextResponse.json({ error: 'Channel not found' }, { status: 404 })\n }\n const status = (err as { statusCode?: number }).statusCode ?? 403\n return NextResponse.json(\n { error: err instanceof Error ? err.message : 'Access denied' },\n { status },\n )\n }\n\n if (body.action === 'ingest-inbound') {\n // Everything below this point is the real path: the adapter normalizes the\n // frame and `ingest_inbound_message` composes the platform message through\n // `messages.messages.compose`. Nothing is short-circuited, so a hub contract\n // the provider cannot satisfy surfaces here as a failure instead of hiding\n // behind seeded rows (#4975).\n // The channel's own provider key, never a hardcoded one: ingest does not\n // check that `providerKey` matches the channel it names, so passing a\n // different one would silently stamp the wrong provider onto the link.\n const channelProviderKey = ownedChannel.providerKey\n if (channelProviderKey !== TEST_SEED_CHAT_PROVIDER_KEY) {\n return NextResponse.json(\n {\n error:\n 'ingest-inbound requires a channel connected with providerFlavor: \"chat\"; ' +\n `channel ${body.channelId} is '${channelProviderKey}'`,\n },\n { status: 422 },\n )\n }\n\n const commandBus = container.resolve('commandBus') as CommandBus\n const adapterRegistry = container.resolve('channelAdapterRegistry') as {\n get: (key: string) => { normalizeInbound: (raw: unknown) => Promise<unknown> } | undefined\n }\n const adapter = adapterRegistry.get(channelProviderKey)\n if (!adapter) {\n return NextResponse.json(\n { error: '[internal] test-seed chat adapter is not registered' },\n { status: 500 },\n )\n }\n\n const normalized = await adapter.normalizeInbound({\n raw: {\n externalMessageId: body.externalMessageId,\n externalConversationId: body.externalConversationId,\n senderIdentifier: body.senderIdentifier,\n senderDisplayName: body.senderDisplayName,\n body: body.body ?? '',\n },\n eventType: 'message',\n metadata: {},\n })\n\n const ingestInput = {\n channelId: body.channelId,\n providerKey: channelProviderKey,\n channelType: ownedChannel.channelType,\n scope: { tenantId, organizationId },\n message: normalized,\n } as IngestInboundMessageInput\n\n const { result } = await commandBus.execute<\n IngestInboundMessageInput,\n IngestInboundMessageResult\n >(COMMUNICATION_CHANNELS_INGEST_INBOUND_COMMAND_ID, {\n input: ingestInput,\n ctx: {\n container,\n auth: auth as never,\n organizationScope: null,\n selectedOrganizationId: organizationId,\n organizationIds: organizationId ? [organizationId] : null,\n },\n })\n\n return NextResponse.json(\n {\n status: result.status,\n messageId: result.messageId ?? null,\n conversationId: result.externalConversationId ?? null,\n channelLinkId: result.channelLinkId ?? null,\n channelType: ownedChannel.channelType,\n },\n { status: 201 },\n )\n }\n\n // A MessageChannelLink requires a non-null external_conversation_id (FK) and\n // message_id. Create a synthetic conversation + (optionally threaded) message\n // so the link is shaped like a real inbound row the subscriber can consume.\n const conversation = em.create(ExternalConversation, {\n channelId: body.channelId,\n externalConversationId: `inbound-seed:${Date.now()}:${Math.random().toString(16).slice(2, 8)}`,\n subject: body.subject ?? null,\n tenantId,\n organizationId,\n lastMessageAt: new Date(),\n })\n em.persist(conversation)\n await em.flush()\n\n const messageId = await createTestSeedPlatformMessage(em, {\n providerKey,\n threadId: body.messageThreadId,\n senderUserId: userId,\n subject: body.subject,\n bodyText: body.bodyText,\n channelId: body.channelId,\n tenantId,\n organizationId,\n })\n if (!messageId) {\n return NextResponse.json({ error: '[internal] failed to seed message row' }, { status: 500 })\n }\n\n const link = em.create(MessageChannelLink, {\n messageId,\n externalConversationId: conversation.id,\n providerKey,\n channelType: 'email',\n direction: 'inbound',\n deliveryStatus: 'delivered',\n channelPayload: {\n ...(body.from !== undefined ? { from: body.from } : {}),\n ...(body.to !== undefined ? { to: body.to } : {}),\n ...(body.cc !== undefined ? { cc: body.cc } : {}),\n ...(body.subject !== undefined ? { subject: body.subject } : {}),\n ...(body.bodyText !== undefined ? { text: body.bodyText } : {}),\n ...(body.inReplyTo !== undefined ? { inReplyTo: body.inReplyTo } : {}),\n ...(body.references !== undefined ? { references: body.references } : {}),\n },\n channelContentType: 'text/plain',\n channelMetadata: {\n ...(body.messageId !== undefined ? { messageId: body.messageId } : {}),\n },\n tenantId,\n organizationId,\n })\n em.persist(link)\n await em.flush()\n\n // Optionally mirror `ingest-inbound-message`: a real inbound message always\n // lands a ChannelThreadMapping that the reaction + thread-assign routes use to\n // resolve the owning channel. Seeded inbound messages skip it by default; opt\n // in for tests that exercise those routes. Keyed by `messageThreadId ?? messageId`\n // to match how those commands resolve the mapping (`message.threadId ?? message.id`).\n if (body.createThreadMapping) {\n const mapping = em.create(ChannelThreadMapping, {\n externalConversationId: conversation.id,\n messageThreadId: body.messageThreadId ?? messageId,\n channelId: body.channelId,\n providerKey,\n externalThreadRef: conversation.externalConversationId,\n tenantId,\n organizationId,\n })\n em.persist(mapping)\n await em.flush()\n }\n\n // Emit the hub event through the real event bus so the persistent customers\n // link-channel-message-received subscriber is enqueued to the `events` queue.\n await emitCommunicationChannelsEvent(\n 'communication_channels.message.received',\n {\n channelLinkId: link.id,\n channelId: body.channelId,\n providerKey,\n direction: 'inbound',\n tenantId,\n organizationId,\n },\n { persistent: true },\n )\n\n return NextResponse.json(\n { channelLinkId: link.id, messageId, conversationId: conversation.id },\n { status: 201 },\n )\n}\n\nexport const openApi = {\n tags: ['CommunicationChannels'],\n methods: {\n POST: {\n summary:\n 'Test-only: seed a connected channel, ingest a real inbound message, or emit a seeded inbound link (env-gated)',\n tags: ['CommunicationChannels'],\n responses: [\n { status: 201, description: 'Channel seeded / inbound message emitted' },\n { status: 401, description: 'Unauthorized' },\n {\n status: 404,\n description:\n 'Test channel seeding disabled (production default), or the requested channel does not belong to the caller',\n },\n { status: 422, description: 'Invalid request body' },\n { status: 500, description: 'Seed failed' },\n ],\n },\n },\n}\n\nexport default POST\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["em"]
|
|
7
7
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { asValue } from "awilix";
|
|
2
|
+
import { registerEmailTransport } from "@open-mercato/shared/lib/email/transport";
|
|
2
3
|
import {
|
|
3
4
|
CommunicationChannel,
|
|
4
5
|
ExternalConversation,
|
|
@@ -10,9 +11,15 @@ import {
|
|
|
10
11
|
import { getChannelAdapterRegistry } from "./lib/adapter-registry-singleton.js";
|
|
11
12
|
import { ensureTestSeedAdapterRegistered } from "./lib/test-seed.js";
|
|
12
13
|
import { sendAsUser } from "./lib/send-as-user.js";
|
|
14
|
+
import { isSystemEmailTransportConfigured, sendSystemEmail } from "./lib/system-email.js";
|
|
13
15
|
import { resolveChannelTypeSafely } from "./lib/resolve-channel-type.js";
|
|
14
16
|
function register(container) {
|
|
15
17
|
ensureTestSeedAdapterRegistered();
|
|
18
|
+
registerEmailTransport({
|
|
19
|
+
id: "communication_channels",
|
|
20
|
+
send: (payload) => sendSystemEmail(container, payload),
|
|
21
|
+
isConfigured: isSystemEmailTransportConfigured
|
|
22
|
+
});
|
|
16
23
|
container.register({
|
|
17
24
|
// Entity class registrations (for EntityManager lookups by string)
|
|
18
25
|
CommunicationChannel: asValue(CommunicationChannel),
|
|
@@ -28,6 +35,7 @@ function register(container) {
|
|
|
28
35
|
// In-process send-as-user facade. Cross-module callers (e.g. the customers
|
|
29
36
|
// compose route) resolve this instead of making an HTTP self-call.
|
|
30
37
|
communicationChannelsSendAsUser: asValue(sendAsUser),
|
|
38
|
+
communicationChannelsSendSystemEmail: asValue(sendSystemEmail),
|
|
31
39
|
// Cross-module channel-type lookup. The messages compose route resolves this
|
|
32
40
|
// to decide whether an external correspondent must carry an email address
|
|
33
41
|
// (#4975); absent module or failed lookup reads as "unknown", which the
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/modules/communication_channels/di.ts"],
|
|
4
|
-
"sourcesContent": ["import { asValue } from 'awilix'\nimport type { AppContainer } from '@open-mercato/shared/lib/di/container'\nimport {\n CommunicationChannel,\n ExternalConversation,\n ExternalMessage,\n MessageChannelLink,\n ChannelThreadMapping,\n MessageReaction,\n} from './data/entities'\nimport { getChannelAdapterRegistry } from './lib/adapter-registry-singleton'\nimport { ensureTestSeedAdapterRegistered } from './lib/test-seed'\nimport { sendAsUser } from './lib/send-as-user'\nimport { resolveChannelTypeSafely } from './lib/resolve-channel-type'\n\nexport function register(container: AppContainer) {\n // Test-only: register the network-free stub channel adapter when\n // `OM_ENABLE_TEST_CHANNEL_SEEDING` is set (no-op in production). Lets the\n // integration harness connect a channel + complete the outbound send chain.\n // See lib/test-seed.ts.\n ensureTestSeedAdapterRegistered()\n\n container.register({\n // Entity class registrations (for EntityManager lookups by string)\n CommunicationChannel: asValue(CommunicationChannel),\n ExternalConversation: asValue(ExternalConversation),\n ExternalMessage: asValue(ExternalMessage),\n MessageChannelLink: asValue(MessageChannelLink),\n ChannelThreadMapping: asValue(ChannelThreadMapping),\n MessageReaction: asValue(MessageReaction),\n\n // Channel adapter registry \u2014 process-wide singleton backed by globalThis so\n // the auth-less webhook route resolves the same registry as DI consumers.\n // See lib/adapter-registry-singleton.ts.\n channelAdapterRegistry: asValue(getChannelAdapterRegistry()),\n\n // In-process send-as-user facade. Cross-module callers (e.g. the customers\n // compose route) resolve this instead of making an HTTP self-call.\n communicationChannelsSendAsUser: asValue(sendAsUser),\n\n // Cross-module channel-type lookup. The messages compose route resolves this\n // to decide whether an external correspondent must carry an email address\n // (#4975); absent module or failed lookup reads as \"unknown\", which the\n // messages validator handles fail-closed.\n communicationChannelsResolveChannelType: asValue(resolveChannelTypeSafely),\n })\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,eAAe;AAExB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,iCAAiC;AAC1C,SAAS,uCAAuC;AAChD,SAAS,kBAAkB;AAC3B,SAAS,gCAAgC;AAElC,SAAS,SAAS,WAAyB;AAKhD,kCAAgC;
|
|
4
|
+
"sourcesContent": ["import { asValue } from 'awilix'\nimport type { AppContainer } from '@open-mercato/shared/lib/di/container'\nimport { registerEmailTransport } from '@open-mercato/shared/lib/email/transport'\nimport {\n CommunicationChannel,\n ExternalConversation,\n ExternalMessage,\n MessageChannelLink,\n ChannelThreadMapping,\n MessageReaction,\n} from './data/entities'\nimport { getChannelAdapterRegistry } from './lib/adapter-registry-singleton'\nimport { ensureTestSeedAdapterRegistered } from './lib/test-seed'\nimport { sendAsUser } from './lib/send-as-user'\nimport { isSystemEmailTransportConfigured, sendSystemEmail } from './lib/system-email'\nimport { resolveChannelTypeSafely } from './lib/resolve-channel-type'\n\nexport function register(container: AppContainer) {\n // Test-only: register the network-free stub channel adapter when\n // `OM_ENABLE_TEST_CHANNEL_SEEDING` is set (no-op in production). Lets the\n // integration harness connect a channel + complete the outbound send chain.\n // See lib/test-seed.ts.\n ensureTestSeedAdapterRegistered()\n registerEmailTransport({\n id: 'communication_channels',\n send: (payload) => sendSystemEmail(container, payload),\n isConfigured: isSystemEmailTransportConfigured,\n })\n\n container.register({\n // Entity class registrations (for EntityManager lookups by string)\n CommunicationChannel: asValue(CommunicationChannel),\n ExternalConversation: asValue(ExternalConversation),\n ExternalMessage: asValue(ExternalMessage),\n MessageChannelLink: asValue(MessageChannelLink),\n ChannelThreadMapping: asValue(ChannelThreadMapping),\n MessageReaction: asValue(MessageReaction),\n\n // Channel adapter registry \u2014 process-wide singleton backed by globalThis so\n // the auth-less webhook route resolves the same registry as DI consumers.\n // See lib/adapter-registry-singleton.ts.\n channelAdapterRegistry: asValue(getChannelAdapterRegistry()),\n\n // In-process send-as-user facade. Cross-module callers (e.g. the customers\n // compose route) resolve this instead of making an HTTP self-call.\n communicationChannelsSendAsUser: asValue(sendAsUser),\n communicationChannelsSendSystemEmail: asValue(sendSystemEmail),\n\n // Cross-module channel-type lookup. The messages compose route resolves this\n // to decide whether an external correspondent must carry an email address\n // (#4975); absent module or failed lookup reads as \"unknown\", which the\n // messages validator handles fail-closed.\n communicationChannelsResolveChannelType: asValue(resolveChannelTypeSafely),\n })\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,eAAe;AAExB,SAAS,8BAA8B;AACvC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,iCAAiC;AAC1C,SAAS,uCAAuC;AAChD,SAAS,kBAAkB;AAC3B,SAAS,kCAAkC,uBAAuB;AAClE,SAAS,gCAAgC;AAElC,SAAS,SAAS,WAAyB;AAKhD,kCAAgC;AAChC,yBAAuB;AAAA,IACrB,IAAI;AAAA,IACJ,MAAM,CAAC,YAAY,gBAAgB,WAAW,OAAO;AAAA,IACrD,cAAc;AAAA,EAChB,CAAC;AAED,YAAU,SAAS;AAAA;AAAA,IAEjB,sBAAsB,QAAQ,oBAAoB;AAAA,IAClD,sBAAsB,QAAQ,oBAAoB;AAAA,IAClD,iBAAiB,QAAQ,eAAe;AAAA,IACxC,oBAAoB,QAAQ,kBAAkB;AAAA,IAC9C,sBAAsB,QAAQ,oBAAoB;AAAA,IAClD,iBAAiB,QAAQ,eAAe;AAAA;AAAA;AAAA;AAAA,IAKxC,wBAAwB,QAAQ,0BAA0B,CAAC;AAAA;AAAA;AAAA,IAI3D,iCAAiC,QAAQ,UAAU;AAAA,IACnD,sCAAsC,QAAQ,eAAe;AAAA;AAAA;AAAA;AAAA;AAAA,IAM7D,yCAAyC,QAAQ,wBAAwB;AAAA,EAC3E,CAAC;AACH;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
2
|
+
import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
3
|
+
import { CommunicationChannel } from "../data/entities.js";
|
|
4
|
+
const logger = createLogger("communication_channels");
|
|
5
|
+
async function ensureSystemEmailChannel(em, input) {
|
|
6
|
+
const { tenantId, organizationId, providerKey, externalIdentifier } = input;
|
|
7
|
+
const displayName = input.displayName ?? `${providerKey} system email`;
|
|
8
|
+
const dscope = { tenantId, organizationId };
|
|
9
|
+
try {
|
|
10
|
+
const existing = await findOneWithDecryption(
|
|
11
|
+
em,
|
|
12
|
+
CommunicationChannel,
|
|
13
|
+
{
|
|
14
|
+
providerKey,
|
|
15
|
+
channelType: "email",
|
|
16
|
+
tenantId,
|
|
17
|
+
organizationId,
|
|
18
|
+
userId: null,
|
|
19
|
+
deletedAt: null
|
|
20
|
+
},
|
|
21
|
+
void 0,
|
|
22
|
+
dscope
|
|
23
|
+
);
|
|
24
|
+
if (existing) {
|
|
25
|
+
existing.displayName = displayName;
|
|
26
|
+
existing.externalIdentifier = externalIdentifier;
|
|
27
|
+
if (input.capabilities) existing.capabilities = { ...input.capabilities };
|
|
28
|
+
existing.isActive = true;
|
|
29
|
+
existing.status = "connected";
|
|
30
|
+
existing.lastError = null;
|
|
31
|
+
await em.flush();
|
|
32
|
+
return "updated";
|
|
33
|
+
}
|
|
34
|
+
const channel = em.create(CommunicationChannel, {
|
|
35
|
+
providerKey,
|
|
36
|
+
channelType: "email",
|
|
37
|
+
displayName,
|
|
38
|
+
externalIdentifier,
|
|
39
|
+
capabilities: { ...input.capabilities ?? {} },
|
|
40
|
+
isActive: true,
|
|
41
|
+
status: "connected",
|
|
42
|
+
userId: null,
|
|
43
|
+
pollIntervalSeconds: null,
|
|
44
|
+
tenantId,
|
|
45
|
+
organizationId
|
|
46
|
+
});
|
|
47
|
+
await em.persist(channel).flush();
|
|
48
|
+
return "created";
|
|
49
|
+
} catch (err) {
|
|
50
|
+
logger.warn("Could not ensure system email channel", { err, tenantId, providerKey });
|
|
51
|
+
return "failed";
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
export {
|
|
55
|
+
ensureSystemEmailChannel
|
|
56
|
+
};
|
|
57
|
+
//# sourceMappingURL=ensure-system-email-channel.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/communication_channels/lib/ensure-system-email-channel.ts"],
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { CommunicationChannel } from '../data/entities'\n\nconst logger = createLogger('communication_channels')\n\nexport type EnsureSystemEmailChannelInput = {\n tenantId: string\n organizationId: string\n providerKey: string\n externalIdentifier: string\n displayName?: string\n capabilities?: Record<string, unknown>\n}\n\nexport type EnsureSystemEmailChannelResult = 'created' | 'updated' | 'failed'\n\n/**\n * Idempotently create or refresh a tenant's system email channel row.\n *\n * Shared by the two places that need the same row: the provider env preset, which runs at tenant setup\n * and again on every `yarn mercato seed:defaults --module channel_<provider>` used to migrate tenants\n * that predate the Communications Hub, and the lazy repair in `sendSystemEmail` for a tenant whose\n * credentials were saved through the integrations admin UI (which stores credentials but has no hook\n * to create the channel).\n *\n * Never throws: a channel row is a convenience for the Hub UI and for later lookups, so a failure to\n * write one must not take down the caller \u2014 an email send in particular still has usable credentials.\n */\nexport async function ensureSystemEmailChannel(\n em: EntityManager,\n input: EnsureSystemEmailChannelInput,\n): Promise<EnsureSystemEmailChannelResult> {\n const { tenantId, organizationId, providerKey, externalIdentifier } = input\n const displayName = input.displayName ?? `${providerKey} system email`\n const dscope = { tenantId, organizationId }\n\n try {\n const existing = await findOneWithDecryption(\n em,\n CommunicationChannel,\n {\n providerKey,\n channelType: 'email',\n tenantId,\n organizationId,\n userId: null,\n deletedAt: null,\n },\n undefined,\n dscope,\n )\n\n if (existing) {\n existing.displayName = displayName\n existing.externalIdentifier = externalIdentifier\n if (input.capabilities) existing.capabilities = { ...input.capabilities }\n existing.isActive = true\n existing.status = 'connected'\n existing.lastError = null\n await em.flush()\n return 'updated'\n }\n\n const channel = em.create(CommunicationChannel, {\n providerKey,\n channelType: 'email',\n displayName,\n externalIdentifier,\n capabilities: { ...(input.capabilities ?? {}) },\n isActive: true,\n status: 'connected',\n userId: null,\n pollIntervalSeconds: null,\n tenantId,\n organizationId,\n })\n await em.persist(channel).flush()\n return 'created'\n } catch (err) {\n logger.warn('Could not ensure system email channel', { err, tenantId, providerKey })\n return 'failed'\n }\n}\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,oBAAoB;AAC7B,SAAS,6BAA6B;AACtC,SAAS,4BAA4B;AAErC,MAAM,SAAS,aAAa,wBAAwB;AAyBpD,eAAsB,yBACpB,IACA,OACyC;AACzC,QAAM,EAAE,UAAU,gBAAgB,aAAa,mBAAmB,IAAI;AACtE,QAAM,cAAc,MAAM,eAAe,GAAG,WAAW;AACvD,QAAM,SAAS,EAAE,UAAU,eAAe;AAE1C,MAAI;AACF,UAAM,WAAW,MAAM;AAAA,MACrB;AAAA,MACA;AAAA,MACA;AAAA,QACE;AAAA,QACA,aAAa;AAAA,QACb;AAAA,QACA;AAAA,QACA,QAAQ;AAAA,QACR,WAAW;AAAA,MACb;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAEA,QAAI,UAAU;AACZ,eAAS,cAAc;AACvB,eAAS,qBAAqB;AAC9B,UAAI,MAAM,aAAc,UAAS,eAAe,EAAE,GAAG,MAAM,aAAa;AACxE,eAAS,WAAW;AACpB,eAAS,SAAS;AAClB,eAAS,YAAY;AACrB,YAAM,GAAG,MAAM;AACf,aAAO;AAAA,IACT;AAEA,UAAM,UAAU,GAAG,OAAO,sBAAsB;AAAA,MAC9C;AAAA,MACA,aAAa;AAAA,MACb;AAAA,MACA;AAAA,MACA,cAAc,EAAE,GAAI,MAAM,gBAAgB,CAAC,EAAG;AAAA,MAC9C,UAAU;AAAA,MACV,QAAQ;AAAA,MACR,QAAQ;AAAA,MACR,qBAAqB;AAAA,MACrB;AAAA,MACA;AAAA,IACF,CAAC;AACD,UAAM,GAAG,QAAQ,OAAO,EAAE,MAAM;AAChC,WAAO;AAAA,EACT,SAAS,KAAK;AACZ,WAAO,KAAK,yCAAyC,EAAE,KAAK,UAAU,YAAY,CAAC;AACnF,WAAO;AAAA,EACT;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { normalizeEnvString } from "@open-mercato/shared/lib/email/config";
|
|
2
|
+
const DEFAULT_SYSTEM_EMAIL_PROVIDER = "resend";
|
|
3
|
+
function resolveSystemEmailProvider() {
|
|
4
|
+
return normalizeEnvString(process.env.SYSTEM_EMAIL_PROVIDER) ?? DEFAULT_SYSTEM_EMAIL_PROVIDER;
|
|
5
|
+
}
|
|
6
|
+
function isSelectedSystemEmailProvider(providerKey) {
|
|
7
|
+
return resolveSystemEmailProvider() === providerKey;
|
|
8
|
+
}
|
|
9
|
+
const SYSTEM_EMAIL_PROVIDER_CONFIG_REGISTRY = /* @__PURE__ */ Symbol.for(
|
|
10
|
+
"open-mercato.communication-channels.system-email-provider-config"
|
|
11
|
+
);
|
|
12
|
+
function getRegistry() {
|
|
13
|
+
const root = globalThis;
|
|
14
|
+
if (!root[SYSTEM_EMAIL_PROVIDER_CONFIG_REGISTRY]) {
|
|
15
|
+
root[SYSTEM_EMAIL_PROVIDER_CONFIG_REGISTRY] = /* @__PURE__ */ new Map();
|
|
16
|
+
}
|
|
17
|
+
return root[SYSTEM_EMAIL_PROVIDER_CONFIG_REGISTRY];
|
|
18
|
+
}
|
|
19
|
+
function registerSystemEmailProviderConfigResolver(resolver) {
|
|
20
|
+
getRegistry().set(resolver.providerKey, resolver);
|
|
21
|
+
}
|
|
22
|
+
function getSystemEmailProviderConfigResolver(providerKey) {
|
|
23
|
+
return getRegistry().get(providerKey);
|
|
24
|
+
}
|
|
25
|
+
export {
|
|
26
|
+
DEFAULT_SYSTEM_EMAIL_PROVIDER,
|
|
27
|
+
getSystemEmailProviderConfigResolver,
|
|
28
|
+
isSelectedSystemEmailProvider,
|
|
29
|
+
registerSystemEmailProviderConfigResolver,
|
|
30
|
+
resolveSystemEmailProvider
|
|
31
|
+
};
|
|
32
|
+
//# sourceMappingURL=system-email-provider-config.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/communication_channels/lib/system-email-provider-config.ts"],
|
|
4
|
+
"sourcesContent": ["import { normalizeEnvString } from '@open-mercato/shared/lib/email/config'\n\ntype SystemEmailProviderConfigContext = {\n fromAddress: string\n}\n\nexport const DEFAULT_SYSTEM_EMAIL_PROVIDER = 'resend'\n\n/**\n * The provider key `sendSystemEmail` resolves channels and credentials against.\n *\n * Lives here rather than in `system-email.ts` so a provider package's `setup.ts` can read it without\n * importing the send path \u2014 that module pulls the ORM and the React renderer in behind it, and a\n * preset that runs at tenant seeding has no business loading either.\n */\nexport function resolveSystemEmailProvider(): string {\n return normalizeEnvString(process.env.SYSTEM_EMAIL_PROVIDER) ?? DEFAULT_SYSTEM_EMAIL_PROVIDER\n}\n\n/**\n * Whether a provider package's env preset should seed anything for this instance.\n *\n * Only the selected provider seeds. Without this gate a preset fires off whatever env variables\n * happen to be present, and `AWS_REGION` in particular is a general-purpose variable this repo\n * already ships uncommented for vector search \u2014 so an instance sending through Resend would get an\n * `isEnabled: true` SES integration and a `status: 'connected'` SES channel it never configured.\n */\nexport function isSelectedSystemEmailProvider(providerKey: string): boolean {\n return resolveSystemEmailProvider() === providerKey\n}\n\nexport type SystemEmailProviderConfigResolver = {\n providerKey: string\n isConfigured: () => boolean\n resolveCredentials: (ctx: SystemEmailProviderConfigContext) => Record<string, unknown>\n}\n\nconst SYSTEM_EMAIL_PROVIDER_CONFIG_REGISTRY = Symbol.for(\n 'open-mercato.communication-channels.system-email-provider-config',\n)\n\ntype RegistryGlobal = typeof globalThis & {\n [SYSTEM_EMAIL_PROVIDER_CONFIG_REGISTRY]?: Map<string, SystemEmailProviderConfigResolver>\n}\n\nfunction getRegistry(): Map<string, SystemEmailProviderConfigResolver> {\n const root = globalThis as RegistryGlobal\n if (!root[SYSTEM_EMAIL_PROVIDER_CONFIG_REGISTRY]) {\n root[SYSTEM_EMAIL_PROVIDER_CONFIG_REGISTRY] = new Map()\n }\n return root[SYSTEM_EMAIL_PROVIDER_CONFIG_REGISTRY]\n}\n\nexport function registerSystemEmailProviderConfigResolver(resolver: SystemEmailProviderConfigResolver): void {\n getRegistry().set(resolver.providerKey, resolver)\n}\n\nexport function getSystemEmailProviderConfigResolver(\n providerKey: string,\n): SystemEmailProviderConfigResolver | undefined {\n return getRegistry().get(providerKey)\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,0BAA0B;AAM5B,MAAM,gCAAgC;AAStC,SAAS,6BAAqC;AACnD,SAAO,mBAAmB,QAAQ,IAAI,qBAAqB,KAAK;AAClE;AAUO,SAAS,8BAA8B,aAA8B;AAC1E,SAAO,2BAA2B,MAAM;AAC1C;AAQA,MAAM,wCAAwC,uBAAO;AAAA,EACnD;AACF;AAMA,SAAS,cAA8D;AACrE,QAAM,OAAO;AACb,MAAI,CAAC,KAAK,qCAAqC,GAAG;AAChD,SAAK,qCAAqC,IAAI,oBAAI,IAAI;AAAA,EACxD;AACA,SAAO,KAAK,qCAAqC;AACnD;AAEO,SAAS,0CAA0C,UAAmD;AAC3G,cAAY,EAAE,IAAI,SAAS,aAAa,QAAQ;AAClD;AAEO,SAAS,qCACd,aAC+C;AAC/C,SAAO,YAAY,EAAE,IAAI,WAAW;AACtC;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { normalizeEnvString } from "@open-mercato/shared/lib/email/config";
|
|
2
|
+
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
3
|
+
import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
4
|
+
import { htmlToText } from "./email-mime.js";
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_SYSTEM_EMAIL_PROVIDER,
|
|
7
|
+
getSystemEmailProviderConfigResolver,
|
|
8
|
+
resolveSystemEmailProvider
|
|
9
|
+
} from "./system-email-provider-config.js";
|
|
10
|
+
import { isTestChannelSeedingEnabled, TEST_SEED_PROVIDER_KEY } from "./test-seed.js";
|
|
11
|
+
import { ensureSystemEmailChannel } from "./ensure-system-email-channel.js";
|
|
12
|
+
import { CommunicationChannel } from "../data/entities.js";
|
|
13
|
+
const logger = createLogger("communication_channels");
|
|
14
|
+
function isSystemEmailTransportConfigured() {
|
|
15
|
+
return getSystemEmailProviderConfigResolver(resolveSystemEmailProvider())?.isConfigured() ?? false;
|
|
16
|
+
}
|
|
17
|
+
async function renderReactEmail(react) {
|
|
18
|
+
if (!react) return void 0;
|
|
19
|
+
const { renderToStaticMarkup } = await import("react-dom/server");
|
|
20
|
+
const markup = renderToStaticMarkup(react);
|
|
21
|
+
return markup.startsWith("<!doctype html>") || markup.startsWith("<!DOCTYPE html>") ? markup : `<!doctype html>${markup}`;
|
|
22
|
+
}
|
|
23
|
+
async function resolveEmailBody(payload) {
|
|
24
|
+
const html = payload.html ?? await renderReactEmail(payload.react);
|
|
25
|
+
const text = payload.text ?? (html ? htmlToText(html) : void 0);
|
|
26
|
+
if (html) return { html, text, body: html, bodyFormat: "html" };
|
|
27
|
+
if (text) return { text, body: text, bodyFormat: "text" };
|
|
28
|
+
throw new Error("EMAIL_BODY_NOT_CONFIGURED: provide react, html, or text");
|
|
29
|
+
}
|
|
30
|
+
async function findTenantSystemEmailChannel(em, payload, providerKey) {
|
|
31
|
+
const tenantId = payload.tenantId;
|
|
32
|
+
const dscope = { tenantId, organizationId: payload.organizationId ?? null };
|
|
33
|
+
const base = {
|
|
34
|
+
providerKey,
|
|
35
|
+
channelType: "email",
|
|
36
|
+
tenantId,
|
|
37
|
+
userId: null,
|
|
38
|
+
deletedAt: null
|
|
39
|
+
};
|
|
40
|
+
const organizationScopes = [payload.organizationId ?? null];
|
|
41
|
+
if (payload.organizationId) organizationScopes.push(null);
|
|
42
|
+
for (const organizationId of organizationScopes) {
|
|
43
|
+
const found = await findOneWithDecryption(
|
|
44
|
+
em,
|
|
45
|
+
CommunicationChannel,
|
|
46
|
+
{ ...base, organizationId },
|
|
47
|
+
void 0,
|
|
48
|
+
dscope
|
|
49
|
+
);
|
|
50
|
+
if (found) return found;
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
async function resolveSystemEmailChannel(em, payload) {
|
|
55
|
+
const providerKey = resolveSystemEmailProvider();
|
|
56
|
+
const envTarget = {
|
|
57
|
+
channel: {
|
|
58
|
+
providerKey,
|
|
59
|
+
channelType: "email",
|
|
60
|
+
organizationId: payload.organizationId ?? null
|
|
61
|
+
},
|
|
62
|
+
hasTenantChannel: false
|
|
63
|
+
};
|
|
64
|
+
if (!payload.tenantId) {
|
|
65
|
+
return { ...envTarget, channel: { ...envTarget.channel, organizationId: null } };
|
|
66
|
+
}
|
|
67
|
+
const dscope = {
|
|
68
|
+
tenantId: payload.tenantId,
|
|
69
|
+
organizationId: payload.organizationId ?? null
|
|
70
|
+
};
|
|
71
|
+
const explicitChannelId = normalizeEnvString(process.env.SYSTEM_EMAIL_CHANNEL_ID);
|
|
72
|
+
const channel = explicitChannelId ? await findOneWithDecryption(
|
|
73
|
+
em,
|
|
74
|
+
CommunicationChannel,
|
|
75
|
+
{
|
|
76
|
+
id: explicitChannelId,
|
|
77
|
+
channelType: "email",
|
|
78
|
+
tenantId: payload.tenantId,
|
|
79
|
+
userId: null,
|
|
80
|
+
deletedAt: null,
|
|
81
|
+
$or: [{ organizationId: payload.organizationId ?? null }, { organizationId: null }]
|
|
82
|
+
},
|
|
83
|
+
void 0,
|
|
84
|
+
dscope
|
|
85
|
+
) : await findTenantSystemEmailChannel(em, payload, providerKey);
|
|
86
|
+
if (channel) {
|
|
87
|
+
if (!channel.isActive || channel.status !== "connected") {
|
|
88
|
+
throw new Error(`SYSTEM_EMAIL_CHANNEL_UNAVAILABLE: channel is ${channel.status}`);
|
|
89
|
+
}
|
|
90
|
+
return { channel, hasTenantChannel: true };
|
|
91
|
+
}
|
|
92
|
+
if (explicitChannelId) {
|
|
93
|
+
throw new Error(
|
|
94
|
+
`SYSTEM_EMAIL_CHANNEL_NOT_CONFIGURED: SYSTEM_EMAIL_CHANNEL_ID '${explicitChannelId}' matches no active channel this organization may use`
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
if (isTestChannelSeedingEnabled() && providerKey === TEST_SEED_PROVIDER_KEY) {
|
|
98
|
+
return {
|
|
99
|
+
channel: { providerKey: TEST_SEED_PROVIDER_KEY, channelType: "email", organizationId: payload.organizationId ?? null },
|
|
100
|
+
hasTenantChannel: false
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
return envTarget;
|
|
104
|
+
}
|
|
105
|
+
function resolveEnvCredentials(providerKey, fromAddress) {
|
|
106
|
+
return getSystemEmailProviderConfigResolver(providerKey)?.resolveCredentials({ fromAddress }) ?? { fromAddress };
|
|
107
|
+
}
|
|
108
|
+
async function resolveTenantCredentials(container, em, payload, channel, hasTenantChannel) {
|
|
109
|
+
const tenantId = payload.tenantId;
|
|
110
|
+
const organizationId = channel.organizationId ?? payload.organizationId ?? tenantId;
|
|
111
|
+
const credentialsService = container.resolve("integrationCredentialsService");
|
|
112
|
+
const resolvedCredentials = await credentialsService.resolve(`channel_${channel.providerKey}`, {
|
|
113
|
+
tenantId,
|
|
114
|
+
organizationId,
|
|
115
|
+
userId: null
|
|
116
|
+
});
|
|
117
|
+
if (resolvedCredentials) {
|
|
118
|
+
if (!hasTenantChannel) {
|
|
119
|
+
await ensureSystemEmailChannel(em, {
|
|
120
|
+
tenantId,
|
|
121
|
+
organizationId,
|
|
122
|
+
providerKey: channel.providerKey,
|
|
123
|
+
externalIdentifier: typeof resolvedCredentials.fromAddress === "string" ? resolvedCredentials.fromAddress : payload.from
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
return resolvedCredentials;
|
|
127
|
+
}
|
|
128
|
+
if (hasTenantChannel) {
|
|
129
|
+
throw new Error(
|
|
130
|
+
`SYSTEM_EMAIL_CREDENTIALS_NOT_CONFIGURED: configure tenant credentials for '${channel.providerKey}'`
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
if (!isSystemEmailTransportConfigured()) {
|
|
134
|
+
throw new Error("SYSTEM_EMAIL_CHANNEL_NOT_CONFIGURED: configure a tenant-wide email channel");
|
|
135
|
+
}
|
|
136
|
+
logger.warn("Sending system email with instance-wide environment credentials", {
|
|
137
|
+
tenantId,
|
|
138
|
+
providerKey: channel.providerKey,
|
|
139
|
+
reason: `no tenant email channel or credentials configured for '${channel.providerKey}'`,
|
|
140
|
+
remedy: `run 'yarn mercato seed:defaults --module channel_${channel.providerKey}' to move this tenant onto the Communications Hub`
|
|
141
|
+
});
|
|
142
|
+
return resolveEnvCredentials(channel.providerKey, payload.from);
|
|
143
|
+
}
|
|
144
|
+
function resolveOutboundFromAddress(payload, credentials) {
|
|
145
|
+
if (!payload.fromIsInstanceDefault) return payload.from;
|
|
146
|
+
const configured = credentials.fromAddress;
|
|
147
|
+
return typeof configured === "string" && configured.trim().length > 0 ? configured : payload.from;
|
|
148
|
+
}
|
|
149
|
+
async function sendSystemEmail(container, payload) {
|
|
150
|
+
const em = container.resolve("em").fork();
|
|
151
|
+
const { channel, hasTenantChannel } = await resolveSystemEmailChannel(em, payload);
|
|
152
|
+
const registry = container.resolve("channelAdapterRegistry");
|
|
153
|
+
const adapter = registry.get(channel.providerKey);
|
|
154
|
+
if (!adapter) {
|
|
155
|
+
throw new Error(
|
|
156
|
+
`[internal] No ChannelAdapter registered for providerKey '${channel.providerKey}'. Enable the provider module.`
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
const credentials = payload.tenantId ? await resolveTenantCredentials(container, em, payload, channel, hasTenantChannel) : resolveEnvCredentials(channel.providerKey, payload.from);
|
|
160
|
+
const body = await resolveEmailBody(payload);
|
|
161
|
+
const converted = await adapter.convertOutbound({
|
|
162
|
+
body: body.body,
|
|
163
|
+
bodyFormat: body.bodyFormat,
|
|
164
|
+
channelMetadata: {
|
|
165
|
+
to: payload.to,
|
|
166
|
+
subject: payload.subject,
|
|
167
|
+
from: resolveOutboundFromAddress(payload, credentials),
|
|
168
|
+
replyTo: payload.replyTo,
|
|
169
|
+
attachments: payload.attachments
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
const sendResult = await adapter.sendMessage({
|
|
173
|
+
content: converted.content,
|
|
174
|
+
credentials,
|
|
175
|
+
scope: {
|
|
176
|
+
tenantId: payload.tenantId ?? "system",
|
|
177
|
+
organizationId: payload.organizationId ?? payload.tenantId ?? "system"
|
|
178
|
+
},
|
|
179
|
+
metadata: converted.metadata
|
|
180
|
+
});
|
|
181
|
+
if (sendResult.status === "failed") {
|
|
182
|
+
throw new Error(sendResult.error ?? `SYSTEM_EMAIL_SEND_FAILED: ${channel.providerKey}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
export {
|
|
186
|
+
DEFAULT_SYSTEM_EMAIL_PROVIDER,
|
|
187
|
+
isSystemEmailTransportConfigured,
|
|
188
|
+
sendSystemEmail
|
|
189
|
+
};
|
|
190
|
+
//# sourceMappingURL=system-email.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/communication_channels/lib/system-email.ts"],
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { AppContainer } from '@open-mercato/shared/lib/di/container'\nimport type { ResolvedEmailPayload } from '@open-mercato/shared/lib/email/send'\nimport { normalizeEnvString } from '@open-mercato/shared/lib/email/config'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport type { ChannelAdapterRegistry } from './registry'\nimport { htmlToText } from './email-mime'\nimport {\n DEFAULT_SYSTEM_EMAIL_PROVIDER,\n getSystemEmailProviderConfigResolver,\n resolveSystemEmailProvider,\n} from './system-email-provider-config'\nimport { isTestChannelSeedingEnabled, TEST_SEED_PROVIDER_KEY } from './test-seed'\nimport { ensureSystemEmailChannel } from './ensure-system-email-channel'\nimport { CommunicationChannel } from '../data/entities'\n\nexport { DEFAULT_SYSTEM_EMAIL_PROVIDER }\n\nconst logger = createLogger('communication_channels')\n\ntype CredentialsServiceLike = {\n resolve: (\n integrationId: string,\n scope: { organizationId: string; tenantId: string; userId?: string | null },\n ) => Promise<Record<string, unknown> | null>\n}\n\ntype ResolvedSystemEmailChannel = Pick<CommunicationChannel, 'providerKey' | 'channelType' | 'organizationId'>\n\n/**\n * Outcome of resolving which channel a system email should go out through.\n *\n * `hasTenantChannel` drives the credential posture and is the whole point of the split: a tenant that\n * has an explicitly configured Hub channel MUST fail closed when its credentials are missing (never\n * silently borrow the instance-wide env key), while a tenant with no channel at all is an instance\n * that simply has not been migrated onto the Communications Hub yet and MUST keep working off env.\n */\ntype ResolvedSystemEmailTarget = {\n channel: ResolvedSystemEmailChannel\n hasTenantChannel: boolean\n}\n\nexport function isSystemEmailTransportConfigured(): boolean {\n return getSystemEmailProviderConfigResolver(resolveSystemEmailProvider())?.isConfigured() ?? false\n}\n\nasync function renderReactEmail(react: ResolvedEmailPayload['react']): Promise<string | undefined> {\n if (!react) return undefined\n const { renderToStaticMarkup } = await import('react-dom/server')\n const markup = renderToStaticMarkup(react)\n return markup.startsWith('<!doctype html>') || markup.startsWith('<!DOCTYPE html>')\n ? markup\n : `<!doctype html>${markup}`\n}\n\nasync function resolveEmailBody(payload: ResolvedEmailPayload): Promise<{\n html?: string\n text?: string\n body: string\n bodyFormat: 'text' | 'html'\n}> {\n const html = payload.html ?? (await renderReactEmail(payload.react))\n const text = payload.text ?? (html ? htmlToText(html) : undefined)\n if (html) return { html, text, body: html, bodyFormat: 'html' }\n if (text) return { text, body: text, bodyFormat: 'text' }\n throw new Error('EMAIL_BODY_NOT_CONFIGURED: provide react, html, or text')\n}\n\n/**\n * Locate the tenant's system email channel.\n *\n * Two probes, both inside the caller's own scope: the caller's organization first, then the\n * organization-agnostic (`organization_id IS NULL`) row that represents a genuinely tenant-wide\n * channel. A channel belonging to a *different* organization is deliberately never used \u2014 organization\n * is a scoping boundary, and borrowing another organization's channel would send through credentials\n * that organization owns. A caller whose organization has no channel falls through to the\n * instance-wide environment credentials instead, which cross no boundary at all.\n */\nasync function findTenantSystemEmailChannel(\n em: EntityManager,\n payload: ResolvedEmailPayload,\n providerKey: string,\n): Promise<CommunicationChannel | null> {\n const tenantId = payload.tenantId as string\n const dscope = { tenantId, organizationId: payload.organizationId ?? null }\n const base = {\n providerKey,\n channelType: 'email',\n tenantId,\n userId: null,\n deletedAt: null,\n }\n\n const organizationScopes: Array<string | null> = [payload.organizationId ?? null]\n if (payload.organizationId) organizationScopes.push(null)\n\n for (const organizationId of organizationScopes) {\n const found = await findOneWithDecryption(\n em,\n CommunicationChannel,\n { ...base, organizationId },\n undefined,\n dscope,\n )\n if (found) return found\n }\n return null\n}\n\nasync function resolveSystemEmailChannel(\n em: EntityManager,\n payload: ResolvedEmailPayload,\n): Promise<ResolvedSystemEmailTarget> {\n const providerKey = resolveSystemEmailProvider()\n const envTarget: ResolvedSystemEmailTarget = {\n channel: {\n providerKey,\n channelType: 'email',\n organizationId: payload.organizationId ?? null,\n },\n hasTenantChannel: false,\n }\n\n if (!payload.tenantId) {\n return { ...envTarget, channel: { ...envTarget.channel, organizationId: null } }\n }\n\n const dscope = {\n tenantId: payload.tenantId,\n organizationId: payload.organizationId ?? null,\n }\n const explicitChannelId = normalizeEnvString(process.env.SYSTEM_EMAIL_CHANNEL_ID)\n\n // An operator who pinned SYSTEM_EMAIL_CHANNEL_ID asked for exactly one channel. Honour that\n // literally and never widen the search or fall back to env \u2014 a silent substitution would send\n // through a provider the operator deliberately did not choose.\n //\n // The pin narrows the search; it does not lift the organization boundary. A pinned id is accepted\n // only when the row is the caller's own organization or the tenant-wide (`organization_id IS NULL`)\n // one the setting is documented to name. Matching on the id alone would let a pin aimed at one\n // organization's channel send every other organization's mail through credentials that\n // organization owns \u2014 the same borrowing `findTenantSystemEmailChannel` refuses.\n const channel = explicitChannelId\n ? await findOneWithDecryption(\n em,\n CommunicationChannel,\n {\n id: explicitChannelId,\n channelType: 'email',\n tenantId: payload.tenantId,\n userId: null,\n deletedAt: null,\n $or: [{ organizationId: payload.organizationId ?? null }, { organizationId: null }],\n },\n undefined,\n dscope,\n )\n : await findTenantSystemEmailChannel(em, payload, providerKey)\n\n if (channel) {\n if (!channel.isActive || channel.status !== 'connected') {\n throw new Error(`SYSTEM_EMAIL_CHANNEL_UNAVAILABLE: channel is ${channel.status}`)\n }\n return { channel, hasTenantChannel: true }\n }\n\n if (explicitChannelId) {\n throw new Error(\n `SYSTEM_EMAIL_CHANNEL_NOT_CONFIGURED: SYSTEM_EMAIL_CHANNEL_ID '${explicitChannelId}' matches no active channel this organization may use`,\n )\n }\n\n if (isTestChannelSeedingEnabled() && providerKey === TEST_SEED_PROVIDER_KEY) {\n return {\n channel: { providerKey: TEST_SEED_PROVIDER_KEY, channelType: 'email', organizationId: payload.organizationId ?? null },\n hasTenantChannel: false,\n }\n }\n\n // No channel row for this tenant. This is the ordinary state of an instance that upgraded into the\n // Communications Hub: tenants predate the feature, so nothing ever seeded a channel for them.\n // Do not decide here \u2014 `sendSystemEmail` still has to check whether the tenant configured the\n // provider through the integrations UI before it considers the instance-wide env credentials.\n return envTarget\n}\n\nfunction resolveEnvCredentials(providerKey: string, fromAddress: string): Record<string, unknown> {\n return getSystemEmailProviderConfigResolver(providerKey)?.resolveCredentials({ fromAddress }) ?? { fromAddress }\n}\n\n/**\n * Decide which credentials a tenant-scoped send uses.\n *\n * Three cases, in priority order:\n *\n * 1. The tenant has credentials stored for the provider (seeded, or saved through\n * `/backend/integrations/channel_<provider>`) \u2014 always win. When no Hub channel row exists yet we\n * also create one, so configuring the integration in the admin UI produces a usable channel\n * instead of credentials nothing reads.\n * 2. The tenant has a configured channel but no credentials \u2014 **fail closed**. Borrowing the\n * instance key here would send a tenant's mail through a provider account it did not choose.\n * 3. The tenant has neither \u2014 fall back to the instance-wide env credentials. This is the pre-Hub\n * behaviour and the upgrade path: env credentials are instance-wide by definition, so this leaks\n * nothing across tenants, and without it every tenant that predates the Hub loses all mail.\n */\nasync function resolveTenantCredentials(\n container: AppContainer,\n em: EntityManager,\n payload: ResolvedEmailPayload,\n channel: ResolvedSystemEmailChannel,\n hasTenantChannel: boolean,\n): Promise<Record<string, unknown>> {\n const tenantId = payload.tenantId as string\n const organizationId = channel.organizationId ?? payload.organizationId ?? tenantId\n const credentialsService = container.resolve('integrationCredentialsService') as CredentialsServiceLike\n const resolvedCredentials = await credentialsService.resolve(`channel_${channel.providerKey}`, {\n tenantId,\n organizationId,\n userId: null,\n })\n\n if (resolvedCredentials) {\n if (!hasTenantChannel) {\n await ensureSystemEmailChannel(em, {\n tenantId,\n organizationId,\n providerKey: channel.providerKey,\n externalIdentifier: typeof resolvedCredentials.fromAddress === 'string' ? resolvedCredentials.fromAddress : payload.from,\n })\n }\n return resolvedCredentials\n }\n\n if (hasTenantChannel) {\n throw new Error(\n `SYSTEM_EMAIL_CREDENTIALS_NOT_CONFIGURED: configure tenant credentials for '${channel.providerKey}'`,\n )\n }\n\n if (!isSystemEmailTransportConfigured()) {\n throw new Error('SYSTEM_EMAIL_CHANNEL_NOT_CONFIGURED: configure a tenant-wide email channel')\n }\n\n logger.warn('Sending system email with instance-wide environment credentials', {\n tenantId,\n providerKey: channel.providerKey,\n reason: `no tenant email channel or credentials configured for '${channel.providerKey}'`,\n remedy: `run 'yarn mercato seed:defaults --module channel_${channel.providerKey}' to move this tenant onto the Communications Hub`,\n })\n return resolveEnvCredentials(channel.providerKey, payload.from)\n}\n\n/**\n * Choose the address a system email actually leaves from.\n *\n * The tenant's configured sender wins whenever the caller did not name one itself. `sendEmail` always\n * fills `from` in \u2014 from `NOTIFICATIONS_EMAIL_FROM` / `EMAIL_FROM` / `ADMIN_EMAIL` \u2014 so an adapter that\n * merely falls back to its credentials when `from` is missing would never reach the tenant's value, and\n * the \"From address\" saved in the integrations UI would be inert. A caller that passed an explicit\n * `from` meant it, and keeps it.\n */\nfunction resolveOutboundFromAddress(\n payload: ResolvedEmailPayload,\n credentials: Record<string, unknown>,\n): string {\n if (!payload.fromIsInstanceDefault) return payload.from\n const configured = credentials.fromAddress\n return typeof configured === 'string' && configured.trim().length > 0 ? configured : payload.from\n}\n\nexport async function sendSystemEmail(\n container: AppContainer,\n payload: ResolvedEmailPayload,\n): Promise<void> {\n const em = (container.resolve('em') as EntityManager).fork()\n const { channel, hasTenantChannel } = await resolveSystemEmailChannel(em, payload)\n const registry = container.resolve('channelAdapterRegistry') as ChannelAdapterRegistry\n const adapter = registry.get(channel.providerKey)\n if (!adapter) {\n throw new Error(\n `[internal] No ChannelAdapter registered for providerKey '${channel.providerKey}'. Enable the provider module.`,\n )\n }\n\n const credentials = payload.tenantId\n ? await resolveTenantCredentials(container, em, payload, channel, hasTenantChannel)\n : resolveEnvCredentials(channel.providerKey, payload.from)\n\n const body = await resolveEmailBody(payload)\n const converted = await adapter.convertOutbound({\n body: body.body,\n bodyFormat: body.bodyFormat,\n channelMetadata: {\n to: payload.to,\n subject: payload.subject,\n from: resolveOutboundFromAddress(payload, credentials),\n replyTo: payload.replyTo,\n attachments: payload.attachments,\n },\n })\n\n const sendResult = await adapter.sendMessage({\n content: converted.content,\n credentials,\n scope: {\n tenantId: payload.tenantId ?? 'system',\n organizationId: payload.organizationId ?? payload.tenantId ?? 'system',\n },\n metadata: converted.metadata,\n })\n\n if (sendResult.status === 'failed') {\n throw new Error(sendResult.error ?? `SYSTEM_EMAIL_SEND_FAILED: ${channel.providerKey}`)\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|