@open-mercato/core 0.10.1-develop.7768.1.90fa052c11 → 0.10.1-develop.7770.1.970b396ebb

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/notifications/lib/notificationService.ts"],
4
- "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { type Kysely, sql } from 'kysely'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { invalidateCrudCache } from '@open-mercato/shared/lib/crud/cache'\nimport { Notification, type NotificationStatus } from '../data/entities'\nimport type { CreateNotificationInput, CreateBatchNotificationInput, CreateRoleNotificationInput, CreateFeatureNotificationInput, ExecuteActionInput } from '../data/validators'\nimport {\n NOTIFICATION_ACTION_ALREADY_EXECUTED_ERROR_CODE,\n NOTIFICATION_ACTION_FORBIDDEN_ERROR_CODE,\n type NotificationAction,\n type NotificationPollData,\n type NotificationTypeAction,\n} from '@open-mercato/shared/modules/notifications/types'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'\nimport { NOTIFICATION_EVENTS, NOTIFICATION_SSE_EVENTS } from './events'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n buildNotificationEntity,\n emitNotificationCreated,\n emitNotificationCreatedBatch,\n type NotificationContentInput,\n type NotificationTenantContext,\n} from './notificationFactory'\nimport { toNotificationDto } from './notificationMapper'\nimport { buildNotificationReadScopeWhere } from './notificationScope'\nimport {\n getRecipientUserIdsForFeature,\n getRecipientUserIdsForRole,\n getScopedNotificationRecipientUserIds,\n} from './notificationRecipients'\nimport { assertSafeNotificationHref, sanitizeNotificationActions } from './safeHref'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { getNotificationType } from './notification-type-registry'\nimport { getNotificationTypeOverrides, type NotificationTypeOverrides } from './typeOverrides'\nimport { getNotificationDeliveryStrategies } from './deliveryStrategies'\nimport { resolveEffectiveChannels } from './shouldDeliver'\nimport {\n createNotificationPreferenceService,\n type NotificationPreferenceService,\n} from './notificationPreferenceService'\nimport { inAppVisibleFilter, inAppVisibleSql, isInAppVisible } from './notificationVisibility'\n\nconst logger = createLogger('notifications').child({ component: 'service' })\n\nfunction debug(message: string, ...details: unknown[]): void {\n logger.debug(message, details.length ? { details } : undefined)\n}\n\nfunction getDb(em: EntityManager): Kysely<any> {\n return em.getKysely<any>()\n}\n\nconst UNIQUE_NOTIFICATION_ACTIVE_STATUSES: NotificationStatus[] = ['unread', 'read', 'actioned']\nconst NOTIFICATION_RESOURCE_KIND = 'notifications.notification'\n\nfunction normalizeOrgScope(organizationId: string | null | undefined): string | null {\n return organizationId ?? null\n}\n\nasync function invalidateNotificationCache(\n container: NotificationServiceDeps['container'],\n ctx: Pick<NotificationServiceContext, 'tenantId' | 'organizationId'>,\n reason: string,\n notificationId?: string,\n): Promise<void> {\n if (!container) return\n await invalidateCrudCache(\n container as Parameters<typeof invalidateCrudCache>[0],\n NOTIFICATION_RESOURCE_KIND,\n {\n id: notificationId,\n tenantId: ctx.tenantId,\n organizationId: normalizeOrgScope(ctx.organizationId),\n },\n ctx.tenantId,\n reason,\n )\n}\n\nasync function assertNotificationRecipientsInScope(\n em: EntityManager,\n recipientUserIds: string[],\n ctx: NotificationServiceContext,\n): Promise<void> {\n const scopedRecipientUserIds = await getScopedNotificationRecipientUserIds(\n getDb(em),\n ctx.tenantId,\n normalizeOrgScope(ctx.organizationId),\n recipientUserIds,\n )\n\n if (scopedRecipientUserIds.length !== recipientUserIds.length) {\n throw new CrudHttpError(404, { error: 'Notification recipient not found' })\n }\n}\n\nfunction actionAlreadyExecuted(): CrudHttpError {\n return new CrudHttpError(409, {\n error: 'Notification action already executed',\n code: NOTIFICATION_ACTION_ALREADY_EXECUTED_ERROR_CODE,\n })\n}\n\nasync function actionForbidden(): Promise<CrudHttpError> {\n const { translate } = await resolveTranslations()\n return new CrudHttpError(403, {\n error: translate('notifications.error.actionForbidden', 'You are not allowed to run this notification action'),\n code: NOTIFICATION_ACTION_FORBIDDEN_ERROR_CODE,\n })\n}\n\n/**\n * The command action the notification's registered type declares for a stored action: same action\n * id (across `actions` and `optionalActions`) and same `commandId`. Anything else is not dispatched.\n */\nfunction findDeclaredCommandAction(type: string, action: NotificationAction): NotificationTypeAction | null {\n const definition = getNotificationType(type)\n if (!definition) return null\n const declared = [...definition.actions, ...(definition.optionalActions ?? [])]\n .find((candidate) => candidate.id === action.id)\n if (!declared?.commandId || declared.commandId !== action.commandId) return null\n return declared\n}\n\nfunction applyNotificationContent(\n notification: Notification,\n input: NotificationContentInput,\n recipientUserId: string,\n ctx: NotificationTenantContext,\n channels: string[] | null,\n) {\n const actions = sanitizeNotificationActions(input.actions)\n const linkHref = assertSafeNotificationHref(input.linkHref)\n\n notification.recipientUserId = recipientUserId\n notification.type = input.type\n notification.titleKey = input.titleKey\n notification.bodyKey = input.bodyKey\n notification.titleVariables = input.titleVariables\n notification.bodyVariables = input.bodyVariables\n notification.title = input.title || input.titleKey || ''\n notification.body = input.body\n notification.icon = input.icon\n notification.severity = input.severity ?? 'info'\n notification.actionData = actions\n ? {\n actions,\n primaryActionId: input.primaryActionId,\n }\n : null\n notification.sourceModule = input.sourceModule\n notification.sourceEntityType = input.sourceEntityType\n notification.sourceEntityId = input.sourceEntityId\n notification.linkHref = linkHref\n notification.groupKey = input.groupKey\n notification.channels = channels\n notification.expiresAt = input.expiresAt ? new Date(input.expiresAt) : null\n notification.tenantId = ctx.tenantId\n notification.organizationId = normalizeOrgScope(ctx.organizationId)\n notification.status = 'unread'\n notification.readAt = null\n notification.actionedAt = null\n notification.dismissedAt = null\n notification.actionTaken = null\n notification.actionResult = null\n notification.createdAt = new Date()\n}\n\nasync function findScopedNotificationOrThrow(\n em: EntityManager,\n notificationId: string,\n ctx: NotificationServiceContext,\n): Promise<Notification> {\n const notification = await findOneWithDecryption(\n em,\n Notification,\n {\n id: notificationId,\n recipientUserId: ctx.userId,\n tenantId: ctx.tenantId,\n },\n undefined,\n {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n },\n )\n if (!notification) {\n throw new CrudHttpError(404, { error: 'Notification not found' })\n }\n return notification\n}\n\nasync function emitNotificationSseEvents(\n eventBus: { emit: (event: string, payload: unknown) => Promise<void> },\n notifications: Notification[],\n ctx: NotificationServiceContext,\n): Promise<void> {\n // Live bell updates only for notifications actually delivered to the in-app channel; the others\n // exist as records but must not bump anyone's badge/inbox.\n const visible = notifications.filter((notification) => isInAppVisible(notification.channels))\n if (visible.length === 0) return\n\n const visibleRecipientUserIds = Array.from(new Set(visible.map((n) => n.recipientUserId)))\n await eventBus.emit(NOTIFICATION_SSE_EVENTS.BATCH_CREATED, {\n tenantId: ctx.tenantId,\n organizationId: normalizeOrgScope(ctx.organizationId),\n recipientUserIds: visibleRecipientUserIds,\n count: visible.length,\n })\n\n for (const notification of visible) {\n await eventBus.emit(NOTIFICATION_SSE_EVENTS.CREATED, {\n tenantId: notification.tenantId,\n organizationId: notification.organizationId ?? null,\n recipientUserId: notification.recipientUserId,\n notification: toNotificationDto(notification),\n })\n }\n}\n\nasync function createOrRefreshNotification(\n em: EntityManager,\n input: NotificationContentInput,\n recipientUserId: string,\n ctx: NotificationTenantContext,\n channels: string[] | null,\n): Promise<Notification> {\n if (input.groupKey && input.groupKey.trim().length > 0) {\n const orgScope = normalizeOrgScope(ctx.organizationId) ?? 'global'\n const lockKey = `notifications:${ctx.tenantId}:${orgScope}:${recipientUserId}:${input.type}:${input.groupKey}`\n try {\n const db = getDb(em)\n await sql`select pg_advisory_xact_lock(hashtext(${lockKey}))`.execute(db)\n } catch {\n // If advisory locks are unavailable, continue with best-effort dedupe.\n }\n\n const existing = await em.findOne(Notification, {\n recipientUserId,\n tenantId: ctx.tenantId,\n organizationId: normalizeOrgScope(ctx.organizationId),\n type: input.type,\n groupKey: input.groupKey,\n status: { $in: UNIQUE_NOTIFICATION_ACTIVE_STATUSES },\n }, {\n orderBy: { createdAt: 'desc' },\n })\n\n if (existing) {\n applyNotificationContent(existing, input, recipientUserId, ctx, channels)\n return existing\n }\n }\n\n return buildNotificationEntity(em, input, recipientUserId, ctx, channels)\n}\n\nexport interface NotificationServiceContext {\n tenantId: string\n organizationId?: string | null\n organizationIds?: string[] | null\n userId?: string | null\n}\n\nexport type CreateFeatureNotificationServiceInput = CreateFeatureNotificationInput & {\n restrictRecipientsToOrganization?: boolean\n}\n\nexport interface NotificationService {\n create(input: CreateNotificationInput, ctx: NotificationServiceContext): Promise<Notification>\n createBatch(input: CreateBatchNotificationInput, ctx: NotificationServiceContext): Promise<Notification[]>\n createForRole(input: CreateRoleNotificationInput, ctx: NotificationServiceContext): Promise<Notification[]>\n createForFeature(input: CreateFeatureNotificationServiceInput, ctx: NotificationServiceContext): Promise<Notification[]>\n markAsRead(notificationId: string, ctx: NotificationServiceContext): Promise<Notification>\n markAllAsRead(ctx: NotificationServiceContext): Promise<number>\n dismiss(notificationId: string, ctx: NotificationServiceContext): Promise<Notification>\n restoreDismissed(\n notificationId: string,\n status: 'read' | 'unread' | undefined,\n ctx: NotificationServiceContext\n ): Promise<Notification>\n executeAction(\n notificationId: string,\n input: ExecuteActionInput,\n ctx: NotificationServiceContext\n ): Promise<{ notification: Notification; result: unknown }>\n getUnreadCount(ctx: NotificationServiceContext): Promise<number>\n getPollData(ctx: NotificationServiceContext, since?: string): Promise<NotificationPollData>\n cleanupExpired(): Promise<number>\n deleteBySource(\n sourceEntityType: string,\n sourceEntityId: string,\n ctx: NotificationServiceContext\n ): Promise<number>\n}\n\nexport interface NotificationServiceDeps {\n em: EntityManager\n eventBus: { emit: (event: string, payload: unknown) => Promise<void> }\n commandBus?: {\n execute: (\n commandId: string,\n options: { input: unknown; ctx: unknown; metadata?: unknown }\n ) => Promise<{ result: unknown }>\n }\n container?: { resolve: (name: string) => unknown }\n}\n\nexport function createNotificationService(deps: NotificationServiceDeps): NotificationService {\n const { em: rootEm, eventBus, commandBus, container } = deps\n\n /**\n * Whether the clicking user holds every feature a declared notification action requires.\n * Fails closed: no user, no container, or a failing RBAC check denies the action.\n */\n const userHoldsActionFeatures = async (\n requiredFeatures: string[] | undefined,\n scopeCtx: NotificationServiceContext,\n ): Promise<boolean> => {\n if (!requiredFeatures || requiredFeatures.length === 0) return true\n if (!scopeCtx.userId || !container) return false\n try {\n const rbacService = container.resolve('rbacService') as {\n userHasAllFeatures: (\n userId: string,\n features: string[],\n scope: { tenantId: string | null; organizationId: string | null },\n ) => Promise<boolean>\n }\n const allowed = await rbacService.userHasAllFeatures(\n scopeCtx.userId,\n requiredFeatures,\n { tenantId: scopeCtx.tenantId, organizationId: scopeCtx.organizationId ?? null },\n )\n return allowed === true\n } catch (err) {\n getTelemetryRuntime()?.reportError(err, {\n module: 'notifications',\n code: 'notifications.action_feature_check_failed',\n })\n return false\n }\n }\n\n /**\n * Resolves the authoritative delivery-channel set for one recipient at create time \u2014 the single\n * gate that folds per-send target, per-type eligibility, registered strategies, and the\n * recipient's per-channel preferences (`nonOptOut` bypasses opt-out). Stored on the row and\n * replayed by the dispatcher; `in_app` membership also drives bell/inbox visibility.\n *\n * Returns `null` (\u21D2 \"all channels\", legacy behavior) when no delivery strategies are registered\n * (e.g. a minimal bootstrap/test), so notifications never become silently undeliverable/invisible\n * in an environment that simply hasn't wired the seam.\n */\n const resolveChannelsFor = async (\n content: NotificationContentInput,\n recipientUserId: string,\n scopeCtx: NotificationServiceContext,\n preferences: NotificationPreferenceService = createNotificationPreferenceService({ em: rootEm.fork() }),\n typeOverrides?: NotificationTypeOverrides | null,\n ): Promise<string[] | null> => {\n const registeredChannels = getNotificationDeliveryStrategies().map((strategy) => strategy.id)\n if (registeredChannels.length === 0) return null\n // Treat an empty target as \"no restriction\" (all channels) rather than \"no deliverable channel\":\n // a programmatic caller that computed an empty array should not silently black-hole the\n // notification. The HTTP layer rejects an empty `channels` outright (see validators.ts).\n const targetChannels = content.channels && content.channels.length > 0 ? content.channels : null\n const overrides = typeOverrides === undefined\n ? (await getNotificationTypeOverrides(rootEm.fork(), scopeCtx.tenantId, [content.type])).get(content.type) ?? null\n : typeOverrides\n return resolveEffectiveChannels({\n typeId: content.type,\n type: getNotificationType(content.type),\n scope: { tenantId: scopeCtx.tenantId, userId: recipientUserId },\n targetChannels,\n registeredChannels,\n preferences,\n channelsOverride: overrides?.channels ?? null,\n nonOptOutOverride: overrides?.nonOptOut ?? null,\n })\n }\n\n /**\n * Broadcast counterpart of {@link resolveChannelsFor}: resolves the channel set for every recipient\n * up front, BEFORE the write transaction opens, reusing a single forked EM / preference service\n * across the whole set. This keeps the preference reads out of the write transaction (matching\n * `create`, which resolves before its transaction) and avoids one EM fork per recipient on large\n * role/feature broadcasts.\n */\n const resolveChannelsForRecipients = async (\n content: NotificationContentInput,\n recipientUserIds: string[],\n scopeCtx: NotificationServiceContext,\n ): Promise<Array<{ recipientUserId: string; channels: string[] | null }>> => {\n const preferences = createNotificationPreferenceService({ em: rootEm.fork() })\n // Stored overrides are per-type (not per-recipient) \u2014 read once for the whole broadcast.\n const typeOverrides =\n (await getNotificationTypeOverrides(rootEm.fork(), scopeCtx.tenantId, [content.type])).get(content.type) ?? null\n const resolved: Array<{ recipientUserId: string; channels: string[] | null }> = []\n for (const recipientUserId of recipientUserIds) {\n resolved.push({\n recipientUserId,\n channels: await resolveChannelsFor(content, recipientUserId, scopeCtx, preferences, typeOverrides),\n })\n }\n return resolved\n }\n\n return {\n async create(input, ctx) {\n const { recipientUserId, ...content } = input\n const channels = await resolveChannelsFor(content, recipientUserId, ctx)\n const writeEm = rootEm.fork()\n const notification = await writeEm.transactional(async (tx) => {\n await assertNotificationRecipientsInScope(tx, [recipientUserId], ctx)\n const entity = await createOrRefreshNotification(tx, content, recipientUserId, ctx, channels)\n await tx.flush()\n return entity\n })\n\n await invalidateNotificationCache(container, ctx, 'created')\n // Always emit the domain event (drives the deliver subscriber \u2192 push/email/\u2026). Only bump the\n // live in-app bell when this notification is actually visible in-app.\n await emitNotificationCreated(eventBus, notification, ctx)\n if (isInAppVisible(notification.channels)) {\n await eventBus.emit(NOTIFICATION_SSE_EVENTS.CREATED, {\n tenantId: notification.tenantId,\n organizationId: notification.organizationId ?? null,\n recipientUserId: notification.recipientUserId,\n notification: toNotificationDto(notification),\n })\n }\n\n return notification\n },\n\n async createBatch(input, ctx) {\n const recipientUserIds = Array.from(new Set(input.recipientUserIds))\n const { recipientUserIds: _recipientUserIds, ...content } = input\n const notifications: Notification[] = []\n const resolved = await resolveChannelsForRecipients(content, recipientUserIds, ctx)\n const writeEm = rootEm.fork()\n\n await writeEm.transactional(async (tx) => {\n await assertNotificationRecipientsInScope(tx, recipientUserIds, ctx)\n for (const { recipientUserId, channels } of resolved) {\n const notification = await createOrRefreshNotification(tx, content, recipientUserId, ctx, channels)\n notifications.push(notification)\n }\n await tx.flush()\n })\n\n await invalidateNotificationCache(container, ctx, 'created')\n await emitNotificationCreatedBatch(eventBus, notifications, ctx)\n await emitNotificationSseEvents(eventBus, notifications, ctx)\n\n return notifications\n },\n\n async createForRole(input, ctx) {\n const em = rootEm.fork()\n\n const db = getDb(em)\n const recipientUserIds = await getRecipientUserIdsForRole(db, ctx.tenantId, input.roleId)\n if (recipientUserIds.length === 0) {\n return []\n }\n\n const { roleId: _roleId, ...content } = input\n const notifications: Notification[] = []\n const uniqueRecipientUserIds = Array.from(new Set(recipientUserIds))\n const resolved = await resolveChannelsForRecipients(content, uniqueRecipientUserIds, ctx)\n const writeEm = rootEm.fork()\n\n await writeEm.transactional(async (tx) => {\n for (const { recipientUserId, channels } of resolved) {\n const notification = await createOrRefreshNotification(tx, content, recipientUserId, ctx, channels)\n notifications.push(notification)\n }\n await tx.flush()\n })\n\n await invalidateNotificationCache(container, ctx, 'created')\n await emitNotificationCreatedBatch(eventBus, notifications, ctx)\n await emitNotificationSseEvents(eventBus, notifications, ctx)\n\n return notifications\n },\n\n async createForFeature(input, ctx) {\n const em = rootEm.fork()\n const db = getDb(em)\n const recipientUserIds = await getRecipientUserIdsForFeature(db, ctx.tenantId, input.requiredFeature)\n\n if (recipientUserIds.length === 0) {\n debug('No users found with feature:', input.requiredFeature, 'in tenant:', ctx.tenantId)\n return []\n }\n\n let authorizedRecipientUserIds = recipientUserIds\n if (input.restrictRecipientsToOrganization) {\n const uniqueCandidateUserIds = Array.from(new Set(recipientUserIds))\n if (!ctx.organizationId) {\n logger.warn('Organization-restricted feature fan-out skipped because organization scope is missing', {\n tenantId: ctx.tenantId,\n requiredFeature: input.requiredFeature,\n })\n return []\n }\n\n if (uniqueCandidateUserIds.length > 200) {\n logger.warn('Organization-restricted feature fan-out skipped because the candidate cap was exceeded', {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n requiredFeature: input.requiredFeature,\n candidateCount: uniqueCandidateUserIds.length,\n candidateCap: 200,\n })\n return []\n }\n\n if (!container) {\n logger.warn('Organization-restricted feature fan-out skipped because the DI container is unavailable', {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n requiredFeature: input.requiredFeature,\n })\n return []\n }\n\n try {\n const rbacService = container.resolve('rbacService') as {\n userHasAllFeatures: (\n userId: string,\n features: string[],\n scope: { tenantId: string | null; organizationId: string | null },\n ) => Promise<boolean>\n }\n const allowedRecipientUserIds: string[] = []\n for (let offset = 0; offset < uniqueCandidateUserIds.length; offset += 10) {\n const candidates = uniqueCandidateUserIds.slice(offset, offset + 10)\n const results = await Promise.all(candidates.map(async (recipientUserId) => ({\n recipientUserId,\n allowed: await rbacService.userHasAllFeatures(\n recipientUserId,\n [input.requiredFeature],\n { tenantId: ctx.tenantId, organizationId: ctx.organizationId ?? null },\n ),\n })))\n for (const result of results) {\n if (result.allowed) allowedRecipientUserIds.push(result.recipientUserId)\n }\n }\n authorizedRecipientUserIds = allowedRecipientUserIds\n } catch (err) {\n logger.warn('Organization-restricted feature fan-out skipped because RBAC filtering failed', {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n requiredFeature: input.requiredFeature,\n err,\n })\n return []\n }\n\n if (authorizedRecipientUserIds.length === 0) {\n debug('No users found with feature in organization:', input.requiredFeature, ctx.organizationId)\n return []\n }\n }\n\n debug('Creating notifications for', authorizedRecipientUserIds.length, 'user(s) with feature:', input.requiredFeature)\n\n const {\n requiredFeature: _requiredFeature,\n restrictRecipientsToOrganization: _restrictRecipientsToOrganization,\n ...content\n } = input\n const notifications: Notification[] = []\n const uniqueRecipientUserIds = Array.from(new Set(authorizedRecipientUserIds))\n const resolved = await resolveChannelsForRecipients(content, uniqueRecipientUserIds, ctx)\n const writeEm = rootEm.fork()\n\n await writeEm.transactional(async (tx) => {\n for (const { recipientUserId, channels } of resolved) {\n const notification = await createOrRefreshNotification(tx, content, recipientUserId, ctx, channels)\n notifications.push(notification)\n }\n await tx.flush()\n })\n\n await invalidateNotificationCache(container, ctx, 'created')\n await emitNotificationCreatedBatch(eventBus, notifications, ctx)\n await emitNotificationSseEvents(eventBus, notifications, ctx)\n\n return notifications\n },\n\n async markAsRead(notificationId, ctx) {\n const em = rootEm.fork()\n const notification = await findScopedNotificationOrThrow(em, notificationId, ctx)\n\n if (notification.status === 'unread') {\n notification.status = 'read'\n notification.readAt = new Date()\n await em.flush()\n\n await invalidateNotificationCache(container, ctx, 'updated', notification.id)\n await eventBus.emit(NOTIFICATION_EVENTS.READ, {\n notificationId: notification.id,\n userId: ctx.userId,\n tenantId: ctx.tenantId,\n })\n }\n\n return notification\n },\n\n async markAllAsRead(ctx) {\n const em = rootEm.fork()\n const db = getDb(em)\n const applyScope = <QB extends { where: (...args: any[]) => QB }>(q: QB): QB => {\n let chain = q\n .where('recipient_user_id' as any, '=', ctx.userId as any)\n .where('tenant_id' as any, '=', ctx.tenantId)\n .where('status' as any, '=', 'unread')\n // Only in-app-visible rows count toward the badge (see getUnreadCount),\n // so \"mark all as read\" must scope to the SAME set \u2014 otherwise it flips\n // push/email-only rows the user never saw and SSE-broadcasts a read for\n // notifications that were never in the bell, inflating the returned\n // count past what the badge showed.\n .where(inAppVisibleSql() as any)\n if (ctx.organizationId) {\n chain = chain.where('organization_id' as any, '=', ctx.organizationId)\n }\n return chain\n }\n\n const targetRows = await applyScope(\n db\n .selectFrom('notifications' as any)\n .select([\n 'id' as any,\n 'organization_id' as any,\n 'recipient_user_id' as any,\n ]),\n ).execute() as Array<{ id: string }>\n\n if (!targetRows.length) {\n return 0\n }\n\n const updateResult = await applyScope(\n db.updateTable('notifications' as any).set({\n status: 'read',\n read_at: sql`now()`,\n } as any) as any,\n ).executeTakeFirst() as { numUpdatedRows?: bigint | number } | undefined\n const result = Number(updateResult?.numUpdatedRows ?? targetRows.length)\n\n await invalidateNotificationCache(container, ctx, 'updated')\n\n const notifications = await findWithDecryption(em, Notification, {\n id: { $in: targetRows.map((row) => row.id) },\n }, undefined, {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n })\n\n for (const notification of notifications) {\n await eventBus.emit(NOTIFICATION_EVENTS.READ, {\n notificationId: notification.id,\n userId: ctx.userId,\n tenantId: ctx.tenantId,\n })\n\n await eventBus.emit(NOTIFICATION_SSE_EVENTS.CREATED, {\n tenantId: notification.tenantId,\n organizationId: notification.organizationId ?? null,\n recipientUserId: notification.recipientUserId,\n notification: toNotificationDto(notification),\n })\n }\n\n return result\n },\n\n async dismiss(notificationId, ctx) {\n const em = rootEm.fork()\n const notification = await findScopedNotificationOrThrow(em, notificationId, ctx)\n\n notification.status = 'dismissed'\n notification.dismissedAt = new Date()\n await em.flush()\n\n await invalidateNotificationCache(container, ctx, 'updated', notification.id)\n await eventBus.emit(NOTIFICATION_EVENTS.DISMISSED, {\n notificationId: notification.id,\n userId: ctx.userId,\n tenantId: ctx.tenantId,\n })\n\n return notification\n },\n\n async restoreDismissed(notificationId, status, ctx) {\n const em = rootEm.fork()\n const notification = await findScopedNotificationOrThrow(em, notificationId, ctx)\n\n if (notification.status !== 'dismissed') {\n return notification\n }\n\n // A dismissed notification whose action already ran comes back as `actioned`,\n // whatever the caller asked for: `actioned_at` outlives the dismissal.\n const targetStatus: NotificationStatus = notification.actionedAt != null ? 'actioned' : status ?? 'read'\n notification.status = targetStatus\n notification.dismissedAt = null\n\n if (targetStatus === 'unread') {\n notification.readAt = null\n } else if (!notification.readAt) {\n notification.readAt = new Date()\n }\n\n await em.flush()\n\n await invalidateNotificationCache(container, ctx, 'updated', notification.id)\n await eventBus.emit(NOTIFICATION_EVENTS.RESTORED, {\n notificationId: notification.id,\n userId: ctx.userId,\n tenantId: ctx.tenantId,\n status: targetStatus,\n })\n\n return notification\n },\n\n async executeAction(notificationId, input, ctx) {\n const em = rootEm.fork()\n const notification = await findScopedNotificationOrThrow(em, notificationId, ctx)\n\n const actionData = notification.actionData\n const action = actionData?.actions?.find((a) => a.id === input.actionId)\n\n if (!action) {\n throw new Error('Action not found')\n }\n\n // Reject an already-actioned notification before dispatching the command,\n // so a retry or double-click cannot re-run the side effect. `actioned_at` is\n // the durable marker: `status` is overwritten by a dismissal.\n if (notification.status === 'actioned' || notification.actionedAt != null) {\n throw actionAlreadyExecuted()\n }\n\n if (action.commandId) {\n const declared = findDeclaredCommandAction(notification.type, action)\n if (!declared || !(await userHoldsActionFeatures(declared.requiredFeatures, ctx))) {\n throw await actionForbidden()\n }\n }\n\n const actionedAt = new Date()\n const previousStatus = notification.status\n const previousActionedAt = notification.actionedAt ?? null\n const previousActionTaken = notification.actionTaken ?? null\n\n // Atomically claim the notification so only one concurrent request can run\n // the side-effecting command. The conditional UPDATE matches only while the\n // notification has not been actioned yet; a losing request updates 0 rows.\n const claimResult = (await getDb(em)\n .updateTable('notifications' as any)\n .set({\n status: 'actioned',\n actioned_at: actionedAt,\n action_taken: input.actionId,\n } as any)\n .where('id' as any, '=', notification.id)\n .where('recipient_user_id' as any, '=', ctx.userId as any)\n .where('tenant_id' as any, '=', ctx.tenantId)\n .where('status' as any, '!=', 'actioned')\n .where('actioned_at' as any, 'is', null)\n .executeTakeFirst()) as { numUpdatedRows?: bigint | number } | undefined\n\n if (Number(claimResult?.numUpdatedRows ?? 0) === 0) {\n throw actionAlreadyExecuted()\n }\n\n // The claim is provisional: if the side-effecting command fails, the action\n // never actually completed, so release the claim to its prior state. This\n // lets the user retry the action instead of the notification being locked as\n // `actioned` forever. The claim is owned through its own `actioned_at` and\n // `action_taken`, not through `status`: a dismissal that lands while the\n // command runs overwrites `status`, and the release must still clear the\n // claim then \u2014 while leaving that dismissal in place.\n const releaseClaim = async () => {\n await getDb(em)\n .updateTable('notifications' as any)\n .set({\n status: sql`case when status = 'actioned' then ${previousStatus} else status end`,\n actioned_at: previousActionedAt,\n action_taken: previousActionTaken,\n } as any)\n .where('id' as any, '=', notification.id)\n .where('recipient_user_id' as any, '=', ctx.userId as any)\n .where('tenant_id' as any, '=', ctx.tenantId)\n .where('actioned_at' as any, '=', actionedAt)\n .where('action_taken' as any, '=', input.actionId)\n .executeTakeFirst()\n }\n\n let result: unknown = null\n\n if (action.commandId && commandBus && container) {\n const commandInput = {\n ...input.payload,\n id: notification.sourceEntityId,\n }\n\n // Build a CommandRuntimeContext from the notification service context\n const commandCtx = {\n container,\n auth: {\n sub: ctx.userId,\n tenantId: ctx.tenantId,\n orgId: ctx.organizationId,\n },\n organizationScope: null,\n selectedOrganizationId: ctx.organizationId ?? null,\n organizationIds: ctx.organizationId ? [ctx.organizationId] : null,\n }\n\n let commandResult: { result: unknown }\n try {\n commandResult = await commandBus.execute(action.commandId, {\n input: commandInput,\n ctx: commandCtx,\n metadata: {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n resourceKind: 'notifications',\n },\n })\n } catch (err) {\n // Never let a rollback failure mask the original command error \u2014 the\n // caller needs the real failure to decide whether to retry.\n try {\n await releaseClaim()\n } catch (releaseErr) {\n debug('failed to release notification action claim', releaseErr)\n }\n throw err\n }\n\n result = commandResult.result\n }\n\n notification.status = 'actioned'\n notification.actionedAt = actionedAt\n notification.actionTaken = input.actionId\n notification.actionResult = result as Record<string, unknown>\n\n if (!notification.readAt) {\n notification.readAt = new Date()\n }\n\n await em.flush()\n\n await invalidateNotificationCache(container, ctx, 'updated', notification.id)\n await eventBus.emit(NOTIFICATION_EVENTS.ACTIONED, {\n notificationId: notification.id,\n actionId: input.actionId,\n userId: ctx.userId,\n tenantId: ctx.tenantId,\n })\n\n return { notification, result }\n },\n\n async getUnreadCount(ctx) {\n const em = rootEm.fork()\n return em.count(Notification, {\n recipientUserId: ctx.userId,\n tenantId: ctx.tenantId,\n status: 'unread',\n $and: [\n buildNotificationReadScopeWhere(ctx),\n inAppVisibleFilter(),\n ],\n })\n },\n\n async getPollData(ctx, since) {\n const em = rootEm.fork()\n const filters: Record<string, unknown> = {\n recipientUserId: ctx.userId,\n tenantId: ctx.tenantId,\n $and: [\n buildNotificationReadScopeWhere(ctx),\n inAppVisibleFilter(),\n ],\n }\n\n if (since) {\n filters.createdAt = { $gt: new Date(since) }\n }\n\n const [notifications, unreadCount] = await Promise.all([\n em.find(Notification, filters, {\n orderBy: { createdAt: 'desc' },\n limit: 50,\n }),\n em.count(Notification, {\n recipientUserId: ctx.userId,\n tenantId: ctx.tenantId,\n status: 'unread',\n $and: [\n buildNotificationReadScopeWhere(ctx),\n inAppVisibleFilter(),\n ],\n }),\n ])\n\n const recent = notifications.map(toNotificationDto)\n const hasNew = since ? recent.length > 0 : false\n\n return {\n unreadCount,\n recent,\n hasNew,\n lastId: recent[0]?.id,\n }\n },\n\n async cleanupExpired() {\n const em = rootEm.fork()\n const db = getDb(em)\n\n const affectedScopes = await db\n .selectFrom('notifications' as any)\n .select([\n 'tenant_id' as any,\n 'organization_id' as any,\n ])\n .where('expires_at' as any, '<', sql`now()`)\n .where('status' as any, 'not in', ['actioned', 'dismissed'])\n .distinct()\n .execute() as Array<{ tenant_id: string; organization_id: string | null }>\n\n if (!affectedScopes.length) return 0\n\n const updateResult = await db\n .updateTable('notifications' as any)\n .set({\n status: 'dismissed',\n dismissed_at: sql`now()`,\n } as any)\n .where('expires_at' as any, '<', sql`now()`)\n .where('status' as any, 'not in', ['actioned', 'dismissed'])\n .executeTakeFirst() as { numUpdatedRows?: bigint | number } | undefined\n\n for (const scope of affectedScopes) {\n await invalidateNotificationCache(container, {\n tenantId: scope.tenant_id,\n organizationId: scope.organization_id,\n }, 'updated')\n }\n\n return Number(updateResult?.numUpdatedRows ?? 0)\n },\n\n async deleteBySource(sourceEntityType, sourceEntityId, ctx) {\n const em = rootEm.fork()\n const db = getDb(em)\n\n const deleteResult = await db\n .deleteFrom('notifications' as any)\n .where('source_entity_type' as any, '=', sourceEntityType)\n .where('source_entity_id' as any, '=', sourceEntityId)\n .where('tenant_id' as any, '=', ctx.tenantId)\n .executeTakeFirst() as { numDeletedRows?: bigint | number } | undefined\n\n const deletedCount = Number(deleteResult?.numDeletedRows ?? 0)\n if (deletedCount > 0) {\n await invalidateNotificationCache(container, ctx, 'deleted')\n }\n\n return deletedCount\n },\n }\n}\n\n/**\n * Helper to create notification service from a DI container.\n * Use this in API routes and commands to avoid DI resolution issues.\n */\nexport function resolveNotificationService(container: {\n resolve: (name: string) => unknown\n}): NotificationService {\n const em = container.resolve('em') as EntityManager\n const eventBus = container.resolve('eventBus') as { emit: (event: string, payload: unknown) => Promise<void> }\n\n // commandBus may not be registered in all contexts, so resolve it safely\n let commandBus: NotificationServiceDeps['commandBus']\n try {\n commandBus = container.resolve('commandBus') as typeof commandBus\n } catch {\n // commandBus not available - actions with commandId won't work\n commandBus = undefined\n }\n\n return createNotificationService({ em, eventBus, commandBus, container })\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { type Kysely, sql } from 'kysely'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { invalidateCrudCache } from '@open-mercato/shared/lib/crud/cache'\nimport { Notification, type NotificationStatus } from '../data/entities'\nimport type { CreateNotificationInput, CreateBatchNotificationInput, CreateRoleNotificationInput, CreateFeatureNotificationInput, ExecuteActionInput } from '../data/validators'\nimport {\n NOTIFICATION_ACTION_ALREADY_EXECUTED_ERROR_CODE,\n NOTIFICATION_ACTION_FORBIDDEN_ERROR_CODE,\n type NotificationAction,\n type NotificationPollData,\n type NotificationTypeAction,\n} from '@open-mercato/shared/modules/notifications/types'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'\nimport { NOTIFICATION_EVENTS, NOTIFICATION_SSE_EVENTS } from './events'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n buildNotificationEntity,\n emitNotificationCreated,\n emitNotificationCreatedBatch,\n type NotificationContentInput,\n type NotificationTenantContext,\n} from './notificationFactory'\nimport { toNotificationDto } from './notificationMapper'\nimport { buildNotificationReadScopeWhere } from './notificationScope'\nimport {\n getRecipientUserIdsForFeature,\n getRecipientUserIdsForRole,\n getScopedNotificationRecipientUserIds,\n} from './notificationRecipients'\nimport { assertSafeNotificationHref, sanitizeNotificationActions } from './safeHref'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { getNotificationType } from './notification-type-registry'\nimport { getNotificationTypeOverrides, type NotificationTypeOverrides } from './typeOverrides'\nimport { getNotificationDeliveryStrategies } from './deliveryStrategies'\nimport { resolveEffectiveChannels } from './shouldDeliver'\nimport {\n createNotificationPreferenceService,\n type NotificationPreferenceService,\n} from './notificationPreferenceService'\nimport { inAppVisibleFilter, inAppVisibleSql, isInAppVisible } from './notificationVisibility'\n\nconst logger = createLogger('notifications').child({ component: 'service' })\n\nfunction debug(message: string, ...details: unknown[]): void {\n logger.debug(message, details.length ? { details } : undefined)\n}\n\nfunction getDb(em: EntityManager): Kysely<any> {\n return em.getKysely<any>()\n}\n\nconst UNIQUE_NOTIFICATION_ACTIVE_STATUSES: NotificationStatus[] = ['unread', 'read', 'actioned']\nconst NOTIFICATION_RESOURCE_KIND = 'notifications.notification'\n\nfunction normalizeOrgScope(organizationId: string | null | undefined): string | null {\n return organizationId ?? null\n}\n\nasync function invalidateNotificationCache(\n container: NotificationServiceDeps['container'],\n ctx: Pick<NotificationServiceContext, 'tenantId' | 'organizationId'>,\n reason: string,\n notificationId?: string,\n): Promise<void> {\n if (!container) return\n await invalidateCrudCache(\n container as Parameters<typeof invalidateCrudCache>[0],\n NOTIFICATION_RESOURCE_KIND,\n {\n id: notificationId,\n tenantId: ctx.tenantId,\n organizationId: normalizeOrgScope(ctx.organizationId),\n },\n ctx.tenantId,\n reason,\n )\n}\n\nasync function assertNotificationRecipientsInScope(\n em: EntityManager,\n recipientUserIds: string[],\n ctx: NotificationServiceContext,\n): Promise<void> {\n const scopedRecipientUserIds = await getScopedNotificationRecipientUserIds(\n getDb(em),\n ctx.tenantId,\n normalizeOrgScope(ctx.organizationId),\n recipientUserIds,\n )\n\n if (scopedRecipientUserIds.length !== recipientUserIds.length) {\n throw new CrudHttpError(404, { error: 'Notification recipient not found' })\n }\n}\n\nfunction actionAlreadyExecuted(): CrudHttpError {\n return new CrudHttpError(409, {\n error: 'Notification action already executed',\n code: NOTIFICATION_ACTION_ALREADY_EXECUTED_ERROR_CODE,\n })\n}\n\nasync function actionForbidden(): Promise<CrudHttpError> {\n const { translate } = await resolveTranslations()\n return new CrudHttpError(403, {\n error: translate('notifications.error.actionForbidden', 'You are not allowed to run this notification action'),\n code: NOTIFICATION_ACTION_FORBIDDEN_ERROR_CODE,\n })\n}\n\n/**\n * The command action the notification's registered type declares for a stored action: same action\n * id (across `actions` and `optionalActions`) and same `commandId`. Anything else is not dispatched.\n */\nfunction findDeclaredCommandAction(type: string, action: NotificationAction): NotificationTypeAction | null {\n const definition = getNotificationType(type)\n if (!definition) return null\n const declared = [...definition.actions, ...(definition.optionalActions ?? [])]\n .find((candidate) => candidate.id === action.id)\n if (!declared?.commandId || declared.commandId !== action.commandId) return null\n return declared\n}\n\nfunction applyNotificationContent(\n notification: Notification,\n input: NotificationContentInput,\n recipientUserId: string,\n ctx: NotificationTenantContext,\n channels: string[] | null,\n) {\n const actions = sanitizeNotificationActions(input.actions)\n const linkHref = assertSafeNotificationHref(input.linkHref)\n\n notification.recipientUserId = recipientUserId\n notification.type = input.type\n notification.titleKey = input.titleKey\n notification.bodyKey = input.bodyKey\n notification.titleVariables = input.titleVariables\n notification.bodyVariables = input.bodyVariables\n notification.title = input.title || input.titleKey || ''\n notification.body = input.body\n notification.icon = input.icon\n notification.severity = input.severity ?? 'info'\n notification.actionData = actions\n ? {\n actions,\n primaryActionId: input.primaryActionId,\n }\n : null\n notification.sourceModule = input.sourceModule\n notification.sourceEntityType = input.sourceEntityType\n notification.sourceEntityId = input.sourceEntityId\n notification.linkHref = linkHref\n notification.groupKey = input.groupKey\n notification.channels = channels\n notification.expiresAt = input.expiresAt ? new Date(input.expiresAt) : null\n notification.tenantId = ctx.tenantId\n notification.organizationId = normalizeOrgScope(ctx.organizationId)\n notification.status = 'unread'\n notification.readAt = null\n notification.actionedAt = null\n notification.dismissedAt = null\n notification.actionTaken = null\n notification.actionResult = null\n notification.createdAt = new Date()\n}\n\nasync function findScopedNotificationOrThrow(\n em: EntityManager,\n notificationId: string,\n ctx: NotificationServiceContext,\n): Promise<Notification> {\n const notification = await findOneWithDecryption(\n em,\n Notification,\n {\n id: notificationId,\n recipientUserId: ctx.userId,\n tenantId: ctx.tenantId,\n },\n undefined,\n {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n },\n )\n if (!notification) {\n throw new CrudHttpError(404, { error: 'Notification not found' })\n }\n return notification\n}\n\nasync function emitNotificationSseEvents(\n eventBus: { emit: (event: string, payload: unknown) => Promise<void> },\n notifications: Notification[],\n ctx: NotificationServiceContext,\n): Promise<void> {\n // Live bell updates only for notifications actually delivered to the in-app channel; the others\n // exist as records but must not bump anyone's badge/inbox.\n const visible = notifications.filter((notification) => isInAppVisible(notification.channels))\n if (visible.length === 0) return\n\n const visibleRecipientUserIds = Array.from(new Set(visible.map((n) => n.recipientUserId)))\n await eventBus.emit(NOTIFICATION_SSE_EVENTS.BATCH_CREATED, {\n tenantId: ctx.tenantId,\n organizationId: normalizeOrgScope(ctx.organizationId),\n recipientUserIds: visibleRecipientUserIds,\n count: visible.length,\n })\n\n for (const notification of visible) {\n await eventBus.emit(NOTIFICATION_SSE_EVENTS.CREATED, {\n tenantId: notification.tenantId,\n organizationId: notification.organizationId ?? null,\n recipientUserId: notification.recipientUserId,\n notification: toNotificationDto(notification),\n })\n }\n}\n\nasync function createOrRefreshNotification(\n em: EntityManager,\n input: NotificationContentInput,\n recipientUserId: string,\n ctx: NotificationTenantContext,\n channels: string[] | null,\n): Promise<Notification> {\n if (input.groupKey && input.groupKey.trim().length > 0) {\n const orgScope = normalizeOrgScope(ctx.organizationId) ?? 'global'\n const lockKey = `notifications:${ctx.tenantId}:${orgScope}:${recipientUserId}:${input.type}:${input.groupKey}`\n try {\n const db = getDb(em)\n await sql`select pg_advisory_xact_lock(hashtext(${lockKey}))`.execute(db)\n } catch {\n // If advisory locks are unavailable, continue with best-effort dedupe.\n }\n\n const existing = await em.findOne(Notification, {\n recipientUserId,\n tenantId: ctx.tenantId,\n organizationId: normalizeOrgScope(ctx.organizationId),\n type: input.type,\n groupKey: input.groupKey,\n status: { $in: UNIQUE_NOTIFICATION_ACTIVE_STATUSES },\n }, {\n orderBy: { createdAt: 'desc' },\n })\n\n if (existing) {\n applyNotificationContent(existing, input, recipientUserId, ctx, channels)\n return existing\n }\n }\n\n return buildNotificationEntity(em, input, recipientUserId, ctx, channels)\n}\n\nexport interface NotificationServiceContext {\n tenantId: string\n organizationId?: string | null\n organizationIds?: string[] | null\n userId?: string | null\n}\n\nexport type CreateFeatureNotificationServiceInput = CreateFeatureNotificationInput & {\n restrictRecipientsToOrganization?: boolean\n}\n\nexport type CreateBatchNotificationOptions = {\n skipExistingGroup?: boolean\n}\n\nexport interface NotificationService {\n create(input: CreateNotificationInput, ctx: NotificationServiceContext): Promise<Notification>\n createBatch(input: CreateBatchNotificationInput, ctx: NotificationServiceContext, options?: CreateBatchNotificationOptions): Promise<Notification[]>\n createForRole(input: CreateRoleNotificationInput, ctx: NotificationServiceContext): Promise<Notification[]>\n createForFeature(input: CreateFeatureNotificationServiceInput, ctx: NotificationServiceContext): Promise<Notification[]>\n markAsRead(notificationId: string, ctx: NotificationServiceContext): Promise<Notification>\n markAllAsRead(ctx: NotificationServiceContext): Promise<number>\n dismiss(notificationId: string, ctx: NotificationServiceContext): Promise<Notification>\n restoreDismissed(\n notificationId: string,\n status: 'read' | 'unread' | undefined,\n ctx: NotificationServiceContext\n ): Promise<Notification>\n executeAction(\n notificationId: string,\n input: ExecuteActionInput,\n ctx: NotificationServiceContext\n ): Promise<{ notification: Notification; result: unknown }>\n getUnreadCount(ctx: NotificationServiceContext): Promise<number>\n getPollData(ctx: NotificationServiceContext, since?: string): Promise<NotificationPollData>\n cleanupExpired(): Promise<number>\n deleteBySource(\n sourceEntityType: string,\n sourceEntityId: string,\n ctx: NotificationServiceContext\n ): Promise<number>\n}\n\nexport interface NotificationServiceDeps {\n em: EntityManager\n eventBus: { emit: (event: string, payload: unknown) => Promise<void> }\n commandBus?: {\n execute: (\n commandId: string,\n options: { input: unknown; ctx: unknown; metadata?: unknown }\n ) => Promise<{ result: unknown }>\n }\n container?: { resolve: (name: string) => unknown }\n}\n\nexport function createNotificationService(deps: NotificationServiceDeps): NotificationService {\n const { em: rootEm, eventBus, commandBus, container } = deps\n\n /**\n * Whether the clicking user holds every feature a declared notification action requires.\n * Fails closed: no user, no container, or a failing RBAC check denies the action.\n */\n const userHoldsActionFeatures = async (\n requiredFeatures: string[] | undefined,\n scopeCtx: NotificationServiceContext,\n ): Promise<boolean> => {\n if (!requiredFeatures || requiredFeatures.length === 0) return true\n if (!scopeCtx.userId || !container) return false\n try {\n const rbacService = container.resolve('rbacService') as {\n userHasAllFeatures: (\n userId: string,\n features: string[],\n scope: { tenantId: string | null; organizationId: string | null },\n ) => Promise<boolean>\n }\n const allowed = await rbacService.userHasAllFeatures(\n scopeCtx.userId,\n requiredFeatures,\n { tenantId: scopeCtx.tenantId, organizationId: scopeCtx.organizationId ?? null },\n )\n return allowed === true\n } catch (err) {\n getTelemetryRuntime()?.reportError(err, {\n module: 'notifications',\n code: 'notifications.action_feature_check_failed',\n })\n return false\n }\n }\n\n /**\n * Resolves the authoritative delivery-channel set for one recipient at create time \u2014 the single\n * gate that folds per-send target, per-type eligibility, registered strategies, and the\n * recipient's per-channel preferences (`nonOptOut` bypasses opt-out). Stored on the row and\n * replayed by the dispatcher; `in_app` membership also drives bell/inbox visibility.\n *\n * Returns `null` (\u21D2 \"all channels\", legacy behavior) when no delivery strategies are registered\n * (e.g. a minimal bootstrap/test), so notifications never become silently undeliverable/invisible\n * in an environment that simply hasn't wired the seam.\n */\n const resolveChannelsFor = async (\n content: NotificationContentInput,\n recipientUserId: string,\n scopeCtx: NotificationServiceContext,\n preferences: NotificationPreferenceService = createNotificationPreferenceService({ em: rootEm.fork() }),\n typeOverrides?: NotificationTypeOverrides | null,\n ): Promise<string[] | null> => {\n const registeredChannels = getNotificationDeliveryStrategies().map((strategy) => strategy.id)\n if (registeredChannels.length === 0) return null\n // Treat an empty target as \"no restriction\" (all channels) rather than \"no deliverable channel\":\n // a programmatic caller that computed an empty array should not silently black-hole the\n // notification. The HTTP layer rejects an empty `channels` outright (see validators.ts).\n const targetChannels = content.channels && content.channels.length > 0 ? content.channels : null\n const overrides = typeOverrides === undefined\n ? (await getNotificationTypeOverrides(rootEm.fork(), scopeCtx.tenantId, [content.type])).get(content.type) ?? null\n : typeOverrides\n return resolveEffectiveChannels({\n typeId: content.type,\n type: getNotificationType(content.type),\n scope: { tenantId: scopeCtx.tenantId, userId: recipientUserId },\n targetChannels,\n registeredChannels,\n preferences,\n channelsOverride: overrides?.channels ?? null,\n nonOptOutOverride: overrides?.nonOptOut ?? null,\n })\n }\n\n /**\n * Broadcast counterpart of {@link resolveChannelsFor}: resolves the channel set for every recipient\n * up front, BEFORE the write transaction opens, reusing a single forked EM / preference service\n * across the whole set. This keeps the preference reads out of the write transaction (matching\n * `create`, which resolves before its transaction) and avoids one EM fork per recipient on large\n * role/feature broadcasts.\n */\n const resolveChannelsForRecipients = async (\n content: NotificationContentInput,\n recipientUserIds: string[],\n scopeCtx: NotificationServiceContext,\n ): Promise<Array<{ recipientUserId: string; channels: string[] | null }>> => {\n const preferences = createNotificationPreferenceService({ em: rootEm.fork() })\n // Stored overrides are per-type (not per-recipient) \u2014 read once for the whole broadcast.\n const typeOverrides =\n (await getNotificationTypeOverrides(rootEm.fork(), scopeCtx.tenantId, [content.type])).get(content.type) ?? null\n const resolved: Array<{ recipientUserId: string; channels: string[] | null }> = []\n for (const recipientUserId of recipientUserIds) {\n resolved.push({\n recipientUserId,\n channels: await resolveChannelsFor(content, recipientUserId, scopeCtx, preferences, typeOverrides),\n })\n }\n return resolved\n }\n\n return {\n async create(input, ctx) {\n const { recipientUserId, ...content } = input\n const channels = await resolveChannelsFor(content, recipientUserId, ctx)\n const writeEm = rootEm.fork()\n const notification = await writeEm.transactional(async (tx) => {\n await assertNotificationRecipientsInScope(tx, [recipientUserId], ctx)\n const entity = await createOrRefreshNotification(tx, content, recipientUserId, ctx, channels)\n await tx.flush()\n return entity\n })\n\n await invalidateNotificationCache(container, ctx, 'created')\n // Always emit the domain event (drives the deliver subscriber \u2192 push/email/\u2026). Only bump the\n // live in-app bell when this notification is actually visible in-app.\n await emitNotificationCreated(eventBus, notification, ctx)\n if (isInAppVisible(notification.channels)) {\n await eventBus.emit(NOTIFICATION_SSE_EVENTS.CREATED, {\n tenantId: notification.tenantId,\n organizationId: notification.organizationId ?? null,\n recipientUserId: notification.recipientUserId,\n notification: toNotificationDto(notification),\n })\n }\n\n return notification\n },\n\n async createBatch(input, ctx, options) {\n if (options?.skipExistingGroup && !input.groupKey?.trim()) {\n throw new Error('[internal] Notification group key is required for deduplicated batch delivery')\n }\n const recipientUserIds = Array.from(new Set(input.recipientUserIds))\n if (options?.skipExistingGroup) recipientUserIds.sort((left, right) => left.localeCompare(right))\n const { recipientUserIds: _recipientUserIds, ...content } = input\n const notifications: Notification[] = []\n const resolved = await resolveChannelsForRecipients(content, recipientUserIds, ctx)\n const writeEm = rootEm.fork()\n\n await writeEm.transactional(async (tx) => {\n await assertNotificationRecipientsInScope(tx, recipientUserIds, ctx)\n for (const { recipientUserId, channels } of resolved) {\n if (options?.skipExistingGroup) {\n const organizationId = normalizeOrgScope(ctx.organizationId)\n const lockKey = `notifications:${ctx.tenantId}:${organizationId ?? 'global'}:${recipientUserId}:${content.type}:${content.groupKey}`\n await sql`select pg_advisory_xact_lock(hashtext(${lockKey}))`.execute(getDb(tx))\n const existing = await tx.findOne(Notification, {\n recipientUserId,\n tenantId: ctx.tenantId,\n organizationId,\n type: content.type,\n groupKey: content.groupKey,\n })\n if (existing) continue\n }\n const notification = await createOrRefreshNotification(tx, content, recipientUserId, ctx, channels)\n notifications.push(notification)\n }\n await tx.flush()\n })\n\n if (options?.skipExistingGroup && notifications.length === 0) return []\n await invalidateNotificationCache(container, ctx, 'created')\n await emitNotificationCreatedBatch(eventBus, notifications, ctx)\n await emitNotificationSseEvents(eventBus, notifications, ctx)\n\n return notifications\n },\n\n async createForRole(input, ctx) {\n const em = rootEm.fork()\n\n const db = getDb(em)\n const recipientUserIds = await getRecipientUserIdsForRole(db, ctx.tenantId, input.roleId)\n if (recipientUserIds.length === 0) {\n return []\n }\n\n const { roleId: _roleId, ...content } = input\n const notifications: Notification[] = []\n const uniqueRecipientUserIds = Array.from(new Set(recipientUserIds))\n const resolved = await resolveChannelsForRecipients(content, uniqueRecipientUserIds, ctx)\n const writeEm = rootEm.fork()\n\n await writeEm.transactional(async (tx) => {\n for (const { recipientUserId, channels } of resolved) {\n const notification = await createOrRefreshNotification(tx, content, recipientUserId, ctx, channels)\n notifications.push(notification)\n }\n await tx.flush()\n })\n\n await invalidateNotificationCache(container, ctx, 'created')\n await emitNotificationCreatedBatch(eventBus, notifications, ctx)\n await emitNotificationSseEvents(eventBus, notifications, ctx)\n\n return notifications\n },\n\n async createForFeature(input, ctx) {\n const em = rootEm.fork()\n const db = getDb(em)\n const recipientUserIds = await getRecipientUserIdsForFeature(db, ctx.tenantId, input.requiredFeature)\n\n if (recipientUserIds.length === 0) {\n debug('No users found with feature:', input.requiredFeature, 'in tenant:', ctx.tenantId)\n return []\n }\n\n let authorizedRecipientUserIds = recipientUserIds\n if (input.restrictRecipientsToOrganization) {\n const uniqueCandidateUserIds = Array.from(new Set(recipientUserIds))\n if (!ctx.organizationId) {\n logger.warn('Organization-restricted feature fan-out skipped because organization scope is missing', {\n tenantId: ctx.tenantId,\n requiredFeature: input.requiredFeature,\n })\n return []\n }\n\n if (uniqueCandidateUserIds.length > 200) {\n logger.warn('Organization-restricted feature fan-out skipped because the candidate cap was exceeded', {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n requiredFeature: input.requiredFeature,\n candidateCount: uniqueCandidateUserIds.length,\n candidateCap: 200,\n })\n return []\n }\n\n if (!container) {\n logger.warn('Organization-restricted feature fan-out skipped because the DI container is unavailable', {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n requiredFeature: input.requiredFeature,\n })\n return []\n }\n\n try {\n const rbacService = container.resolve('rbacService') as {\n userHasAllFeatures: (\n userId: string,\n features: string[],\n scope: { tenantId: string | null; organizationId: string | null },\n ) => Promise<boolean>\n }\n const allowedRecipientUserIds: string[] = []\n for (let offset = 0; offset < uniqueCandidateUserIds.length; offset += 10) {\n const candidates = uniqueCandidateUserIds.slice(offset, offset + 10)\n const results = await Promise.all(candidates.map(async (recipientUserId) => ({\n recipientUserId,\n allowed: await rbacService.userHasAllFeatures(\n recipientUserId,\n [input.requiredFeature],\n { tenantId: ctx.tenantId, organizationId: ctx.organizationId ?? null },\n ),\n })))\n for (const result of results) {\n if (result.allowed) allowedRecipientUserIds.push(result.recipientUserId)\n }\n }\n authorizedRecipientUserIds = allowedRecipientUserIds\n } catch (err) {\n logger.warn('Organization-restricted feature fan-out skipped because RBAC filtering failed', {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n requiredFeature: input.requiredFeature,\n err,\n })\n return []\n }\n\n if (authorizedRecipientUserIds.length === 0) {\n debug('No users found with feature in organization:', input.requiredFeature, ctx.organizationId)\n return []\n }\n }\n\n debug('Creating notifications for', authorizedRecipientUserIds.length, 'user(s) with feature:', input.requiredFeature)\n\n const {\n requiredFeature: _requiredFeature,\n restrictRecipientsToOrganization: _restrictRecipientsToOrganization,\n ...content\n } = input\n const notifications: Notification[] = []\n const uniqueRecipientUserIds = Array.from(new Set(authorizedRecipientUserIds))\n const resolved = await resolveChannelsForRecipients(content, uniqueRecipientUserIds, ctx)\n const writeEm = rootEm.fork()\n\n await writeEm.transactional(async (tx) => {\n for (const { recipientUserId, channels } of resolved) {\n const notification = await createOrRefreshNotification(tx, content, recipientUserId, ctx, channels)\n notifications.push(notification)\n }\n await tx.flush()\n })\n\n await invalidateNotificationCache(container, ctx, 'created')\n await emitNotificationCreatedBatch(eventBus, notifications, ctx)\n await emitNotificationSseEvents(eventBus, notifications, ctx)\n\n return notifications\n },\n\n async markAsRead(notificationId, ctx) {\n const em = rootEm.fork()\n const notification = await findScopedNotificationOrThrow(em, notificationId, ctx)\n\n if (notification.status === 'unread') {\n notification.status = 'read'\n notification.readAt = new Date()\n await em.flush()\n\n await invalidateNotificationCache(container, ctx, 'updated', notification.id)\n await eventBus.emit(NOTIFICATION_EVENTS.READ, {\n notificationId: notification.id,\n userId: ctx.userId,\n tenantId: ctx.tenantId,\n })\n }\n\n return notification\n },\n\n async markAllAsRead(ctx) {\n const em = rootEm.fork()\n const db = getDb(em)\n const applyScope = <QB extends { where: (...args: any[]) => QB }>(q: QB): QB => {\n let chain = q\n .where('recipient_user_id' as any, '=', ctx.userId as any)\n .where('tenant_id' as any, '=', ctx.tenantId)\n .where('status' as any, '=', 'unread')\n // Only in-app-visible rows count toward the badge (see getUnreadCount),\n // so \"mark all as read\" must scope to the SAME set \u2014 otherwise it flips\n // push/email-only rows the user never saw and SSE-broadcasts a read for\n // notifications that were never in the bell, inflating the returned\n // count past what the badge showed.\n .where(inAppVisibleSql() as any)\n if (ctx.organizationId) {\n chain = chain.where('organization_id' as any, '=', ctx.organizationId)\n }\n return chain\n }\n\n const targetRows = await applyScope(\n db\n .selectFrom('notifications' as any)\n .select([\n 'id' as any,\n 'organization_id' as any,\n 'recipient_user_id' as any,\n ]),\n ).execute() as Array<{ id: string }>\n\n if (!targetRows.length) {\n return 0\n }\n\n const updateResult = await applyScope(\n db.updateTable('notifications' as any).set({\n status: 'read',\n read_at: sql`now()`,\n } as any) as any,\n ).executeTakeFirst() as { numUpdatedRows?: bigint | number } | undefined\n const result = Number(updateResult?.numUpdatedRows ?? targetRows.length)\n\n await invalidateNotificationCache(container, ctx, 'updated')\n\n const notifications = await findWithDecryption(em, Notification, {\n id: { $in: targetRows.map((row) => row.id) },\n }, undefined, {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n })\n\n for (const notification of notifications) {\n await eventBus.emit(NOTIFICATION_EVENTS.READ, {\n notificationId: notification.id,\n userId: ctx.userId,\n tenantId: ctx.tenantId,\n })\n\n await eventBus.emit(NOTIFICATION_SSE_EVENTS.CREATED, {\n tenantId: notification.tenantId,\n organizationId: notification.organizationId ?? null,\n recipientUserId: notification.recipientUserId,\n notification: toNotificationDto(notification),\n })\n }\n\n return result\n },\n\n async dismiss(notificationId, ctx) {\n const em = rootEm.fork()\n const notification = await findScopedNotificationOrThrow(em, notificationId, ctx)\n\n notification.status = 'dismissed'\n notification.dismissedAt = new Date()\n await em.flush()\n\n await invalidateNotificationCache(container, ctx, 'updated', notification.id)\n await eventBus.emit(NOTIFICATION_EVENTS.DISMISSED, {\n notificationId: notification.id,\n userId: ctx.userId,\n tenantId: ctx.tenantId,\n })\n\n return notification\n },\n\n async restoreDismissed(notificationId, status, ctx) {\n const em = rootEm.fork()\n const notification = await findScopedNotificationOrThrow(em, notificationId, ctx)\n\n if (notification.status !== 'dismissed') {\n return notification\n }\n\n // A dismissed notification whose action already ran comes back as `actioned`,\n // whatever the caller asked for: `actioned_at` outlives the dismissal.\n const targetStatus: NotificationStatus = notification.actionedAt != null ? 'actioned' : status ?? 'read'\n notification.status = targetStatus\n notification.dismissedAt = null\n\n if (targetStatus === 'unread') {\n notification.readAt = null\n } else if (!notification.readAt) {\n notification.readAt = new Date()\n }\n\n await em.flush()\n\n await invalidateNotificationCache(container, ctx, 'updated', notification.id)\n await eventBus.emit(NOTIFICATION_EVENTS.RESTORED, {\n notificationId: notification.id,\n userId: ctx.userId,\n tenantId: ctx.tenantId,\n status: targetStatus,\n })\n\n return notification\n },\n\n async executeAction(notificationId, input, ctx) {\n const em = rootEm.fork()\n const notification = await findScopedNotificationOrThrow(em, notificationId, ctx)\n\n const actionData = notification.actionData\n const action = actionData?.actions?.find((a) => a.id === input.actionId)\n\n if (!action) {\n throw new Error('Action not found')\n }\n\n // Reject an already-actioned notification before dispatching the command,\n // so a retry or double-click cannot re-run the side effect. `actioned_at` is\n // the durable marker: `status` is overwritten by a dismissal.\n if (notification.status === 'actioned' || notification.actionedAt != null) {\n throw actionAlreadyExecuted()\n }\n\n if (action.commandId) {\n const declared = findDeclaredCommandAction(notification.type, action)\n if (!declared || !(await userHoldsActionFeatures(declared.requiredFeatures, ctx))) {\n throw await actionForbidden()\n }\n }\n\n const actionedAt = new Date()\n const previousStatus = notification.status\n const previousActionedAt = notification.actionedAt ?? null\n const previousActionTaken = notification.actionTaken ?? null\n\n // Atomically claim the notification so only one concurrent request can run\n // the side-effecting command. The conditional UPDATE matches only while the\n // notification has not been actioned yet; a losing request updates 0 rows.\n const claimResult = (await getDb(em)\n .updateTable('notifications' as any)\n .set({\n status: 'actioned',\n actioned_at: actionedAt,\n action_taken: input.actionId,\n } as any)\n .where('id' as any, '=', notification.id)\n .where('recipient_user_id' as any, '=', ctx.userId as any)\n .where('tenant_id' as any, '=', ctx.tenantId)\n .where('status' as any, '!=', 'actioned')\n .where('actioned_at' as any, 'is', null)\n .executeTakeFirst()) as { numUpdatedRows?: bigint | number } | undefined\n\n if (Number(claimResult?.numUpdatedRows ?? 0) === 0) {\n throw actionAlreadyExecuted()\n }\n\n // The claim is provisional: if the side-effecting command fails, the action\n // never actually completed, so release the claim to its prior state. This\n // lets the user retry the action instead of the notification being locked as\n // `actioned` forever. The claim is owned through its own `actioned_at` and\n // `action_taken`, not through `status`: a dismissal that lands while the\n // command runs overwrites `status`, and the release must still clear the\n // claim then \u2014 while leaving that dismissal in place.\n const releaseClaim = async () => {\n await getDb(em)\n .updateTable('notifications' as any)\n .set({\n status: sql`case when status = 'actioned' then ${previousStatus} else status end`,\n actioned_at: previousActionedAt,\n action_taken: previousActionTaken,\n } as any)\n .where('id' as any, '=', notification.id)\n .where('recipient_user_id' as any, '=', ctx.userId as any)\n .where('tenant_id' as any, '=', ctx.tenantId)\n .where('actioned_at' as any, '=', actionedAt)\n .where('action_taken' as any, '=', input.actionId)\n .executeTakeFirst()\n }\n\n let result: unknown = null\n\n if (action.commandId && commandBus && container) {\n const commandInput = {\n ...input.payload,\n id: notification.sourceEntityId,\n }\n\n // Build a CommandRuntimeContext from the notification service context\n const commandCtx = {\n container,\n auth: {\n sub: ctx.userId,\n tenantId: ctx.tenantId,\n orgId: ctx.organizationId,\n },\n organizationScope: null,\n selectedOrganizationId: ctx.organizationId ?? null,\n organizationIds: ctx.organizationId ? [ctx.organizationId] : null,\n }\n\n let commandResult: { result: unknown }\n try {\n commandResult = await commandBus.execute(action.commandId, {\n input: commandInput,\n ctx: commandCtx,\n metadata: {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n resourceKind: 'notifications',\n },\n })\n } catch (err) {\n // Never let a rollback failure mask the original command error \u2014 the\n // caller needs the real failure to decide whether to retry.\n try {\n await releaseClaim()\n } catch (releaseErr) {\n debug('failed to release notification action claim', releaseErr)\n }\n throw err\n }\n\n result = commandResult.result\n }\n\n notification.status = 'actioned'\n notification.actionedAt = actionedAt\n notification.actionTaken = input.actionId\n notification.actionResult = result as Record<string, unknown>\n\n if (!notification.readAt) {\n notification.readAt = new Date()\n }\n\n await em.flush()\n\n await invalidateNotificationCache(container, ctx, 'updated', notification.id)\n await eventBus.emit(NOTIFICATION_EVENTS.ACTIONED, {\n notificationId: notification.id,\n actionId: input.actionId,\n userId: ctx.userId,\n tenantId: ctx.tenantId,\n })\n\n return { notification, result }\n },\n\n async getUnreadCount(ctx) {\n const em = rootEm.fork()\n return em.count(Notification, {\n recipientUserId: ctx.userId,\n tenantId: ctx.tenantId,\n status: 'unread',\n $and: [\n buildNotificationReadScopeWhere(ctx),\n inAppVisibleFilter(),\n ],\n })\n },\n\n async getPollData(ctx, since) {\n const em = rootEm.fork()\n const filters: Record<string, unknown> = {\n recipientUserId: ctx.userId,\n tenantId: ctx.tenantId,\n $and: [\n buildNotificationReadScopeWhere(ctx),\n inAppVisibleFilter(),\n ],\n }\n\n if (since) {\n filters.createdAt = { $gt: new Date(since) }\n }\n\n const [notifications, unreadCount] = await Promise.all([\n em.find(Notification, filters, {\n orderBy: { createdAt: 'desc' },\n limit: 50,\n }),\n em.count(Notification, {\n recipientUserId: ctx.userId,\n tenantId: ctx.tenantId,\n status: 'unread',\n $and: [\n buildNotificationReadScopeWhere(ctx),\n inAppVisibleFilter(),\n ],\n }),\n ])\n\n const recent = notifications.map(toNotificationDto)\n const hasNew = since ? recent.length > 0 : false\n\n return {\n unreadCount,\n recent,\n hasNew,\n lastId: recent[0]?.id,\n }\n },\n\n async cleanupExpired() {\n const em = rootEm.fork()\n const db = getDb(em)\n\n const affectedScopes = await db\n .selectFrom('notifications' as any)\n .select([\n 'tenant_id' as any,\n 'organization_id' as any,\n ])\n .where('expires_at' as any, '<', sql`now()`)\n .where('status' as any, 'not in', ['actioned', 'dismissed'])\n .distinct()\n .execute() as Array<{ tenant_id: string; organization_id: string | null }>\n\n if (!affectedScopes.length) return 0\n\n const updateResult = await db\n .updateTable('notifications' as any)\n .set({\n status: 'dismissed',\n dismissed_at: sql`now()`,\n } as any)\n .where('expires_at' as any, '<', sql`now()`)\n .where('status' as any, 'not in', ['actioned', 'dismissed'])\n .executeTakeFirst() as { numUpdatedRows?: bigint | number } | undefined\n\n for (const scope of affectedScopes) {\n await invalidateNotificationCache(container, {\n tenantId: scope.tenant_id,\n organizationId: scope.organization_id,\n }, 'updated')\n }\n\n return Number(updateResult?.numUpdatedRows ?? 0)\n },\n\n async deleteBySource(sourceEntityType, sourceEntityId, ctx) {\n const em = rootEm.fork()\n const db = getDb(em)\n\n const deleteResult = await db\n .deleteFrom('notifications' as any)\n .where('source_entity_type' as any, '=', sourceEntityType)\n .where('source_entity_id' as any, '=', sourceEntityId)\n .where('tenant_id' as any, '=', ctx.tenantId)\n .executeTakeFirst() as { numDeletedRows?: bigint | number } | undefined\n\n const deletedCount = Number(deleteResult?.numDeletedRows ?? 0)\n if (deletedCount > 0) {\n await invalidateNotificationCache(container, ctx, 'deleted')\n }\n\n return deletedCount\n },\n }\n}\n\n/**\n * Helper to create notification service from a DI container.\n * Use this in API routes and commands to avoid DI resolution issues.\n */\nexport function resolveNotificationService(container: {\n resolve: (name: string) => unknown\n}): NotificationService {\n const em = container.resolve('em') as EntityManager\n const eventBus = container.resolve('eventBus') as { emit: (event: string, payload: unknown) => Promise<void> }\n\n // commandBus may not be registered in all contexts, so resolve it safely\n let commandBus: NotificationServiceDeps['commandBus']\n try {\n commandBus = container.resolve('commandBus') as typeof commandBus\n } catch {\n // commandBus not available - actions with commandId won't work\n commandBus = undefined\n }\n\n return createNotificationService({ em, eventBus, commandBus, container })\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.7768.1.90fa052c11",
3
+ "version": "0.10.1-develop.7770.1.970b396ebb",
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.7768.1.90fa052c11",
270
- "@open-mercato/shared": "0.10.1-develop.7768.1.90fa052c11",
271
- "@open-mercato/ui": "0.10.1-develop.7768.1.90fa052c11",
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",
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.7768.1.90fa052c11",
277
- "@open-mercato/shared": "0.10.1-develop.7768.1.90fa052c11",
278
- "@open-mercato/ui": "0.10.1-develop.7768.1.90fa052c11",
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",
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",
@@ -1,6 +1,5 @@
1
1
  import type {
2
2
  SearchModuleConfig,
3
- SearchBuildContext,
4
3
  SearchResultPresenter,
5
4
  SearchIndexSource,
6
5
  } from '@open-mercato/shared/modules/search'
