@open-mercato/core 0.10.1-develop.7770.1.970b396ebb → 0.10.1-develop.7772.1.9d3d6ce261

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/communication_channels/commands/ingest-inbound-message.js +6 -7
  3. package/dist/modules/communication_channels/commands/ingest-inbound-message.js.map +2 -2
  4. package/dist/modules/communication_channels/data/validators.js +2 -1
  5. package/dist/modules/communication_channels/data/validators.js.map +2 -2
  6. package/dist/modules/communication_channels/lib/email-mime.js +13 -3
  7. package/dist/modules/communication_channels/lib/email-mime.js.map +2 -2
  8. package/dist/modules/communication_channels/lib/thread-matcher.js.map +2 -2
  9. package/dist/modules/customers/api/activities/route.js +16 -6
  10. package/dist/modules/customers/api/activities/route.js.map +2 -2
  11. package/dist/modules/customers/lib/interactionOrderBy.js +12 -0
  12. package/dist/modules/customers/lib/interactionOrderBy.js.map +7 -0
  13. package/dist/modules/customers/lib/link-channel-message-handler.js +12 -11
  14. package/dist/modules/customers/lib/link-channel-message-handler.js.map +2 -2
  15. package/dist/modules/customers/lib/personEmailThreads.js +43 -12
  16. package/dist/modules/customers/lib/personEmailThreads.js.map +3 -3
  17. package/dist/modules/messages/api/route.js +2 -1
  18. package/dist/modules/messages/api/route.js.map +2 -2
  19. package/dist/modules/messages/commands/messages.js +2 -1
  20. package/dist/modules/messages/commands/messages.js.map +2 -2
  21. package/dist/modules/messages/components/MessagesInboxPageClient.js +1 -0
  22. package/dist/modules/messages/components/MessagesInboxPageClient.js.map +2 -2
  23. package/dist/modules/messages/components/defaults/DefaultMessageListItem.js +10 -8
  24. package/dist/modules/messages/components/defaults/DefaultMessageListItem.js.map +2 -2
  25. package/dist/modules/messages/components/message-detail/panels/MessageHeader.js +1 -1
  26. package/dist/modules/messages/components/message-detail/panels/MessageHeader.js.map +2 -2
  27. package/dist/modules/messages/components/message-detail/panels/MessageListComponent.js +3 -3
  28. package/dist/modules/messages/components/message-detail/panels/MessageListComponent.js.map +2 -2
  29. package/dist/modules/messages/components/message-detail/panels/thread-panel.js +1 -1
  30. package/dist/modules/messages/components/message-detail/panels/thread-panel.js.map +2 -2
  31. package/dist/modules/messages/components/message-detail/utils.js +3 -3
  32. package/dist/modules/messages/components/message-detail/utils.js.map +2 -2
  33. package/dist/modules/messages/data/validators.js +3 -2
  34. package/dist/modules/messages/data/validators.js.map +2 -2
  35. package/dist/modules/messages/frontend/messages/view/[token]/page.js +6 -5
  36. package/dist/modules/messages/frontend/messages/view/[token]/page.js.map +2 -2
  37. package/dist/modules/messages/lib/forwarding.js +3 -1
  38. package/dist/modules/messages/lib/forwarding.js.map +2 -2
  39. package/package.json +7 -7
  40. package/src/modules/communication_channels/commands/ingest-inbound-message.ts +5 -7
  41. package/src/modules/communication_channels/data/validators.ts +2 -1
  42. package/src/modules/communication_channels/lib/adapter.ts +4 -1
  43. package/src/modules/communication_channels/lib/email-mime.ts +24 -7
  44. package/src/modules/communication_channels/lib/thread-matcher.ts +2 -1
  45. package/src/modules/customers/api/activities/route.ts +17 -7
  46. package/src/modules/customers/lib/interactionOrderBy.ts +9 -0
  47. package/src/modules/customers/lib/link-channel-message-handler.ts +27 -38
  48. package/src/modules/customers/lib/personEmailThreads.ts +63 -21
  49. package/src/modules/messages/api/route.ts +3 -1
  50. package/src/modules/messages/commands/messages.ts +2 -1
  51. package/src/modules/messages/components/MessagesInboxPageClient.tsx +2 -0
  52. package/src/modules/messages/components/defaults/DefaultMessageListItem.tsx +10 -8
  53. package/src/modules/messages/components/message-detail/panels/MessageHeader.tsx +3 -1
  54. package/src/modules/messages/components/message-detail/panels/MessageListComponent.tsx +5 -3
  55. package/src/modules/messages/components/message-detail/panels/thread-panel.tsx +1 -1
  56. package/src/modules/messages/components/message-detail/utils.ts +3 -3
  57. package/src/modules/messages/data/validators.ts +4 -3
  58. package/src/modules/messages/frontend/messages/view/[token]/page.tsx +5 -4
  59. package/src/modules/messages/i18n/de.json +1 -0
  60. package/src/modules/messages/i18n/en.json +1 -0
  61. package/src/modules/messages/i18n/es.json +1 -0
  62. package/src/modules/messages/i18n/ko.json +1 -0
  63. package/src/modules/messages/i18n/pl.json +2 -1
  64. package/src/modules/messages/lib/forwarding.ts +3 -1
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../../src/modules/messages/frontend/messages/view/%5Btoken%5D/page.tsx"],
4
- "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport Link from 'next/link'\nimport type {\n MessageContentProps,\n MessageObjectAction,\n} from '@open-mercato/shared/modules/messages/types'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { MarkdownContent } from '@open-mercato/ui/backend/markdown/MarkdownContent'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport {\n getMessageUiComponentRegistry,\n} from '../../../../components/utils/typeUiRegistry'\nimport { getMessageObjectType } from '../../../../lib/message-objects-registry'\nimport { getMessageTypeOrDefault } from '../../../../lib/message-types-registry'\n\ntype TokenMessageObject = {\n id: string\n entityModule: string\n entityType: string\n entityId: string\n actionRequired: boolean\n actionType?: string | null\n actionLabel?: string | null\n snapshot?: Record<string, unknown> | null\n}\n\ntype MessageTokenDetailResponse = {\n id: string\n type: string\n subject: string\n body: string\n bodyFormat: 'text' | 'markdown'\n priority: 'low' | 'normal' | 'high' | 'urgent'\n senderUserId: string\n sentAt?: string | null\n actionData?: {\n actions: Array<{\n id: string\n label: string\n labelKey?: string\n variant?: 'default' | 'secondary' | 'destructive' | 'outline' | 'ghost'\n icon?: string\n commandId?: string\n href?: string\n isTerminal?: boolean\n confirmRequired?: boolean\n confirmMessage?: string\n }>\n primaryActionId?: string\n expiresAt?: string\n } | null\n actionTaken?: string | null\n actionTakenAt?: string | null\n actionTakenByUserId?: string | null\n objects: TokenMessageObject[]\n requiresAuth: boolean\n recipientUserId: string\n}\n\ntype MessageTokenPreflightResponse = {\n requiresAuth: true\n}\n\ntype MessageTokenResponse = MessageTokenDetailResponse | MessageTokenPreflightResponse\n\nfunction isProtectedPreflight(data: MessageTokenResponse): data is MessageTokenPreflightResponse {\n return data.requiresAuth && !('id' in data)\n}\n\nfunction toErrorMessage(payload: unknown): string | null {\n if (!payload) return null\n if (typeof payload === 'string') return payload\n if (Array.isArray(payload)) {\n for (const entry of payload) {\n const nested = toErrorMessage(entry)\n if (nested) return nested\n }\n return null\n }\n if (typeof payload === 'object') {\n const record = payload as Record<string, unknown>\n return (\n toErrorMessage(record.error)\n ?? toErrorMessage(record.message)\n ?? toErrorMessage(record.detail)\n ?? toErrorMessage(record.details)\n ?? null\n )\n }\n return null\n}\n\nfunction formatDateTime(value: string | null | undefined): string {\n if (!value) return '\u2014'\n const date = new Date(value)\n if (Number.isNaN(date.getTime())) return '\u2014'\n return date.toLocaleString()\n}\n\nfunction toObjectActions(\n objectActions: Array<{\n id: string\n labelKey: string\n variant?: 'default' | 'secondary' | 'destructive' | 'outline'\n icon?: string\n commandId?: string\n href?: string\n isTerminal?: boolean\n confirmRequired?: boolean\n confirmMessage?: string\n }>,\n): MessageObjectAction[] {\n return objectActions.map((action) => ({\n id: action.id,\n labelKey: action.labelKey,\n variant: action.variant,\n icon: action.icon,\n commandId: action.commandId,\n href: action.href,\n isTerminal: action.isTerminal,\n confirmRequired: action.confirmRequired,\n confirmMessage: action.confirmMessage,\n }))\n}\n\nfunction mergeTokenActions(\n messageActions: MessageTokenDetailResponse['actionData'],\n defaultActions: Array<{\n id: string\n label: string\n labelKey?: string\n variant?: 'default' | 'secondary' | 'destructive' | 'outline' | 'ghost'\n icon?: string\n commandId?: string\n href?: string\n isTerminal?: boolean\n confirmRequired?: boolean\n confirmMessage?: string\n }> | undefined,\n): MessageTokenDetailResponse['actionData'] {\n const deduped = new Map<string, NonNullable<MessageTokenDetailResponse['actionData']>['actions'][number]>()\n\n for (const action of messageActions?.actions ?? []) {\n const normalizedId = action.id.trim()\n if (!normalizedId) continue\n deduped.set(normalizedId, { ...action, id: normalizedId })\n }\n\n for (const action of defaultActions ?? []) {\n const normalizedId = action.id.trim()\n if (!normalizedId || deduped.has(normalizedId)) continue\n deduped.set(normalizedId, { ...action, id: normalizedId })\n }\n\n const actions = Array.from(deduped.values())\n if (actions.length === 0 && !messageActions?.expiresAt) return null\n\n return {\n actions,\n primaryActionId: messageActions?.primaryActionId,\n expiresAt: messageActions?.expiresAt,\n }\n}\n\nexport default function MessageTokenPage({ params }: { params: { token: string } }) {\n const t = useT()\n const messageUiRegistry = React.useMemo(() => getMessageUiComponentRegistry(), [])\n const token = params?.token\n\n const [loading, setLoading] = React.useState(true)\n const [data, setData] = React.useState<MessageTokenResponse | null>(null)\n const [errorMessage, setErrorMessage] = React.useState<string | null>(null)\n const [errorStatus, setErrorStatus] = React.useState<number | null>(null)\n\n React.useEffect(() => {\n let mounted = true\n\n async function run() {\n if (!token) return\n\n setLoading(true)\n setData(null)\n setErrorMessage(null)\n setErrorStatus(null)\n\n try {\n const call = await apiCall<MessageTokenResponse>(`/api/messages/token/${encodeURIComponent(token)}`)\n\n if (!mounted) return\n\n if (!call.ok || !call.result) {\n const status = call.status\n setErrorStatus(status)\n\n const fallback = status === 404\n ? t('messages.token.errors.notFound', 'This message link is invalid or has already been used.')\n : status === 409\n ? t('messages.token.errors.limitExceeded', 'This message link reached its usage limit.')\n : status === 410\n ? t('messages.token.errors.expired', 'This message link has expired.')\n : t('messages.token.errors.generic', 'Unable to load this message.')\n\n setErrorMessage(toErrorMessage(call.result) ?? fallback)\n return\n }\n\n setData(call.result)\n } catch (error) {\n if (!mounted) return\n setErrorMessage(\n error instanceof Error\n ? error.message\n : t('messages.token.errors.generic', 'Unable to load this message.'),\n )\n } finally {\n if (mounted) {\n setLoading(false)\n }\n }\n }\n\n void run()\n\n return () => {\n mounted = false\n }\n }, [t, token])\n\n if (loading) {\n return (\n <main className=\"mx-auto max-w-3xl p-6\">\n <p className=\"flex items-center gap-2 text-sm text-muted-foreground\">\n <Spinner className=\"h-4 w-4 animate-spin\" />\n {t('messages.token.loading', 'Loading message...')}\n </p>\n </main>\n )\n }\n\n if (errorMessage || !data) {\n return (\n <main className=\"mx-auto max-w-3xl space-y-3 p-6\">\n <h1 className=\"text-2xl font-semibold\">{t('messages.token.pageTitle', 'Message')}</h1>\n <p className=\"text-sm text-destructive\">{errorMessage ?? t('messages.token.errors.generic', 'Unable to load this message.')}</p>\n {errorStatus ? <p className=\"text-xs text-muted-foreground\">HTTP {errorStatus}</p> : null}\n </main>\n )\n }\n\n if (isProtectedPreflight(data)) {\n return (\n <main className=\"mx-auto max-w-3xl space-y-4 p-6\">\n <h1 className=\"text-2xl font-semibold\">{t('messages.token.authRequired.title', 'Sign in required')}</h1>\n <p className=\"text-sm text-muted-foreground\">\n {t('messages.token.authRequired.description', 'This message includes protected objects. Sign in to continue.')}\n </p>\n <Button asChild>\n <Link href=\"/login\">{t('auth.signIn', 'Sign in')}</Link>\n </Button>\n </main>\n )\n }\n\n const messageType = getMessageTypeOrDefault(data.type)\n const contentComponentKey = messageType.ui?.contentComponent\n const actionsComponentKey = messageType.ui?.actionsComponent\n const ContentComponent = contentComponentKey\n ? messageUiRegistry.contentComponents[contentComponentKey] ?? null\n : null\n const ActionsComponent = actionsComponentKey\n ? messageUiRegistry.actionsComponents[actionsComponentKey] ?? null\n : null\n const resolvedActionData = mergeTokenActions(data.actionData ?? null, messageType.defaultActions)\n\n const contentProps: MessageContentProps = {\n message: {\n id: data.id,\n type: data.type,\n subject: data.subject,\n body: data.body,\n bodyFormat: data.bodyFormat,\n priority: data.priority,\n sentAt: data.sentAt ? new Date(data.sentAt) : null,\n senderUserId: data.senderUserId,\n actionData: resolvedActionData,\n actionTaken: data.actionTaken ?? null,\n actionTakenAt: data.actionTakenAt ? new Date(data.actionTakenAt) : null,\n },\n objects: data.objects.map((objectItem) => ({\n id: objectItem.id,\n entityModule: objectItem.entityModule,\n entityType: objectItem.entityType,\n entityId: objectItem.entityId,\n actionRequired: objectItem.actionRequired,\n snapshot: objectItem.snapshot ?? undefined,\n })),\n attachments: [],\n }\n\n const tokenActions = resolvedActionData?.actions ?? []\n\n return (\n <main className=\"mx-auto max-w-3xl space-y-6 p-6\">\n <header className=\"space-y-1\">\n <h1 className=\"text-2xl font-semibold\">{data.subject}</h1>\n <p className=\"text-sm text-muted-foreground\">\n {t('messages.token.sentAt', 'Sent')}: {formatDateTime(data.sentAt)}\n </p>\n </header>\n\n <section className=\"rounded-xl border bg-card p-4\">\n {ContentComponent ? (\n <ContentComponent {...contentProps} />\n ) : (\n <MarkdownContent\n body={data.body}\n format={data.bodyFormat}\n className=\"text-sm whitespace-pre-wrap [&>*]:mb-2 [&>*:last-child]:mb-0 [&_ul]:ml-4 [&_ul]:list-disc [&_ol]:ml-4 [&_ol]:list-decimal [&_code]:rounded [&_code]:bg-muted [&_code]:px-1 [&_code]:py-0.5 [&_pre]:rounded-md [&_pre]:bg-muted [&_pre]:p-3 [&_pre]:text-xs\"\n />\n )}\n </section>\n\n {tokenActions.length > 0 ? (\n <section className=\"space-y-3 rounded-xl border bg-card p-4\">\n <h2 className=\"text-base font-semibold\">{t('messages.actions.title', 'Actions')}</h2>\n {ActionsComponent ? (\n <ActionsComponent\n message={{\n id: data.id,\n type: data.type,\n actionData: resolvedActionData,\n actionTaken: data.actionTaken ?? null,\n }}\n onExecuteAction={async (_actionId) => {\n return\n }}\n isExecuting={false}\n executingActionId={null}\n />\n ) : (\n <div className=\"flex flex-wrap gap-2\">\n {tokenActions.map((action) => (\n <Button\n key={action.id}\n type=\"button\"\n variant={action.variant ?? 'default'}\n disabled\n >\n {t(action.labelKey ?? action.label, action.label)}\n </Button>\n ))}\n </div>\n )}\n </section>\n ) : null}\n\n <section className=\"space-y-2 rounded-xl border bg-card p-4\">\n <h2 className=\"text-base font-semibold\">{t('messages.attachedObjects', 'Attached objects')}</h2>\n {data.objects.length === 0 ? (\n null\n ) : (\n <div className=\"space-y-2\">\n {data.objects.map((objectItem) => {\n const objectType = getMessageObjectType(objectItem.entityModule, objectItem.entityType)\n const objectActions = toObjectActions(objectType?.actions ?? [])\n const detailComponentKey = `${objectItem.entityModule}:${objectItem.entityType}`\n const DetailComponent = messageUiRegistry.objectDetailComponents[detailComponentKey] ?? null\n\n return DetailComponent ? (\n <DetailComponent\n key={objectItem.id}\n entityId={objectItem.entityId}\n entityModule={objectItem.entityModule}\n entityType={objectItem.entityType}\n snapshot={objectItem.snapshot ?? undefined}\n previewData={undefined}\n actionRequired={objectItem.actionRequired}\n actionType={objectItem.actionType ?? undefined}\n actionLabel={objectItem.actionLabel ?? undefined}\n actions={objectActions}\n actionTaken={data.actionTaken ?? null}\n actionTakenAt={data.actionTakenAt ? new Date(data.actionTakenAt) : null}\n actionTakenByUserId={data.actionTakenByUserId ?? null}\n icon={objectType?.icon}\n onAction={async () => {\n return\n }}\n />\n ) : null\n })}\n </div>\n )}\n </section>\n </main>\n )\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport Link from 'next/link'\nimport type {\n MessageContentProps,\n MessageObjectAction,\n} from '@open-mercato/shared/modules/messages/types'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { MarkdownContent } from '@open-mercato/ui/backend/markdown/MarkdownContent'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport {\n getMessageUiComponentRegistry,\n} from '../../../../components/utils/typeUiRegistry'\nimport { getMessageObjectType } from '../../../../lib/message-objects-registry'\nimport { getMessageTypeOrDefault } from '../../../../lib/message-types-registry'\n\ntype TokenMessageObject = {\n id: string\n entityModule: string\n entityType: string\n entityId: string\n actionRequired: boolean\n actionType?: string | null\n actionLabel?: string | null\n snapshot?: Record<string, unknown> | null\n}\n\ntype MessageTokenDetailResponse = {\n id: string\n type: string\n subject: string\n body: string\n bodyFormat: 'text' | 'markdown'\n priority: 'low' | 'normal' | 'high' | 'urgent'\n senderUserId: string\n sentAt?: string | null\n actionData?: {\n actions: Array<{\n id: string\n label: string\n labelKey?: string\n variant?: 'default' | 'secondary' | 'destructive' | 'outline' | 'ghost'\n icon?: string\n commandId?: string\n href?: string\n isTerminal?: boolean\n confirmRequired?: boolean\n confirmMessage?: string\n }>\n primaryActionId?: string\n expiresAt?: string\n } | null\n actionTaken?: string | null\n actionTakenAt?: string | null\n actionTakenByUserId?: string | null\n objects: TokenMessageObject[]\n requiresAuth: boolean\n recipientUserId: string\n}\n\ntype MessageTokenPreflightResponse = {\n requiresAuth: true\n}\n\ntype MessageTokenResponse = MessageTokenDetailResponse | MessageTokenPreflightResponse\n\nfunction isProtectedPreflight(data: MessageTokenResponse): data is MessageTokenPreflightResponse {\n return data.requiresAuth && !('id' in data)\n}\n\nfunction toErrorMessage(payload: unknown): string | null {\n if (!payload) return null\n if (typeof payload === 'string') return payload\n if (Array.isArray(payload)) {\n for (const entry of payload) {\n const nested = toErrorMessage(entry)\n if (nested) return nested\n }\n return null\n }\n if (typeof payload === 'object') {\n const record = payload as Record<string, unknown>\n return (\n toErrorMessage(record.error)\n ?? toErrorMessage(record.message)\n ?? toErrorMessage(record.detail)\n ?? toErrorMessage(record.details)\n ?? null\n )\n }\n return null\n}\n\nfunction formatDateTime(value: string | null | undefined, unknownDate: string): string {\n if (!value) return unknownDate\n const date = new Date(value)\n if (Number.isNaN(date.getTime())) return unknownDate\n return date.toLocaleString()\n}\n\nfunction toObjectActions(\n objectActions: Array<{\n id: string\n labelKey: string\n variant?: 'default' | 'secondary' | 'destructive' | 'outline'\n icon?: string\n commandId?: string\n href?: string\n isTerminal?: boolean\n confirmRequired?: boolean\n confirmMessage?: string\n }>,\n): MessageObjectAction[] {\n return objectActions.map((action) => ({\n id: action.id,\n labelKey: action.labelKey,\n variant: action.variant,\n icon: action.icon,\n commandId: action.commandId,\n href: action.href,\n isTerminal: action.isTerminal,\n confirmRequired: action.confirmRequired,\n confirmMessage: action.confirmMessage,\n }))\n}\n\nfunction mergeTokenActions(\n messageActions: MessageTokenDetailResponse['actionData'],\n defaultActions: Array<{\n id: string\n label: string\n labelKey?: string\n variant?: 'default' | 'secondary' | 'destructive' | 'outline' | 'ghost'\n icon?: string\n commandId?: string\n href?: string\n isTerminal?: boolean\n confirmRequired?: boolean\n confirmMessage?: string\n }> | undefined,\n): MessageTokenDetailResponse['actionData'] {\n const deduped = new Map<string, NonNullable<MessageTokenDetailResponse['actionData']>['actions'][number]>()\n\n for (const action of messageActions?.actions ?? []) {\n const normalizedId = action.id.trim()\n if (!normalizedId) continue\n deduped.set(normalizedId, { ...action, id: normalizedId })\n }\n\n for (const action of defaultActions ?? []) {\n const normalizedId = action.id.trim()\n if (!normalizedId || deduped.has(normalizedId)) continue\n deduped.set(normalizedId, { ...action, id: normalizedId })\n }\n\n const actions = Array.from(deduped.values())\n if (actions.length === 0 && !messageActions?.expiresAt) return null\n\n return {\n actions,\n primaryActionId: messageActions?.primaryActionId,\n expiresAt: messageActions?.expiresAt,\n }\n}\n\nexport default function MessageTokenPage({ params }: { params: { token: string } }) {\n const t = useT()\n const messageUiRegistry = React.useMemo(() => getMessageUiComponentRegistry(), [])\n const token = params?.token\n\n const [loading, setLoading] = React.useState(true)\n const [data, setData] = React.useState<MessageTokenResponse | null>(null)\n const [errorMessage, setErrorMessage] = React.useState<string | null>(null)\n const [errorStatus, setErrorStatus] = React.useState<number | null>(null)\n\n React.useEffect(() => {\n let mounted = true\n\n async function run() {\n if (!token) return\n\n setLoading(true)\n setData(null)\n setErrorMessage(null)\n setErrorStatus(null)\n\n try {\n const call = await apiCall<MessageTokenResponse>(`/api/messages/token/${encodeURIComponent(token)}`)\n\n if (!mounted) return\n\n if (!call.ok || !call.result) {\n const status = call.status\n setErrorStatus(status)\n\n const fallback = status === 404\n ? t('messages.token.errors.notFound', 'This message link is invalid or has already been used.')\n : status === 409\n ? t('messages.token.errors.limitExceeded', 'This message link reached its usage limit.')\n : status === 410\n ? t('messages.token.errors.expired', 'This message link has expired.')\n : t('messages.token.errors.generic', 'Unable to load this message.')\n\n setErrorMessage(toErrorMessage(call.result) ?? fallback)\n return\n }\n\n setData(call.result)\n } catch (error) {\n if (!mounted) return\n setErrorMessage(\n error instanceof Error\n ? error.message\n : t('messages.token.errors.generic', 'Unable to load this message.'),\n )\n } finally {\n if (mounted) {\n setLoading(false)\n }\n }\n }\n\n void run()\n\n return () => {\n mounted = false\n }\n }, [t, token])\n\n if (loading) {\n return (\n <main className=\"mx-auto max-w-3xl p-6\">\n <p className=\"flex items-center gap-2 text-sm text-muted-foreground\">\n <Spinner className=\"h-4 w-4 animate-spin\" />\n {t('messages.token.loading', 'Loading message...')}\n </p>\n </main>\n )\n }\n\n if (errorMessage || !data) {\n return (\n <main className=\"mx-auto max-w-3xl space-y-3 p-6\">\n <h1 className=\"text-2xl font-semibold\">{t('messages.token.pageTitle', 'Message')}</h1>\n <p className=\"text-sm text-destructive\">{errorMessage ?? t('messages.token.errors.generic', 'Unable to load this message.')}</p>\n {errorStatus ? <p className=\"text-xs text-muted-foreground\">HTTP {errorStatus}</p> : null}\n </main>\n )\n }\n\n if (isProtectedPreflight(data)) {\n return (\n <main className=\"mx-auto max-w-3xl space-y-4 p-6\">\n <h1 className=\"text-2xl font-semibold\">{t('messages.token.authRequired.title', 'Sign in required')}</h1>\n <p className=\"text-sm text-muted-foreground\">\n {t('messages.token.authRequired.description', 'This message includes protected objects. Sign in to continue.')}\n </p>\n <Button asChild>\n <Link href=\"/login\">{t('auth.signIn', 'Sign in')}</Link>\n </Button>\n </main>\n )\n }\n\n const messageType = getMessageTypeOrDefault(data.type)\n const contentComponentKey = messageType.ui?.contentComponent\n const actionsComponentKey = messageType.ui?.actionsComponent\n const ContentComponent = contentComponentKey\n ? messageUiRegistry.contentComponents[contentComponentKey] ?? null\n : null\n const ActionsComponent = actionsComponentKey\n ? messageUiRegistry.actionsComponents[actionsComponentKey] ?? null\n : null\n const resolvedActionData = mergeTokenActions(data.actionData ?? null, messageType.defaultActions)\n\n const contentProps: MessageContentProps = {\n message: {\n id: data.id,\n type: data.type,\n subject: data.subject,\n body: data.body,\n bodyFormat: data.bodyFormat,\n priority: data.priority,\n sentAt: data.sentAt ? new Date(data.sentAt) : null,\n senderUserId: data.senderUserId,\n actionData: resolvedActionData,\n actionTaken: data.actionTaken ?? null,\n actionTakenAt: data.actionTakenAt ? new Date(data.actionTakenAt) : null,\n },\n objects: data.objects.map((objectItem) => ({\n id: objectItem.id,\n entityModule: objectItem.entityModule,\n entityType: objectItem.entityType,\n entityId: objectItem.entityId,\n actionRequired: objectItem.actionRequired,\n snapshot: objectItem.snapshot ?? undefined,\n })),\n attachments: [],\n }\n\n const tokenActions = resolvedActionData?.actions ?? []\n\n return (\n <main className=\"mx-auto max-w-3xl space-y-6 p-6\">\n <header className=\"space-y-1\">\n <h1 className=\"text-2xl font-semibold\">{data.subject}</h1>\n <p className=\"text-sm text-muted-foreground\">\n {t('messages.token.sentAt', 'Sent')}:{' '}\n {formatDateTime(data.sentAt, t('messages.unknownDate', 'Unknown date'))}\n </p>\n </header>\n\n <section className=\"rounded-xl border bg-card p-4\">\n {ContentComponent ? (\n <ContentComponent {...contentProps} />\n ) : (\n <MarkdownContent\n body={data.body}\n format={data.bodyFormat}\n className=\"text-sm whitespace-pre-wrap [&>*]:mb-2 [&>*:last-child]:mb-0 [&_ul]:ml-4 [&_ul]:list-disc [&_ol]:ml-4 [&_ol]:list-decimal [&_code]:rounded [&_code]:bg-muted [&_code]:px-1 [&_code]:py-0.5 [&_pre]:rounded-md [&_pre]:bg-muted [&_pre]:p-3 [&_pre]:text-xs\"\n />\n )}\n </section>\n\n {tokenActions.length > 0 ? (\n <section className=\"space-y-3 rounded-xl border bg-card p-4\">\n <h2 className=\"text-base font-semibold\">{t('messages.actions.title', 'Actions')}</h2>\n {ActionsComponent ? (\n <ActionsComponent\n message={{\n id: data.id,\n type: data.type,\n actionData: resolvedActionData,\n actionTaken: data.actionTaken ?? null,\n }}\n onExecuteAction={async (_actionId) => {\n return\n }}\n isExecuting={false}\n executingActionId={null}\n />\n ) : (\n <div className=\"flex flex-wrap gap-2\">\n {tokenActions.map((action) => (\n <Button\n key={action.id}\n type=\"button\"\n variant={action.variant ?? 'default'}\n disabled\n >\n {t(action.labelKey ?? action.label, action.label)}\n </Button>\n ))}\n </div>\n )}\n </section>\n ) : null}\n\n <section className=\"space-y-2 rounded-xl border bg-card p-4\">\n <h2 className=\"text-base font-semibold\">{t('messages.attachedObjects', 'Attached objects')}</h2>\n {data.objects.length === 0 ? (\n null\n ) : (\n <div className=\"space-y-2\">\n {data.objects.map((objectItem) => {\n const objectType = getMessageObjectType(objectItem.entityModule, objectItem.entityType)\n const objectActions = toObjectActions(objectType?.actions ?? [])\n const detailComponentKey = `${objectItem.entityModule}:${objectItem.entityType}`\n const DetailComponent = messageUiRegistry.objectDetailComponents[detailComponentKey] ?? null\n\n return DetailComponent ? (\n <DetailComponent\n key={objectItem.id}\n entityId={objectItem.entityId}\n entityModule={objectItem.entityModule}\n entityType={objectItem.entityType}\n snapshot={objectItem.snapshot ?? undefined}\n previewData={undefined}\n actionRequired={objectItem.actionRequired}\n actionType={objectItem.actionType ?? undefined}\n actionLabel={objectItem.actionLabel ?? undefined}\n actions={objectActions}\n actionTaken={data.actionTaken ?? null}\n actionTakenAt={data.actionTakenAt ? new Date(data.actionTakenAt) : null}\n actionTakenByUserId={data.actionTakenByUserId ?? null}\n icon={objectType?.icon}\n onAction={async () => {\n return\n }}\n />\n ) : null\n })}\n </div>\n )}\n </section>\n </main>\n )\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -90,6 +90,8 @@ async function buildForwardThreadSlice(em, scope, selectedMessage, options = {})
90
90
  parentMessageId = parentMessage.parentMessageId ?? null;
