@open-mercato/core 0.6.6-develop.6507.1.cc8b36505d → 0.6.6-develop.6515.1.01911d00e2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (23) hide show
  1. package/dist/modules/communication_channels/data/enrichers.js +22 -1
  2. package/dist/modules/communication_channels/data/enrichers.js.map +2 -2
  3. package/dist/modules/inbox_ops/api/proposals/[id]/translate/route.js +1 -1
  4. package/dist/modules/inbox_ops/api/proposals/[id]/translate/route.js.map +2 -2
  5. package/dist/modules/integrations/lib/credentials-field-validation.js +1 -0
  6. package/dist/modules/integrations/lib/credentials-field-validation.js.map +2 -2
  7. package/dist/modules/integrations/lib/credentials-masking.js +16 -0
  8. package/dist/modules/integrations/lib/credentials-masking.js.map +2 -2
  9. package/dist/modules/notifications/lib/notificationRecipients.js +11 -1
  10. package/dist/modules/notifications/lib/notificationRecipients.js.map +2 -2
  11. package/dist/modules/notifications/lib/notificationService.js +18 -1
  12. package/dist/modules/notifications/lib/notificationService.js.map +2 -2
  13. package/dist/modules/sales/commands/returns.js +48 -17
  14. package/dist/modules/sales/commands/returns.js.map +3 -3
  15. package/package.json +7 -7
  16. package/src/modules/communication_channels/data/enrichers.ts +55 -1
  17. package/src/modules/inbox_ops/api/proposals/[id]/translate/route.ts +1 -1
  18. package/src/modules/integrations/AGENTS.md +1 -0
  19. package/src/modules/integrations/lib/credentials-field-validation.ts +1 -0
  20. package/src/modules/integrations/lib/credentials-masking.ts +17 -0
  21. package/src/modules/notifications/lib/notificationRecipients.ts +24 -0
  22. package/src/modules/notifications/lib/notificationService.ts +24 -1
  23. package/src/modules/sales/commands/returns.ts +49 -2
@@ -13,7 +13,7 @@ import {
13
13
  } from '../../../routeHelpers'
14
14
 
15
15
  export const metadata = {
16
- POST: { requireAuth: true, requireFeatures: ['inbox_ops.proposals.view'] },
16
+ POST: { requireAuth: true, requireFeatures: ['inbox_ops.proposals.manage'] },
17
17
  }
18
18
 
