@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.
- package/dist/modules/communication_channels/data/enrichers.js +22 -1
- package/dist/modules/communication_channels/data/enrichers.js.map +2 -2
- package/dist/modules/inbox_ops/api/proposals/[id]/translate/route.js +1 -1
- package/dist/modules/inbox_ops/api/proposals/[id]/translate/route.js.map +2 -2
- package/dist/modules/integrations/lib/credentials-field-validation.js +1 -0
- package/dist/modules/integrations/lib/credentials-field-validation.js.map +2 -2
- package/dist/modules/integrations/lib/credentials-masking.js +16 -0
- package/dist/modules/integrations/lib/credentials-masking.js.map +2 -2
- package/dist/modules/notifications/lib/notificationRecipients.js +11 -1
- package/dist/modules/notifications/lib/notificationRecipients.js.map +2 -2
- package/dist/modules/notifications/lib/notificationService.js +18 -1
- package/dist/modules/notifications/lib/notificationService.js.map +2 -2
- package/dist/modules/sales/commands/returns.js +48 -17
- package/dist/modules/sales/commands/returns.js.map +3 -3
- package/package.json +7 -7
- package/src/modules/communication_channels/data/enrichers.ts +55 -1
- package/src/modules/inbox_ops/api/proposals/[id]/translate/route.ts +1 -1
- package/src/modules/integrations/AGENTS.md +1 -0
- package/src/modules/integrations/lib/credentials-field-validation.ts +1 -0
- package/src/modules/integrations/lib/credentials-masking.ts +17 -0
- package/src/modules/notifications/lib/notificationRecipients.ts +24 -0
- package/src/modules/notifications/lib/notificationService.ts +24 -1
- 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.
|
|
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`
|
|
@@ -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 {
|
|
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,
|