@open-mercato/core 0.10.1-develop.7954.1.48da4a7d13 → 0.10.1-develop.7972.1.03f813eaaf
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/modules/auth/api/logout.js +3 -3
- package/dist/modules/auth/api/logout.js.map +2 -2
- package/dist/modules/auth/commands/roles.js +42 -31
- package/dist/modules/auth/commands/roles.js.map +2 -2
- package/dist/modules/communication_channels/setup.js +49 -45
- package/dist/modules/communication_channels/setup.js.map +2 -2
- package/dist/modules/customer_accounts/services/domainMappingService.js +55 -29
- package/dist/modules/customer_accounts/services/domainMappingService.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivitiesSection.js +8 -3
- package/dist/modules/customers/components/detail/ActivitiesSection.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivityCard.js +3 -3
- package/dist/modules/customers/components/detail/ActivityCard.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivityHistorySection.js +19 -7
- package/dist/modules/customers/components/detail/ActivityHistorySection.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivityTimeline.js +5 -5
- package/dist/modules/customers/components/detail/ActivityTimeline.js.map +2 -2
- package/dist/modules/directory/subscribers/seedOrganizationDefaults.js +67 -0
- package/dist/modules/directory/subscribers/seedOrganizationDefaults.js.map +7 -0
- package/dist/modules/ecommerce/api/storefront/categories/[slug]/route.js +1 -1
- package/dist/modules/ecommerce/api/storefront/categories/[slug]/route.js.map +2 -2
- package/dist/modules/ecommerce/api/storefront/categories/route.js +1 -1
- package/dist/modules/ecommerce/api/storefront/categories/route.js.map +2 -2
- package/dist/modules/ecommerce/api/storefront/context/route.js +1 -1
- package/dist/modules/ecommerce/api/storefront/context/route.js.map +2 -2
- package/dist/modules/ecommerce/api/storefront/products/[idOrHandle]/route.js +1 -1
- package/dist/modules/ecommerce/api/storefront/products/[idOrHandle]/route.js.map +2 -2
- package/dist/modules/ecommerce/api/storefront/products/route.js +1 -1
- package/dist/modules/ecommerce/api/storefront/products/route.js.map +2 -2
- package/dist/modules/ecommerce/api/storefront/search/suggest/route.js +1 -1
- package/dist/modules/ecommerce/api/storefront/search/suggest/route.js.map +2 -2
- package/dist/modules/ecommerce/lib/cacheInvalidation.js +3 -0
- package/dist/modules/ecommerce/lib/cacheInvalidation.js.map +2 -2
- package/dist/modules/ecommerce/lib/cacheKeys.js +4 -0
- package/dist/modules/ecommerce/lib/cacheKeys.js.map +2 -2
- package/dist/modules/ecommerce/lib/storeContext.js +36 -12
- package/dist/modules/ecommerce/lib/storeContext.js.map +2 -2
- package/dist/modules/ecommerce/subscribers/sales-channel-cache-invalidation.js +14 -0
- package/dist/modules/ecommerce/subscribers/sales-channel-cache-invalidation.js.map +7 -0
- package/dist/modules/integrations/setup.js +34 -24
- package/dist/modules/integrations/setup.js.map +2 -2
- package/dist/modules/messages/components/MessagesInboxPageClient.js +1 -1
- package/dist/modules/messages/components/MessagesInboxPageClient.js.map +2 -2
- package/dist/modules/messages/components/message-detail/panels/MainMessageHeader.js +3 -3
- package/dist/modules/messages/components/message-detail/panels/MainMessageHeader.js.map +2 -2
- package/dist/modules/messages/components/message-detail/panels/body-panel.js +2 -2
- package/dist/modules/messages/components/message-detail/panels/body-panel.js.map +2 -2
- package/dist/modules/payment_gateways/setup.js +3 -0
- package/dist/modules/payment_gateways/setup.js.map +2 -2
- package/dist/modules/warranty_claims/backend/warranty_claims/[id]/page.js +30 -11
- package/dist/modules/warranty_claims/backend/warranty_claims/[id]/page.js.map +3 -3
- package/dist/modules/warranty_claims/setup.js +34 -30
- package/dist/modules/warranty_claims/setup.js.map +2 -2
- package/dist/modules/workflows/setup.js +7 -0
- package/dist/modules/workflows/setup.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/auth/api/logout.ts +3 -3
- package/src/modules/auth/commands/roles.ts +43 -32
- package/src/modules/communication_channels/setup.ts +59 -53
- package/src/modules/customer_accounts/services/domainMappingService.ts +57 -29
- package/src/modules/customers/components/detail/ActivitiesSection.tsx +8 -3
- package/src/modules/customers/components/detail/ActivityCard.tsx +3 -3
- package/src/modules/customers/components/detail/ActivityHistorySection.tsx +20 -8
- package/src/modules/customers/components/detail/ActivityTimeline.tsx +7 -7
- package/src/modules/customers/i18n/de.json +1 -0
- package/src/modules/customers/i18n/en.json +1 -0
- package/src/modules/customers/i18n/es.json +1 -0
- package/src/modules/customers/i18n/ko.json +1 -0
- package/src/modules/customers/i18n/pl.json +1 -0
- package/src/modules/directory/subscribers/seedOrganizationDefaults.ts +84 -0
- package/src/modules/ecommerce/api/storefront/categories/[slug]/route.ts +1 -1
- package/src/modules/ecommerce/api/storefront/categories/route.ts +1 -1
- package/src/modules/ecommerce/api/storefront/context/route.ts +1 -1
- package/src/modules/ecommerce/api/storefront/products/[idOrHandle]/route.ts +1 -1
- package/src/modules/ecommerce/api/storefront/products/route.ts +1 -1
- package/src/modules/ecommerce/api/storefront/search/suggest/route.ts +1 -1
- package/src/modules/ecommerce/lib/cacheInvalidation.ts +4 -0
- package/src/modules/ecommerce/lib/cacheKeys.ts +4 -0
- package/src/modules/ecommerce/lib/storeContext.ts +56 -10
- package/src/modules/ecommerce/subscribers/sales-channel-cache-invalidation.ts +12 -0
- package/src/modules/integrations/setup.ts +41 -24
- package/src/modules/messages/components/MessagesInboxPageClient.tsx +1 -1
- package/src/modules/messages/components/message-detail/panels/MainMessageHeader.tsx +3 -3
- package/src/modules/messages/components/message-detail/panels/body-panel.tsx +2 -2
- package/src/modules/payment_gateways/setup.ts +4 -0
- package/src/modules/warranty_claims/backend/warranty_claims/[id]/page.tsx +55 -28
- package/src/modules/warranty_claims/setup.ts +40 -30
- package/src/modules/workflows/setup.ts +8 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/modules/communication_channels/setup.ts"],
|
|
4
|
-
"sourcesContent": ["import { createHash } from 'node:crypto'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport type { ModuleSetupConfig } from '@open-mercato/shared/modules/setup'\nimport { CommunicationChannel } from './data/entities'\nimport { COMMUNICATION_CHANNELS_QUEUES } from './lib/queue'\nimport { isTestChannelSeedingEnabled, TEST_SEED_PROVIDER_KEY } from './lib/test-seed'\n\nconst logger = createLogger('communication_channels')\n\ntype SchedulerServiceLike = {\n register: (registration: {\n id: string\n name: string\n scopeType: 'system' | 'organization' | 'tenant'\n organizationId?: string\n tenantId?: string\n scheduleType: 'cron' | 'interval'\n scheduleValue: string\n timezone?: string\n targetType: 'queue' | 'command'\n targetQueue?: string\n targetPayload?: unknown\n sourceType?: 'user' | 'module'\n sourceModule?: string\n isEnabled?: boolean\n description?: string\n }) => Promise<void>\n}\n\ntype CredentialsServiceLike = {\n resolve: (\n integrationId: string,\n scope: { organizationId: string; tenantId: string; userId?: string | null },\n ) => Promise<Record<string, unknown> | null>\n save: (\n integrationId: string,\n credentials: Record<string, unknown>,\n scope: { organizationId: string; tenantId: string; userId?: string | null },\n ) => Promise<void>\n}\n\n/**\n * Sender the test-seed channel presents. Shared by the channel row's\n * `externalIdentifier` and its seeded credentials so both describe one address.\n */\nconst TEST_SEED_FROM_ADDRESS = 'system@test-seed.local'\n\n/**\n * Tick interval in seconds. Default 60s per email integration spec\n * \u00A7 Hub Deltas \u2192 Delta 6 (scheduler mechanism).\n */\nconst POLL_TICK_INTERVAL_SECONDS = Math.max(\n 10,\n Number.parseInt(process.env.OM_HUB_POLL_SCHEDULER_TICK_SECONDS ?? '60', 10) || 60,\n)\n\n/**\n * `scheduled_jobs.id` is a uuid column, so a module-owned schedule's stable\n * registration key must be hashed into a uuid rather than used verbatim \u2014 this\n * keeps `schedulerService.register()` an idempotent upsert across re-runs of\n * seedDefaults instead of trying to insert a raw string into the uuid PK.\n */\nfunction stableScheduleUuid(stableKey: string): string {\n const hex = createHash('sha256').update(stableKey).digest('hex')\n return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`\n}\n\nfunction stablePollTickScheduleId(organizationId: string): string {\n return stableScheduleUuid(`communication_channels:poll-tick:${organizationId}`)\n}\n\nexport const setup: ModuleSetupConfig = {\n defaultRoleFeatures: {\n superadmin: [\n 'communication_channels.view',\n 'communication_channels.manage',\n 'communication_channels.react',\n 'communication_channels.assign',\n 'communication_channels.connect_user_channel',\n 'communication_channels.share_own_channel',\n 'communication_channels.connect_tenant_channel',\n 'communication_channels.admin',\n 'communication_channels.channel.import_history',\n 'communication_channels.channel.push.manage',\n ],\n admin: [\n 'communication_channels.view',\n 'communication_channels.manage',\n 'communication_channels.react',\n 'communication_channels.assign',\n 'communication_channels.connect_user_channel',\n 'communication_channels.share_own_channel',\n 'communication_channels.connect_tenant_channel',\n 'communication_channels.admin',\n 'communication_channels.channel.import_history',\n 'communication_channels.channel.push.manage',\n ],\n manager: [\n 'communication_channels.view',\n 'communication_channels.manage',\n 'communication_channels.react',\n 'communication_channels.assign',\n 'communication_channels.connect_user_channel',\n 'communication_channels.share_own_channel',\n ],\n employee: [\n 'communication_channels.view',\n 'communication_channels.react',\n 'communication_channels.connect_user_channel',\n 'communication_channels.share_own_channel',\n ],\n },\n\n async seedDefaults({ container, organizationId, tenantId }) {\n if (isTestChannelSeedingEnabled() && process.env.SYSTEM_EMAIL_PROVIDER === TEST_SEED_PROVIDER_KEY) {\n const em = (container.resolve('em') as EntityManager).fork()\n const existing = 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 (!existing) {\n em.persist(em.create(CommunicationChannel, {\n providerKey: TEST_SEED_PROVIDER_KEY,\n channelType: 'email',\n displayName: 'Test Seed System Email',\n externalIdentifier: TEST_SEED_FROM_ADDRESS,\n userId: null,\n isPrimary: false,\n isActive: true,\n status: 'connected',\n tenantId,\n organizationId,\n }))\n await em.flush()\n }\n\n /**\n * Seeding the channel row without credentials is not a neutral state \u2014 it is a\n * fail-closed one. Once a tenant owns a Hub channel, `resolveTenantCredentials`\n * deliberately refuses to fall back to instance-wide env credentials, so every\n * tenant-scoped send throws `SYSTEM_EMAIL_CREDENTIALS_NOT_CONFIGURED`. Callers that\n * treat a failed send as a failed write (customer invitations roll back and return\n * 502) then fail outright rather than merely losing the email.\n *\n * Seeding credentials alongside the row makes the harness model a tenant that\n * finished configuring its provider, which is also the only one of the three\n * credential cases the integration suite would otherwise never exercise.\n */\n // Best-effort, like the scheduler registration below: credential storage needs the\n // integrations module and a usable encryption key, and neither is guaranteed in every\n // harness. A failure here must not abort tenant initialization for every other module.\n try {\n const credentialsService = container.resolve('integrationCredentialsService') as CredentialsServiceLike\n const testSeedIntegrationId = `channel_${TEST_SEED_PROVIDER_KEY}`\n const credentialScope = { tenantId, organizationId, userId: null }\n const existingCredentials = await credentialsService.resolve(testSeedIntegrationId, credentialScope)\n if (!existingCredentials) {\n await credentialsService.save(\n testSeedIntegrationId,\n { testSeed: true, fromAddress: TEST_SEED_FROM_ADDRESS },\n credentialScope,\n )\n }\n } catch (error) {\n logger.warn('Test-seed email credentials could not be stored; tenant-scoped sends will fail closed', {\n err: error,\n tenantId,\n organizationId,\n })\n }\n }\n\n /**\n * Register the per-channel polling tick with `@open-mercato/scheduler`.\n *\n * Per email integration spec \u00A7 Hub Deltas \u2192 Delta 6: every\n * `POLL_TICK_INTERVAL_SECONDS` (default 60s), enqueue a single\n * `communication-channels-poll-tick` job that enumerates due channels and\n * fans out to the `communication-channels-poll` queue.\n *\n * The registration is per-organization (one tick row per org/tenant pair)\n * so multi-tenant deploys schedule independently. Skipped silently when the\n * scheduler module isn't enabled \u2014 keeps the hub usable in scheduler-less\n * test harnesses.\n */\n const cradle = container as { hasRegistration?: (name: string) => boolean }\n if (typeof cradle.hasRegistration !== 'function' || !cradle.hasRegistration('schedulerService')) {\n return\n }\n const schedulerService = container.resolve('schedulerService') as SchedulerServiceLike\n // Best-effort: a scheduler failure must not abort tenant initialization for\n // every other module (mirrors the ai_assistant setup pattern). The schedule\n // ids are deterministic uuids so re-runs upsert idempotently.\n try {\n await schedulerService.register({\n id: stablePollTickScheduleId(organizationId),\n name: 'Communication channels poll tick',\n description:\n `Enumerates active polling channels every ${POLL_TICK_INTERVAL_SECONDS}s and enqueues per-channel poll jobs.`,\n scopeType: 'organization',\n organizationId,\n tenantId,\n scheduleType: 'interval',\n scheduleValue: `${POLL_TICK_INTERVAL_SECONDS}s`,\n timezone: 'UTC',\n targetType: 'queue',\n targetQueue: COMMUNICATION_CHANNELS_QUEUES.pollTick,\n targetPayload: {\n scope: { tenantId, organizationId },\n },\n sourceType: 'module',\n sourceModule: 'communication_channels',\n isEnabled: true,\n })\n\n // Spec C \u00A7 Phase C4 \u2014 Gmail watch renewal cron, per-org so multi-tenant\n // deploys schedule independently.\n await schedulerService.register({\n id: stableScheduleUuid(`communication_channels:gmail-renew-watch:${organizationId}`),\n name: 'Gmail watch renewal',\n description:\n 'Daily 04:00 UTC. Re-issues gmail.users.watch for channels within OM_PUSH_RENEWAL_GMAIL_LEAD_HOURS of expiry.',\n scopeType: 'organization',\n organizationId,\n tenantId,\n scheduleType: 'cron',\n scheduleValue: '0 4 * * *',\n timezone: 'UTC',\n targetType: 'queue',\n targetQueue: COMMUNICATION_CHANNELS_QUEUES.gmailRenewWatch,\n targetPayload: { scope: { tenantId, organizationId } },\n sourceType: 'module',\n sourceModule: 'communication_channels',\n isEnabled: true,\n })\n } catch (error) {\n logger.warn('Failed to register module schedules', { err: error })\n }\n },\n}\n\nexport default setup\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,kBAAkB;AAE3B,SAAS,6BAA6B;AACtC,SAAS,oBAAoB;AAE7B,SAAS,4BAA4B;AACrC,SAAS,qCAAqC;AAC9C,SAAS,6BAA6B,8BAA8B;AAEpE,MAAM,SAAS,aAAa,wBAAwB;AAsCpD,MAAM,yBAAyB;AAM/B,MAAM,6BAA6B,KAAK;AAAA,EACtC;AAAA,EACA,OAAO,SAAS,QAAQ,IAAI,sCAAsC,MAAM,EAAE,KAAK;AACjF;AAQA,SAAS,mBAAmB,WAA2B;AACrD,QAAM,MAAM,WAAW,QAAQ,EAAE,OAAO,SAAS,EAAE,OAAO,KAAK;AAC/D,SAAO,GAAG,IAAI,MAAM,GAAG,CAAC,CAAC,IAAI,IAAI,MAAM,GAAG,EAAE,CAAC,IAAI,IAAI,MAAM,IAAI,EAAE,CAAC,IAAI,IAAI,MAAM,IAAI,EAAE,CAAC,IAAI,IAAI,MAAM,IAAI,EAAE,CAAC;AAC9G;AAEA,SAAS,yBAAyB,gBAAgC;AAChE,SAAO,mBAAmB,oCAAoC,cAAc,EAAE;AAChF;AAEO,MAAM,QAA2B;AAAA,EACtC,qBAAqB;AAAA,IACnB,YAAY;AAAA,MACV;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,IACA,OAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,IACA,SAAS;AAAA,MACP;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,IACA,UAAU;AAAA,MACR;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAM,aAAa,EAAE,WAAW,gBAAgB,SAAS,GAAG;AAC1D,QAAI,4BAA4B,KAAK,QAAQ,IAAI,0BAA0B,wBAAwB;AACjG,YAAM,KAAM,UAAU,QAAQ,IAAI,EAAoB,KAAK;AAC3D,YAAM,WAAW,MAAM;AAAA,QACrB;AAAA,QACA;AAAA,QACA;AAAA,UACE,aAAa;AAAA,UACb,aAAa;AAAA,UACb;AAAA,UACA;AAAA,UACA,QAAQ;AAAA,UACR,WAAW;AAAA,QACb;AAAA,QACA;AAAA,QACA,EAAE,UAAU,eAAe;AAAA,MAC7B;AACA,UAAI,CAAC,UAAU;AACb,WAAG,QAAQ,GAAG,OAAO,sBAAsB;AAAA,UACzC,aAAa;AAAA,UACb,aAAa;AAAA,UACb,aAAa;AAAA,UACb,oBAAoB;AAAA,UACpB,QAAQ;AAAA,UACR,WAAW;AAAA,UACX,UAAU;AAAA,UACV,QAAQ;AAAA,UACR;AAAA,UACA;AAAA,QACF,CAAC,CAAC;AACF,cAAM,GAAG,MAAM;AAAA,MACjB;AAiBA,UAAI;AACF,cAAM,qBAAqB,UAAU,QAAQ,+BAA+B;AAC5E,cAAM,wBAAwB,WAAW,sBAAsB;AAC/D,cAAM,kBAAkB,EAAE,UAAU,gBAAgB,QAAQ,KAAK;AACjE,cAAM,sBAAsB,MAAM,mBAAmB,QAAQ,uBAAuB,eAAe;AACnG,YAAI,CAAC,qBAAqB;AACxB,gBAAM,mBAAmB;AAAA,YACvB;AAAA,YACA,EAAE,UAAU,MAAM,aAAa,uBAAuB;AAAA,YACtD;AAAA,UACF;AAAA,QACF;AAAA,MACF,SAAS,OAAO;AACd,eAAO,KAAK,yFAAyF;AAAA,UACnG,KAAK;AAAA,UACL;AAAA,UACA;AAAA,QACF,CAAC;AAAA,MACH;AAAA,IACF;AAeA,UAAM,
|
|
4
|
+
"sourcesContent": ["import { createHash } from 'node:crypto'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport type { ModuleSetupConfig } from '@open-mercato/shared/modules/setup'\nimport { CommunicationChannel } from './data/entities'\nimport { COMMUNICATION_CHANNELS_QUEUES } from './lib/queue'\nimport { isTestChannelSeedingEnabled, TEST_SEED_PROVIDER_KEY } from './lib/test-seed'\n\nconst logger = createLogger('communication_channels')\n\ntype SchedulerServiceLike = {\n register: (registration: {\n id: string\n name: string\n scopeType: 'system' | 'organization' | 'tenant'\n organizationId?: string\n tenantId?: string\n scheduleType: 'cron' | 'interval'\n scheduleValue: string\n timezone?: string\n targetType: 'queue' | 'command'\n targetQueue?: string\n targetPayload?: unknown\n sourceType?: 'user' | 'module'\n sourceModule?: string\n isEnabled?: boolean\n description?: string\n }) => Promise<void>\n}\n\ntype CredentialsServiceLike = {\n resolve: (\n integrationId: string,\n scope: { organizationId: string; tenantId: string; userId?: string | null },\n ) => Promise<Record<string, unknown> | null>\n save: (\n integrationId: string,\n credentials: Record<string, unknown>,\n scope: { organizationId: string; tenantId: string; userId?: string | null },\n ) => Promise<void>\n}\n\n/**\n * Sender the test-seed channel presents. Shared by the channel row's\n * `externalIdentifier` and its seeded credentials so both describe one address.\n */\nconst TEST_SEED_FROM_ADDRESS = 'system@test-seed.local'\n\n/**\n * Tick interval in seconds. Default 60s per email integration spec\n * \u00A7 Hub Deltas \u2192 Delta 6 (scheduler mechanism).\n */\nconst POLL_TICK_INTERVAL_SECONDS = Math.max(\n 10,\n Number.parseInt(process.env.OM_HUB_POLL_SCHEDULER_TICK_SECONDS ?? '60', 10) || 60,\n)\n\n/**\n * `scheduled_jobs.id` is a uuid column, so a module-owned schedule's stable\n * registration key must be hashed into a uuid rather than used verbatim \u2014 this\n * keeps `schedulerService.register()` an idempotent upsert across re-runs of\n * seedDefaults instead of trying to insert a raw string into the uuid PK.\n */\nfunction stableScheduleUuid(stableKey: string): string {\n const hex = createHash('sha256').update(stableKey).digest('hex')\n return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`\n}\n\nfunction stablePollTickScheduleId(organizationId: string): string {\n return stableScheduleUuid(`communication_channels:poll-tick:${organizationId}`)\n}\n\nasync function ensureCommunicationChannelSchedules(ctx: {\n container: import('awilix').AwilixContainer\n tenantId: string\n organizationId: string\n}): Promise<void> {\n const cradle = ctx.container as { hasRegistration?: (name: string) => boolean }\n if (typeof cradle.hasRegistration !== 'function' || !cradle.hasRegistration('schedulerService')) return\n\n const schedulerService = ctx.container.resolve('schedulerService') as SchedulerServiceLike\n try {\n await schedulerService.register({\n id: stablePollTickScheduleId(ctx.organizationId),\n name: 'Communication channels poll tick',\n description:\n `Enumerates active polling channels every ${POLL_TICK_INTERVAL_SECONDS}s and enqueues per-channel poll jobs.`,\n scopeType: 'organization',\n organizationId: ctx.organizationId,\n tenantId: ctx.tenantId,\n scheduleType: 'interval',\n scheduleValue: `${POLL_TICK_INTERVAL_SECONDS}s`,\n timezone: 'UTC',\n targetType: 'queue',\n targetQueue: COMMUNICATION_CHANNELS_QUEUES.pollTick,\n targetPayload: {\n scope: { tenantId: ctx.tenantId, organizationId: ctx.organizationId },\n },\n sourceType: 'module',\n sourceModule: 'communication_channels',\n isEnabled: true,\n })\n\n await schedulerService.register({\n id: stableScheduleUuid(`communication_channels:gmail-renew-watch:${ctx.organizationId}`),\n name: 'Gmail watch renewal',\n description:\n 'Daily 04:00 UTC. Re-issues gmail.users.watch for channels within OM_PUSH_RENEWAL_GMAIL_LEAD_HOURS of expiry.',\n scopeType: 'organization',\n organizationId: ctx.organizationId,\n tenantId: ctx.tenantId,\n scheduleType: 'cron',\n scheduleValue: '0 4 * * *',\n timezone: 'UTC',\n targetType: 'queue',\n targetQueue: COMMUNICATION_CHANNELS_QUEUES.gmailRenewWatch,\n targetPayload: { scope: { tenantId: ctx.tenantId, organizationId: ctx.organizationId } },\n sourceType: 'module',\n sourceModule: 'communication_channels',\n isEnabled: true,\n })\n } catch (error) {\n logger.warn('Failed to register module schedules', { err: error })\n }\n}\n\nexport const setup: ModuleSetupConfig = {\n defaultRoleFeatures: {\n superadmin: [\n 'communication_channels.view',\n 'communication_channels.manage',\n 'communication_channels.react',\n 'communication_channels.assign',\n 'communication_channels.connect_user_channel',\n 'communication_channels.share_own_channel',\n 'communication_channels.connect_tenant_channel',\n 'communication_channels.admin',\n 'communication_channels.channel.import_history',\n 'communication_channels.channel.push.manage',\n ],\n admin: [\n 'communication_channels.view',\n 'communication_channels.manage',\n 'communication_channels.react',\n 'communication_channels.assign',\n 'communication_channels.connect_user_channel',\n 'communication_channels.share_own_channel',\n 'communication_channels.connect_tenant_channel',\n 'communication_channels.admin',\n 'communication_channels.channel.import_history',\n 'communication_channels.channel.push.manage',\n ],\n manager: [\n 'communication_channels.view',\n 'communication_channels.manage',\n 'communication_channels.react',\n 'communication_channels.assign',\n 'communication_channels.connect_user_channel',\n 'communication_channels.share_own_channel',\n ],\n employee: [\n 'communication_channels.view',\n 'communication_channels.react',\n 'communication_channels.connect_user_channel',\n 'communication_channels.share_own_channel',\n ],\n },\n\n async seedDefaults({ container, organizationId, tenantId }) {\n if (isTestChannelSeedingEnabled() && process.env.SYSTEM_EMAIL_PROVIDER === TEST_SEED_PROVIDER_KEY) {\n const em = (container.resolve('em') as EntityManager).fork()\n const existing = 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 (!existing) {\n em.persist(em.create(CommunicationChannel, {\n providerKey: TEST_SEED_PROVIDER_KEY,\n channelType: 'email',\n displayName: 'Test Seed System Email',\n externalIdentifier: TEST_SEED_FROM_ADDRESS,\n userId: null,\n isPrimary: false,\n isActive: true,\n status: 'connected',\n tenantId,\n organizationId,\n }))\n await em.flush()\n }\n\n /**\n * Seeding the channel row without credentials is not a neutral state \u2014 it is a\n * fail-closed one. Once a tenant owns a Hub channel, `resolveTenantCredentials`\n * deliberately refuses to fall back to instance-wide env credentials, so every\n * tenant-scoped send throws `SYSTEM_EMAIL_CREDENTIALS_NOT_CONFIGURED`. Callers that\n * treat a failed send as a failed write (customer invitations roll back and return\n * 502) then fail outright rather than merely losing the email.\n *\n * Seeding credentials alongside the row makes the harness model a tenant that\n * finished configuring its provider, which is also the only one of the three\n * credential cases the integration suite would otherwise never exercise.\n */\n // Best-effort, like the scheduler registration below: credential storage needs the\n // integrations module and a usable encryption key, and neither is guaranteed in every\n // harness. A failure here must not abort tenant initialization for every other module.\n try {\n const credentialsService = container.resolve('integrationCredentialsService') as CredentialsServiceLike\n const testSeedIntegrationId = `channel_${TEST_SEED_PROVIDER_KEY}`\n const credentialScope = { tenantId, organizationId, userId: null }\n const existingCredentials = await credentialsService.resolve(testSeedIntegrationId, credentialScope)\n if (!existingCredentials) {\n await credentialsService.save(\n testSeedIntegrationId,\n { testSeed: true, fromAddress: TEST_SEED_FROM_ADDRESS },\n credentialScope,\n )\n }\n } catch (error) {\n logger.warn('Test-seed email credentials could not be stored; tenant-scoped sends will fail closed', {\n err: error,\n tenantId,\n organizationId,\n })\n }\n }\n\n /**\n * Register the per-channel polling tick with `@open-mercato/scheduler`.\n *\n * Per email integration spec \u00A7 Hub Deltas \u2192 Delta 6: every\n * `POLL_TICK_INTERVAL_SECONDS` (default 60s), enqueue a single\n * `communication-channels-poll-tick` job that enumerates due channels and\n * fans out to the `communication-channels-poll` queue.\n *\n * The registration is per-organization (one tick row per org/tenant pair)\n * so multi-tenant deploys schedule independently. Skipped silently when the\n * scheduler module isn't enabled \u2014 keeps the hub usable in scheduler-less\n * test harnesses.\n */\n await ensureCommunicationChannelSchedules({ container, tenantId, organizationId })\n },\n\n async onOrganizationCreated({ container, tenantId, organizationId }) {\n await ensureCommunicationChannelSchedules({ container, tenantId, organizationId })\n },\n}\n\nexport default setup\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Resolver, promises as dnsPromises } from "node:dns";
|
|
2
2
|
import { request as httpsRequest } from "node:https";
|
|
3
|
+
import { resolveSafeHostAddress } from "@open-mercato/shared/lib/host-pinning";
|
|
4
|
+
import { getTelemetryRuntime } from "@open-mercato/shared/lib/telemetry/runtime";
|
|
3
5
|
import { setTimeout as delay } from "node:timers/promises";
|
|
4
6
|
import { runWithCacheTenant } from "@open-mercato/cache";
|
|
5
7
|
import {
|
|
@@ -46,36 +48,60 @@ const defaultDnsResolver = {
|
|
|
46
48
|
}
|
|
47
49
|
};
|
|
48
50
|
const defaultHealthCheck = (hostname, timeoutMs) => new Promise((resolve) => {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
(
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
51
|
+
let req;
|
|
52
|
+
let settled = false;
|
|
53
|
+
const failure = { ok: false, originHeaderPresent: false, reason: "TLS health check failed" };
|
|
54
|
+
const finish = (result) => {
|
|
55
|
+
if (settled) return;
|
|
56
|
+
settled = true;
|
|
57
|
+
clearTimeout(deadline);
|
|
58
|
+
resolve(result);
|
|
59
|
+
};
|
|
60
|
+
const deadline = setTimeout(() => {
|
|
61
|
+
finish(failure);
|
|
62
|
+
req?.destroy();
|
|
63
|
+
}, timeoutMs);
|
|
64
|
+
void resolveSafeHostAddress(hostname).then(({ host }) => {
|
|
65
|
+
if (settled) return;
|
|
66
|
+
const headerName = (process.env.CUSTOMER_DOMAIN_ORIGIN_HEADER ?? "X-Open-Mercato-Origin").toLowerCase();
|
|
67
|
+
req = httpsRequest(
|
|
68
|
+
{
|
|
69
|
+
host,
|
|
70
|
+
servername: hostname,
|
|
71
|
+
rejectUnauthorized: true,
|
|
72
|
+
agent: false,
|
|
73
|
+
port: 443,
|
|
74
|
+
path: "/api/customer_accounts/domain-check",
|
|
75
|
+
method: "GET",
|
|
76
|
+
headers: { Host: hostname, "X-Domain-Check-Secret": process.env.DOMAIN_CHECK_SECRET ?? "" },
|
|
77
|
+
timeout: timeoutMs
|
|
78
|
+
},
|
|
79
|
+
(res) => {
|
|
80
|
+
const headerValue = res.headers[headerName];
|
|
81
|
+
const originHeader = Array.isArray(headerValue) ? headerValue[0] : headerValue;
|
|
82
|
+
const originHeaderPresent = typeof originHeader === "string" && originHeader === "1";
|
|
83
|
+
const status = res.statusCode ?? 0;
|
|
84
|
+
res.resume();
|
|
85
|
+
finish({
|
|
86
|
+
ok: status >= 200 && status < 400,
|
|
87
|
+
originHeaderPresent,
|
|
88
|
+
reason: status >= 200 && status < 400 ? void 0 : `HTTP ${status}`
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
);
|
|
92
|
+
req.on("timeout", () => {
|
|
93
|
+
finish(failure);
|
|
94
|
+
req?.destroy();
|
|
95
|
+
});
|
|
96
|
+
req.on("error", () => finish(failure));
|
|
97
|
+
req.end();
|
|
98
|
+
}).catch(() => {
|
|
99
|
+
getTelemetryRuntime()?.reportError(new Error("[internal] Domain TLS health check failed"), {
|
|
100
|
+
module: "customer_accounts",
|
|
101
|
+
code: "customer_accounts.domain_health_check_failed"
|
|
102
|
+
});
|
|
103
|
+
finish(failure);
|
|
77
104
|
});
|
|
78
|
-
req.end();
|
|
79
105
|
});
|
|
80
106
|
class DomainMappingService {
|
|
81
107
|
constructor(em, deps) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/customer_accounts/services/domainMappingService.ts"],
|
|
4
|
-
"sourcesContent": ["import { Resolver, promises as dnsPromises } from 'node:dns'\nimport { request as httpsRequest } from 'node:https'\nimport { setTimeout as delay } from 'node:timers/promises'\nimport { EntityManager } from '@mikro-orm/postgresql'\nimport { runWithCacheTenant } from '@open-mercato/cache'\nimport {\n DomainMapping,\n type DomainStatus,\n} from '@open-mercato/core/modules/customer_accounts/data/entities'\nimport { Organization } from '@open-mercato/core/modules/directory/data/entities'\nimport { emitCustomerAccountsEvent } from '@open-mercato/core/modules/customer_accounts/events'\nimport { normalizeHostname, tryNormalizeHostname } from '@open-mercato/core/modules/customer_accounts/lib/hostname'\nimport { findOrganizationInTenant } from '@open-mercato/core/modules/customer_accounts/lib/organizationLookup'\nimport { platformDomains } from '@open-mercato/core/modules/customer_accounts/lib/platformDomains'\nimport { detectProxy, isInKnownProxyRange } from '@open-mercato/core/modules/customer_accounts/lib/proxyRanges'\n\nexport class DomainMappingOrgScopeError extends Error {\n constructor(organizationId: string) {\n super(`[internal] organizationId ${organizationId} does not belong to the caller's tenant`)\n this.name = 'DomainMappingOrgScopeError'\n }\n}\n\nconst DOMAIN_ROUTING_TAG = 'domain_routing'\nconst RESOLVE_KEY_PREFIX = 'domain_routing:resolve'\nconst ACTIVE_BY_ORG_KEY_PREFIX = 'domain_routing:active-by-org'\nconst RESOLVE_TTL_MS = 5 * 60_000\nconst TLS_HEALTH_CHECK_TIMEOUT_MS = 10_000\nconst TLS_HEALTH_CHECK_RETRY_DELAYS_MS = [1_000, 4_000, 16_000]\nconst DEFAULT_DNS_RECHECK_THRESHOLD_MS = 5 * 60_000\nconst DEFAULT_TLS_MAX_RETRIES = 6\n\nexport type ResolveResult = {\n domainMappingId: string\n hostname: string\n tenantId: string\n organizationId: string\n orgSlug: string | null\n status: DomainStatus\n}\n\nexport type DnsDiagnostics = {\n expectedCnameTarget: string\n expectedARecordTarget: string | null\n detectedRecords: Array<{ type: 'CNAME' | 'A'; value: string; proxy?: string }>\n reverseResolve?: { attempted: boolean; originHeaderPresent: boolean }\n suggestion: string\n}\n\nexport type VerifyResult = {\n domainMapping: DomainMapping\n diagnostics?: DnsDiagnostics\n}\n\nexport type RegisterInput = {\n hostname: string\n organizationId: string\n tenantId: string\n replacesDomainId?: string\n}\n\ntype CacheService = {\n get(key: string, options?: unknown): Promise<unknown>\n set(key: string, value: unknown, options?: { ttl?: number; tags?: string[] }): Promise<void>\n deleteByTags(tags: string[]): Promise<number>\n}\n\ntype DnsResolverContract = {\n resolveCname(hostname: string): Promise<string[]>\n resolve4(hostname: string): Promise<string[]>\n}\n\ntype HealthCheckContract = (hostname: string, timeoutMs: number) => Promise<{\n ok: boolean\n originHeaderPresent: boolean\n reason?: string\n}>\n\nconst defaultDnsResolver: DnsResolverContract = {\n async resolveCname(hostname) {\n try {\n return await dnsPromises.resolveCname(hostname)\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code\n if (code === 'ENODATA' || code === 'ENOTFOUND') return []\n throw err\n }\n },\n async resolve4(hostname) {\n try {\n return await dnsPromises.resolve4(hostname)\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code\n if (code === 'ENODATA' || code === 'ENOTFOUND') return []\n throw err\n }\n },\n}\n\nconst defaultHealthCheck: HealthCheckContract = (hostname, timeoutMs) =>\n new Promise((resolve) => {\n const headerName = (process.env.CUSTOMER_DOMAIN_ORIGIN_HEADER ?? 'X-Open-Mercato-Origin').toLowerCase()\n const req = httpsRequest(\n {\n host: hostname,\n port: 443,\n path: '/api/customer_accounts/domain-check',\n method: 'GET',\n headers: { 'X-Domain-Check-Secret': process.env.DOMAIN_CHECK_SECRET ?? '' },\n timeout: timeoutMs,\n },\n (res) => {\n const headerValue = res.headers[headerName]\n const originHeader = Array.isArray(headerValue) ? headerValue[0] : headerValue\n const originHeaderPresent = typeof originHeader === 'string' && originHeader === '1'\n const status = res.statusCode ?? 0\n res.resume() // discard body\n resolve({\n ok: status >= 200 && status < 400,\n originHeaderPresent,\n reason: status >= 200 && status < 400 ? undefined : `HTTP ${status}`,\n })\n },\n )\n req.on('timeout', () => {\n req.destroy(new Error('TLS health check timed out'))\n })\n req.on('error', (err) => {\n resolve({ ok: false, originHeaderPresent: false, reason: (err as Error).message })\n })\n req.end()\n })\n\nexport class DomainMappingService {\n private cache: CacheService | null\n private dns: DnsResolverContract\n private healthCheckImpl: HealthCheckContract\n\n constructor(\n private em: EntityManager,\n deps?: {\n cacheService?: CacheService\n dnsResolver?: DnsResolverContract\n healthCheck?: HealthCheckContract\n },\n ) {\n this.cache = deps?.cacheService ?? null\n this.dns = deps?.dnsResolver ?? defaultDnsResolver\n this.healthCheckImpl = deps?.healthCheck ?? defaultHealthCheck\n }\n\n // -------------------------------------------------------------------------\n // Read paths\n // -------------------------------------------------------------------------\n\n async findById(id: string, scope?: { tenantId?: string }): Promise<DomainMapping | null> {\n const where: Record<string, unknown> = { id }\n if (scope?.tenantId) where.tenantId = scope.tenantId\n return this.em.findOne(DomainMapping, where as never)\n }\n\n async findByOrganization(\n organizationId: string,\n scope?: { tenantId?: string },\n ): Promise<DomainMapping[]> {\n const where: Record<string, unknown> = { organizationId }\n if (scope?.tenantId) where.tenantId = scope.tenantId\n return this.em.find(DomainMapping, where as never, { orderBy: { createdAt: 'asc' } })\n }\n\n async resolveByHostname(input: string): Promise<ResolveResult | null> {\n const hostname = tryNormalizeHostname(input)\n if (!hostname) return null\n if (platformDomains().includes(hostname)) return null\n\n const cacheKey = `${RESOLVE_KEY_PREFIX}:${hostname}`\n const cache = this.cache\n if (cache) {\n const cached = (await runWithCacheTenant(null, () => cache.get(cacheKey))) as ResolveResult | null | undefined\n if (cached !== undefined && cached !== null) return cached\n }\n\n const result = await this.lookupResolveResult(hostname)\n if (cache) {\n await runWithCacheTenant(null, () => cache.set(cacheKey, result, {\n ttl: RESOLVE_TTL_MS,\n tags: [DOMAIN_ROUTING_TAG, `${DOMAIN_ROUTING_TAG}:${hostname}`],\n }))\n }\n return result\n }\n\n async isAllowedForTls(input: string): Promise<{ organizationId: string; status: DomainStatus } | null> {\n const hostname = tryNormalizeHostname(input)\n if (!hostname) return null\n if (platformDomains().includes(hostname)) return null\n const row = await this.em.findOne(DomainMapping, {\n hostname,\n status: { $in: ['active', 'verified'] },\n } as never)\n if (!row) return null\n return { organizationId: row.organizationId, status: row.status }\n }\n\n async resolveActiveByOrg(organizationId: string): Promise<{ hostname: string; status: DomainStatus } | null> {\n const cacheKey = `${ACTIVE_BY_ORG_KEY_PREFIX}:${organizationId}`\n if (this.cache) {\n const cached = (await this.cache.get(cacheKey)) as { hostname: string; status: DomainStatus } | null | undefined\n if (cached !== undefined && cached !== null) return cached\n }\n\n const row = await this.em.findOne(DomainMapping, {\n organizationId,\n status: 'active',\n } as never)\n const result = row ? { hostname: row.hostname, status: row.status } : null\n\n if (this.cache) {\n await this.cache.set(cacheKey, result, {\n ttl: RESOLVE_TTL_MS,\n tags: [DOMAIN_ROUTING_TAG, `${DOMAIN_ROUTING_TAG}:org:${organizationId}`],\n })\n }\n return result\n }\n\n async resolveAll(): Promise<ResolveResult[]> {\n const rows = await this.em.find(DomainMapping, { status: 'active' } as never)\n if (rows.length === 0) return []\n const orgIds = Array.from(new Set(rows.map((r) => r.organizationId)))\n const orgs = await this.em.find(Organization, { id: { $in: orgIds } } as never)\n const slugByOrg = new Map<string, string | null>(orgs.map((o) => [o.id, o.slug ?? null]))\n\n return rows.map<ResolveResult>((row) => ({\n domainMappingId: row.id,\n hostname: row.hostname,\n tenantId: row.tenantId,\n organizationId: row.organizationId,\n orgSlug: slugByOrg.get(row.organizationId) ?? null,\n status: row.status,\n }))\n }\n\n // -------------------------------------------------------------------------\n // Worker queries\n // -------------------------------------------------------------------------\n\n async findPendingVerification(threshold?: { olderThanMs?: number }): Promise<DomainMapping[]> {\n const olderThan = threshold?.olderThanMs ?? DEFAULT_DNS_RECHECK_THRESHOLD_MS\n const cutoff = new Date(Date.now() - olderThan)\n return this.em.find(\n DomainMapping,\n {\n status: { $in: ['pending', 'dns_failed'] },\n $or: [{ lastDnsCheckAt: null }, { lastDnsCheckAt: { $lt: cutoff } }],\n } as never,\n { orderBy: { lastDnsCheckAt: 'asc' } },\n )\n }\n\n async findPendingTls(options?: { maxRetries?: number; batchSize?: number }): Promise<DomainMapping[]> {\n const maxRetries = options?.maxRetries ?? DEFAULT_TLS_MAX_RETRIES\n const limit = options?.batchSize ?? 50\n return this.em.find(\n DomainMapping,\n {\n $or: [\n { status: 'verified' },\n { status: 'tls_failed', tlsRetryCount: { $lt: maxRetries } },\n ],\n } as never,\n { orderBy: { updatedAt: 'asc' }, limit },\n )\n }\n\n // -------------------------------------------------------------------------\n // Write paths\n // -------------------------------------------------------------------------\n\n async register(input: RegisterInput): Promise<DomainMapping> {\n const hostname = normalizeHostname(input.hostname)\n const organization = await findOrganizationInTenant(this.em, input.organizationId, input.tenantId)\n if (!organization) throw new DomainMappingOrgScopeError(input.organizationId)\n\n let replacesDomain: DomainMapping | null = null\n if (input.replacesDomainId) {\n replacesDomain = await this.em.findOne(DomainMapping, {\n id: input.replacesDomainId,\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n } as never)\n }\n\n const entity = this.em.create(DomainMapping, {\n hostname,\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n replacesDomain: replacesDomain ?? null,\n provider: 'traefik',\n status: 'pending',\n tlsRetryCount: 0,\n createdAt: new Date(),\n } as never)\n await this.em.persist(entity).flush()\n\n await emitCustomerAccountsEvent('customer_accounts.domain_mapping.created', {\n id: entity.id,\n hostname: entity.hostname,\n organizationId: entity.organizationId,\n tenantId: entity.tenantId,\n status: entity.status,\n } as never)\n await this.invalidateCacheFor(entity.hostname, entity.organizationId)\n\n return entity\n }\n\n async verify(id: string): Promise<VerifyResult> {\n const entity = await this.em.findOne(DomainMapping, { id } as never)\n if (!entity) throw new Error(`DomainMapping ${id} not found`)\n\n const expectedCname = process.env.CUSTOM_DOMAIN_CNAME_TARGET ?? ''\n const expectedARecord = process.env.CUSTOM_DOMAIN_A_RECORD_TARGET ?? null\n const now = new Date()\n entity.lastDnsCheckAt = now\n\n const verification = await this.runDnsVerification(entity.hostname, {\n expectedCname,\n expectedARecord,\n })\n\n if (verification.ok) {\n entity.status = 'verified'\n entity.verifiedAt = now\n entity.dnsFailureReason = null\n entity.tlsRetryCount = 0\n entity.tlsFailureReason = null\n await this.em.persist(entity).flush()\n await emitCustomerAccountsEvent('customer_accounts.domain_mapping.verified', {\n id: entity.id,\n hostname: entity.hostname,\n organizationId: entity.organizationId,\n tenantId: entity.tenantId,\n } as never)\n await this.invalidateCacheFor(entity.hostname, entity.organizationId)\n return { domainMapping: entity }\n }\n\n entity.status = 'dns_failed'\n entity.dnsFailureReason = verification.reason\n await this.em.persist(entity).flush()\n await emitCustomerAccountsEvent('customer_accounts.domain_mapping.dns_failed', {\n id: entity.id,\n hostname: entity.hostname,\n organizationId: entity.organizationId,\n tenantId: entity.tenantId,\n reason: verification.reason,\n detectedRecords: verification.diagnostics.detectedRecords,\n } as never)\n await this.invalidateCacheFor(entity.hostname, entity.organizationId)\n return { domainMapping: entity, diagnostics: verification.diagnostics }\n }\n\n async activate(id: string): Promise<DomainMapping> {\n const entity = await this.em.findOne(DomainMapping, { id } as never)\n if (!entity) throw new Error(`DomainMapping ${id} not found`)\n if (entity.status === 'active') return entity\n if (entity.status !== 'verified' && entity.status !== 'tls_failed') {\n throw new Error(`DomainMapping ${id} cannot transition to active from ${entity.status}`)\n }\n\n entity.status = 'active'\n entity.tlsRetryCount = 0\n entity.tlsFailureReason = null\n await this.em.persist(entity).flush()\n\n await emitCustomerAccountsEvent('customer_accounts.domain_mapping.activated', {\n id: entity.id,\n hostname: entity.hostname,\n organizationId: entity.organizationId,\n tenantId: entity.tenantId,\n } as never)\n\n if (entity.replacesDomain) {\n const replaced = await this.em.findOne(DomainMapping, {\n id: (entity.replacesDomain as unknown as { id: string }).id,\n tenantId: entity.tenantId,\n organizationId: entity.organizationId,\n } as never)\n if (replaced) {\n const replacedHostname = replaced.hostname\n const replacedOrg = replaced.organizationId\n this.em.remove(replaced)\n await this.em.flush()\n await emitCustomerAccountsEvent('customer_accounts.domain_mapping.replaced', {\n id: entity.id,\n hostname: entity.hostname,\n organizationId: entity.organizationId,\n tenantId: entity.tenantId,\n replacedDomainId: replaced.id,\n replacedHostname,\n } as never)\n await emitCustomerAccountsEvent('customer_accounts.domain_mapping.deleted', {\n id: replaced.id,\n hostname: replacedHostname,\n organizationId: replacedOrg,\n tenantId: replaced.tenantId,\n } as never)\n await this.invalidateCacheFor(replacedHostname, replacedOrg)\n }\n }\n\n await this.invalidateCacheFor(entity.hostname, entity.organizationId)\n return entity\n }\n\n async remove(id: string, scope?: { tenantId?: string }): Promise<void> {\n const where: Record<string, unknown> = { id }\n if (scope?.tenantId) where.tenantId = scope.tenantId\n const entity = await this.em.findOne(DomainMapping, where as never)\n if (!entity) return\n\n const hostname = entity.hostname\n const organizationId = entity.organizationId\n const tenantId = entity.tenantId\n\n this.em.remove(entity)\n await this.em.flush()\n\n await emitCustomerAccountsEvent('customer_accounts.domain_mapping.deleted', {\n id,\n hostname,\n organizationId,\n tenantId,\n } as never)\n await this.invalidateCacheFor(hostname, organizationId)\n }\n\n async healthCheck(id: string): Promise<DomainMapping> {\n const entity = await this.em.findOne(DomainMapping, { id } as never)\n if (!entity) throw new Error(`DomainMapping ${id} not found`)\n if (entity.status === 'active') return entity\n if (entity.status !== 'verified' && entity.status !== 'tls_failed') {\n throw new Error(`DomainMapping ${id} cannot run health check from status ${entity.status}`)\n }\n\n let lastReason: string | null = null\n for (let attempt = 0; attempt < TLS_HEALTH_CHECK_RETRY_DELAYS_MS.length; attempt++) {\n const result = await this.healthCheckImpl(entity.hostname, TLS_HEALTH_CHECK_TIMEOUT_MS)\n if (result.ok) {\n return this.activate(entity.id)\n }\n lastReason = result.reason ?? 'TLS health check failed'\n if (attempt + 1 < TLS_HEALTH_CHECK_RETRY_DELAYS_MS.length) {\n await delay(TLS_HEALTH_CHECK_RETRY_DELAYS_MS[attempt])\n }\n }\n\n entity.status = 'tls_failed'\n entity.tlsFailureReason = lastReason ?? 'TLS health check failed'\n entity.tlsRetryCount = (entity.tlsRetryCount ?? 0) + 1\n await this.em.persist(entity).flush()\n\n await emitCustomerAccountsEvent('customer_accounts.domain_mapping.tls_failed', {\n id: entity.id,\n hostname: entity.hostname,\n organizationId: entity.organizationId,\n tenantId: entity.tenantId,\n reason: entity.tlsFailureReason,\n retryCount: entity.tlsRetryCount,\n } as never)\n await this.invalidateCacheFor(entity.hostname, entity.organizationId)\n return entity\n }\n\n // -------------------------------------------------------------------------\n // Internals\n // -------------------------------------------------------------------------\n\n private async lookupResolveResult(hostname: string): Promise<ResolveResult | null> {\n const row = await this.em.findOne(DomainMapping, { hostname, status: 'active' } as never)\n if (!row) return null\n const org = await this.em.findOne(Organization, { id: row.organizationId } as never)\n return {\n domainMappingId: row.id,\n hostname: row.hostname,\n tenantId: row.tenantId,\n organizationId: row.organizationId,\n orgSlug: org?.slug ?? null,\n status: row.status,\n }\n }\n\n private async invalidateCacheFor(hostname: string, organizationId: string | null): Promise<void> {\n const cache = this.cache\n if (!cache) return\n const tags = [DOMAIN_ROUTING_TAG, `${DOMAIN_ROUTING_TAG}:${hostname}`]\n if (organizationId) tags.push(`${DOMAIN_ROUTING_TAG}:org:${organizationId}`)\n await Promise.allSettled([\n cache.deleteByTags(tags),\n runWithCacheTenant(null, () => cache.deleteByTags([`${DOMAIN_ROUTING_TAG}:${hostname}`])),\n ])\n }\n\n private async runDnsVerification(\n hostname: string,\n expected: { expectedCname: string; expectedARecord: string | null },\n ): Promise<\n | {\n ok: true\n method: 'cname' | 'a-record' | 'reverse-resolve'\n diagnostics: DnsDiagnostics\n }\n | { ok: false; reason: string; diagnostics: DnsDiagnostics }\n > {\n const detectedRecords: DnsDiagnostics['detectedRecords'] = []\n const baseDiag = (overrides?: Partial<DnsDiagnostics>): DnsDiagnostics => ({\n expectedCnameTarget: expected.expectedCname,\n expectedARecordTarget: expected.expectedARecord,\n detectedRecords,\n suggestion: overrides?.suggestion ?? '',\n reverseResolve: overrides?.reverseResolve,\n })\n\n // Phase 1: CNAME\n let cnameRecords: string[] = []\n try {\n cnameRecords = await this.dns.resolveCname(hostname)\n } catch (err) {\n return {\n ok: false,\n reason: `DNS lookup error (CNAME): ${(err as Error).message}`,\n diagnostics: baseDiag({\n suggestion: 'DNS lookup failed unexpectedly. Try again in a few minutes.',\n }),\n }\n }\n for (const cname of cnameRecords) detectedRecords.push({ type: 'CNAME', value: cname })\n\n if (expected.expectedCname && cnameRecords.length > 0) {\n const expectedCname = tryNormalizeHostname(expected.expectedCname) ?? expected.expectedCname.toLowerCase()\n const match = cnameRecords.some((c) => (tryNormalizeHostname(c) ?? c.toLowerCase()) === expectedCname)\n if (match) {\n return {\n ok: true,\n method: 'cname',\n diagnostics: baseDiag({\n suggestion: 'CNAME record matches the expected target.',\n }),\n }\n }\n return {\n ok: false,\n reason: `CNAME points to ${cnameRecords.join(', ')} instead of ${expected.expectedCname}`,\n diagnostics: baseDiag({\n suggestion: `Update your CNAME record to point to ${expected.expectedCname}.`,\n }),\n }\n }\n\n // Phase 2: A record\n let aRecords: string[] = []\n try {\n aRecords = await this.dns.resolve4(hostname)\n } catch (err) {\n return {\n ok: false,\n reason: `DNS lookup error (A): ${(err as Error).message}`,\n diagnostics: baseDiag({ suggestion: 'DNS lookup failed unexpectedly. Try again in a few minutes.' }),\n }\n }\n for (const a of aRecords) {\n const proxy = detectProxy(a)\n detectedRecords.push({ type: 'A', value: a, ...(proxy ? { proxy } : {}) })\n }\n\n if (aRecords.length === 0) {\n return {\n ok: false,\n reason: `No CNAME or A record found for ${hostname}`,\n diagnostics: baseDiag({\n suggestion: expected.expectedARecord\n ? `For a subdomain, add a CNAME record pointing to ${expected.expectedCname}. For an apex domain, add an A record pointing to ${expected.expectedARecord}. DNS propagation can take up to 48 hours.`\n : `Add a CNAME record pointing to ${expected.expectedCname}. DNS propagation can take up to 48 hours.`,\n }),\n }\n }\n\n if (expected.expectedARecord && aRecords.includes(expected.expectedARecord)) {\n return {\n ok: true,\n method: 'a-record',\n diagnostics: baseDiag({ suggestion: 'A record matches the expected target.' }),\n }\n }\n\n // Phase 3: reverse-resolve through proxy\n const proxiedRecords = aRecords.filter((ip) => isInKnownProxyRange(ip))\n if (proxiedRecords.length > 0) {\n const probe = await this.healthCheckImpl(hostname, TLS_HEALTH_CHECK_TIMEOUT_MS)\n if (probe.ok && probe.originHeaderPresent) {\n return {\n ok: true,\n method: 'reverse-resolve',\n diagnostics: baseDiag({\n reverseResolve: { attempted: true, originHeaderPresent: true },\n suggestion: 'Domain is proxied \u2014 reverse-resolve confirmed traffic reaches our origin.',\n }),\n }\n }\n return {\n ok: false,\n reason: 'A record points to a known proxy IP, but reverse-resolve over HTTPS did not reach our server',\n diagnostics: baseDiag({\n reverseResolve: { attempted: true, originHeaderPresent: false },\n suggestion: expected.expectedCname\n ? `Your DNS uses a proxy. Either disable the proxy and add a CNAME \u2192 ${expected.expectedCname}, or configure your proxy to forward traffic to ${expected.expectedCname}.`\n : 'Disable the DNS proxy or configure it to forward traffic to our platform.',\n }),\n }\n }\n\n return {\n ok: false,\n reason: expected.expectedARecord\n ? `A record points to ${aRecords.join(', ')} instead of ${expected.expectedARecord}`\n : `A record points to ${aRecords.join(', ')} but apex-domain registration is not enabled on this deployment`,\n diagnostics: baseDiag({\n suggestion: expected.expectedARecord\n ? `Update your A record to point to ${expected.expectedARecord}.`\n : `Apex-domain registration is not enabled on this deployment. Use a subdomain (e.g., shop.${hostname}) and add a CNAME pointing to ${expected.expectedCname}.`,\n }),\n }\n }\n}\n\n// Re-exported for tests; allow injection of fakes.\nexport const __testing__ = {\n DEFAULT_DNS_RECHECK_THRESHOLD_MS,\n DEFAULT_TLS_MAX_RETRIES,\n TLS_HEALTH_CHECK_RETRY_DELAYS_MS,\n RESOLVE_TTL_MS,\n DOMAIN_ROUTING_TAG,\n defaultHealthCheck,\n defaultDnsResolver,\n Resolver,\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import { Resolver, promises as dnsPromises } from 'node:dns'\nimport { request as httpsRequest } from 'node:https'\nimport type { ClientRequest } from 'node:http'\nimport { resolveSafeHostAddress } from '@open-mercato/shared/lib/host-pinning'\nimport { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'\nimport { setTimeout as delay } from 'node:timers/promises'\nimport { EntityManager } from '@mikro-orm/postgresql'\nimport { runWithCacheTenant } from '@open-mercato/cache'\nimport {\n DomainMapping,\n type DomainStatus,\n} from '@open-mercato/core/modules/customer_accounts/data/entities'\nimport { Organization } from '@open-mercato/core/modules/directory/data/entities'\nimport { emitCustomerAccountsEvent } from '@open-mercato/core/modules/customer_accounts/events'\nimport { normalizeHostname, tryNormalizeHostname } from '@open-mercato/core/modules/customer_accounts/lib/hostname'\nimport { findOrganizationInTenant } from '@open-mercato/core/modules/customer_accounts/lib/organizationLookup'\nimport { platformDomains } from '@open-mercato/core/modules/customer_accounts/lib/platformDomains'\nimport { detectProxy, isInKnownProxyRange } from '@open-mercato/core/modules/customer_accounts/lib/proxyRanges'\n\nexport class DomainMappingOrgScopeError extends Error {\n constructor(organizationId: string) {\n super(`[internal] organizationId ${organizationId} does not belong to the caller's tenant`)\n this.name = 'DomainMappingOrgScopeError'\n }\n}\n\nconst DOMAIN_ROUTING_TAG = 'domain_routing'\nconst RESOLVE_KEY_PREFIX = 'domain_routing:resolve'\nconst ACTIVE_BY_ORG_KEY_PREFIX = 'domain_routing:active-by-org'\nconst RESOLVE_TTL_MS = 5 * 60_000\nconst TLS_HEALTH_CHECK_TIMEOUT_MS = 10_000\nconst TLS_HEALTH_CHECK_RETRY_DELAYS_MS = [1_000, 4_000, 16_000]\nconst DEFAULT_DNS_RECHECK_THRESHOLD_MS = 5 * 60_000\nconst DEFAULT_TLS_MAX_RETRIES = 6\n\nexport type ResolveResult = {\n domainMappingId: string\n hostname: string\n tenantId: string\n organizationId: string\n orgSlug: string | null\n status: DomainStatus\n}\n\nexport type DnsDiagnostics = {\n expectedCnameTarget: string\n expectedARecordTarget: string | null\n detectedRecords: Array<{ type: 'CNAME' | 'A'; value: string; proxy?: string }>\n reverseResolve?: { attempted: boolean; originHeaderPresent: boolean }\n suggestion: string\n}\n\nexport type VerifyResult = {\n domainMapping: DomainMapping\n diagnostics?: DnsDiagnostics\n}\n\nexport type RegisterInput = {\n hostname: string\n organizationId: string\n tenantId: string\n replacesDomainId?: string\n}\n\ntype CacheService = {\n get(key: string, options?: unknown): Promise<unknown>\n set(key: string, value: unknown, options?: { ttl?: number; tags?: string[] }): Promise<void>\n deleteByTags(tags: string[]): Promise<number>\n}\n\ntype DnsResolverContract = {\n resolveCname(hostname: string): Promise<string[]>\n resolve4(hostname: string): Promise<string[]>\n}\n\ntype HealthCheckContract = (hostname: string, timeoutMs: number) => Promise<{\n ok: boolean\n originHeaderPresent: boolean\n reason?: string\n}>\n\nconst defaultDnsResolver: DnsResolverContract = {\n async resolveCname(hostname) {\n try {\n return await dnsPromises.resolveCname(hostname)\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code\n if (code === 'ENODATA' || code === 'ENOTFOUND') return []\n throw err\n }\n },\n async resolve4(hostname) {\n try {\n return await dnsPromises.resolve4(hostname)\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code\n if (code === 'ENODATA' || code === 'ENOTFOUND') return []\n throw err\n }\n },\n}\n\nconst defaultHealthCheck: HealthCheckContract = (hostname, timeoutMs) =>\n new Promise((resolve) => {\n let req: ClientRequest | undefined\n let settled = false\n const failure = { ok: false, originHeaderPresent: false, reason: 'TLS health check failed' }\n const finish = (result: Awaited<ReturnType<HealthCheckContract>>) => {\n if (settled) return\n settled = true\n clearTimeout(deadline)\n resolve(result)\n }\n const deadline = setTimeout(() => {\n finish(failure)\n req?.destroy()\n }, timeoutMs)\n\n void resolveSafeHostAddress(hostname).then(({ host }) => {\n if (settled) return\n const headerName = (process.env.CUSTOMER_DOMAIN_ORIGIN_HEADER ?? 'X-Open-Mercato-Origin').toLowerCase()\n req = httpsRequest(\n {\n host,\n servername: hostname,\n rejectUnauthorized: true,\n agent: false,\n port: 443,\n path: '/api/customer_accounts/domain-check',\n method: 'GET',\n headers: { Host: hostname, 'X-Domain-Check-Secret': process.env.DOMAIN_CHECK_SECRET ?? '' },\n timeout: timeoutMs,\n },\n (res) => {\n const headerValue = res.headers[headerName]\n const originHeader = Array.isArray(headerValue) ? headerValue[0] : headerValue\n const originHeaderPresent = typeof originHeader === 'string' && originHeader === '1'\n const status = res.statusCode ?? 0\n res.resume()\n finish({\n ok: status >= 200 && status < 400,\n originHeaderPresent,\n reason: status >= 200 && status < 400 ? undefined : `HTTP ${status}`,\n })\n },\n )\n req.on('timeout', () => {\n finish(failure)\n req?.destroy()\n })\n req.on('error', () => finish(failure))\n req.end()\n }).catch(() => {\n getTelemetryRuntime()?.reportError(new Error('[internal] Domain TLS health check failed'), {\n module: 'customer_accounts',\n code: 'customer_accounts.domain_health_check_failed',\n })\n finish(failure)\n })\n })\n\nexport class DomainMappingService {\n private cache: CacheService | null\n private dns: DnsResolverContract\n private healthCheckImpl: HealthCheckContract\n\n constructor(\n private em: EntityManager,\n deps?: {\n cacheService?: CacheService\n dnsResolver?: DnsResolverContract\n healthCheck?: HealthCheckContract\n },\n ) {\n this.cache = deps?.cacheService ?? null\n this.dns = deps?.dnsResolver ?? defaultDnsResolver\n this.healthCheckImpl = deps?.healthCheck ?? defaultHealthCheck\n }\n\n // -------------------------------------------------------------------------\n // Read paths\n // -------------------------------------------------------------------------\n\n async findById(id: string, scope?: { tenantId?: string }): Promise<DomainMapping | null> {\n const where: Record<string, unknown> = { id }\n if (scope?.tenantId) where.tenantId = scope.tenantId\n return this.em.findOne(DomainMapping, where as never)\n }\n\n async findByOrganization(\n organizationId: string,\n scope?: { tenantId?: string },\n ): Promise<DomainMapping[]> {\n const where: Record<string, unknown> = { organizationId }\n if (scope?.tenantId) where.tenantId = scope.tenantId\n return this.em.find(DomainMapping, where as never, { orderBy: { createdAt: 'asc' } })\n }\n\n async resolveByHostname(input: string): Promise<ResolveResult | null> {\n const hostname = tryNormalizeHostname(input)\n if (!hostname) return null\n if (platformDomains().includes(hostname)) return null\n\n const cacheKey = `${RESOLVE_KEY_PREFIX}:${hostname}`\n const cache = this.cache\n if (cache) {\n const cached = (await runWithCacheTenant(null, () => cache.get(cacheKey))) as ResolveResult | null | undefined\n if (cached !== undefined && cached !== null) return cached\n }\n\n const result = await this.lookupResolveResult(hostname)\n if (cache) {\n await runWithCacheTenant(null, () => cache.set(cacheKey, result, {\n ttl: RESOLVE_TTL_MS,\n tags: [DOMAIN_ROUTING_TAG, `${DOMAIN_ROUTING_TAG}:${hostname}`],\n }))\n }\n return result\n }\n\n async isAllowedForTls(input: string): Promise<{ organizationId: string; status: DomainStatus } | null> {\n const hostname = tryNormalizeHostname(input)\n if (!hostname) return null\n if (platformDomains().includes(hostname)) return null\n const row = await this.em.findOne(DomainMapping, {\n hostname,\n status: { $in: ['active', 'verified'] },\n } as never)\n if (!row) return null\n return { organizationId: row.organizationId, status: row.status }\n }\n\n async resolveActiveByOrg(organizationId: string): Promise<{ hostname: string; status: DomainStatus } | null> {\n const cacheKey = `${ACTIVE_BY_ORG_KEY_PREFIX}:${organizationId}`\n if (this.cache) {\n const cached = (await this.cache.get(cacheKey)) as { hostname: string; status: DomainStatus } | null | undefined\n if (cached !== undefined && cached !== null) return cached\n }\n\n const row = await this.em.findOne(DomainMapping, {\n organizationId,\n status: 'active',\n } as never)\n const result = row ? { hostname: row.hostname, status: row.status } : null\n\n if (this.cache) {\n await this.cache.set(cacheKey, result, {\n ttl: RESOLVE_TTL_MS,\n tags: [DOMAIN_ROUTING_TAG, `${DOMAIN_ROUTING_TAG}:org:${organizationId}`],\n })\n }\n return result\n }\n\n async resolveAll(): Promise<ResolveResult[]> {\n const rows = await this.em.find(DomainMapping, { status: 'active' } as never)\n if (rows.length === 0) return []\n const orgIds = Array.from(new Set(rows.map((r) => r.organizationId)))\n const orgs = await this.em.find(Organization, { id: { $in: orgIds } } as never)\n const slugByOrg = new Map<string, string | null>(orgs.map((o) => [o.id, o.slug ?? null]))\n\n return rows.map<ResolveResult>((row) => ({\n domainMappingId: row.id,\n hostname: row.hostname,\n tenantId: row.tenantId,\n organizationId: row.organizationId,\n orgSlug: slugByOrg.get(row.organizationId) ?? null,\n status: row.status,\n }))\n }\n\n // -------------------------------------------------------------------------\n // Worker queries\n // -------------------------------------------------------------------------\n\n async findPendingVerification(threshold?: { olderThanMs?: number }): Promise<DomainMapping[]> {\n const olderThan = threshold?.olderThanMs ?? DEFAULT_DNS_RECHECK_THRESHOLD_MS\n const cutoff = new Date(Date.now() - olderThan)\n return this.em.find(\n DomainMapping,\n {\n status: { $in: ['pending', 'dns_failed'] },\n $or: [{ lastDnsCheckAt: null }, { lastDnsCheckAt: { $lt: cutoff } }],\n } as never,\n { orderBy: { lastDnsCheckAt: 'asc' } },\n )\n }\n\n async findPendingTls(options?: { maxRetries?: number; batchSize?: number }): Promise<DomainMapping[]> {\n const maxRetries = options?.maxRetries ?? DEFAULT_TLS_MAX_RETRIES\n const limit = options?.batchSize ?? 50\n return this.em.find(\n DomainMapping,\n {\n $or: [\n { status: 'verified' },\n { status: 'tls_failed', tlsRetryCount: { $lt: maxRetries } },\n ],\n } as never,\n { orderBy: { updatedAt: 'asc' }, limit },\n )\n }\n\n // -------------------------------------------------------------------------\n // Write paths\n // -------------------------------------------------------------------------\n\n async register(input: RegisterInput): Promise<DomainMapping> {\n const hostname = normalizeHostname(input.hostname)\n const organization = await findOrganizationInTenant(this.em, input.organizationId, input.tenantId)\n if (!organization) throw new DomainMappingOrgScopeError(input.organizationId)\n\n let replacesDomain: DomainMapping | null = null\n if (input.replacesDomainId) {\n replacesDomain = await this.em.findOne(DomainMapping, {\n id: input.replacesDomainId,\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n } as never)\n }\n\n const entity = this.em.create(DomainMapping, {\n hostname,\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n replacesDomain: replacesDomain ?? null,\n provider: 'traefik',\n status: 'pending',\n tlsRetryCount: 0,\n createdAt: new Date(),\n } as never)\n await this.em.persist(entity).flush()\n\n await emitCustomerAccountsEvent('customer_accounts.domain_mapping.created', {\n id: entity.id,\n hostname: entity.hostname,\n organizationId: entity.organizationId,\n tenantId: entity.tenantId,\n status: entity.status,\n } as never)\n await this.invalidateCacheFor(entity.hostname, entity.organizationId)\n\n return entity\n }\n\n async verify(id: string): Promise<VerifyResult> {\n const entity = await this.em.findOne(DomainMapping, { id } as never)\n if (!entity) throw new Error(`DomainMapping ${id} not found`)\n\n const expectedCname = process.env.CUSTOM_DOMAIN_CNAME_TARGET ?? ''\n const expectedARecord = process.env.CUSTOM_DOMAIN_A_RECORD_TARGET ?? null\n const now = new Date()\n entity.lastDnsCheckAt = now\n\n const verification = await this.runDnsVerification(entity.hostname, {\n expectedCname,\n expectedARecord,\n })\n\n if (verification.ok) {\n entity.status = 'verified'\n entity.verifiedAt = now\n entity.dnsFailureReason = null\n entity.tlsRetryCount = 0\n entity.tlsFailureReason = null\n await this.em.persist(entity).flush()\n await emitCustomerAccountsEvent('customer_accounts.domain_mapping.verified', {\n id: entity.id,\n hostname: entity.hostname,\n organizationId: entity.organizationId,\n tenantId: entity.tenantId,\n } as never)\n await this.invalidateCacheFor(entity.hostname, entity.organizationId)\n return { domainMapping: entity }\n }\n\n entity.status = 'dns_failed'\n entity.dnsFailureReason = verification.reason\n await this.em.persist(entity).flush()\n await emitCustomerAccountsEvent('customer_accounts.domain_mapping.dns_failed', {\n id: entity.id,\n hostname: entity.hostname,\n organizationId: entity.organizationId,\n tenantId: entity.tenantId,\n reason: verification.reason,\n detectedRecords: verification.diagnostics.detectedRecords,\n } as never)\n await this.invalidateCacheFor(entity.hostname, entity.organizationId)\n return { domainMapping: entity, diagnostics: verification.diagnostics }\n }\n\n async activate(id: string): Promise<DomainMapping> {\n const entity = await this.em.findOne(DomainMapping, { id } as never)\n if (!entity) throw new Error(`DomainMapping ${id} not found`)\n if (entity.status === 'active') return entity\n if (entity.status !== 'verified' && entity.status !== 'tls_failed') {\n throw new Error(`DomainMapping ${id} cannot transition to active from ${entity.status}`)\n }\n\n entity.status = 'active'\n entity.tlsRetryCount = 0\n entity.tlsFailureReason = null\n await this.em.persist(entity).flush()\n\n await emitCustomerAccountsEvent('customer_accounts.domain_mapping.activated', {\n id: entity.id,\n hostname: entity.hostname,\n organizationId: entity.organizationId,\n tenantId: entity.tenantId,\n } as never)\n\n if (entity.replacesDomain) {\n const replaced = await this.em.findOne(DomainMapping, {\n id: (entity.replacesDomain as unknown as { id: string }).id,\n tenantId: entity.tenantId,\n organizationId: entity.organizationId,\n } as never)\n if (replaced) {\n const replacedHostname = replaced.hostname\n const replacedOrg = replaced.organizationId\n this.em.remove(replaced)\n await this.em.flush()\n await emitCustomerAccountsEvent('customer_accounts.domain_mapping.replaced', {\n id: entity.id,\n hostname: entity.hostname,\n organizationId: entity.organizationId,\n tenantId: entity.tenantId,\n replacedDomainId: replaced.id,\n replacedHostname,\n } as never)\n await emitCustomerAccountsEvent('customer_accounts.domain_mapping.deleted', {\n id: replaced.id,\n hostname: replacedHostname,\n organizationId: replacedOrg,\n tenantId: replaced.tenantId,\n } as never)\n await this.invalidateCacheFor(replacedHostname, replacedOrg)\n }\n }\n\n await this.invalidateCacheFor(entity.hostname, entity.organizationId)\n return entity\n }\n\n async remove(id: string, scope?: { tenantId?: string }): Promise<void> {\n const where: Record<string, unknown> = { id }\n if (scope?.tenantId) where.tenantId = scope.tenantId\n const entity = await this.em.findOne(DomainMapping, where as never)\n if (!entity) return\n\n const hostname = entity.hostname\n const organizationId = entity.organizationId\n const tenantId = entity.tenantId\n\n this.em.remove(entity)\n await this.em.flush()\n\n await emitCustomerAccountsEvent('customer_accounts.domain_mapping.deleted', {\n id,\n hostname,\n organizationId,\n tenantId,\n } as never)\n await this.invalidateCacheFor(hostname, organizationId)\n }\n\n async healthCheck(id: string): Promise<DomainMapping> {\n const entity = await this.em.findOne(DomainMapping, { id } as never)\n if (!entity) throw new Error(`DomainMapping ${id} not found`)\n if (entity.status === 'active') return entity\n if (entity.status !== 'verified' && entity.status !== 'tls_failed') {\n throw new Error(`DomainMapping ${id} cannot run health check from status ${entity.status}`)\n }\n\n let lastReason: string | null = null\n for (let attempt = 0; attempt < TLS_HEALTH_CHECK_RETRY_DELAYS_MS.length; attempt++) {\n const result = await this.healthCheckImpl(entity.hostname, TLS_HEALTH_CHECK_TIMEOUT_MS)\n if (result.ok) {\n return this.activate(entity.id)\n }\n lastReason = result.reason ?? 'TLS health check failed'\n if (attempt + 1 < TLS_HEALTH_CHECK_RETRY_DELAYS_MS.length) {\n await delay(TLS_HEALTH_CHECK_RETRY_DELAYS_MS[attempt])\n }\n }\n\n entity.status = 'tls_failed'\n entity.tlsFailureReason = lastReason ?? 'TLS health check failed'\n entity.tlsRetryCount = (entity.tlsRetryCount ?? 0) + 1\n await this.em.persist(entity).flush()\n\n await emitCustomerAccountsEvent('customer_accounts.domain_mapping.tls_failed', {\n id: entity.id,\n hostname: entity.hostname,\n organizationId: entity.organizationId,\n tenantId: entity.tenantId,\n reason: entity.tlsFailureReason,\n retryCount: entity.tlsRetryCount,\n } as never)\n await this.invalidateCacheFor(entity.hostname, entity.organizationId)\n return entity\n }\n\n // -------------------------------------------------------------------------\n // Internals\n // -------------------------------------------------------------------------\n\n private async lookupResolveResult(hostname: string): Promise<ResolveResult | null> {\n const row = await this.em.findOne(DomainMapping, { hostname, status: 'active' } as never)\n if (!row) return null\n const org = await this.em.findOne(Organization, { id: row.organizationId } as never)\n return {\n domainMappingId: row.id,\n hostname: row.hostname,\n tenantId: row.tenantId,\n organizationId: row.organizationId,\n orgSlug: org?.slug ?? null,\n status: row.status,\n }\n }\n\n private async invalidateCacheFor(hostname: string, organizationId: string | null): Promise<void> {\n const cache = this.cache\n if (!cache) return\n const tags = [DOMAIN_ROUTING_TAG, `${DOMAIN_ROUTING_TAG}:${hostname}`]\n if (organizationId) tags.push(`${DOMAIN_ROUTING_TAG}:org:${organizationId}`)\n await Promise.allSettled([\n cache.deleteByTags(tags),\n runWithCacheTenant(null, () => cache.deleteByTags([`${DOMAIN_ROUTING_TAG}:${hostname}`])),\n ])\n }\n\n private async runDnsVerification(\n hostname: string,\n expected: { expectedCname: string; expectedARecord: string | null },\n ): Promise<\n | {\n ok: true\n method: 'cname' | 'a-record' | 'reverse-resolve'\n diagnostics: DnsDiagnostics\n }\n | { ok: false; reason: string; diagnostics: DnsDiagnostics }\n > {\n const detectedRecords: DnsDiagnostics['detectedRecords'] = []\n const baseDiag = (overrides?: Partial<DnsDiagnostics>): DnsDiagnostics => ({\n expectedCnameTarget: expected.expectedCname,\n expectedARecordTarget: expected.expectedARecord,\n detectedRecords,\n suggestion: overrides?.suggestion ?? '',\n reverseResolve: overrides?.reverseResolve,\n })\n\n // Phase 1: CNAME\n let cnameRecords: string[] = []\n try {\n cnameRecords = await this.dns.resolveCname(hostname)\n } catch (err) {\n return {\n ok: false,\n reason: `DNS lookup error (CNAME): ${(err as Error).message}`,\n diagnostics: baseDiag({\n suggestion: 'DNS lookup failed unexpectedly. Try again in a few minutes.',\n }),\n }\n }\n for (const cname of cnameRecords) detectedRecords.push({ type: 'CNAME', value: cname })\n\n if (expected.expectedCname && cnameRecords.length > 0) {\n const expectedCname = tryNormalizeHostname(expected.expectedCname) ?? expected.expectedCname.toLowerCase()\n const match = cnameRecords.some((c) => (tryNormalizeHostname(c) ?? c.toLowerCase()) === expectedCname)\n if (match) {\n return {\n ok: true,\n method: 'cname',\n diagnostics: baseDiag({\n suggestion: 'CNAME record matches the expected target.',\n }),\n }\n }\n return {\n ok: false,\n reason: `CNAME points to ${cnameRecords.join(', ')} instead of ${expected.expectedCname}`,\n diagnostics: baseDiag({\n suggestion: `Update your CNAME record to point to ${expected.expectedCname}.`,\n }),\n }\n }\n\n // Phase 2: A record\n let aRecords: string[] = []\n try {\n aRecords = await this.dns.resolve4(hostname)\n } catch (err) {\n return {\n ok: false,\n reason: `DNS lookup error (A): ${(err as Error).message}`,\n diagnostics: baseDiag({ suggestion: 'DNS lookup failed unexpectedly. Try again in a few minutes.' }),\n }\n }\n for (const a of aRecords) {\n const proxy = detectProxy(a)\n detectedRecords.push({ type: 'A', value: a, ...(proxy ? { proxy } : {}) })\n }\n\n if (aRecords.length === 0) {\n return {\n ok: false,\n reason: `No CNAME or A record found for ${hostname}`,\n diagnostics: baseDiag({\n suggestion: expected.expectedARecord\n ? `For a subdomain, add a CNAME record pointing to ${expected.expectedCname}. For an apex domain, add an A record pointing to ${expected.expectedARecord}. DNS propagation can take up to 48 hours.`\n : `Add a CNAME record pointing to ${expected.expectedCname}. DNS propagation can take up to 48 hours.`,\n }),\n }\n }\n\n if (expected.expectedARecord && aRecords.includes(expected.expectedARecord)) {\n return {\n ok: true,\n method: 'a-record',\n diagnostics: baseDiag({ suggestion: 'A record matches the expected target.' }),\n }\n }\n\n // Phase 3: reverse-resolve through proxy\n const proxiedRecords = aRecords.filter((ip) => isInKnownProxyRange(ip))\n if (proxiedRecords.length > 0) {\n const probe = await this.healthCheckImpl(hostname, TLS_HEALTH_CHECK_TIMEOUT_MS)\n if (probe.ok && probe.originHeaderPresent) {\n return {\n ok: true,\n method: 'reverse-resolve',\n diagnostics: baseDiag({\n reverseResolve: { attempted: true, originHeaderPresent: true },\n suggestion: 'Domain is proxied \u2014 reverse-resolve confirmed traffic reaches our origin.',\n }),\n }\n }\n return {\n ok: false,\n reason: 'A record points to a known proxy IP, but reverse-resolve over HTTPS did not reach our server',\n diagnostics: baseDiag({\n reverseResolve: { attempted: true, originHeaderPresent: false },\n suggestion: expected.expectedCname\n ? `Your DNS uses a proxy. Either disable the proxy and add a CNAME \u2192 ${expected.expectedCname}, or configure your proxy to forward traffic to ${expected.expectedCname}.`\n : 'Disable the DNS proxy or configure it to forward traffic to our platform.',\n }),\n }\n }\n\n return {\n ok: false,\n reason: expected.expectedARecord\n ? `A record points to ${aRecords.join(', ')} instead of ${expected.expectedARecord}`\n : `A record points to ${aRecords.join(', ')} but apex-domain registration is not enabled on this deployment`,\n diagnostics: baseDiag({\n suggestion: expected.expectedARecord\n ? `Update your A record to point to ${expected.expectedARecord}.`\n : `Apex-domain registration is not enabled on this deployment. Use a subdomain (e.g., shop.${hostname}) and add a CNAME pointing to ${expected.expectedCname}.`,\n }),\n }\n }\n}\n\n// Re-exported for tests; allow injection of fakes.\nexport const __testing__ = {\n DEFAULT_DNS_RECHECK_THRESHOLD_MS,\n DEFAULT_TLS_MAX_RETRIES,\n TLS_HEALTH_CHECK_RETRY_DELAYS_MS,\n RESOLVE_TTL_MS,\n DOMAIN_ROUTING_TAG,\n defaultHealthCheck,\n defaultDnsResolver,\n Resolver,\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -60,8 +60,12 @@ function sortTimelineActivities(items) {
|
|
|
60
60
|
if (leftScheduled === rightScheduled) return left.id.localeCompare(right.id);
|
|
61
61
|
return leftScheduled - rightScheduled;
|
|
62
62
|
}
|
|
63
|
-
const leftTime = left.occurredAt ?? left.
|
|
64
|
-
const rightTime = right.occurredAt ?? right.
|
|
63
|
+
const leftTime = left.occurredAt ?? left.scheduledAt ?? null;
|
|
64
|
+
const rightTime = right.occurredAt ?? right.scheduledAt ?? null;
|
|
65
|
+
if (!leftTime || !rightTime) {
|
|
66
|
+
if (leftTime !== rightTime) return leftTime ? -1 : 1;
|
|
67
|
+
return right.id.localeCompare(left.id);
|
|
68
|
+
}
|
|
65
69
|
const compare = rightTime.localeCompare(leftTime);
|
|
66
70
|
if (compare !== 0) return compare;
|
|
67
71
|
return right.id.localeCompare(left.id);
|
|
@@ -185,7 +189,8 @@ function ActivitiesSection({
|
|
|
185
189
|
}
|
|
186
190
|
const legacyFiltered = legacyItems.filter((entry) => {
|
|
187
191
|
if (filterTypes.length > 0 && !filterTypes.includes(entry.interactionType)) return false;
|
|
188
|
-
const dateOnly = toDateOnly(entry.occurredAt ?? entry.
|
|
192
|
+
const dateOnly = toDateOnly(entry.occurredAt ?? entry.scheduledAt);
|
|
193
|
+
if ((filterDateFrom || filterDateTo) && !dateOnly) return false;
|
|
189
194
|
if (filterDateFrom && dateOnly < filterDateFrom) return false;
|
|
190
195
|
if (filterDateTo && dateOnly > filterDateTo) return false;
|
|
191
196
|
return true;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/customers/components/detail/ActivitiesSection.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { Clock, Search } from 'lucide-react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { apiCallOrThrow, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport type { SectionAction, TabEmptyStateConfig } from '@open-mercato/ui/backend/detail'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Kbd } from '@open-mercato/ui/primitives/kbd'\nimport { ActivityTimelineFilters } from './ActivityTimelineFilters'\nimport { ActivityTimeline } from './ActivityTimeline'\nimport { resolveAuthorUserNames } from './authorUserLookup'\nimport type { ActivitySummary, InteractionSummary } from './types'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('customers')\n\ntype GuardedMutationRunner = <T>(\n operation: () => Promise<T>,\n mutationPayload?: Record<string, unknown>,\n) => Promise<T>\n\nexport type ActivitiesSectionProps = {\n entityId: string | null\n entityName?: string | null\n dealId?: string | null\n useCanonicalInteractions?: boolean\n addActionLabel: string\n emptyState: TabEmptyStateConfig\n onActionChange?: (action: SectionAction | null) => void\n onLoadingChange?: (isLoading: boolean) => void\n onDataRefresh?: () => void\n dealOptions?: Array<{ id: string; label: string }>\n entityOptions?: Array<{ id: string; label: string }>\n defaultEntityId?: string | null\n runGuardedMutation?: GuardedMutationRunner\n refreshKey?: number\n onEditActivity?: (activity: InteractionSummary) => void\n /** Interaction type hidden from the timeline by default ('task' unless overridden); pass null to show every type. */\n excludeInteractionType?: string | null\n}\n\nfunction toDateOnly(value: string | null | undefined): string {\n if (!value) return ''\n const date = new Date(value)\n return Number.isNaN(date.getTime()) ? '' : date.toISOString().slice(0, 10)\n}\n\nfunction normalizeLegacyActivity(activity: ActivitySummary): InteractionSummary {\n return {\n id: activity.id,\n interactionType: activity.activityType,\n title: activity.subject ?? null,\n body: activity.body ?? null,\n status: 'done',\n scheduledAt: null,\n occurredAt: activity.occurredAt ?? null,\n priority: null,\n authorUserId: activity.authorUserId ?? null,\n ownerUserId: null,\n appearanceIcon: activity.appearanceIcon ?? null,\n appearanceColor: activity.appearanceColor ?? null,\n source: 'legacy-activity',\n entityId: activity.entityId ?? null,\n dealId: activity.dealId ?? null,\n organizationId: null,\n tenantId: null,\n authorName: activity.authorName ?? null,\n authorEmail: activity.authorEmail ?? null,\n dealTitle: activity.dealTitle ?? null,\n customValues: activity.customValues ?? null,\n createdAt: activity.createdAt,\n updatedAt: activity.createdAt,\n }\n}\n\nfunction sortTimelineActivities(items: InteractionSummary[]): InteractionSummary[] {\n const now = Date.now()\n return [...items].sort((left, right) => {\n const leftScheduled = left.scheduledAt ? new Date(left.scheduledAt).getTime() : Number.NaN\n const rightScheduled = right.scheduledAt ? new Date(right.scheduledAt).getTime() : Number.NaN\n const leftIsPlanned = left.status === 'planned' && Number.isFinite(leftScheduled)\n const rightIsPlanned = right.status === 'planned' && Number.isFinite(rightScheduled)\n const leftIsUpcoming = leftIsPlanned && leftScheduled >= now\n const rightIsUpcoming = rightIsPlanned && rightScheduled >= now\n\n if (leftIsUpcoming !== rightIsUpcoming) {\n return leftIsUpcoming ? -1 : 1\n }\n\n if (leftIsUpcoming && rightIsUpcoming) {\n if (leftScheduled === rightScheduled) return left.id.localeCompare(right.id)\n return leftScheduled - rightScheduled\n }\n\n const leftTime = left.occurredAt ?? left.createdAt\n const rightTime = right.occurredAt ?? right.createdAt\n const compare = rightTime.localeCompare(leftTime)\n if (compare !== 0) return compare\n return right.id.localeCompare(left.id)\n })\n}\n\nexport function ActivitiesSection({\n entityId,\n entityName,\n dealId,\n useCanonicalInteractions = false,\n onActionChange,\n onLoadingChange,\n onDataRefresh,\n refreshKey = 0,\n onEditActivity,\n runGuardedMutation,\n excludeInteractionType = 'task',\n}: ActivitiesSectionProps) {\n const t = useT()\n const [filterTypes, setFilterTypes] = React.useState<string[]>([])\n const [filterDateFrom, setFilterDateFrom] = React.useState('')\n const [filterDateTo, setFilterDateTo] = React.useState('')\n const [searchTerm, setSearchTerm] = React.useState('')\n const [activities, setActivities] = React.useState<InteractionSummary[]>([])\n const [loading, setLoading] = React.useState(false)\n const [hasMore, setHasMore] = React.useState(false)\n const [loadedPages, setLoadedPages] = React.useState(1)\n const searchInputRef = React.useRef<HTMLInputElement>(null)\n\n React.useEffect(() => {\n if (!entityId) return\n function handleShortcut(event: KeyboardEvent) {\n if ((event.metaKey || event.ctrlKey) && event.key === '1') {\n event.preventDefault()\n searchInputRef.current?.focus()\n }\n }\n window.addEventListener('keydown', handleShortcut)\n return () => window.removeEventListener('keydown', handleShortcut)\n }, [entityId])\n\n const visibleActivities = React.useMemo(() => {\n const term = searchTerm.trim().toLowerCase()\n if (!term) return activities\n return activities.filter((activity) => {\n const haystack = [\n activity.title,\n activity.body,\n activity.authorName,\n activity.dealTitle,\n activity.interactionType,\n ]\n .filter((value): value is string => typeof value === 'string' && value.length > 0)\n .join(' ')\n .toLowerCase()\n return haystack.includes(term)\n })\n }, [activities, searchTerm])\n\n React.useEffect(() => {\n onActionChange?.(null)\n return () => onActionChange?.(null)\n }, [onActionChange])\n\n React.useEffect(() => {\n onLoadingChange?.(loading)\n }, [loading, onLoadingChange])\n\n const loadActivities = React.useCallback(async () => {\n if (!entityId) {\n setActivities([])\n return\n }\n\n setLoading(true)\n try {\n // Always fetch canonical interactions (new activities are always created here)\n const excludedFilterActive = excludeInteractionType ? filterTypes.includes(excludeInteractionType) : true\n const canonicalParams = new URLSearchParams({\n entityId,\n limit: '50',\n sortField: 'occurredAt',\n sortDir: 'desc',\n })\n // Hide the configured type (tasks by default \u2014 they have their own tab)\n // from the activity timeline, but lift the exclusion when the user\n // explicitly toggled that type's chip on (mirrors\n // `ActivityHistorySection.tsx` after the #1805 fix).\n if (excludeInteractionType && !excludedFilterActive) canonicalParams.set('excludeInteractionType', excludeInteractionType)\n if (dealId) canonicalParams.set('dealId', dealId)\n if (filterTypes.length > 0) canonicalParams.set('type', filterTypes.join(','))\n if (filterDateFrom) canonicalParams.set('from', filterDateFrom)\n if (filterDateTo) canonicalParams.set('to', filterDateTo)\n\n const canonicalItems: InteractionSummary[] = []\n let canonicalCursor: string | undefined\n let canonicalHasMore = false\n let pageIndex = 0\n do {\n const params = new URLSearchParams(canonicalParams)\n if (canonicalCursor) params.set('cursor', canonicalCursor)\n const canonicalPayload = await readApiResultOrThrow<{ items?: InteractionSummary[]; nextCursor?: string }>(\n `/api/customers/interactions?${params.toString()}`,\n ).catch(() => ({ items: [] as InteractionSummary[], nextCursor: undefined }))\n canonicalItems.push(...(Array.isArray(canonicalPayload?.items) ? canonicalPayload.items : []))\n canonicalCursor = typeof canonicalPayload?.nextCursor === 'string' ? canonicalPayload.nextCursor : undefined\n canonicalHasMore = Boolean(canonicalCursor)\n pageIndex += 1\n } while (canonicalCursor && pageIndex < loadedPages)\n\n if (useCanonicalInteractions) {\n setActivities(sortTimelineActivities(canonicalItems))\n setHasMore(canonicalHasMore)\n return\n }\n\n // In legacy mode, also fetch legacy activities and merge with canonical.\n // Legacy fallback uses known page numbers, so request every page up front\n // and resolve them together instead of awaiting each one sequentially.\n const legacyPageNumbers = Array.from({ length: loadedPages }, (_, index) => index + 1)\n const legacyPayloads = await Promise.all(\n legacyPageNumbers.map((legacyPage) => {\n const legacyParams = new URLSearchParams({\n entityId,\n page: String(legacyPage),\n pageSize: '50',\n sortField: 'occurredAt',\n sortDir: 'desc',\n })\n if (dealId) legacyParams.set('dealId', dealId)\n return readApiResultOrThrow<{ items?: ActivitySummary[]; totalPages?: number }>(\n `/api/customers/activities?${legacyParams.toString()}`,\n ).catch(() => ({ items: [] as ActivitySummary[], totalPages: 1 }))\n }),\n )\n // Merge in page order so timeline ordering stays stable regardless of\n // which request settles first.\n const legacyItems: InteractionSummary[] = []\n let legacyTotalPages = 1\n for (const legacyPayload of legacyPayloads) {\n legacyItems.push(...(Array.isArray(legacyPayload?.items) ? legacyPayload.items.map(normalizeLegacyActivity) : []))\n legacyTotalPages = typeof legacyPayload?.totalPages === 'number' ? legacyPayload.totalPages : legacyTotalPages\n }\n const legacyFiltered = legacyItems.filter((entry) => {\n if (filterTypes.length > 0 && !filterTypes.includes(entry.interactionType)) return false\n const dateOnly = toDateOnly(entry.occurredAt ?? entry.createdAt)\n if (filterDateFrom && dateOnly < filterDateFrom) return false\n if (filterDateTo && dateOnly > filterDateTo) return false\n return true\n })\n\n // Merge and deduplicate by id, sort newest first\n const seen = new Set<string>()\n const merged: InteractionSummary[] = []\n for (const item of [...canonicalItems, ...legacyFiltered]) {\n if (!seen.has(item.id)) {\n seen.add(item.id)\n merged.push(item)\n }\n }\n setActivities(sortTimelineActivities(merged))\n setHasMore(canonicalHasMore || legacyTotalPages > loadedPages)\n } catch (error) {\n logger.error('customers.activities.history failed', { err: error })\n flash(t('customers.activities.loadFailed', 'Failed to load activities.'), 'error')\n setActivities([])\n setHasMore(false)\n } finally {\n setLoading(false)\n }\n }, [dealId, entityId, excludeInteractionType, filterDateFrom, filterDateTo, filterTypes, loadedPages, useCanonicalInteractions, refreshKey, t])\n\n React.useEffect(() => {\n setLoadedPages(1)\n }, [dealId, entityId, filterDateFrom, filterDateTo, filterTypes, useCanonicalInteractions])\n\n const handleMarkDone = React.useCallback(async (activityId: string) => {\n try {\n const operation = () =>\n apiCallOrThrow('/api/customers/interactions/complete', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ id: activityId, occurredAt: new Date().toISOString() }),\n })\n if (runGuardedMutation) {\n await runGuardedMutation(operation, {\n id: activityId,\n status: 'done',\n operation: 'completeActivity',\n })\n } else {\n await operation()\n }\n flash(t('customers.activities.actions.markDoneSuccess', 'Activity marked done'), 'success')\n await loadActivities()\n } catch (err) {\n logger.warn('Mark done failed', { component: 'ActivitiesSection', activityId, err })\n flash(t('customers.activities.actions.markDoneError', 'Could not mark activity as done'), 'error')\n return\n }\n try {\n await Promise.resolve(onDataRefresh?.())\n } catch (err) {\n logger.warn('Parent refresh after mark done failed', { component: 'ActivitiesSection', activityId, err })\n }\n }, [loadActivities, onDataRefresh, runGuardedMutation, t])\n\n const resolvedUserIdsRef = React.useRef(new Set<string>())\n\n // Resolve missing author names from user IDs\n React.useEffect(() => {\n loadActivities()\n .then(() => { resolvedUserIdsRef.current = new Set() })\n .catch((err) => logger.warn('loadActivities failed', { component: 'ActivitiesSection', err }))\n }, [loadActivities])\n\n React.useEffect(() => {\n const unresolvedIds = new Set<string>()\n for (const a of activities) {\n if (a.authorUserId && !a.authorName && !resolvedUserIdsRef.current.has(a.authorUserId)) {\n unresolvedIds.add(a.authorUserId)\n }\n }\n if (unresolvedIds.size === 0) return\n\n const controller = new AbortController()\n resolveAuthorUserNames([...unresolvedIds], controller.signal)\n .then(({ names: nameMap, resolvedIds }) => {\n if (controller.signal.aborted) return\n // Only ids the server actually answered for are remembered. Marking an id whose request\n // failed \u2014 or one the server silently dropped past its `?ids=` cap \u2014 would leave that\n // author blank for the life of the component, even though the next attempt would work.\n for (const uid of resolvedIds) resolvedUserIdsRef.current.add(uid)\n if (nameMap.size > 0) {\n setActivities((prev) =>\n prev.map((a) => {\n if (a.authorUserId && !a.authorName && nameMap.has(a.authorUserId)) {\n return { ...a, authorName: nameMap.get(a.authorUserId) ?? null }\n }\n return a\n }),\n )\n }\n })\n .catch((err) => logger.warn('resolve author names failed', { component: 'ActivitiesSection', err }))\n return () => controller.abort()\n }, [activities])\n\n const totalCount = activities.length\n const visibleCount = visibleActivities.length\n\n return (\n <div className=\"flex flex-col gap-3.5 rounded-[10px] border border-border bg-card pt-4 pb-[18px] px-[18px]\">\n <div className=\"flex items-center gap-2\">\n <Clock className=\"size-[15px] text-muted-foreground\" />\n <h3 className=\"text-[13px] font-semibold text-foreground\">\n {entityName\n ? t('customers.timeline.history.title', 'Interaction history with {{name}}', { name: entityName })\n : t('customers.timeline.history.titleGeneric', 'Interaction history')}\n </h3>\n </div>\n\n <label className=\"relative flex items-center\">\n <Search className=\"pointer-events-none absolute left-2.5 size-5 text-muted-foreground\" aria-hidden />\n <input\n ref={searchInputRef}\n type=\"search\"\n value={searchTerm}\n onChange={(event) => setSearchTerm(event.target.value)}\n placeholder={t('customers.timeline.history.searchPlaceholder', 'Search...')}\n aria-label={t('customers.timeline.history.searchAriaLabel', 'Search interaction history')}\n className=\"h-9 w-full rounded-[10px] border border-border bg-card pl-9 pr-14 text-sm text-foreground shadow-xs placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring [&::-webkit-search-cancel-button]:hidden [&::-webkit-search-decoration]:hidden\"\n />\n <Kbd className=\"pointer-events-none absolute right-2 hidden text-[11px] uppercase tracking-[0.48px] sm:inline-flex\">\n \u23181\n </Kbd>\n </label>\n\n <ActivityTimelineFilters\n entityId={entityId}\n activeTypes={filterTypes}\n dateFrom={filterDateFrom}\n dateTo={filterDateTo}\n onTypesChange={setFilterTypes}\n onDateFromChange={setFilterDateFrom}\n onDateToChange={setFilterDateTo}\n onReset={() => {\n setFilterTypes([])\n setFilterDateFrom('')\n setFilterDateTo('')\n }}\n />\n\n {loading && totalCount === 0 ? (\n <div className=\"rounded-lg border border-dashed border-border/70 px-4 py-8 text-sm text-muted-foreground\">\n {t('customers.people.detail.activities.loading', 'Loading activities\u2026')}\n </div>\n ) : (\n <>\n <ActivityTimeline\n activities={visibleActivities}\n onEdit={onEditActivity}\n onMarkDone={handleMarkDone}\n />\n {totalCount > 0 ? (\n <div className=\"flex items-center justify-between gap-3 border-t border-border/60 pt-3\">\n <span className=\"text-xs text-muted-foreground\">\n {searchTerm.trim()\n ? t('customers.activities.seeMatching', 'Showing {visible} of {total} activities', {\n visible: visibleCount,\n total: totalCount,\n })\n : t('customers.activities.seeAll', 'See all {count} activities', { count: totalCount })}\n </span>\n {hasMore ? (\n <Button type=\"button\" variant=\"link\" size=\"sm\" onClick={() => setLoadedPages((value) => value + 1)}>\n {t('customers.activities.loadMore', 'Load more')}\n </Button>\n ) : null}\n </div>\n ) : null}\n </>\n )}\n </div>\n )\n}\n\nexport default ActivitiesSection\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { Clock, Search } from 'lucide-react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { apiCallOrThrow, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport type { SectionAction, TabEmptyStateConfig } from '@open-mercato/ui/backend/detail'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Kbd } from '@open-mercato/ui/primitives/kbd'\nimport { ActivityTimelineFilters } from './ActivityTimelineFilters'\nimport { ActivityTimeline } from './ActivityTimeline'\nimport { resolveAuthorUserNames } from './authorUserLookup'\nimport type { ActivitySummary, InteractionSummary } from './types'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('customers')\n\ntype GuardedMutationRunner = <T>(\n operation: () => Promise<T>,\n mutationPayload?: Record<string, unknown>,\n) => Promise<T>\n\nexport type ActivitiesSectionProps = {\n entityId: string | null\n entityName?: string | null\n dealId?: string | null\n useCanonicalInteractions?: boolean\n addActionLabel: string\n emptyState: TabEmptyStateConfig\n onActionChange?: (action: SectionAction | null) => void\n onLoadingChange?: (isLoading: boolean) => void\n onDataRefresh?: () => void\n dealOptions?: Array<{ id: string; label: string }>\n entityOptions?: Array<{ id: string; label: string }>\n defaultEntityId?: string | null\n runGuardedMutation?: GuardedMutationRunner\n refreshKey?: number\n onEditActivity?: (activity: InteractionSummary) => void\n /** Interaction type hidden from the timeline by default ('task' unless overridden); pass null to show every type. */\n excludeInteractionType?: string | null\n}\n\nfunction toDateOnly(value: string | null | undefined): string {\n if (!value) return ''\n const date = new Date(value)\n return Number.isNaN(date.getTime()) ? '' : date.toISOString().slice(0, 10)\n}\n\nfunction normalizeLegacyActivity(activity: ActivitySummary): InteractionSummary {\n return {\n id: activity.id,\n interactionType: activity.activityType,\n title: activity.subject ?? null,\n body: activity.body ?? null,\n status: 'done',\n scheduledAt: null,\n occurredAt: activity.occurredAt ?? null,\n priority: null,\n authorUserId: activity.authorUserId ?? null,\n ownerUserId: null,\n appearanceIcon: activity.appearanceIcon ?? null,\n appearanceColor: activity.appearanceColor ?? null,\n source: 'legacy-activity',\n entityId: activity.entityId ?? null,\n dealId: activity.dealId ?? null,\n organizationId: null,\n tenantId: null,\n authorName: activity.authorName ?? null,\n authorEmail: activity.authorEmail ?? null,\n dealTitle: activity.dealTitle ?? null,\n customValues: activity.customValues ?? null,\n createdAt: activity.createdAt,\n updatedAt: activity.createdAt,\n }\n}\n\nfunction sortTimelineActivities(items: InteractionSummary[]): InteractionSummary[] {\n const now = Date.now()\n return [...items].sort((left, right) => {\n const leftScheduled = left.scheduledAt ? new Date(left.scheduledAt).getTime() : Number.NaN\n const rightScheduled = right.scheduledAt ? new Date(right.scheduledAt).getTime() : Number.NaN\n const leftIsPlanned = left.status === 'planned' && Number.isFinite(leftScheduled)\n const rightIsPlanned = right.status === 'planned' && Number.isFinite(rightScheduled)\n const leftIsUpcoming = leftIsPlanned && leftScheduled >= now\n const rightIsUpcoming = rightIsPlanned && rightScheduled >= now\n\n if (leftIsUpcoming !== rightIsUpcoming) {\n return leftIsUpcoming ? -1 : 1\n }\n\n if (leftIsUpcoming && rightIsUpcoming) {\n if (leftScheduled === rightScheduled) return left.id.localeCompare(right.id)\n return leftScheduled - rightScheduled\n }\n\n const leftTime = left.occurredAt ?? left.scheduledAt ?? null\n const rightTime = right.occurredAt ?? right.scheduledAt ?? null\n if (!leftTime || !rightTime) {\n if (leftTime !== rightTime) return leftTime ? -1 : 1\n return right.id.localeCompare(left.id)\n }\n const compare = rightTime.localeCompare(leftTime)\n if (compare !== 0) return compare\n return right.id.localeCompare(left.id)\n })\n}\n\nexport function ActivitiesSection({\n entityId,\n entityName,\n dealId,\n useCanonicalInteractions = false,\n onActionChange,\n onLoadingChange,\n onDataRefresh,\n refreshKey = 0,\n onEditActivity,\n runGuardedMutation,\n excludeInteractionType = 'task',\n}: ActivitiesSectionProps) {\n const t = useT()\n const [filterTypes, setFilterTypes] = React.useState<string[]>([])\n const [filterDateFrom, setFilterDateFrom] = React.useState('')\n const [filterDateTo, setFilterDateTo] = React.useState('')\n const [searchTerm, setSearchTerm] = React.useState('')\n const [activities, setActivities] = React.useState<InteractionSummary[]>([])\n const [loading, setLoading] = React.useState(false)\n const [hasMore, setHasMore] = React.useState(false)\n const [loadedPages, setLoadedPages] = React.useState(1)\n const searchInputRef = React.useRef<HTMLInputElement>(null)\n\n React.useEffect(() => {\n if (!entityId) return\n function handleShortcut(event: KeyboardEvent) {\n if ((event.metaKey || event.ctrlKey) && event.key === '1') {\n event.preventDefault()\n searchInputRef.current?.focus()\n }\n }\n window.addEventListener('keydown', handleShortcut)\n return () => window.removeEventListener('keydown', handleShortcut)\n }, [entityId])\n\n const visibleActivities = React.useMemo(() => {\n const term = searchTerm.trim().toLowerCase()\n if (!term) return activities\n return activities.filter((activity) => {\n const haystack = [\n activity.title,\n activity.body,\n activity.authorName,\n activity.dealTitle,\n activity.interactionType,\n ]\n .filter((value): value is string => typeof value === 'string' && value.length > 0)\n .join(' ')\n .toLowerCase()\n return haystack.includes(term)\n })\n }, [activities, searchTerm])\n\n React.useEffect(() => {\n onActionChange?.(null)\n return () => onActionChange?.(null)\n }, [onActionChange])\n\n React.useEffect(() => {\n onLoadingChange?.(loading)\n }, [loading, onLoadingChange])\n\n const loadActivities = React.useCallback(async () => {\n if (!entityId) {\n setActivities([])\n return\n }\n\n setLoading(true)\n try {\n // Always fetch canonical interactions (new activities are always created here)\n const excludedFilterActive = excludeInteractionType ? filterTypes.includes(excludeInteractionType) : true\n const canonicalParams = new URLSearchParams({\n entityId,\n limit: '50',\n sortField: 'occurredAt',\n sortDir: 'desc',\n })\n // Hide the configured type (tasks by default \u2014 they have their own tab)\n // from the activity timeline, but lift the exclusion when the user\n // explicitly toggled that type's chip on (mirrors\n // `ActivityHistorySection.tsx` after the #1805 fix).\n if (excludeInteractionType && !excludedFilterActive) canonicalParams.set('excludeInteractionType', excludeInteractionType)\n if (dealId) canonicalParams.set('dealId', dealId)\n if (filterTypes.length > 0) canonicalParams.set('type', filterTypes.join(','))\n if (filterDateFrom) canonicalParams.set('from', filterDateFrom)\n if (filterDateTo) canonicalParams.set('to', filterDateTo)\n\n const canonicalItems: InteractionSummary[] = []\n let canonicalCursor: string | undefined\n let canonicalHasMore = false\n let pageIndex = 0\n do {\n const params = new URLSearchParams(canonicalParams)\n if (canonicalCursor) params.set('cursor', canonicalCursor)\n const canonicalPayload = await readApiResultOrThrow<{ items?: InteractionSummary[]; nextCursor?: string }>(\n `/api/customers/interactions?${params.toString()}`,\n ).catch(() => ({ items: [] as InteractionSummary[], nextCursor: undefined }))\n canonicalItems.push(...(Array.isArray(canonicalPayload?.items) ? canonicalPayload.items : []))\n canonicalCursor = typeof canonicalPayload?.nextCursor === 'string' ? canonicalPayload.nextCursor : undefined\n canonicalHasMore = Boolean(canonicalCursor)\n pageIndex += 1\n } while (canonicalCursor && pageIndex < loadedPages)\n\n if (useCanonicalInteractions) {\n setActivities(sortTimelineActivities(canonicalItems))\n setHasMore(canonicalHasMore)\n return\n }\n\n // In legacy mode, also fetch legacy activities and merge with canonical.\n // Legacy fallback uses known page numbers, so request every page up front\n // and resolve them together instead of awaiting each one sequentially.\n const legacyPageNumbers = Array.from({ length: loadedPages }, (_, index) => index + 1)\n const legacyPayloads = await Promise.all(\n legacyPageNumbers.map((legacyPage) => {\n const legacyParams = new URLSearchParams({\n entityId,\n page: String(legacyPage),\n pageSize: '50',\n sortField: 'occurredAt',\n sortDir: 'desc',\n })\n if (dealId) legacyParams.set('dealId', dealId)\n return readApiResultOrThrow<{ items?: ActivitySummary[]; totalPages?: number }>(\n `/api/customers/activities?${legacyParams.toString()}`,\n ).catch(() => ({ items: [] as ActivitySummary[], totalPages: 1 }))\n }),\n )\n // Merge in page order so timeline ordering stays stable regardless of\n // which request settles first.\n const legacyItems: InteractionSummary[] = []\n let legacyTotalPages = 1\n for (const legacyPayload of legacyPayloads) {\n legacyItems.push(...(Array.isArray(legacyPayload?.items) ? legacyPayload.items.map(normalizeLegacyActivity) : []))\n legacyTotalPages = typeof legacyPayload?.totalPages === 'number' ? legacyPayload.totalPages : legacyTotalPages\n }\n const legacyFiltered = legacyItems.filter((entry) => {\n if (filterTypes.length > 0 && !filterTypes.includes(entry.interactionType)) return false\n const dateOnly = toDateOnly(entry.occurredAt ?? entry.scheduledAt)\n if ((filterDateFrom || filterDateTo) && !dateOnly) return false\n if (filterDateFrom && dateOnly < filterDateFrom) return false\n if (filterDateTo && dateOnly > filterDateTo) return false\n return true\n })\n\n // Merge and deduplicate by id, sort newest first\n const seen = new Set<string>()\n const merged: InteractionSummary[] = []\n for (const item of [...canonicalItems, ...legacyFiltered]) {\n if (!seen.has(item.id)) {\n seen.add(item.id)\n merged.push(item)\n }\n }\n setActivities(sortTimelineActivities(merged))\n setHasMore(canonicalHasMore || legacyTotalPages > loadedPages)\n } catch (error) {\n logger.error('customers.activities.history failed', { err: error })\n flash(t('customers.activities.loadFailed', 'Failed to load activities.'), 'error')\n setActivities([])\n setHasMore(false)\n } finally {\n setLoading(false)\n }\n }, [dealId, entityId, excludeInteractionType, filterDateFrom, filterDateTo, filterTypes, loadedPages, useCanonicalInteractions, refreshKey, t])\n\n React.useEffect(() => {\n setLoadedPages(1)\n }, [dealId, entityId, filterDateFrom, filterDateTo, filterTypes, useCanonicalInteractions])\n\n const handleMarkDone = React.useCallback(async (activityId: string) => {\n try {\n const operation = () =>\n apiCallOrThrow('/api/customers/interactions/complete', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ id: activityId, occurredAt: new Date().toISOString() }),\n })\n if (runGuardedMutation) {\n await runGuardedMutation(operation, {\n id: activityId,\n status: 'done',\n operation: 'completeActivity',\n })\n } else {\n await operation()\n }\n flash(t('customers.activities.actions.markDoneSuccess', 'Activity marked done'), 'success')\n await loadActivities()\n } catch (err) {\n logger.warn('Mark done failed', { component: 'ActivitiesSection', activityId, err })\n flash(t('customers.activities.actions.markDoneError', 'Could not mark activity as done'), 'error')\n return\n }\n try {\n await Promise.resolve(onDataRefresh?.())\n } catch (err) {\n logger.warn('Parent refresh after mark done failed', { component: 'ActivitiesSection', activityId, err })\n }\n }, [loadActivities, onDataRefresh, runGuardedMutation, t])\n\n const resolvedUserIdsRef = React.useRef(new Set<string>())\n\n // Resolve missing author names from user IDs\n React.useEffect(() => {\n loadActivities()\n .then(() => { resolvedUserIdsRef.current = new Set() })\n .catch((err) => logger.warn('loadActivities failed', { component: 'ActivitiesSection', err }))\n }, [loadActivities])\n\n React.useEffect(() => {\n const unresolvedIds = new Set<string>()\n for (const a of activities) {\n if (a.authorUserId && !a.authorName && !resolvedUserIdsRef.current.has(a.authorUserId)) {\n unresolvedIds.add(a.authorUserId)\n }\n }\n if (unresolvedIds.size === 0) return\n\n const controller = new AbortController()\n resolveAuthorUserNames([...unresolvedIds], controller.signal)\n .then(({ names: nameMap, resolvedIds }) => {\n if (controller.signal.aborted) return\n // Only ids the server actually answered for are remembered. Marking an id whose request\n // failed \u2014 or one the server silently dropped past its `?ids=` cap \u2014 would leave that\n // author blank for the life of the component, even though the next attempt would work.\n for (const uid of resolvedIds) resolvedUserIdsRef.current.add(uid)\n if (nameMap.size > 0) {\n setActivities((prev) =>\n prev.map((a) => {\n if (a.authorUserId && !a.authorName && nameMap.has(a.authorUserId)) {\n return { ...a, authorName: nameMap.get(a.authorUserId) ?? null }\n }\n return a\n }),\n )\n }\n })\n .catch((err) => logger.warn('resolve author names failed', { component: 'ActivitiesSection', err }))\n return () => controller.abort()\n }, [activities])\n\n const totalCount = activities.length\n const visibleCount = visibleActivities.length\n\n return (\n <div className=\"flex flex-col gap-3.5 rounded-[10px] border border-border bg-card pt-4 pb-[18px] px-[18px]\">\n <div className=\"flex items-center gap-2\">\n <Clock className=\"size-[15px] text-muted-foreground\" />\n <h3 className=\"text-[13px] font-semibold text-foreground\">\n {entityName\n ? t('customers.timeline.history.title', 'Interaction history with {{name}}', { name: entityName })\n : t('customers.timeline.history.titleGeneric', 'Interaction history')}\n </h3>\n </div>\n\n <label className=\"relative flex items-center\">\n <Search className=\"pointer-events-none absolute left-2.5 size-5 text-muted-foreground\" aria-hidden />\n <input\n ref={searchInputRef}\n type=\"search\"\n value={searchTerm}\n onChange={(event) => setSearchTerm(event.target.value)}\n placeholder={t('customers.timeline.history.searchPlaceholder', 'Search...')}\n aria-label={t('customers.timeline.history.searchAriaLabel', 'Search interaction history')}\n className=\"h-9 w-full rounded-[10px] border border-border bg-card pl-9 pr-14 text-sm text-foreground shadow-xs placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring [&::-webkit-search-cancel-button]:hidden [&::-webkit-search-decoration]:hidden\"\n />\n <Kbd className=\"pointer-events-none absolute right-2 hidden text-[11px] uppercase tracking-[0.48px] sm:inline-flex\">\n \u23181\n </Kbd>\n </label>\n\n <ActivityTimelineFilters\n entityId={entityId}\n activeTypes={filterTypes}\n dateFrom={filterDateFrom}\n dateTo={filterDateTo}\n onTypesChange={setFilterTypes}\n onDateFromChange={setFilterDateFrom}\n onDateToChange={setFilterDateTo}\n onReset={() => {\n setFilterTypes([])\n setFilterDateFrom('')\n setFilterDateTo('')\n }}\n />\n\n {loading && totalCount === 0 ? (\n <div className=\"rounded-lg border border-dashed border-border/70 px-4 py-8 text-sm text-muted-foreground\">\n {t('customers.people.detail.activities.loading', 'Loading activities\u2026')}\n </div>\n ) : (\n <>\n <ActivityTimeline\n activities={visibleActivities}\n onEdit={onEditActivity}\n onMarkDone={handleMarkDone}\n />\n {totalCount > 0 ? (\n <div className=\"flex items-center justify-between gap-3 border-t border-border/60 pt-3\">\n <span className=\"text-xs text-muted-foreground\">\n {searchTerm.trim()\n ? t('customers.activities.seeMatching', 'Showing {visible} of {total} activities', {\n visible: visibleCount,\n total: totalCount,\n })\n : t('customers.activities.seeAll', 'See all {count} activities', { count: totalCount })}\n </span>\n {hasMore ? (\n <Button type=\"button\" variant=\"link\" size=\"sm\" onClick={() => setLoadedPages((value) => value + 1)}>\n {t('customers.activities.loadMore', 'Load more')}\n </Button>\n ) : null}\n </div>\n ) : null}\n </>\n )}\n </div>\n )\n}\n\nexport default ActivitiesSection\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -74,7 +74,7 @@ function buildEmailCardWidgetData(activity) {
|
|
|
74
74
|
}
|
|
75
75
|
function ActivityCard({ activity, onOpen, onChanged, runMutation }) {
|
|
76
76
|
const t = useT();
|
|
77
|
-
const timestamp = activity.occurredAt ?? activity.scheduledAt
|
|
77
|
+
const timestamp = activity.occurredAt ?? activity.scheduledAt;
|
|
78
78
|
const TypeIcon = TYPE_ICONS[activity.interactionType] ?? StickyNote;
|
|
79
79
|
const titleBase = activity.title ?? activity.body ?? activity.interactionType;
|
|
80
80
|
const title = activity.duration ? `${titleBase} (${activity.duration} min)` : titleBase;
|
|
@@ -131,8 +131,8 @@ function ActivityCard({ activity, onOpen, onChanged, runMutation }) {
|
|
|
131
131
|
} : void 0,
|
|
132
132
|
children: [
|
|
133
133
|
/* @__PURE__ */ jsxs("div", { className: "pt-1 text-xs text-muted-foreground", children: [
|
|
134
|
-
/* @__PURE__ */ jsx("div", { className: "font-semibold text-foreground", children: formatDayLabel(timestamp, t) }),
|
|
135
|
-
/* @__PURE__ */ jsx("div", { children: formatTimeLabel(timestamp) })
|
|
134
|
+
/* @__PURE__ */ jsx("div", { className: "font-semibold text-foreground", children: timestamp ? formatDayLabel(timestamp, t) : t("customers.timeline.date.unknown", "Unknown date") }),
|
|
135
|
+
/* @__PURE__ */ jsx("div", { children: timestamp ? formatTimeLabel(timestamp) : null })
|
|
136
136
|
] }),
|
|
137
137
|
/* @__PURE__ */ jsx("div", { className: "flex size-9 items-center justify-center rounded-lg bg-muted/80", children: /* @__PURE__ */ jsx(TypeIcon, { className: "size-4 text-muted-foreground" }) }),
|
|
138
138
|
/* @__PURE__ */ jsxs("div", { className: "rounded-xl border bg-card px-4 py-3 shadow-sm", children: [
|