@@ -40,7 +39,6 @@ function formatSubtitle(...parts: Array<unknown>): string | undefined {
40
39
  }
41
40
 
42
41
  function buildIndexSource(
43
- ctx: SearchBuildContext,
44
42
  presenter: SearchResultPresenter,
45
43
  lines: string[],
46
44
  ): SearchIndexSource | null {
@@ -48,7 +46,7 @@ function buildIndexSource(
48
46
  return {
49
47
  text: lines,
50
48
  presenter,
51
- checksumSource: { record: ctx.record, customFields: ctx.customFields },
49
+ checksumSource: { text: lines, presenter },
52
50
  }
53
51
  }
54
52
 
@@ -63,13 +61,10 @@ export const searchConfig: SearchModuleConfig = {
63
61
  const { t } = await resolveTranslations()
64
62
  const record = ctx.record
65
63
  const lines: string[] = []
66
- appendLine(lines, 'Name', record.display_name ?? record.displayName)
67
- appendLine(lines, 'Email', record.email)
64
+ appendLine(lines, 'Customer user ID', record.id ?? 'customer_accounts:customer_user')
68
65
  return buildIndexSource(
69
- ctx,
70
66
  {
71
67
  title: pickString(record.display_name, record.displayName) ?? String(record.id),
72
- subtitle: formatSubtitle(record.email),
73
68
  icon: 'user',
74
69
  badge: t('customer_accounts.search.badge.customerUser', 'Customer User'),
75
70
  },
@@ -81,14 +76,14 @@ export const searchConfig: SearchModuleConfig = {
81
76
  const record = ctx.record
82
77
  return {
83
78
  title: pickString(record.display_name, record.displayName) ?? String(record.id),
84
- subtitle: formatSubtitle(record.email),
85
79
  icon: 'user',
86
80
  badge: t('customer_accounts.search.badge.customerUser', 'Customer User'),
87
81
  }
88
82
  },
89
83
  resolveUrl: async (ctx) => `/backend/customer-accounts/users/${encodeURIComponent(String(ctx.record.id))}`,
90
84
  fieldPolicy: {
91
- searchable: ['display_name', 'email'],
85
+ searchable: ['display_name'],
86
+ hashOnly: ['email'],
92
87
  excluded: ['password_hash', 'email_hash'],
93
88
  },
94
89
  },
@@ -104,7 +99,6 @@ export const searchConfig: SearchModuleConfig = {
104
99
  appendLine(lines, 'Name', record.name)
105
100
  appendLine(lines, 'Description', record.description)
106
101
  return buildIndexSource(
107
- ctx,
108
102
  {
109
103
  title: pickString(record.name) ?? String(record.id),
110
104
  subtitle: formatSubtitle(snippet(record.description)),
@@ -10,6 +10,9 @@ import type { TranslateFn } from '@open-mercato/shared/lib/i18n/context'
10
10
  import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
11
11
  import { CUSTOMER_INTERACTION_TASK_SOURCE, EXAMPLE_TODO_SOURCE } from './lib/interactionCompatibility'
12
12
  import { createLogger } from '@open-mercato/shared/lib/logger'
13
+ import { defaultEncryptionMaps } from './encryption'
14
+ import type { TenantDataEncryptionService } from '@open-mercato/shared/lib/encryption/tenantDataEncryptionService'
15
+ import { isTenantDataEncryptionEnabled } from '@open-mercato/shared/lib/encryption/toggles'
13
16
 
14
17
  const logger = createLogger('customers')
15
18
 
@@ -414,6 +417,61 @@ function appendCustomFieldLines(lines: string[], customFields: Record<string, un
414
417
  }
415
418
  }
416
419
 
420
+ function normalizeSearchFieldKey(key: string): string {
421
+ return key.replace(/^cf[:_]/, '').replace(/([a-z0-9])([A-Z])/g, '$1_$2').toLowerCase()
422
+ }
423
+
424
+ async function resolveProfileEncryptedFields(ctx: SearchContext, kind: CustomerProfileKind, entity: Record<string, unknown> | null): Promise<string[]> {
425
+ if (!isTenantDataEncryptionEnabled()) {
426
+ throw new Error('[internal] Customer vector indexing requires available encryption declarations')
427
+ }
428
+ const container = ctx.container as { resolve?: (name: string) => unknown } | undefined
429
+ if (typeof container?.resolve !== 'function') {
430
+ throw new Error('[internal] Customer search encryption policy container is unavailable')
431
+ }
432
+ const service = container.resolve('tenantEncryptionService') as Pick<TenantDataEncryptionService, 'getEncryptedFieldNames'> | undefined
433
+ if (typeof service?.getEncryptedFieldNames !== 'function') {
434
+ throw new Error('[internal] Customer search encryption policy service is unavailable')
435
+ }
436
+ const organizationIds = new Set([ctx.organizationId])
437
+ for (const record of [ctx.record, entity]) {
438
+ const organizationId = record?.organization_id ?? record?.organizationId
439
+ if (typeof organizationId === 'string' && organizationId) organizationIds.add(organizationId)
440
+ }
441
+ const fields = await Promise.all(Array.from(organizationIds).flatMap((organizationId) => [
442
+ `customers:customer_${kind}_profile`,
443
+ 'customers:customer_entity',
444
+ ].map((entityId) => service.getEncryptedFieldNames(entityId, ctx.tenantId, organizationId, { ignoreRuntimeHealth: true }))))
445
+ return fields.flat()
446
+ }
447
+
448
+ function selectProfileSearchFields(
449
+ source: Record<string, unknown> | null,
450
+ kind: CustomerProfileKind,
451
+ options: { forEmbedding?: boolean; customerEntity?: boolean; encryptedFields?: string[] } = {},
452
+ ): Record<string, unknown> {
453
+ const entityId = `customers:customer_${kind}_profile`
454
+ const policy = searchConfig.entities.find((entry) => entry.entityId === entityId)?.fieldPolicy
455
+ const allowed = new Set((options.customerEntity
456
+ ? ['display_name', 'description', 'status', 'lifecycle_stage']
457
+ : policy?.searchable ?? []).map(normalizeSearchFieldKey))
458
+ const excluded = new Set([...(policy?.hashOnly ?? []), ...(policy?.excluded ?? [])].map(normalizeSearchFieldKey))
459
+ if (options.forEmbedding) {
460
+ for (const field of options.encryptedFields ?? []) excluded.add(normalizeSearchFieldKey(field))
461
+ for (const map of defaultEncryptionMaps) {
462
+ if (map.entityId !== entityId && map.entityId !== 'customers:customer_entity') continue
463
+ for (const entry of map.fields) excluded.add(normalizeSearchFieldKey(entry.field))
464
+ }
465
+ }
466
+ const fields: Record<string, unknown> = {}
467
+ for (const [key, value] of Object.entries(source ?? {})) {
468
+ if (value !== null && typeof value === 'object') continue
469
+ const field = normalizeSearchFieldKey(key)
470
+ if (allowed.has(field) && !excluded.has(field)) fields[field] = value
471
+ }
472
+ return fields
473
+ }
474
+
417
475
  function pickValue(source: Record<string, unknown> | null | undefined, ...keys: string[]): unknown {
418
476
  if (!source) return undefined
419
477
  for (const key of keys) {
@@ -444,12 +502,8 @@ function pickLabel(...candidates: Array<unknown>): string | null {
444
502
  function appendCustomerEntityLines(
445
503
  lines: string[],
446
504
  entity: Record<string, unknown> | null,
447
- contactLabel: 'Customer' | 'Primary' = 'Customer',
448
505
  ) {
449
506
  if (!entity) return
450
- appendLine(lines, 'Customer', pickValue(entity, 'display_name', 'displayName') ?? entity.id)
451
- appendLine(lines, `${contactLabel} email`, pickValue(entity, 'primary_email', 'primaryEmail'))
452
- appendLine(lines, `${contactLabel} phone`, pickValue(entity, 'primary_phone', 'primaryPhone'))
453
507
  appendLine(lines, 'Lifecycle stage', pickValue(entity, 'lifecycle_stage', 'lifecycleStage'))
454
508
  appendLine(lines, 'Status', pickValue(entity, 'status'))
455
509
  }
@@ -488,6 +542,9 @@ function resolvePersonPresenter(
488
542
  customFields: Record<string, unknown>,
489
543
  ): SearchResultPresenter {
490
544
  const fallbackEntityId = resolveCustomerEntityId(record)
545
+ record = { id: record.id, ...selectProfileSearchFields(record, 'person') }
546
+ entity = entity ? { id: entity.id, ...selectProfileSearchFields(entity, 'person', { customerEntity: true }) } : null
547
+ customFields = selectProfileSearchFields(customFields, 'person')
491
548
  const firstName = record.first_name ?? record.firstName ?? customFields.first_name ?? customFields.firstName ?? ''
492
549
  const lastName = record.last_name ?? record.lastName ?? customFields.last_name ?? customFields.lastName ?? ''
493
550
  const nameParts = [firstName, lastName].filter(Boolean).join(' ')
@@ -504,10 +561,6 @@ function resolvePersonPresenter(
504
561
  if (jobTitle) subtitlePieces.push(String(jobTitle))
505
562
  const department = record.department ?? customFields.department
506
563
  if (department) subtitlePieces.push(String(department))
507
- const primaryEmail = pickValue(entity, 'primary_email', 'primaryEmail')
508
- if (primaryEmail) subtitlePieces.push(String(primaryEmail))
509
- const primaryPhone = pickValue(entity, 'primary_phone', 'primaryPhone')
510
- if (primaryPhone) subtitlePieces.push(String(primaryPhone))
511
564
  const summary = snippet(
512
565
  (pickValue(entity, 'description') as string | undefined) ??
513
566
  (customFields.summary as string | undefined) ??
@@ -531,6 +584,9 @@ function resolveCompanyPresenter(
531
584
  customFields: Record<string, unknown>,
532
585
  ): SearchResultPresenter {
533
586
  const fallbackEntityId = resolveCustomerEntityId(record)
587
+ record = { id: record.id, ...selectProfileSearchFields(record, 'company') }
588
+ entity = entity ? { id: entity.id, ...selectProfileSearchFields(entity, 'company', { customerEntity: true }) } : null
589
+ customFields = selectProfileSearchFields(customFields, 'company')
534
590
  const title =
535
591
  (pickValue(entity, 'display_name', 'displayName') as string | undefined) ??
536
592
  (customFields.display_name as string | undefined) ??
@@ -549,10 +605,6 @@ function resolveCompanyPresenter(
549
605
  if (industry) subtitlePieces.push(String(industry))
550
606
  const sizeBucket = record.size_bucket ?? record.sizeBucket
551
607
  if (sizeBucket) subtitlePieces.push(String(sizeBucket))
552
- if (entity) {
553
- const primaryEmail = pickValue(entity, 'primary_email', 'primaryEmail')
554
- if (primaryEmail) subtitlePieces.push(String(primaryEmail))
555
- }
556
608
  const summary = snippet(
557
609
  (pickValue(entity, 'description') as string | undefined) ??
558
610
  (customFields.summary as string | undefined) ??
@@ -616,21 +668,14 @@ export const searchConfig: SearchModuleConfig = {
616
668
  const { t } = await resolveTranslations()
617
669
  const lines: string[] = []
618
670
  const record = ctx.record
619
- appendLine(lines, 'Preferred name', record.preferred_name ?? record.preferredName ?? ctx.customFields.preferred_name)
620
- appendLine(lines, 'First name', record.first_name ?? record.firstName ?? ctx.customFields.first_name)
621
- appendLine(lines, 'Last name', record.last_name ?? record.lastName ?? ctx.customFields.last_name)
622
- appendLine(lines, 'Job title', record.job_title ?? record.jobTitle ?? ctx.customFields.job_title)
623
- appendLine(lines, 'Department', record.department ?? record.department_name ?? record.departmentName ?? ctx.customFields.department)
624
- appendLine(lines, 'Seniority', record.seniority ?? record.seniority_level ?? record.seniorityLevel ?? ctx.customFields.seniority)
625
- appendLine(lines, 'Timezone', record.timezone ?? record.time_zone ?? record.timeZone ?? ctx.customFields.timezone)
626
- appendLine(lines, 'LinkedIn', record.linked_in_url ?? record.linkedInUrl ?? ctx.customFields.linked_in_url)
627
- appendLine(lines, 'Twitter', record.twitter_url ?? record.twitterUrl ?? ctx.customFields.twitter_url)
628
-
629
- const { entity, entityId, profileCustomFields, entityCustomFields, entityOnlyCustomFields } =
671
+
672
+ const { entity, entityId, profileCustomFields, entityOnlyCustomFields } =
630
673
  await hydrateProfileContext(ctx, 'person')
631
- appendCustomFieldLines(lines, profileCustomFields, 'Person custom')
674
+ const embeddingOptions = { forEmbedding: true, encryptedFields: await resolveProfileEncryptedFields(ctx, 'person', entity) }
675
+ appendCustomFieldLines(lines, selectProfileSearchFields(record, 'person', embeddingOptions), '')
676
+ appendCustomFieldLines(lines, selectProfileSearchFields(profileCustomFields, 'person', embeddingOptions), 'Person custom')
632
677
  if (Object.keys(entityOnlyCustomFields).length) {
633
- appendCustomFieldLines(lines, entityOnlyCustomFields, 'Customer custom')
678
+ appendCustomFieldLines(lines, selectProfileSearchFields(entityOnlyCustomFields, 'person', embeddingOptions), 'Customer custom')
634
679
  }
635
680
  if (!entity) {
636
681
  logger.warn('Failed to load customer entity for person profile', {
@@ -640,8 +685,8 @@ export const searchConfig: SearchModuleConfig = {
640
685
  recordKeys: Object.keys(record),
641
686
  })
642
687
  }
643
- appendCustomerEntityLines(lines, entity, 'Customer')
644
- ensureFallbackLines(lines, record)
688
+ appendCustomerEntityLines(lines, selectProfileSearchFields(entity, 'person', { ...embeddingOptions, customerEntity: true }))
689
+ ensureFallbackLines(lines, { id: record.id ?? entityId ?? 'customers:customer_person_profile' })
645
690
  if (!lines.length) return null
646
691
 
647
692
  if (!entityId) {
@@ -667,12 +712,7 @@ export const searchConfig: SearchModuleConfig = {
667
712
  text: lines,
668
713
  presenter,
669
714
  links,
670
- checksumSource: {
671
- record: ctx.record,
672
- customFields: profileCustomFields,
673
- entity,
674
- entityCustomFields,
675
- },
715
+ checksumSource: { text: lines, presenter, links },
676
716
  }
677
717
  },
678
718
 
@@ -728,22 +768,17 @@ export const searchConfig: SearchModuleConfig = {
728
768
  const { t } = await resolveTranslations()
729
769
  const lines: string[] = []
730
770
  const record = ctx.record
731
- appendLine(lines, 'Legal name', record.legal_name ?? record.legalName ?? ctx.customFields.legal_name)
732
- appendLine(lines, 'Brand name', record.brand_name ?? record.brandName ?? ctx.customFields.brand_name)
733
- appendLine(lines, 'Domain', record.domain ?? record.website_domain ?? record.websiteDomain ?? ctx.customFields.domain)
734
- appendLine(lines, 'Website', record.website_url ?? record.websiteUrl ?? ctx.customFields.website_url)
735
- appendLine(lines, 'Industry', record.industry ?? ctx.customFields.industry)
736
- appendLine(lines, 'Company size', record.size_bucket ?? record.sizeBucket ?? ctx.customFields.size_bucket)
737
- appendLine(lines, 'Annual revenue', record.annual_revenue ?? record.annualRevenue ?? ctx.customFields.annual_revenue)
738
-
739
- const { entity, entityId, profileCustomFields, entityCustomFields, entityOnlyCustomFields } =
771
+
772
+ const { entity, entityId, profileCustomFields, entityOnlyCustomFields } =
740
773
  await hydrateProfileContext(ctx, 'company')
741
- appendCustomFieldLines(lines, profileCustomFields, 'Company custom')
774
+ const embeddingOptions = { forEmbedding: true, encryptedFields: await resolveProfileEncryptedFields(ctx, 'company', entity) }
775
+ appendCustomFieldLines(lines, selectProfileSearchFields(record, 'company', embeddingOptions), '')
776
+ appendCustomFieldLines(lines, selectProfileSearchFields(profileCustomFields, 'company', embeddingOptions), 'Company custom')
742
777
  if (Object.keys(entityOnlyCustomFields).length) {
743
- appendCustomFieldLines(lines, entityOnlyCustomFields, 'Customer custom')
778
+ appendCustomFieldLines(lines, selectProfileSearchFields(entityOnlyCustomFields, 'company', embeddingOptions), 'Customer custom')
744
779
  }
745
- appendCustomerEntityLines(lines, entity, 'Primary')
746
- ensureFallbackLines(lines, record)
780
+ appendCustomerEntityLines(lines, selectProfileSearchFields(entity, 'company', { ...embeddingOptions, customerEntity: true }))
781
+ ensureFallbackLines(lines, { id: record.id ?? entityId ?? 'customers:customer_company_profile' })
747
782
  if (!lines.length) return null
748
783
 
749
784
  const presenter = resolveCompanyPresenter(t, record, entity, ctx.customFields)
@@ -761,12 +796,7 @@ export const searchConfig: SearchModuleConfig = {
761
796
  text: lines,
762
797
  presenter,
763
798
  links,
764
- checksumSource: {
765
- record: ctx.record,
766
- customFields: profileCustomFields,
767
- entity,
768
- entityCustomFields,
769
- },
799
+ checksumSource: { text: lines, presenter, links },
770
800
  }
771
801
  },
772
802