91
91
  }
92
92
  const orderedThreadMessages = Array.from(messagesById.values()).sort((a, b) => {
93
+ if (a.sentAt && !b.sentAt) return -1;
94
+ if (!a.sentAt && b.sentAt) return 1;
93
95
  const aTime = a.sentAt?.getTime() ?? 0;
94
96
  const bTime = b.sentAt?.getTime() ?? 0;
95
97
  if (aTime !== bTime) return aTime - bTime;
@@ -159,7 +161,7 @@ async function buildForwardPreviewFromThreadSlice(em, scope, selectedMessage, th
159
161
  return [
160
162
  FORWARD_MARKER,
161
163
  `From: ${formatUserLabel(sender, item.senderUserId)}`,
162
- `Date: ${formatDateLabel(item.sentAt ?? item.createdAt)}`,
164
+ `Date: ${formatDateLabel(item.sentAt)}`,
163
165
  `Subject: ${normalizeSubject(item.subject)}`,
164
166
  `To: ${toLabel}`,
165
167
  "",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/messages/lib/forwarding.ts"],
4
- "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { User } from '../../auth/data/entities'\nimport { Message, MessageRecipient } from '../data/entities'\n\nconst FORWARD_MARKER = '---------- Forwarded message ----------'\nconst MAX_FORWARD_BODY_LENGTH = 50_000\n\ntype MessageScope = {\n tenantId: string\n organizationId: string | null\n userId?: string | null\n}\n\n/**\n * `includePublicThreadMessages` widens the participant filter to every\n * explicitly public message in the slice. Set it only for a caller who reached\n * the thread through channel-thread access (#6355): on a channel conversation\n * the inbound messages have no platform participant, so the participant filter\n * alone would forward an empty body.\n */\ntype ForwardThreadSliceOptions = {\n includePublicThreadMessages?: boolean\n}\n\ntype ForwardMessageBlock = {\n id: string\n senderUserId: string\n subject: string\n body: string\n sentAt: Date | null | undefined\n createdAt: Date\n}\n\nfunction formatForwardSubject(subject: string): string {\n if (/^fwd:\\s*/i.test(subject)) return subject\n return `Fwd: ${subject}`\n}\n\nfunction formatDateLabel(date: Date | null | undefined): string {\n if (!date) return '-'\n return date.toISOString()\n}\n\nfunction normalizeSubject(subject: string | null | undefined): string {\n if (typeof subject !== 'string') return '(no subject)'\n const trimmed = subject.trim()\n return trimmed.length > 0 ? trimmed : '(no subject)'\n}\n\nfunction formatUserLabel(user: { name?: string | null; email?: string | null } | undefined, fallbackUserId: string): string {\n const name = user?.name?.trim()\n const email = user?.email?.trim()\n if (name && email) return `${name} <${email}>`\n if (name) return name\n if (email) return email\n return fallbackUserId\n}\n\nfunction ensureMaxForwardBodyLength(body: string): void {\n if (body.length > MAX_FORWARD_BODY_LENGTH) {\n throw new Error('Forward body exceeds maximum length')\n }\n}\n\nexport async function buildForwardThreadSlice(\n em: EntityManager,\n scope: MessageScope,\n selectedMessage: Message,\n options: ForwardThreadSliceOptions = {},\n): Promise<ForwardMessageBlock[]> {\n const decryptedThreadMessages = await findWithDecryption(\n em,\n Message,\n {\n threadId: selectedMessage.threadId ?? selectedMessage.id,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n deletedAt: null,\n isDraft: false,\n },\n {\n orderBy: {\n sentAt: 'ASC',\n createdAt: 'ASC',\n id: 'ASC',\n },\n },\n { tenantId: scope.tenantId, organizationId: scope.organizationId },\n )\n\n const messagesById = new Map(decryptedThreadMessages.map((item) => [item.id, item]))\n messagesById.set(selectedMessage.id, selectedMessage)\n\n const threadRootId = selectedMessage.threadId ?? null\n if (threadRootId && !messagesById.has(threadRootId)) {\n const threadRoot = await findOneWithDecryption(\n em,\n Message,\n {\n id: threadRootId,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n deletedAt: null,\n isDraft: false,\n },\n undefined,\n { tenantId: scope.tenantId, organizationId: scope.organizationId },\n )\n if (threadRoot) {\n messagesById.set(threadRoot.id, threadRoot)\n }\n }\n\n let parentMessageId = selectedMessage.parentMessageId ?? null\n while (parentMessageId) {\n const parentMessage = await findOneWithDecryption(\n em,\n Message,\n {\n id: parentMessageId,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n deletedAt: null,\n isDraft: false,\n },\n undefined,\n { tenantId: scope.tenantId, organizationId: scope.organizationId },\n )\n if (!parentMessage) break\n messagesById.set(parentMessage.id, parentMessage)\n parentMessageId = parentMessage.parentMessageId ?? null\n }\n\n const orderedThreadMessages = Array.from(messagesById.values()).sort((a, b) => {\n const aTime = a.sentAt?.getTime() ?? 0\n const bTime = b.sentAt?.getTime() ?? 0\n if (aTime !== bTime) return aTime - bTime\n\n const aCreatedAt = a.createdAt.getTime()\n const bCreatedAt = b.createdAt.getTime()\n if (aCreatedAt !== bCreatedAt) return aCreatedAt - bCreatedAt\n\n return a.id.localeCompare(b.id)\n })\n\n const selectedIndex = orderedThreadMessages.findIndex((item) => item.id === selectedMessage.id)\n const threadSlice = selectedIndex >= 0 ? orderedThreadMessages.slice(0, selectedIndex + 1) : [selectedMessage]\n\n if (scope.userId) {\n const sliceIds = threadSlice.map((item) => item.id)\n const recipientRows = sliceIds.length > 0\n ? await em.find(MessageRecipient, {\n messageId: { $in: sliceIds },\n recipientUserId: scope.userId,\n deletedAt: null,\n })\n : []\n const recipientMessageIds = new Set(recipientRows.map((row) => row.messageId))\n const visibleSlice = threadSlice.filter(\n (item) => item.senderUserId === scope.userId\n || recipientMessageIds.has(item.id)\n || (options.includePublicThreadMessages === true && item.visibility === 'public'),\n )\n return visibleSlice.map((item) => ({\n id: item.id,\n senderUserId: item.senderUserId,\n subject: item.subject,\n body: item.body,\n sentAt: item.sentAt,\n createdAt: item.createdAt,\n }))\n }\n\n return threadSlice.map((item) => ({\n id: item.id,\n senderUserId: item.senderUserId,\n subject: item.subject,\n body: item.body,\n sentAt: item.sentAt,\n createdAt: item.createdAt,\n }))\n}\n\nexport async function buildForwardPreviewFromThreadSlice(\n em: EntityManager,\n scope: MessageScope,\n selectedMessage: Message,\n threadSlice: ForwardMessageBlock[],\n): Promise<{ subject: string; body: string }> {\n const allMessageIds = threadSlice.map((item) => item.id)\n const allSenderIds = Array.from(new Set(threadSlice.map((item) => item.senderUserId)))\n\n const recipients = allMessageIds.length > 0\n ? await em.find(MessageRecipient, {\n messageId: { $in: allMessageIds },\n deletedAt: null,\n })\n : []\n\n const allRecipientUserIds = Array.from(new Set(recipients.map((item) => item.recipientUserId)))\n const allUserIds = Array.from(new Set([...allSenderIds, ...allRecipientUserIds]))\n const users = allUserIds.length > 0\n ? await findWithDecryption(\n em,\n User,\n { id: { $in: allUserIds } },\n undefined,\n { tenantId: scope.tenantId, organizationId: scope.organizationId },\n )\n : []\n\n const userById = new Map(users.map((user) => [user.id, user]))\n const recipientsByMessageId = new Map<string, MessageRecipient[]>()\n for (const recipient of recipients) {\n const currentRecipients = recipientsByMessageId.get(recipient.messageId) ?? []\n currentRecipients.push(recipient)\n recipientsByMessageId.set(recipient.messageId, currentRecipients)\n }\n\n const body = threadSlice.map((item) => {\n const sender = userById.get(item.senderUserId)\n const toRecipients = recipientsByMessageId.get(item.id) ?? []\n const toLabel = toRecipients.length > 0\n ? toRecipients\n .map((recipient) => formatUserLabel(userById.get(recipient.recipientUserId), recipient.recipientUserId))\n .join(', ')\n : '-'\n\n return [\n FORWARD_MARKER,\n `From: ${formatUserLabel(sender, item.senderUserId)}`,\n `Date: ${formatDateLabel(item.sentAt ?? item.createdAt)}`,\n `Subject: ${normalizeSubject(item.subject)}`,\n `To: ${toLabel}`,\n '',\n item.body ?? '',\n ].join('\\n')\n }).join('\\n\\n')\n\n ensureMaxForwardBodyLength(body)\n\n return {\n subject: formatForwardSubject(selectedMessage.subject),\n body,\n }\n}\n\nexport async function buildForwardPreview(\n em: EntityManager,\n scope: MessageScope,\n selectedMessage: Message,\n options: ForwardThreadSliceOptions = {},\n): Promise<{ subject: string; body: string }> {\n const threadSlice = await buildForwardThreadSlice(em, scope, selectedMessage, options)\n return buildForwardPreviewFromThreadSlice(em, scope, selectedMessage, threadSlice)\n}\n\nexport function buildForwardBodyFromLegacyInput(\n generatedBody: string,\n additionalBody?: string,\n): string {\n const legacyPrefix = additionalBody?.trim()\n const finalBody = legacyPrefix\n ? `${legacyPrefix}\\n\\n${generatedBody}`\n : generatedBody\n ensureMaxForwardBodyLength(finalBody)\n return finalBody\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { User } from '../../auth/data/entities'\nimport { Message, MessageRecipient } from '../data/entities'\n\nconst FORWARD_MARKER = '---------- Forwarded message ----------'\nconst MAX_FORWARD_BODY_LENGTH = 50_000\n\ntype MessageScope = {\n tenantId: string\n organizationId: string | null\n userId?: string | null\n}\n\n/**\n * `includePublicThreadMessages` widens the participant filter to every\n * explicitly public message in the slice. Set it only for a caller who reached\n * the thread through channel-thread access (#6355): on a channel conversation\n * the inbound messages have no platform participant, so the participant filter\n * alone would forward an empty body.\n */\ntype ForwardThreadSliceOptions = {\n includePublicThreadMessages?: boolean\n}\n\ntype ForwardMessageBlock = {\n id: string\n senderUserId: string\n subject: string\n body: string\n sentAt: Date | null | undefined\n createdAt: Date\n}\n\nfunction formatForwardSubject(subject: string): string {\n if (/^fwd:\\s*/i.test(subject)) return subject\n return `Fwd: ${subject}`\n}\n\nfunction formatDateLabel(date: Date | null | undefined): string {\n if (!date) return '-'\n return date.toISOString()\n}\n\nfunction normalizeSubject(subject: string | null | undefined): string {\n if (typeof subject !== 'string') return '(no subject)'\n const trimmed = subject.trim()\n return trimmed.length > 0 ? trimmed : '(no subject)'\n}\n\nfunction formatUserLabel(user: { name?: string | null; email?: string | null } | undefined, fallbackUserId: string): string {\n const name = user?.name?.trim()\n const email = user?.email?.trim()\n if (name && email) return `${name} <${email}>`\n if (name) return name\n if (email) return email\n return fallbackUserId\n}\n\nfunction ensureMaxForwardBodyLength(body: string): void {\n if (body.length > MAX_FORWARD_BODY_LENGTH) {\n throw new Error('Forward body exceeds maximum length')\n }\n}\n\nexport async function buildForwardThreadSlice(\n em: EntityManager,\n scope: MessageScope,\n selectedMessage: Message,\n options: ForwardThreadSliceOptions = {},\n): Promise<ForwardMessageBlock[]> {\n const decryptedThreadMessages = await findWithDecryption(\n em,\n Message,\n {\n threadId: selectedMessage.threadId ?? selectedMessage.id,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n deletedAt: null,\n isDraft: false,\n },\n {\n orderBy: {\n sentAt: 'ASC',\n createdAt: 'ASC',\n id: 'ASC',\n },\n },\n { tenantId: scope.tenantId, organizationId: scope.organizationId },\n )\n\n const messagesById = new Map(decryptedThreadMessages.map((item) => [item.id, item]))\n messagesById.set(selectedMessage.id, selectedMessage)\n\n const threadRootId = selectedMessage.threadId ?? null\n if (threadRootId && !messagesById.has(threadRootId)) {\n const threadRoot = await findOneWithDecryption(\n em,\n Message,\n {\n id: threadRootId,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n deletedAt: null,\n isDraft: false,\n },\n undefined,\n { tenantId: scope.tenantId, organizationId: scope.organizationId },\n )\n if (threadRoot) {\n messagesById.set(threadRoot.id, threadRoot)\n }\n }\n\n let parentMessageId = selectedMessage.parentMessageId ?? null\n while (parentMessageId) {\n const parentMessage = await findOneWithDecryption(\n em,\n Message,\n {\n id: parentMessageId,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n deletedAt: null,\n isDraft: false,\n },\n undefined,\n { tenantId: scope.tenantId, organizationId: scope.organizationId },\n )\n if (!parentMessage) break\n messagesById.set(parentMessage.id, parentMessage)\n parentMessageId = parentMessage.parentMessageId ?? null\n }\n\n const orderedThreadMessages = Array.from(messagesById.values()).sort((a, b) => {\n if (a.sentAt && !b.sentAt) return -1\n if (!a.sentAt && b.sentAt) return 1\n const aTime = a.sentAt?.getTime() ?? 0\n const bTime = b.sentAt?.getTime() ?? 0\n if (aTime !== bTime) return aTime - bTime\n\n const aCreatedAt = a.createdAt.getTime()\n const bCreatedAt = b.createdAt.getTime()\n if (aCreatedAt !== bCreatedAt) return aCreatedAt - bCreatedAt\n\n return a.id.localeCompare(b.id)\n })\n\n const selectedIndex = orderedThreadMessages.findIndex((item) => item.id === selectedMessage.id)\n const threadSlice = selectedIndex >= 0 ? orderedThreadMessages.slice(0, selectedIndex + 1) : [selectedMessage]\n\n if (scope.userId) {\n const sliceIds = threadSlice.map((item) => item.id)\n const recipientRows = sliceIds.length > 0\n ? await em.find(MessageRecipient, {\n messageId: { $in: sliceIds },\n recipientUserId: scope.userId,\n deletedAt: null,\n })\n : []\n const recipientMessageIds = new Set(recipientRows.map((row) => row.messageId))\n const visibleSlice = threadSlice.filter(\n (item) => item.senderUserId === scope.userId\n || recipientMessageIds.has(item.id)\n || (options.includePublicThreadMessages === true && item.visibility === 'public'),\n )\n return visibleSlice.map((item) => ({\n id: item.id,\n senderUserId: item.senderUserId,\n subject: item.subject,\n body: item.body,\n sentAt: item.sentAt,\n createdAt: item.createdAt,\n }))\n }\n\n return threadSlice.map((item) => ({\n id: item.id,\n senderUserId: item.senderUserId,\n subject: item.subject,\n body: item.body,\n sentAt: item.sentAt,\n createdAt: item.createdAt,\n }))\n}\n\nexport async function buildForwardPreviewFromThreadSlice(\n em: EntityManager,\n scope: MessageScope,\n selectedMessage: Message,\n threadSlice: ForwardMessageBlock[],\n): Promise<{ subject: string; body: string }> {\n const allMessageIds = threadSlice.map((item) => item.id)\n const allSenderIds = Array.from(new Set(threadSlice.map((item) => item.senderUserId)))\n\n const recipients = allMessageIds.length > 0\n ? await em.find(MessageRecipient, {\n messageId: { $in: allMessageIds },\n deletedAt: null,\n })\n : []\n\n const allRecipientUserIds = Array.from(new Set(recipients.map((item) => item.recipientUserId)))\n const allUserIds = Array.from(new Set([...allSenderIds, ...allRecipientUserIds]))\n const users = allUserIds.length > 0\n ? await findWithDecryption(\n em,\n User,\n { id: { $in: allUserIds } },\n undefined,\n { tenantId: scope.tenantId, organizationId: scope.organizationId },\n )\n : []\n\n const userById = new Map(users.map((user) => [user.id, user]))\n const recipientsByMessageId = new Map<string, MessageRecipient[]>()\n for (const recipient of recipients) {\n const currentRecipients = recipientsByMessageId.get(recipient.messageId) ?? []\n currentRecipients.push(recipient)\n recipientsByMessageId.set(recipient.messageId, currentRecipients)\n }\n\n const body = threadSlice.map((item) => {\n const sender = userById.get(item.senderUserId)\n const toRecipients = recipientsByMessageId.get(item.id) ?? []\n const toLabel = toRecipients.length > 0\n ? toRecipients\n .map((recipient) => formatUserLabel(userById.get(recipient.recipientUserId), recipient.recipientUserId))\n .join(', ')\n : '-'\n\n return [\n FORWARD_MARKER,\n `From: ${formatUserLabel(sender, item.senderUserId)}`,\n `Date: ${formatDateLabel(item.sentAt)}`,\n `Subject: ${normalizeSubject(item.subject)}`,\n `To: ${toLabel}`,\n '',\n item.body ?? '',\n ].join('\\n')\n }).join('\\n\\n')\n\n ensureMaxForwardBodyLength(body)\n\n return {\n subject: formatForwardSubject(selectedMessage.subject),\n body,\n }\n}\n\nexport async function buildForwardPreview(\n em: EntityManager,\n scope: MessageScope,\n selectedMessage: Message,\n options: ForwardThreadSliceOptions = {},\n): Promise<{ subject: string; body: string }> {\n const threadSlice = await buildForwardThreadSlice(em, scope, selectedMessage, options)\n return buildForwardPreviewFromThreadSlice(em, scope, selectedMessage, threadSlice)\n}\n\nexport function buildForwardBodyFromLegacyInput(\n generatedBody: string,\n additionalBody?: string,\n): string {\n const legacyPrefix = additionalBody?.trim()\n const finalBody = legacyPrefix\n ? `${legacyPrefix}\\n\\n${generatedBody}`\n : generatedBody\n ensureMaxForwardBodyLength(finalBody)\n return finalBody\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/core",
3
- "version": "0.10.1-develop.7770.1.970b396ebb",
3
+ "version": "0.10.1-develop.7772.1.9d3d6ce261",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -266,16 +266,16 @@
266
266
  "zod": "^4.4.3"
267
267
  },
268
268
  "peerDependencies": {
269
- "@open-mercato/ai-assistant": "0.10.1-develop.7770.1.970b396ebb",
270
- "@open-mercato/shared": "0.10.1-develop.7770.1.970b396ebb",
271
- "@open-mercato/ui": "0.10.1-develop.7770.1.970b396ebb",
269
+ "@open-mercato/ai-assistant": "0.10.1-develop.7772.1.9d3d6ce261",
270
+ "@open-mercato/shared": "0.10.1-develop.7772.1.9d3d6ce261",
271
+ "@open-mercato/ui": "0.10.1-develop.7772.1.9d3d6ce261",
272
272
  "react": "^19.0.0",
273
273
  "react-dom": "^19.0.0"
274
274
  },
275
275
  "devDependencies": {
276
- "@open-mercato/ai-assistant": "0.10.1-develop.7770.1.970b396ebb",
277
- "@open-mercato/shared": "0.10.1-develop.7770.1.970b396ebb",
278
- "@open-mercato/ui": "0.10.1-develop.7770.1.970b396ebb",
276
+ "@open-mercato/ai-assistant": "0.10.1-develop.7772.1.9d3d6ce261",
277
+ "@open-mercato/shared": "0.10.1-develop.7772.1.9d3d6ce261",
278
+ "@open-mercato/ui": "0.10.1-develop.7772.1.9d3d6ce261",
279
279
  "@testing-library/dom": "^10.4.1",
280
280
  "@testing-library/jest-dom": "^7.0.1",
281
281
  "@testing-library/react": "^16.3.3",
@@ -8,7 +8,6 @@ import { htmlToPlainText } from '@open-mercato/shared/lib/html/htmlToPlainText'
8
8
  import { emitCommunicationChannelsEvent } from '../events'
9
9
  import { resolveContact } from '../lib/contact-resolver'
10
10
  import type { ChannelAdapterRegistry } from '../lib/registry'
11
- import type { NormalizedInboundMessage } from '../lib/adapter'
12
11
  import { matchThread, type ThreadMatch } from '../lib/thread-matcher'
13
12
  import {
14
13
  ChannelThreadMapping,
@@ -213,7 +212,7 @@ const ingestInboundMessageCommand: CommandHandler<IngestInboundMessageInput, Ing
213
212
  subject: m.subject ?? null,
214
213
  tenantId: input.scope.tenantId,
215
214
  organizationId: input.scope.organizationId ?? null,
216
- lastMessageAt: m.timestamp ?? new Date(),
215
+ lastMessageAt: m.timestamp,
217
216
  assignedUserId: channel.userId ?? null,
218
217
  })
219
218
  em.persist(conversation)
@@ -281,7 +280,6 @@ const ingestInboundMessageCommand: CommandHandler<IngestInboundMessageInput, Ing
281
280
  ccAddresses: extractStringArrayFromMeta(metaForMatcher, 'cc'),
282
281
  bodyPlain: m.bodyFormat === 'html' ? null : m.body ?? null,
283
282
  bodyHtml: m.bodyFormat === 'html' ? m.body ?? null : null,
284
- receivedAt: m.timestamp ?? new Date(),
285
283
  },
286
284
  { em },
287
285
  )
@@ -422,8 +420,8 @@ const ingestInboundMessageCommand: CommandHandler<IngestInboundMessageInput, Ing
422
420
  // mail, not when this worker ran. Otherwise a history import lands every
423
421
  // message on the import minute and the inbox (sorted on `sent_at`) shows
424
422
  // a 90-day mailbox as one block in import order. Adapters without a
425
- // timestamp fall through to compose's own `new Date()`.
426
- sentAt: m.timestamp ?? undefined,
423
+ // timestamp retain an explicitly unknown occurrence time.
424
+ sentAt: m.timestamp,
427
425
  // Stable dedup key so a retried ingest (after a transient failure between
428
426
  // compose and the ExternalMessage anchor insert) reuses the message
429
427
  // composed by the first attempt instead of duplicating it. Mirrors the
@@ -490,7 +488,7 @@ const ingestInboundMessageCommand: CommandHandler<IngestInboundMessageInput, Ing
490
488
  direction: 'inbound',
491
489
  senderIdentifier: m.senderIdentifier,
492
490
  senderDisplayName: m.senderDisplayName ?? null,
493
- providerTimestamp: m.timestamp,
491
+ providerTimestamp: m.providerTimestamp ?? null,
494
492
  tenantId: input.scope.tenantId,
495
493
  organizationId: input.scope.organizationId ?? null,
496
494
  })
