@open-mercato/core 0.10.1-develop.7669.1.120a2cf7f1 → 0.10.1-develop.7670.1.7b5ad74924

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,4 +1,4 @@
1
- [build:core] found 5991 entry points
1
+ [build:core] found 5992 entry points
2
2
  [build:core] built successfully
3
3
  [build:core:generated] found 266 entry points
4
4
  [build:core:generated] built successfully
@@ -40,7 +40,8 @@ import {
40
40
  INTERACTION_STATUS_COMPLETED,
41
41
  INTERACTION_STATUS_PLANNED
42
42
  } from "../lib/interactionStatus.js";
43
- import { canChangeEmailVisibility } from "../lib/visibilityFilter.js";
43
+ import { canChangeEmailVisibility, isEmailHiddenFrom } from "../lib/visibilityFilter.js";
44
+ import { listGrantsForViewerOnPerson, listSharedChannelIds } from "../lib/conversationShares.js";
44
45
  import { createLogger } from "@open-mercato/shared/lib/logger";
45
46
  import { loadCustomFieldDefinitionIndex } from "@open-mercato/shared/lib/crud/custom-fields";
46
47
  import { resolveCatalogEventType, resolveScopedCalendarEventTypes } from "../lib/calendar/eventTypeResolver.js";
@@ -73,9 +74,56 @@ const interactionCrudEvents = {
73
74
  const INTERACTION_UNDO_DATE_OPTIONS = {
74
75
  datePaths: ["before.interaction.scheduledAt", "before.interaction.occurredAt", "before.interaction.recurrenceEnd"]
75
76
  };
76
- async function loadInteractionSnapshot(em, id) {
77
+ async function ensureInteractionMutationAccess(ctx, em, interaction) {
78
+ ensureTenantScope(ctx, interaction.tenantId);
79
+ ensureOrganizationScope(ctx, interaction.organizationId);
80
+ const currentUserId = ctx.auth?.isApiKey ? null : ctx.auth?.sub ?? null;
81
+ const personEntityId = "entityId" in interaction ? interaction.entityId : typeof interaction.entity === "string" ? interaction.entity : interaction.entity.id;
82
+ const visibility = {
83
+ interactionType: interaction.interactionType,
84
+ visibility: interaction.visibility,
85
+ authorUserId: interaction.authorUserId,
86
+ currentUserId,
87
+ personEntityId,
88
+ channelId: interaction.channelId
89
+ };
90
+ if (!isEmailHiddenFrom(visibility)) return;
91
+ const scope = { tenantId: interaction.tenantId, organizationId: interaction.organizationId };
92
+ const sharedConversations = await listGrantsForViewerOnPerson(em, scope, currentUserId, personEntityId);
93
+ const sharedChannelIds = await listSharedChannelIds(em, scope, currentUserId);
94
+ if (isEmailHiddenFrom({ ...visibility, sharedConversations, sharedChannelIds })) {
95
+ const { translate } = await resolveTranslations();
96
+ throw notFound(translate("customers.errors.entity_not_found", "Entity not found"));
97
+ }
98
+ }
99
+ async function ensureInteractionPrivacyChangeAccess(ctx, interaction, next) {
100
+ if (interaction.interactionType !== "email") return;
101
+ const actorUserId = ctx.auth?.isApiKey ? null : ctx.auth?.sub ?? null;
102
+ const changesOwnership = next.authorUserId !== void 0 && (next.authorUserId ?? null) !== (interaction.authorUserId ?? null);
103
+ const changesType = next.interactionType !== void 0 && next.interactionType !== interaction.interactionType;
104
+ if ((changesOwnership || changesType) && (!actorUserId || actorUserId !== interaction.authorUserId) || next.visibility !== void 0 && !canChangeEmailVisibility({
105
+ interactionType: interaction.interactionType,
106
+ currentVisibility: interaction.visibility,
107
+ nextVisibility: next.visibility,
108
+ authorUserId: interaction.authorUserId,
109
+ actorUserId,
110
+ userFeatures: void 0
111
+ })) {
112
+ const { translate } = await resolveTranslations();
113
+ throw notFound(translate("customers.errors.entity_not_found", "Entity not found"));
114
+ }
115
+ }
116
+ async function ensureInteractionRestoreAccess(ctx, em, current, restored) {
117
+ if (current) {
118
+ await ensureInteractionMutationAccess(ctx, em, current);
119
+ await ensureInteractionPrivacyChangeAccess(ctx, current, { ...restored, visibility: restored.visibility ?? null });
120
+ }
121
+ await ensureInteractionMutationAccess(ctx, em, { ...restored, channelId: current?.channelId });
122
+ }
123
+ async function loadInteractionSnapshot(em, id, ctx) {
77
124
  const interaction = await findOneWithDecryption(em, CustomerInteraction, { id }, { populate: ["entity"] });
78
125
  if (!interaction) return null;
126
+ if (ctx) await ensureInteractionMutationAccess(ctx, em, interaction);
79
127
  const custom = await loadCustomFieldSnapshot(em, {
80
128
  entityId: INTERACTION_ENTITY_ID,
81
129
  recordId: interaction.id,
@@ -422,6 +470,7 @@ const createInteractionCommand = {
422
470
  const result = await runInTransaction(em, async (trx) => {
423
471
  const record = await findOneWithDecryption(trx, CustomerInteraction, { id: interactionId });
424
472
  if (!record) return null;
473
+ await ensureInteractionMutationAccess(ctx, trx, record);
425
474
  const entityId = typeof record.entity === "string" ? record.entity : record.entity.id;
426
475
  trx.remove(record);
427
476
  await trx.flush();
@@ -451,6 +500,7 @@ const createInteractionCommand = {
451
500
  const { interaction, nextInteractionId } = await runInTransaction(em, async (trx) => {
452
501
  const entity = await requireTimelineParentEntity(trx, after.interaction.entityId, { tenantId: after.interaction.tenantId, organizationId: after.interaction.organizationId });
453
502
  let interaction2 = await findOneWithDecryption(trx, CustomerInteraction, { id: after.interaction.id });
503
+ await ensureInteractionRestoreAccess(ctx, trx, interaction2, after.interaction);
454
504
  if (!interaction2) {
455
505
  interaction2 = buildInteractionGraph(trx, {
456
506
  id: after.interaction.id,
@@ -555,7 +605,7 @@ const createInteractionCommand = {
555
605
  async function prepareUpdateInteractionSnapshot(rawInput, ctx) {
556
606
  const { parsed } = parseWithCustomFields(interactionUpdateSchema, rawInput);
557
607
  const em = ctx.transactionalEm ?? ctx.container.resolve("em").fork();
558
- const snapshot = await loadInteractionSnapshot(em, parsed.id);
608
+ const snapshot = await loadInteractionSnapshot(em, parsed.id, ctx);
559
609
  return snapshot ? { before: snapshot } : {};
560
610
  }
561
611
  const updateInteractionCommand = {
@@ -573,12 +623,14 @@ const updateInteractionCommand = {
573
623
  }
574
624
  ensureTenantScope(ctx, interaction2.tenantId);
575
625
  ensureOrganizationScope(ctx, interaction2.organizationId);
626
+ await ensureInteractionMutationAccess(ctx, trx, interaction2);
576
627
  await enforceCommandOptimisticLockWithGuards(ctx.container, {
577
628
  resourceKind: "customers.interaction",
578
629
  resourceId: interaction2.id,
579
630
  current: interaction2.updatedAt,
580
631
  request: ctx.request ?? null
581
632
  });
633
+ await ensureInteractionPrivacyChangeAccess(ctx, interaction2, parsed);
582
634
  const nextType = parsed.interactionType ?? interaction2.interactionType;
583
635
  const changingType = nextType !== interaction2.interactionType;
584
636
  const enforceSelectableType = requestsSelectableTypeEnforcement(rawInput);
@@ -637,21 +689,6 @@ const updateInteractionCommand = {
637
689
  ignoreHiddenClears: enforceSelectableType
638
690
  });
639
691
  const sameTypeCustom = changingType || !customKeys || !enforceSelectableType ? custom : Object.fromEntries(Object.entries(custom).filter(([key]) => !customKeys.defined.has(key.toLowerCase()) || customKeys.allowed.has(key.toLowerCase())));
640
- if (parsed.visibility !== void 0 && interaction2.interactionType === "email" && (parsed.visibility ?? null) !== (interaction2.visibility ?? null)) {
641
- const actorUserId = ctx.auth?.sub ?? null;
642
- if (!canChangeEmailVisibility({
643
- interactionType: interaction2.interactionType,
644
- currentVisibility: interaction2.visibility,
645
- nextVisibility: parsed.visibility,
646
- authorUserId: interaction2.authorUserId,
647
- actorUserId,
648
- // v1 strict owner-only: only the author may flip visibility; no admin
649
- // bypass (canChangeEmailVisibility ignores caller features in v1).
650
- userFeatures: void 0
651
- })) {
652
- throw notFound("Email not found");
653
- }
654
- }
655
692
  const previousEntityId = typeof interaction2.entity === "string" ? interaction2.entity : interaction2.entity.id;
656
693
  const nextEntity = parsed.entityId !== void 0 && parsed.entityId !== previousEntityId ? await requireTimelineParentEntity(trx, parsed.entityId, {
657
694
  tenantId: interaction2.tenantId,
@@ -773,6 +810,7 @@ const updateInteractionCommand = {
773
810
  const em = ctx.container.resolve("em").fork();
774
811
  const { interaction, projections } = await runInTransaction(em, async (trx) => {
775
812
  let interaction2 = await findOneWithDecryption(trx, CustomerInteraction, { id: before.interaction.id });
813
+ await ensureInteractionRestoreAccess(ctx, trx, interaction2, before.interaction);
776
814
  const entity = await requireTimelineParentEntity(trx, before.interaction.entityId, { tenantId: before.interaction.tenantId, organizationId: before.interaction.organizationId });
777
815
  if (!interaction2) {
778
816
  interaction2 = trx.create(CustomerInteraction, {
@@ -890,7 +928,7 @@ const completeInteractionCommand = {
890
928
  async prepare(rawInput, ctx) {
891
929
  const parsed = interactionCompleteSchema.parse(rawInput);
892
930
  const em = ctx.container.resolve("em").fork();
893
- const snapshot = await loadInteractionSnapshot(em, parsed.id);
931
+ const snapshot = await loadInteractionSnapshot(em, parsed.id, ctx);
894
932
  return snapshot ? { before: snapshot } : {};
895
933
  },
896
934
  async execute(rawInput, ctx) {
@@ -904,6 +942,7 @@ const completeInteractionCommand = {
904
942
  }
905
943
  ensureTenantScope(ctx, interaction2.tenantId);
906
944
  ensureOrganizationScope(ctx, interaction2.organizationId);
945
+ await ensureInteractionMutationAccess(ctx, trx, interaction2);
907
946
  await enforceCommandOptimisticLockWithGuards(ctx.container, {
908
947
  resourceKind: "customers.interaction",
909
948
  resourceId: interaction2.id,
@@ -979,6 +1018,7 @@ const completeInteractionCommand = {
979
1018
  const result = await runInTransaction(em, async (trx) => {
980
1019
  const interaction = await findOneWithDecryption(trx, CustomerInteraction, { id: before.interaction.id });
981
1020
  if (!interaction) return null;
1021
+ await ensureInteractionMutationAccess(ctx, trx, interaction);
982
1022
  interaction.status = before.interaction.status;
983
1023
  interaction.occurredAt = before.interaction.occurredAt;
984
1024
  await trx.flush();
@@ -1019,7 +1059,7 @@ const cancelInteractionCommand = {
1019
1059
  async prepare(rawInput, ctx) {
1020
1060
  const parsed = interactionCancelSchema.parse(rawInput);
1021
1061
  const em = ctx.container.resolve("em").fork();
1022
- const snapshot = await loadInteractionSnapshot(em, parsed.id);
1062
+ const snapshot = await loadInteractionSnapshot(em, parsed.id, ctx);
1023
1063
  return snapshot ? { before: snapshot } : {};
1024
1064
  },
1025
1065
  async execute(rawInput, ctx) {
@@ -1033,6 +1073,7 @@ const cancelInteractionCommand = {
1033
1073
  }
1034
1074
  ensureTenantScope(ctx, interaction2.tenantId);
1035
1075
  ensureOrganizationScope(ctx, interaction2.organizationId);
1076
+ await ensureInteractionMutationAccess(ctx, trx, interaction2);
1036
1077
  await enforceCommandOptimisticLockWithGuards(ctx.container, {
1037
1078
  resourceKind: "customers.interaction",
1038
1079
  resourceId: interaction2.id,
@@ -1106,6 +1147,7 @@ const cancelInteractionCommand = {
1106
1147
  const result = await runInTransaction(em, async (trx) => {
1107
1148
  const interaction = await findOneWithDecryption(trx, CustomerInteraction, { id: before.interaction.id });
1108
1149
  if (!interaction) return null;
1150
+ await ensureInteractionMutationAccess(ctx, trx, interaction);
1109
1151
  interaction.status = before.interaction.status;
1110
1152
  await trx.flush();
1111
1153
  const projection = await recomputeNextInteraction(trx, before.interaction.entityId);
@@ -1145,7 +1187,7 @@ const deleteInteractionCommand = {
1145
1187
  async prepare(input, ctx) {
1146
1188
  const id = requireId(input, "Interaction id required");
1147
1189
  const em = ctx.container.resolve("em").fork();
1148
- const snapshot = await loadInteractionSnapshot(em, id);
1190
+ const snapshot = await loadInteractionSnapshot(em, id, ctx);
1149
1191
  return snapshot ? { before: snapshot } : {};
1150
1192
  },
1151
1193
  async execute(input, ctx) {
@@ -1159,6 +1201,7 @@ const deleteInteractionCommand = {
1159
1201
  }
1160
1202
  ensureTenantScope(ctx, interaction2.tenantId);
1161
1203
  ensureOrganizationScope(ctx, interaction2.organizationId);
1204
+ await ensureInteractionMutationAccess(ctx, trx, interaction2);
1162
1205
  await enforceCommandOptimisticLockWithGuards(ctx.container, {
1163
1206
  resourceKind: "customers.interaction",
1164
1207
  resourceId: interaction2.id,
@@ -1220,6 +1263,7 @@ const deleteInteractionCommand = {
1220
1263
  const { interaction, nextInteractionId } = await runInTransaction(em, async (trx) => {
1221
1264
  const entity = await requireTimelineParentEntity(trx, before.interaction.entityId, { tenantId: before.interaction.tenantId, organizationId: before.interaction.organizationId });
1222
1265
  let interaction2 = await findOneWithDecryption(trx, CustomerInteraction, { id: before.interaction.id });
1266
+ await ensureInteractionRestoreAccess(ctx, trx, interaction2, before.interaction);
1223
1267
  if (!interaction2) {
1224
1268
  interaction2 = trx.create(CustomerInteraction, {
1225
1269
  id: before.interaction.id,