19
19
  export async function POST(req: Request) {
@@ -11,6 +11,7 @@ The `integrations` module is the foundation layer for all external connectors (p
11
11
  - **Always scope by organizationId + tenantId** — every entity query and service call
12
12
  - **Use `findWithDecryption`/`findOneWithDecryption`** for credential reads
13
13
  - **New providers MUST support provider-owned env preconfiguration** when credentials/settings are deployment-managed; implement it in the provider package, not in core
14
+ - **Secret-bearing credential fields MUST use `secret`, `oauth`, or `ssh_keypair`** — never declare tokens, passwords, or private keys as `text`, and never embed credentials in a `url` field
14
15
  - **Health check services** must be registered in DI by the provider module, not by integrations
15
16
  - **API routes must export `openApi`** for documentation generation
16
17
  - **All user-facing strings** via i18n keys in `i18n/en.json`
@@ -17,6 +17,7 @@ export function isValidCredentialUrl(value: string): boolean {
17
17
  }
18
18
  if (!ALLOWED_CREDENTIAL_URL_PROTOCOLS.has(parsed.protocol)) return false
19
19
  if (!parsed.hostname) return false
20
+ if (parsed.username || parsed.password) return false
20
21
  return true
21
22
  }
22
23
 
@@ -26,6 +26,19 @@ function isSecretField(type: CredentialFieldType): boolean {
26
26
  return SECRET_CREDENTIAL_FIELD_TYPES.has(type)
27
27
  }
28
28
 
29
+ function redactUrlUserinfo(value: unknown): unknown {
30
+ if (typeof value !== 'string') return value
31
+ try {
32
+ const parsed = new URL(value)
33
+ if (!parsed.username && !parsed.password) return value
34
+ parsed.username = ''
35
+ parsed.password = ''
36
+ return parsed.toString()
37
+ } catch {
38
+ return value
39
+ }
40
+ }
41
+
29
42
  function hasPresentValue(value: unknown): boolean {
30
43
  if (value === undefined || value === null) return false
31
44
  if (typeof value === 'string') return value.length > 0
@@ -53,6 +66,10 @@ export function maskSecretCredentials(
53
66
  const secretFieldsConfigured: Record<string, boolean> = {}
54
67
 
55
68
  for (const field of schema?.fields ?? []) {
69
+ if (field.type === 'url') {
70
+ credentials[field.key] = redactUrlUserinfo(credentials[field.key])
71
+ continue
72
+ }
56
73
  if (!isSecretField(field.type)) continue
57
74
  const configured = hasPresentValue(credentials[field.key])
58
75
  secretFieldsConfigured[field.key] = configured
@@ -34,6 +34,30 @@ function collectUsersWithFeature(
34
34
  }
35
35
  }
36
36
 
37
+ export async function getScopedNotificationRecipientUserIds(
38
+ db: Kysely<any>,
39
+ tenantId: string,
40
+ organizationId: string | null,
41
+ recipientUserIds: string[],
42
+ ): Promise<string[]> {
43
+ const builder: any = db
44
+ let query = builder
45
+ .selectFrom('users')
46
+ .where('users.id', 'in', recipientUserIds)
47
+ .where('users.deleted_at', 'is', null)
48
+ .where('users.tenant_id', '=', tenantId)
49
+
50
+ if (organizationId) {
51
+ query = query.where('users.organization_id', '=', organizationId)
52
+ }
53
+
54
+ const users = await query
55
+ .select('users.id as user_id')
56
+ .execute() as Array<{ user_id: string }>
57
+
58
+ return users.map((user) => user.user_id)
59
+ }
60
+
37
61
  export async function getRecipientUserIdsForRole(
38
62
  db: Kysely<any>,
39
63
  tenantId: string,
@@ -14,7 +14,11 @@ import {
14
14
  type NotificationTenantContext,
15
15
  } from './notificationFactory'
16
16
  import { toNotificationDto } from './notificationMapper'
17
- import { getRecipientUserIdsForFeature, getRecipientUserIdsForRole } from './notificationRecipients'
17
+ import {
18
+ getRecipientUserIdsForFeature,
19
+ getRecipientUserIdsForRole,
20
+ getScopedNotificationRecipientUserIds,
21
+ } from './notificationRecipients'
18
22
  import { assertSafeNotificationHref, sanitizeNotificationActions } from './safeHref'
19
23
  import { createLogger } from '@open-mercato/shared/lib/logger'
20
24
 
@@ -34,6 +38,23 @@ function normalizeOrgScope(organizationId: string | null | undefined): string |
34
38
  return organizationId ?? null
35
39
  }
36
40
 
41
+ async function assertNotificationRecipientsInScope(
42
+ em: EntityManager,
43
+ recipientUserIds: string[],
44
+ ctx: NotificationServiceContext,
45
+ ): Promise<void> {
46
+ const scopedRecipientUserIds = await getScopedNotificationRecipientUserIds(
47
+ getDb(em),
48
+ ctx.tenantId,
49
+ normalizeOrgScope(ctx.organizationId),
50
+ recipientUserIds,
51
+ )
52
+
53
+ if (scopedRecipientUserIds.length !== recipientUserIds.length) {
54
+ throw new CrudHttpError(404, { error: 'Notification recipient not found' })
55
+ }
56
+ }
57
+
37
58
  function applyNotificationContent(
38
59
  notification: Notification,
39
60
  input: NotificationContentInput,
@@ -214,6 +235,7 @@ export function createNotificationService(deps: NotificationServiceDeps): Notifi
214
235
  const { recipientUserId, ...content } = input
215
236
  const writeEm = rootEm.fork()
216
237
  const notification = await writeEm.transactional(async (tx) => {
238
+ await assertNotificationRecipientsInScope(tx, [recipientUserId], ctx)
217
239
  const entity = await createOrRefreshNotification(tx, content, recipientUserId, ctx)
218
240
  await tx.flush()
219
241
  return entity
@@ -237,6 +259,7 @@ export function createNotificationService(deps: NotificationServiceDeps): Notifi
237
259
  const writeEm = rootEm.fork()
238
260
 
239
261
  await writeEm.transactional(async (tx) => {
262
+ await assertNotificationRecipientsInScope(tx, recipientUserIds, ctx)
240
263
  for (const recipientUserId of recipientUserIds) {
241
264
  const notification = await createOrRefreshNotification(tx, content, recipientUserId, ctx)
242
265
  notifications.push(notification)
@@ -4,6 +4,7 @@ import { withAtomicFlush } from '@open-mercato/shared/lib/commands/flush'
4
4
  import { LockMode } from '@mikro-orm/core'
5
5
  import type { EntityManager } from '@mikro-orm/postgresql'
6
6
  import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
7
+ import { invalidateCrudCache } from '@open-mercato/shared/lib/crud/cache'
7
8
  import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
8
9
  import { emitCrudSideEffects } from '@open-mercato/shared/lib/commands/helpers'
9
10
  import type { CrudEventsConfig } from '@open-mercato/shared/lib/crud/types'
@@ -65,6 +66,28 @@ const returnCrudEvents: CrudEventsConfig = {
65
66
  }),
66
67
  }
67
68
 
69
+ type OrderCacheRecord = Pick<SalesOrder, 'id' | 'organizationId' | 'tenantId'>
70
+
71
+ /**
72
+ * Return mutations update the order aggregate, which is also the cache resource
73
+ * used by the order-lines and order-adjustments routes. Invalidate it after
74
+ * each committed return lifecycle change so reloads receive its fresh
75
+ * `updatedAt` optimistic-lock token.
76
+ */
77
+ async function invalidateOrderCache(
78
+ container: Parameters<typeof invalidateCrudCache>[0],
79
+ order: OrderCacheRecord,
80
+ fallbackTenant: string | null,
81
+ ): Promise<void> {
82
+ await invalidateCrudCache(
83
+ container,
84
+ SALES_RESOURCE_KIND_ORDER,
85
+ { id: order.id, organizationId: order.organizationId, tenantId: order.tenantId },
86
+ fallbackTenant,
87
+ 'updated',
88
+ )
89
+ }
90
+
68
91
  function toNumeric(value: unknown): number {
69
92
  if (typeof value === 'number' && Number.isFinite(value)) return value
70
93
  if (typeof value === 'string' && value.trim().length) {
@@ -618,7 +641,7 @@ const createReturnCommand: CommandHandler<ReturnCreateInput, { returnId: string
618
641
  }
619
642
 
620
643
  const salesCalculationService = ctx.container.resolve<SalesCalculationService>('salesCalculationService')
621
- const { header, createdLines } = await em.transactional(async (tx) => {
644
+ const { header, createdLines, order } = await em.transactional(async (tx) => {
622
645
  const order = await findOneWithDecryption(
623
646
  tx,
624
647
  SalesOrder,
@@ -769,9 +792,11 @@ const createReturnCommand: CommandHandler<ReturnCreateInput, { returnId: string
769
792
 
770
793
  await tx.flush()
771
794
 
772
- return { header: entity, createdLines: createdReturnLines }
795
+ return { header: entity, createdLines: createdReturnLines, order }
773
796
  })
774
797
 
798
+ await invalidateOrderCache(ctx.container, order, ctx.auth?.tenantId ?? null)
799
+
775
800
  const dataEngine = ctx.container.resolve('dataEngine') as DataEngine
776
801
  await emitCrudSideEffects({
777
802
  dataEngine,
@@ -827,6 +852,11 @@ const createReturnCommand: CommandHandler<ReturnCreateInput, { returnId: string
827
852
  const em = (ctx.container.resolve('em') as EntityManager).fork()
828
853
  const salesCalculationService = ctx.container.resolve<SalesCalculationService>('salesCalculationService')
829
854
  await reverseReturnEffects(em, salesCalculationService, after)
855
+ await invalidateOrderCache(ctx.container, {
856
+ id: after.orderId,
857
+ organizationId: after.organizationId,
858
+ tenantId: after.tenantId,
859
+ }, ctx.auth?.tenantId ?? null)
830
860
  },
831
861
  redo: async ({ ctx, logEntry }) => {
832
862
  const after = resolveRedoSnapshot<ReturnSnapshot>(logEntry)
@@ -849,6 +879,12 @@ const createReturnCommand: CommandHandler<ReturnCreateInput, { returnId: string
849
879
  throw new CrudHttpError(404, { error: 'sales.returns.orderMissing' })
850
880
  }
851
881
 
882
+ await invalidateOrderCache(ctx.container, {
883
+ id: after.orderId,
884
+ organizationId: after.organizationId,
885
+ tenantId: after.tenantId,
886
+ }, ctx.auth?.tenantId ?? null)
887
+
852
888
  const dataEngine = ctx.container.resolve('dataEngine') as DataEngine
853
889
  await emitCrudSideEffects({
854
890
  dataEngine,
@@ -1047,6 +1083,11 @@ const deleteReturnCommand: CommandHandler<ReturnDeleteInput, { returnId: string
1047
1083
  await enforceSalesDocumentOptimisticLock(ctx, header, SALES_RESOURCE_KIND_RETURN)
1048
1084
 
1049
1085
  await reverseReturnEffects(em, salesCalculationService, snapshot)
1086
+ await invalidateOrderCache(ctx.container, {
1087
+ id: snapshot.orderId,
1088
+ organizationId: snapshot.organizationId,
1089
+ tenantId: snapshot.tenantId,
1090
+ }, ctx.auth?.tenantId ?? null)
1050
1091
 
1051
1092
  const dataEngine = ctx.container.resolve('dataEngine') as DataEngine
1052
1093
  await emitCrudSideEffects({
@@ -1110,6 +1151,12 @@ const deleteReturnCommand: CommandHandler<ReturnDeleteInput, { returnId: string
1110
1151
  )
1111
1152
  if (!header) return
1112
1153
 
1154
+ await invalidateOrderCache(ctx.container, {
1155
+ id: before.orderId,
1156
+ organizationId: before.organizationId,
1157
+ tenantId: before.tenantId,
1158
+ }, ctx.auth?.tenantId ?? null)
1159
+
1113
1160
  const dataEngine = ctx.container.resolve('dataEngine') as DataEngine
1114
1161
  await emitCrudSideEffects({
1115
1162
  dataEngine,