@@ -588,7 +586,7 @@ const ingestInboundMessageCommand: CommandHandler<IngestInboundMessageInput, Ing
588
586
  // this worker ran. Carried here so no consumer has to read the
589
587
  // ExternalMessage row across the module boundary. ISO string because a
590
588
  // persistent event is serialized onto the queue; null when the adapter
591
- // supplied no timestamp, which leaves the consumer's own fallback.
589
+ // supplied no provider-native timestamp.
592
590
  providerTimestamp:
593
591
  externalMessage.providerTimestamp instanceof Date
594
592
  ? externalMessage.providerTimestamp.toISOString()
@@ -121,7 +121,8 @@ export const normalizedInboundMessageSchema = z.object({
121
121
  body: z.string(),
122
122
  bodyFormat: z.enum(['text', 'markdown', 'html']),
123
123
  attachments: z.array(normalizedAttachmentSchema).optional(),
124
- timestamp: z.date(),
124
+ timestamp: z.date().nullable(),
125
+ providerTimestamp: z.date().nullable().optional(),
125
126
  replyToExternalId: z.string().optional(),
126
127
  channelPayload: z.record(z.string(), z.unknown()),
127
128
  channelContentType: z.string().min(1),
@@ -174,7 +174,10 @@ export interface NormalizedInboundMessage {
174
174
  body: string
175
175
  bodyFormat: 'text' | 'markdown' | 'html'
176
176
  attachments?: NormalizedAttachment[]
177
- timestamp: Date
177
+ /** Best meaningful occurrence time selected by the adapter; null when unknown. */
178
+ timestamp: Date | null
179
+ /** Provider-native transport or storage time when separately available. */
180
+ providerTimestamp?: Date | null
178
181
  replyToExternalId?: string
179
182
  channelPayload: Record<string, unknown>
180
183
  channelContentType: string
@@ -426,13 +426,19 @@ export interface NormalizeMimeInboundOptions {
426
426
  * Fallback timestamp when neither `receivedAt` nor a usable Date header is
427
427
  * available.
428
428
  */
429
- fallbackDate?: Date
429
+ fallbackDate?: Date | null
430
430
  /** Provider-specific fields merged into `channelMetadata`. */
431
431
  channelMetadata?: (parsed: ParsedMail) => Record<string, unknown>
432
432
  /** Provider-specific fields merged into `channelPayload`. */
433
433
  channelPayload?: (parsed: ParsedMail) => Record<string, unknown>
434
434
  }
435
435
 
436
+ function validDate(value: Date | string | null | undefined): Date | null {
437
+ if (value == null) return null
438
+ const date = value instanceof Date ? new Date(value.getTime()) : new Date(value)
439
+ return Number.isNaN(date.getTime()) ? null : date
440
+ }
441
+
436
442
  /**
437
443
  * A MIME `Date` header this far ahead of the clock is treated as bogus. Clock
438
444
  * skew between real mail servers is seconds to minutes; a day of tolerance
@@ -450,21 +456,30 @@ function isValidDate(value: unknown): value is Date {
450
456
  *
451
457
  * Order: the provider's receipt time; then the sender's MIME `Date` header,
452
458
  * unless it is unparsable or more than a day in the future; then the caller's
453
- * fallback; then now. Exported for tests.
459
+ * fallback; unknown remains null.
454
460
  */
455
- export function resolveInboundTimestamp(input: {
461
+ function resolveKnownInboundTimestamp(input: {
456
462
  receivedAt?: Date
457
463
  mimeDate?: Date
458
- fallbackDate?: Date
464
+ fallbackDate?: Date | null
459
465
  now?: Date
460
- }): Date {
466
+ }): Date | null {
461
467
  if (isValidDate(input.receivedAt)) return input.receivedAt
462
468
  const now = input.now ?? new Date()
463
469
  if (isValidDate(input.mimeDate) && input.mimeDate.getTime() <= now.getTime() + MAX_MIME_DATE_FUTURE_SKEW_MS) {
464
470
  return input.mimeDate
465
471
  }
466
472
  if (isValidDate(input.fallbackDate)) return input.fallbackDate
467
- return now
473
+ return null
474
+ }
475
+
476
+ export function resolveInboundTimestamp(input: {
477
+ receivedAt?: Date
478
+ mimeDate?: Date
479
+ fallbackDate?: Date | null
480
+ now?: Date
481
+ }): Date {
482
+ return resolveKnownInboundTimestamp(input) ?? input.now ?? new Date()
468
483
  }
469
484
 
470
485
  /**
@@ -475,6 +490,7 @@ export function resolveInboundTimestamp(input: {
475
490
  */
476
491
  export function normalizeMimeInbound(options: NormalizeMimeInboundOptions): NormalizedInboundMessage {
477
492
  const { parsed } = options
493
+ const providerTimestamp = validDate(options.receivedAt) ?? validDate(options.fallbackDate)
478
494
 
479
495
  const messageId = stripBrackets(parsed.messageId) ?? options.fallbackMessageId
480
496
  const inReplyTo = stripBrackets(parsed.inReplyTo)
@@ -519,11 +535,12 @@ export function normalizeMimeInbound(options: NormalizeMimeInboundOptions): Norm
519
535
  body,
520
536
  bodyFormat,
521
537
  attachments,
522
- timestamp: resolveInboundTimestamp({
538
+ timestamp: resolveKnownInboundTimestamp({
523
539
  receivedAt: options.receivedAt,
524
540
  mimeDate: parsed.date ? new Date(parsed.date) : undefined,
525
541
  fallbackDate: options.fallbackDate,
526
542
  }),
543
+ providerTimestamp,
527
544
  replyToExternalId: inReplyTo ?? undefined,
528
545
  channelPayload,
529
546
  channelContentType: 'email/mime',
@@ -44,7 +44,8 @@ export type ThreadMatchInput = {
44
44
  bodyPlain: string | null
45
45
  bodyHtml: string | null
46
46
 
47
- receivedAt: Date
47
+ /** @deprecated Ignored by the matcher; retained for backward compatibility. */
48
+ receivedAt?: Date
48
49
  }
49
50
 
50
51
  export type ThreadMatchStrategy =
@@ -30,6 +30,7 @@ import { resolveCustomersRequestContext } from '../../lib/interactionRequestCont
30
30
  import { hydrateCanonicalInteractions } from '../../lib/interactionReadModel'
31
31
  import { resolveCanonicalActivityTargetId } from '../../lib/legacyActivityBridge'
32
32
  import { buildEmailVisibilityMikroFilter } from '../../lib/visibilityFilter'
33
+ import { buildInteractionOccurredAtOrderBy } from '../../lib/interactionOrderBy'
33
34
  import { listGrantsForViewer, listSharedChannelIds } from '../../lib/conversationShares'
34
35
  import { createLogger } from '@open-mercato/shared/lib/logger'
35
36
  import { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'
@@ -148,25 +149,25 @@ async function legacyAdaptersDisabledResponse(): Promise<Response> {
148
149
 
149
150
  function buildLegacyOrderBy(sortField: string | undefined, sortDir: 'asc' | 'desc') {
150
151
  if (sortField === 'createdAt') {
151
- return { createdAt: sortDir }
152
+ return { createdAt: sortDir, id: sortDir }
152
153
  }
153
- return { occurredAt: sortDir, createdAt: sortDir } as const
154
+ return buildInteractionOccurredAtOrderBy(sortDir)
154
155
  }
155
156
 
156
157
  function buildCanonicalOrderBy(sortField: string | undefined, sortDir: 'asc' | 'desc') {
157
158
  if (sortField === 'createdAt') {
158
- return { createdAt: sortDir }
159
+ return { createdAt: sortDir, id: sortDir }
159
160
  }
160
- return { occurredAt: sortDir, createdAt: sortDir } as const
161
+ return buildInteractionOccurredAtOrderBy(sortDir)
161
162
  }
162
163
 
163
- function resolveActivitySortValue(item: ActivityItem, sortField: string | undefined): number {
164
+ function resolveActivitySortValue(item: ActivityItem, sortField: string | undefined): number | null {
164
165
  const raw =
165
166
  sortField === 'createdAt'
166
167
  ? item.createdAt
167
- : item.occurredAt ?? item.createdAt
168
+ : item.occurredAt
168
169
  const timestamp = raw ? new Date(raw).getTime() : Number.NaN
169
- return Number.isNaN(timestamp) ? 0 : timestamp
170
+ return Number.isNaN(timestamp) ? null : timestamp
170
171
  }
171
172
 
172
173
  function sortActivityItems(
@@ -178,10 +179,19 @@ function sortActivityItems(
178
179
  const leftValue = resolveActivitySortValue(left, sortField)
179
180
  const rightValue = resolveActivitySortValue(right, sortField)
180
181
  if (leftValue === rightValue) {
182
+ const leftCreatedAt = resolveActivitySortValue(left, 'createdAt')
183
+ const rightCreatedAt = resolveActivitySortValue(right, 'createdAt')
184
+ if (leftCreatedAt !== rightCreatedAt) {
185
+ if (leftCreatedAt === null) return 1
186
+ if (rightCreatedAt === null) return -1
187
+ return sortDir === 'asc' ? leftCreatedAt - rightCreatedAt : rightCreatedAt - leftCreatedAt
188
+ }
181
189
  return sortDir === 'asc'
182
190
  ? left.id.localeCompare(right.id)
183
191
  : right.id.localeCompare(left.id)
184
192
  }
193
+ if (leftValue === null) return 1
194
+ if (rightValue === null) return -1
185
195
  return sortDir === 'asc' ? leftValue - rightValue : rightValue - leftValue
186
196
  })
187
197
  }
@@ -0,0 +1,9 @@
1
+ import { raw } from '@mikro-orm/core'
2
+
3
+ export function buildInteractionOccurredAtOrderBy(sortDir: 'asc' | 'desc') {
4
+ return [
5
+ { [raw('occurred_at')]: `${sortDir} nulls last` },
6
+ { createdAt: sortDir },
7
+ { id: sortDir },
8
+ ]
9
+ }
@@ -95,17 +95,6 @@ export default async function handler(
95
95
  const metaJson = (link.channelMetadata ?? null) as Record<string, unknown> | null
96
96
  const payloadJson = (link.channelPayload ?? null) as Record<string, unknown> | null
97
97
 
98
- // ── (1b) Resolve when the email actually happened ─────────────────────
99
- //
100
- // `link.createdAt` is when the hub ingested the message, which for a history
101
- // import is the import minute, not the day the mail arrived (#6095). The hub
102
- // carries the provider's own timestamp on the event payload, so take it from
103
- // there: reading the communication_channels ExternalMessage row here would
104
- // cross the storage boundary (Cross-Module Coupling in AGENTS.md) and add a
105
- // peer-table query per event. Resolved once and threaded through every branch
106
- // below so the address-match and threading-inheritance paths agree.
107
- const occurredAt = resolveOccurredAt(payload, link)
108
-
109
98
  // ── (2) Resolve the channel to get its owner userId ───────────────────
110
99
  //
111
100
  // The channel.userId is needed for two purposes:
@@ -130,6 +119,8 @@ export default async function handler(
130
119
  channelUserId = channel?.userId ?? null
131
120
  }
132
121
 
122
+ const occurredAt = await resolveEmailOccurredAt(em, link, tenantId, organizationId, payload)
123
+
133
124
  // ── (3) Collect recipient addresses ───────────────────────────────────
134
125
  //
135
126
  // The channel metadata shape differs by provider and direction:
@@ -281,7 +272,7 @@ async function handleThreadingInheritance(
281
272
  channelId: string | null,
282
273
  metaJson: Record<string, unknown> | null,
283
274
  payloadJson: Record<string, unknown> | null,
284
- occurredAt: Date,
275
+ occurredAt: Date | null,
285
276
  ): Promise<void> {
286
277
  // ── Primary: inherit Person(s) from the hub's authoritative thread ──────
287
278
  //
@@ -474,35 +465,33 @@ async function handleThreadingInheritance(
474
465
  )
475
466
  }
476
467
 
477
- // ── Helpers ───────────────────────────────────────────────────────────────
478
-
479
- /**
480
- * When the linked email happened, for `CustomerInteraction.occurredAt` (#6095).
481
- *
482
- * Preference order:
483
- * 1. `payload.providerTimestamp` — the provider's own receive/send time, put
484
- * on the hub event by ingest / delivery;
485
- * 2. `MessageChannelLink.createdAt` — the ingest time, the only date this
486
- * handler knew before #6095, and the compatibility path for events that
487
- * were enqueued before the field existed;
488
- * 3. now, for a link row with no usable `createdAt` (test stubs).
489
- */
490
- function resolveOccurredAt(
491
- payload: LinkChannelMessagePayload,
468
+ async function resolveEmailOccurredAt(
469
+ em: EntityManager,
492
470
  link: Record<string, unknown>,
493
- ): Date {
494
- const ingestedAt = link.createdAt instanceof Date ? link.createdAt : new Date()
471
+ tenantId: string,
472
+ organizationId: string | null,
473
+ payload: LinkChannelMessagePayload,
474
+ ): Promise<Date | null> {
475
+ const message = typeof link.messageId === 'string'
476
+ ? await findOneWithDecryption(
477
+ em,
478
+ 'Message' as never,
479
+ { id: link.messageId, tenantId, organizationId, deletedAt: null } as never,
480
+ undefined,
481
+ { tenantId, organizationId },
482
+ ) as { sentAt?: Date | null } | null
483
+ : null
484
+
485
+ if (message) return message.sentAt ?? null
495
486
  const carried = payload.providerTimestamp
496
- if (carried instanceof Date) {
497
- return Number.isNaN(carried.getTime()) ? ingestedAt : carried
498
- }
499
- if (typeof carried === 'string' && carried) {
500
- const parsed = new Date(carried)
501
- if (!Number.isNaN(parsed.getTime())) return parsed
502
- }
503
- return ingestedAt
487
+ const providerTimestamp = carried instanceof Date
488
+ ? carried
489
+ : typeof carried === 'string' && carried ? new Date(carried) : null
490
+ return providerTimestamp && !Number.isNaN(providerTimestamp.getTime()) ? providerTimestamp : null
504
491
  }
505
492
 
493
+ // ── Helpers ───────────────────────────────────────────────────────────────
494
+
506
495
  interface InteractionData {
507
496
  linkId: string
508
497
  tenantId: string
@@ -511,7 +500,7 @@ interface InteractionData {
511
500
  title: string | null
512
501
  body: string | null
513
502
  authorUserId: string | null
514
- occurredAt: Date
503
+ occurredAt: Date | null
515
504
  visibility: 'private' | 'shared'
516
505
  channelProviderKey: string | null
517
506
  /**