@open-mercato/core 0.6.4-develop.4038.1.91ce075c8a → 0.6.4-develop.4106.1.12c205a613
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/currencies/acl.js +10 -5
- package/dist/modules/currencies/acl.js.map +2 -2
- package/dist/modules/directory/acl.js +12 -2
- package/dist/modules/directory/acl.js.map +2 -2
- package/dist/modules/directory/api/tenants/route.js +48 -34
- package/dist/modules/directory/api/tenants/route.js.map +2 -2
- package/dist/modules/entities/acl.js +12 -2
- package/dist/modules/entities/acl.js.map +2 -2
- package/dist/modules/integrations/api/[id]/credentials/route.js +20 -2
- package/dist/modules/integrations/api/[id]/credentials/route.js.map +2 -2
- package/dist/modules/integrations/lib/credentials-service.js +15 -25
- package/dist/modules/integrations/lib/credentials-service.js.map +2 -2
- package/dist/modules/query_index/acl.js +12 -2
- package/dist/modules/query_index/acl.js.map +2 -2
- package/dist/modules/query_index/lib/engine.js +32 -4
- package/dist/modules/query_index/lib/engine.js.map +2 -2
- package/dist/modules/resources/acl.js +6 -1
- package/dist/modules/resources/acl.js.map +2 -2
- package/dist/modules/sync_excel/acl.js +6 -1
- package/dist/modules/sync_excel/acl.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/currencies/acl.ts +5 -0
- package/src/modules/directory/acl.ts +12 -2
- package/src/modules/directory/api/tenants/route.ts +55 -41
- package/src/modules/entities/acl.ts +12 -2
- package/src/modules/integrations/api/[id]/credentials/route.ts +21 -3
- package/src/modules/integrations/lib/credentials-service.ts +15 -33
- package/src/modules/query_index/acl.ts +12 -2
- package/src/modules/query_index/lib/engine.ts +37 -7
- package/src/modules/resources/acl.ts +6 -1
- package/src/modules/sync_excel/acl.ts +6 -1
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
const features = [
|
|
2
2
|
{ id: "resources.view", title: "View resources", module: "resources" },
|
|
3
|
-
{
|
|
3
|
+
{
|
|
4
|
+
id: "resources.manage_resources",
|
|
5
|
+
title: "Manage resources",
|
|
6
|
+
module: "resources",
|
|
7
|
+
dependsOn: ["resources.view"]
|
|
8
|
+
}
|
|
4
9
|
];
|
|
5
10
|
var acl_default = features;
|
|
6
11
|
export {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/modules/resources/acl.ts"],
|
|
4
|
-
"sourcesContent": ["export const features = [\n { id: 'resources.view', title: 'View resources', module: 'resources' },\n {
|
|
5
|
-
"mappings": "AAAO,MAAM,WAAW;AAAA,EACtB,EAAE,IAAI,kBAAkB,OAAO,kBAAkB,QAAQ,YAAY;AAAA,EACrE,
|
|
4
|
+
"sourcesContent": ["export const features = [\n { id: 'resources.view', title: 'View resources', module: 'resources' },\n {\n id: 'resources.manage_resources',\n title: 'Manage resources',\n module: 'resources',\n dependsOn: ['resources.view'],\n },\n]\n\nexport default features\n"],
|
|
5
|
+
"mappings": "AAAO,MAAM,WAAW;AAAA,EACtB,EAAE,IAAI,kBAAkB,OAAO,kBAAkB,QAAQ,YAAY;AAAA,EACrE;AAAA,IACE,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,WAAW,CAAC,gBAAgB;AAAA,EAC9B;AACF;AAEA,IAAO,cAAQ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
const features = [
|
|
2
2
|
{ id: "sync_excel.view", title: "View sync_excel uploads and previews", module: "sync_excel" },
|
|
3
|
-
{
|
|
3
|
+
{
|
|
4
|
+
id: "sync_excel.run",
|
|
5
|
+
title: "Upload CSV files and prepare imports",
|
|
6
|
+
module: "sync_excel",
|
|
7
|
+
dependsOn: ["sync_excel.view"]
|
|
8
|
+
}
|
|
4
9
|
];
|
|
5
10
|
var acl_default = features;
|
|
6
11
|
export {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/modules/sync_excel/acl.ts"],
|
|
4
|
-
"sourcesContent": ["export const features = [\n { id: 'sync_excel.view', title: 'View sync_excel uploads and previews', module: 'sync_excel' },\n {
|
|
5
|
-
"mappings": "AAAO,MAAM,WAAW;AAAA,EACtB,EAAE,IAAI,mBAAmB,OAAO,wCAAwC,QAAQ,aAAa;AAAA,EAC7F,
|
|
4
|
+
"sourcesContent": ["export const features = [\n { id: 'sync_excel.view', title: 'View sync_excel uploads and previews', module: 'sync_excel' },\n {\n id: 'sync_excel.run',\n title: 'Upload CSV files and prepare imports',\n module: 'sync_excel',\n dependsOn: ['sync_excel.view'],\n },\n]\n\nexport default features\n"],
|
|
5
|
+
"mappings": "AAAO,MAAM,WAAW;AAAA,EACtB,EAAE,IAAI,mBAAmB,OAAO,wCAAwC,QAAQ,aAAa;AAAA,EAC7F;AAAA,IACE,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,WAAW,CAAC,iBAAiB;AAAA,EAC/B;AACF;AAEA,IAAO,cAAQ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.6.4-develop.
|
|
3
|
+
"version": "0.6.4-develop.4106.1.12c205a613",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -243,16 +243,16 @@
|
|
|
243
243
|
"zod": "^4.4.3"
|
|
244
244
|
},
|
|
245
245
|
"peerDependencies": {
|
|
246
|
-
"@open-mercato/ai-assistant": "0.6.4-develop.
|
|
247
|
-
"@open-mercato/shared": "0.6.4-develop.
|
|
248
|
-
"@open-mercato/ui": "0.6.4-develop.
|
|
246
|
+
"@open-mercato/ai-assistant": "0.6.4-develop.4106.1.12c205a613",
|
|
247
|
+
"@open-mercato/shared": "0.6.4-develop.4106.1.12c205a613",
|
|
248
|
+
"@open-mercato/ui": "0.6.4-develop.4106.1.12c205a613",
|
|
249
249
|
"react": "^19.0.0",
|
|
250
250
|
"react-dom": "^19.0.0"
|
|
251
251
|
},
|
|
252
252
|
"devDependencies": {
|
|
253
|
-
"@open-mercato/ai-assistant": "0.6.4-develop.
|
|
254
|
-
"@open-mercato/shared": "0.6.4-develop.
|
|
255
|
-
"@open-mercato/ui": "0.6.4-develop.
|
|
253
|
+
"@open-mercato/ai-assistant": "0.6.4-develop.4106.1.12c205a613",
|
|
254
|
+
"@open-mercato/shared": "0.6.4-develop.4106.1.12c205a613",
|
|
255
|
+
"@open-mercato/ui": "0.6.4-develop.4106.1.12c205a613",
|
|
256
256
|
"@testing-library/dom": "^10.4.1",
|
|
257
257
|
"@testing-library/jest-dom": "^6.9.1",
|
|
258
258
|
"@testing-library/react": "^16.3.1",
|
|
@@ -8,26 +8,31 @@ export const features = [
|
|
|
8
8
|
id: 'currencies.manage',
|
|
9
9
|
title: 'Manage currencies',
|
|
10
10
|
module: 'currencies',
|
|
11
|
+
dependsOn: ['currencies.view'],
|
|
11
12
|
},
|
|
12
13
|
{
|
|
13
14
|
id: 'currencies.rates.view',
|
|
14
15
|
title: 'View exchange rates',
|
|
15
16
|
module: 'currencies',
|
|
17
|
+
dependsOn: ['currencies.view'],
|
|
16
18
|
},
|
|
17
19
|
{
|
|
18
20
|
id: 'currencies.rates.manage',
|
|
19
21
|
title: 'Manage exchange rates',
|
|
20
22
|
module: 'currencies',
|
|
23
|
+
dependsOn: ['currencies.rates.view'],
|
|
21
24
|
},
|
|
22
25
|
{
|
|
23
26
|
id: 'currencies.fetch.view',
|
|
24
27
|
title: 'View currency fetch configuration',
|
|
25
28
|
module: 'currencies',
|
|
29
|
+
dependsOn: ['currencies.view'],
|
|
26
30
|
},
|
|
27
31
|
{
|
|
28
32
|
id: 'currencies.fetch.manage',
|
|
29
33
|
title: 'Manage currency fetch configuration',
|
|
30
34
|
module: 'currencies',
|
|
35
|
+
dependsOn: ['currencies.fetch.view'],
|
|
31
36
|
},
|
|
32
37
|
]
|
|
33
38
|
|
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
export const features = [
|
|
2
2
|
{ id: 'directory.tenants.view', title: 'View tenants', module: 'directory' },
|
|
3
|
-
{
|
|
3
|
+
{
|
|
4
|
+
id: 'directory.tenants.manage',
|
|
5
|
+
title: 'Manage tenants',
|
|
6
|
+
module: 'directory',
|
|
7
|
+
dependsOn: ['directory.tenants.view'],
|
|
8
|
+
},
|
|
4
9
|
{ id: 'directory.organizations.view', title: 'View organizations', module: 'directory' },
|
|
5
|
-
{
|
|
10
|
+
{
|
|
11
|
+
id: 'directory.organizations.manage',
|
|
12
|
+
title: 'Manage organizations',
|
|
13
|
+
module: 'directory',
|
|
14
|
+
dependsOn: ['directory.organizations.view'],
|
|
15
|
+
},
|
|
6
16
|
]
|
|
7
17
|
|
|
8
18
|
export default features
|
|
@@ -136,27 +136,6 @@ export async function GET(req: Request) {
|
|
|
136
136
|
orderBy.name = 'ASC'
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
const all = await em.find(Tenant, where, { orderBy })
|
|
140
|
-
const recordIds = all.map((tenant) => String(tenant.id))
|
|
141
|
-
|
|
142
|
-
const tenantIdByRecord: Record<string, string | null> = {}
|
|
143
|
-
const organizationIdByRecord: Record<string, string | null> = {}
|
|
144
|
-
for (const rid of recordIds) {
|
|
145
|
-
tenantIdByRecord[rid] = null
|
|
146
|
-
organizationIdByRecord[rid] = null
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
const cfValues = recordIds.length
|
|
150
|
-
? await loadCustomFieldValues({
|
|
151
|
-
em,
|
|
152
|
-
entityId: E.directory.tenant,
|
|
153
|
-
recordIds,
|
|
154
|
-
tenantIdByRecord,
|
|
155
|
-
organizationIdByRecord,
|
|
156
|
-
tenantFallbacks: auth.tenantId ? [auth.tenantId] : [],
|
|
157
|
-
})
|
|
158
|
-
: {}
|
|
159
|
-
|
|
160
139
|
const rawQuery = Array.from(url.searchParams.keys()).reduce<Record<string, unknown>>((acc, key) => {
|
|
161
140
|
const values = url.searchParams.getAll(key)
|
|
162
141
|
if (!values.length) return acc
|
|
@@ -175,29 +154,64 @@ export async function GET(req: Request) {
|
|
|
175
154
|
return [normalizedKey, condition] as const
|
|
176
155
|
})
|
|
177
156
|
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
157
|
+
const loadCfValues = (tenants: Tenant[]): Promise<Record<string, Record<string, unknown>>> => {
|
|
158
|
+
const recordIds = tenants.map((tenant) => String(tenant.id))
|
|
159
|
+
if (!recordIds.length) return Promise.resolve({})
|
|
160
|
+
const tenantIdByRecord: Record<string, string | null> = {}
|
|
161
|
+
const organizationIdByRecord: Record<string, string | null> = {}
|
|
162
|
+
for (const rid of recordIds) {
|
|
163
|
+
tenantIdByRecord[rid] = null
|
|
164
|
+
organizationIdByRecord[rid] = null
|
|
165
|
+
}
|
|
166
|
+
return loadCustomFieldValues({
|
|
167
|
+
em,
|
|
168
|
+
entityId: E.directory.tenant,
|
|
169
|
+
recordIds,
|
|
170
|
+
tenantIdByRecord,
|
|
171
|
+
organizationIdByRecord,
|
|
172
|
+
tenantFallbacks: auth.tenantId ? [auth.tenantId] : [],
|
|
173
|
+
})
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const start = (page - 1) * pageSize
|
|
177
|
+
|
|
178
|
+
let pageTenants: Tenant[]
|
|
179
|
+
let total: number
|
|
180
|
+
let cfValues: Record<string, Record<string, unknown>>
|
|
181
|
+
|
|
182
|
+
if (cfFilterEntries.length) {
|
|
183
|
+
// Custom-field filters are matched in JS against separately-loaded values,
|
|
184
|
+
// so the full result set must be fetched before filtering and paging.
|
|
185
|
+
const all = await em.find(Tenant, where, { orderBy })
|
|
186
|
+
cfValues = await loadCfValues(all)
|
|
187
|
+
const filtered = all.filter((tenant) => {
|
|
188
|
+
const rid = String(tenant.id)
|
|
189
|
+
const payload = cfValues[rid] ?? {}
|
|
190
|
+
return cfFilterEntries.every(([key, expected]) => {
|
|
191
|
+
const value = payload[`cf_${key}`]
|
|
192
|
+
if (expected && typeof expected === 'object' && !Array.isArray(expected)) {
|
|
193
|
+
const maybeIn = (expected as { $in?: unknown[] }).$in
|
|
194
|
+
if (Array.isArray(maybeIn)) {
|
|
195
|
+
if (value === undefined || value === null) return false
|
|
196
|
+
if (Array.isArray(value)) return value.some((val) => maybeIn.includes(val))
|
|
197
|
+
return maybeIn.includes(value)
|
|
191
198
|
}
|
|
192
|
-
|
|
193
|
-
|
|
199
|
+
}
|
|
200
|
+
return matchesValue(value, expected)
|
|
194
201
|
})
|
|
195
|
-
|
|
202
|
+
})
|
|
203
|
+
total = filtered.length
|
|
204
|
+
pageTenants = filtered.slice(start, start + pageSize)
|
|
205
|
+
} else {
|
|
206
|
+
// No JS-side filtering applies, so pagination is pushed to the database
|
|
207
|
+
// instead of fetching the whole table and slicing in memory.
|
|
208
|
+
const [rows, count] = await em.findAndCount(Tenant, where, { orderBy, limit: pageSize, offset: start })
|
|
209
|
+
total = count
|
|
210
|
+
pageTenants = rows
|
|
211
|
+
cfValues = await loadCfValues(rows)
|
|
212
|
+
}
|
|
196
213
|
|
|
197
|
-
const
|
|
198
|
-
const start = (page - 1) * pageSize
|
|
199
|
-
const paged = filtered.slice(start, start + pageSize)
|
|
200
|
-
const items = paged.map((tenant) => {
|
|
214
|
+
const items = pageTenants.map((tenant) => {
|
|
201
215
|
const rid = String(tenant.id)
|
|
202
216
|
const cf = cfValues[rid] ?? {}
|
|
203
217
|
return toRow(tenant, cf)
|
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
export const features = [
|
|
2
2
|
{ id: 'entities.definitions.view', title: 'View custom field definitions', module: 'entities' },
|
|
3
|
-
{
|
|
3
|
+
{
|
|
4
|
+
id: 'entities.definitions.manage',
|
|
5
|
+
title: 'Manage custom field definitions',
|
|
6
|
+
module: 'entities',
|
|
7
|
+
dependsOn: ['entities.definitions.view'],
|
|
8
|
+
},
|
|
4
9
|
{ id: 'entities.records.view', title: 'View records', module: 'entities' },
|
|
5
|
-
{
|
|
10
|
+
{
|
|
11
|
+
id: 'entities.records.manage',
|
|
12
|
+
title: 'Manage records',
|
|
13
|
+
module: 'entities',
|
|
14
|
+
dependsOn: ['entities.records.view'],
|
|
15
|
+
},
|
|
6
16
|
]
|
|
7
17
|
|
|
8
18
|
export default features
|
|
@@ -5,7 +5,10 @@ import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
|
5
5
|
import { getIntegration } from '@open-mercato/shared/modules/integrations/types'
|
|
6
6
|
import { emitIntegrationsEvent } from '../../../events'
|
|
7
7
|
import { saveCredentialsSchema } from '../../../data/validators'
|
|
8
|
-
import
|
|
8
|
+
import {
|
|
9
|
+
isCredentialsEncryptionUnavailableError,
|
|
10
|
+
type CredentialsService,
|
|
11
|
+
} from '../../../lib/credentials-service'
|
|
9
12
|
import {
|
|
10
13
|
resolveUserFeatures,
|
|
11
14
|
runIntegrationMutationGuardAfterSuccess,
|
|
@@ -53,7 +56,15 @@ export async function GET(req: Request, ctx: { params?: Promise<{ id?: string }>
|
|
|
53
56
|
const credentialsService = container.resolve('integrationCredentialsService') as CredentialsService
|
|
54
57
|
const scope = { organizationId: auth.orgId as string, tenantId: auth.tenantId }
|
|
55
58
|
|
|
56
|
-
|
|
59
|
+
let values: Record<string, unknown> | null
|
|
60
|
+
try {
|
|
61
|
+
values = await credentialsService.resolve(integration.id, scope)
|
|
62
|
+
} catch (error) {
|
|
63
|
+
if (isCredentialsEncryptionUnavailableError(error)) {
|
|
64
|
+
return NextResponse.json({ error: 'Integration credentials encryption is unavailable' }, { status: 503 })
|
|
65
|
+
}
|
|
66
|
+
throw error
|
|
67
|
+
}
|
|
57
68
|
|
|
58
69
|
return NextResponse.json({
|
|
59
70
|
integrationId: integration.id,
|
|
@@ -118,7 +129,14 @@ export async function PUT(req: Request, ctx: { params?: Promise<{ id?: string }>
|
|
|
118
129
|
const credentialsService = container.resolve('integrationCredentialsService') as CredentialsService
|
|
119
130
|
const scope = { organizationId: auth.orgId as string, tenantId: auth.tenantId }
|
|
120
131
|
|
|
121
|
-
|
|
132
|
+
try {
|
|
133
|
+
await credentialsService.save(integration.id, payloadData.credentials, scope)
|
|
134
|
+
} catch (error) {
|
|
135
|
+
if (isCredentialsEncryptionUnavailableError(error)) {
|
|
136
|
+
return NextResponse.json({ error: 'Integration credentials encryption is unavailable' }, { status: 503 })
|
|
137
|
+
}
|
|
138
|
+
throw error
|
|
139
|
+
}
|
|
122
140
|
|
|
123
141
|
await emitIntegrationsEvent('integrations.credentials.updated', {
|
|
124
142
|
integrationId: integration.id,
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import crypto from 'node:crypto'
|
|
2
1
|
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
3
2
|
import { decryptWithAesGcm, encryptWithAesGcm } from '@open-mercato/shared/lib/encryption/aes'
|
|
4
3
|
import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
@@ -14,7 +13,19 @@ import { EncryptionMap } from '../../entities/data/entities'
|
|
|
14
13
|
import { IntegrationCredentials } from '../data/entities'
|
|
15
14
|
|
|
16
15
|
const ENCRYPTED_CREDENTIALS_BLOB_KEY = '__om_encrypted_credentials_blob_v1'
|
|
17
|
-
const
|
|
16
|
+
const CREDENTIALS_ENCRYPTION_UNAVAILABLE_MESSAGE =
|
|
17
|
+
'Integration credentials encryption key is unavailable. Configure Vault KMS or TENANT_DATA_ENCRYPTION_FALLBACK_KEY.'
|
|
18
|
+
|
|
19
|
+
export class CredentialsEncryptionUnavailableError extends Error {
|
|
20
|
+
constructor() {
|
|
21
|
+
super(CREDENTIALS_ENCRYPTION_UNAVAILABLE_MESSAGE)
|
|
22
|
+
this.name = 'CredentialsEncryptionUnavailableError'
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function isCredentialsEncryptionUnavailableError(error: unknown): error is CredentialsEncryptionUnavailableError {
|
|
27
|
+
return error instanceof CredentialsEncryptionUnavailableError
|
|
28
|
+
}
|
|
18
29
|
|
|
19
30
|
function isRecordValue(value: unknown): value is Record<string, unknown> {
|
|
20
31
|
return !!value && typeof value === 'object' && !Array.isArray(value)
|
|
@@ -28,35 +39,6 @@ function normalizeCredentialsRecord(value: unknown): Record<string, unknown> {
|
|
|
28
39
|
return isRecordValue(parsed) ? parsed : {}
|
|
29
40
|
}
|
|
30
41
|
|
|
31
|
-
function resolveFallbackEncryptionSecret(): string {
|
|
32
|
-
const candidates = [
|
|
33
|
-
process.env.TENANT_DATA_ENCRYPTION_FALLBACK_KEY,
|
|
34
|
-
process.env.TENANT_DATA_ENCRYPTION_KEY,
|
|
35
|
-
process.env.AUTH_SECRET,
|
|
36
|
-
process.env.NEXTAUTH_SECRET,
|
|
37
|
-
]
|
|
38
|
-
|
|
39
|
-
for (const value of candidates) {
|
|
40
|
-
const normalized = value?.trim()
|
|
41
|
-
if (normalized) return normalized
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
if (process.env.NODE_ENV !== 'production') return 'om-dev-tenant-encryption'
|
|
45
|
-
|
|
46
|
-
console.warn(
|
|
47
|
-
'[integrations.credentials] No encryption secret configured; using emergency fallback secret. Configure TENANT_DATA_ENCRYPTION_FALLBACK_KEY immediately.',
|
|
48
|
-
)
|
|
49
|
-
return 'om-emergency-fallback-rotate-me'
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function deriveDekFromSecret(secret: string, tenantId: string): string {
|
|
53
|
-
return crypto
|
|
54
|
-
.createHash('sha256')
|
|
55
|
-
.update(`${DERIVED_KEY_CONTEXT}:${tenantId}:${secret}`)
|
|
56
|
-
.digest()
|
|
57
|
-
.toString('base64')
|
|
58
|
-
}
|
|
59
|
-
|
|
60
42
|
export function createCredentialsService(em: EntityManager) {
|
|
61
43
|
const credentialsEncryptionSpec = [{ field: 'credentials' }]
|
|
62
44
|
|
|
@@ -100,7 +82,7 @@ export function createCredentialsService(em: EntityManager) {
|
|
|
100
82
|
const created = await kms.createTenantDek(scope.tenantId)
|
|
101
83
|
if (created?.key) return created.key
|
|
102
84
|
|
|
103
|
-
|
|
85
|
+
throw new CredentialsEncryptionUnavailableError()
|
|
104
86
|
}
|
|
105
87
|
|
|
106
88
|
async function encryptCredentialsBlob(
|
|
@@ -162,8 +144,8 @@ export function createCredentialsService(em: EntityManager) {
|
|
|
162
144
|
},
|
|
163
145
|
|
|
164
146
|
async save(integrationId: string, credentials: Record<string, unknown>, scope: IntegrationScope): Promise<void> {
|
|
165
|
-
await ensureCredentialsEncryptionMap(scope)
|
|
166
147
|
const encryptedCredentials = await encryptCredentialsBlob(credentials, scope)
|
|
148
|
+
await ensureCredentialsEncryptionMap(scope)
|
|
167
149
|
|
|
168
150
|
const row = await findOneWithDecryption(
|
|
169
151
|
em,
|
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
export const features = [
|
|
2
2
|
{ id: 'query_index.status.view', title: 'View index status', module: 'query_index' },
|
|
3
|
-
{
|
|
4
|
-
|
|
3
|
+
{
|
|
4
|
+
id: 'query_index.reindex',
|
|
5
|
+
title: 'Trigger reindex',
|
|
6
|
+
module: 'query_index',
|
|
7
|
+
dependsOn: ['query_index.status.view'],
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
id: 'query_index.purge',
|
|
11
|
+
title: 'Purge index',
|
|
12
|
+
module: 'query_index',
|
|
13
|
+
dependsOn: ['query_index.status.view'],
|
|
14
|
+
},
|
|
5
15
|
]
|
|
6
16
|
|
|
7
17
|
export default features
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { QueryEngine, QueryOptions, QueryResult, FilterOp, Filter, QueryCustomFieldSource, PartialIndexWarning, QueryExtensionsConfig } from '@open-mercato/shared/lib/query/types'
|
|
1
|
+
import type { QueryEngine, QueryOptions, QueryResult, FilterOp, Filter, QueryCustomFieldSource, PartialIndexWarning, QueryExtensionsConfig, Sort } from '@open-mercato/shared/lib/query/types'
|
|
2
2
|
import { SortDir } from '@open-mercato/shared/lib/query/types'
|
|
3
3
|
import type { EntityId } from '@open-mercato/shared/modules/entities'
|
|
4
4
|
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
import { resolveSearchConfig, type SearchConfig } from '@open-mercato/shared/lib/search/config'
|
|
22
22
|
import { tokenizeText } from '@open-mercato/shared/lib/search/tokenize'
|
|
23
23
|
import { runBeforeQueryPipeline, runAfterQueryPipeline, type QueryExtensionContext } from '@open-mercato/shared/lib/query/query-extension-runner'
|
|
24
|
+
import { resolveEncryptedSortFields, sortRowsInMemory } from '@open-mercato/shared/lib/query/encrypted-sort'
|
|
24
25
|
|
|
25
26
|
function buildFilterableCustomFieldJoins(
|
|
26
27
|
sources: QueryCustomFieldSource[] | undefined,
|
|
@@ -90,6 +91,7 @@ type SearchRuntime = {
|
|
|
90
91
|
|
|
91
92
|
type EncryptionResolver = () => {
|
|
92
93
|
decryptEntityPayload?: (entityId: EntityId, payload: Record<string, unknown>, tenantId?: string | null, organizationId?: string | null) => Promise<Record<string, unknown>>
|
|
94
|
+
getEncryptedFieldNames?: (entityId: EntityId, tenantId?: string | null, organizationId?: string | null) => Promise<readonly string[]>
|
|
93
95
|
isEnabled?: () => boolean
|
|
94
96
|
} | null
|
|
95
97
|
|
|
@@ -504,6 +506,28 @@ export class HybridQueryEngine implements QueryEngine {
|
|
|
504
506
|
if (field === 'organization_id' && columns.has('id')) return 'id'
|
|
505
507
|
return null
|
|
506
508
|
}
|
|
509
|
+
const fallbackOrgId =
|
|
510
|
+
opts.organizationId
|
|
511
|
+
?? (Array.isArray(opts.organizationIds) && opts.organizationIds.length === 1 ? opts.organizationIds[0] : null)
|
|
512
|
+
const encSvc = this.getEncryptionService()
|
|
513
|
+
const resolvedSorts: Sort[] = []
|
|
514
|
+
for (const sort of opts.sort || []) {
|
|
515
|
+
const field = String(sort.field)
|
|
516
|
+
if (field.startsWith('cf:')) {
|
|
517
|
+
resolvedSorts.push({ ...sort, field })
|
|
518
|
+
} else {
|
|
519
|
+
const baseField = resolveBaseColumn(field)
|
|
520
|
+
if (baseField) resolvedSorts.push({ ...sort, field: baseField })
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
const encryptedSortFields = await resolveEncryptedSortFields(
|
|
524
|
+
encSvc,
|
|
525
|
+
entity,
|
|
526
|
+
resolvedSorts.filter((sort) => !sort.field.startsWith('cf:')).map((sort) => sort.field),
|
|
527
|
+
opts.tenantId ?? null,
|
|
528
|
+
fallbackOrgId,
|
|
529
|
+
)
|
|
530
|
+
const requiresPlaintextSort = encryptedSortFields.size > 0
|
|
507
531
|
|
|
508
532
|
// ────────────────────────────────────────────────────────────────
|
|
509
533
|
// Build a reusable "applyQueryShape" function that applies every
|
|
@@ -735,6 +759,9 @@ export class HybridQueryEngine implements QueryEngine {
|
|
|
735
759
|
|
|
736
760
|
// Selection (for data query)
|
|
737
761
|
const selectFieldSet = new Set<string>((opts.fields && opts.fields.length) ? opts.fields.map(String) : Array.from(columns.keys()))
|
|
762
|
+
if (requiresPlaintextSort) {
|
|
763
|
+
for (const field of encryptedSortFields) selectFieldSet.add(field)
|
|
764
|
+
}
|
|
738
765
|
if (opts.includeCustomFields === true) {
|
|
739
766
|
const entityIds = Array.from(new Set(indexSources.map((src) => String(src.entityId))))
|
|
740
767
|
try {
|
|
@@ -767,7 +794,8 @@ export class HybridQueryEngine implements QueryEngine {
|
|
|
767
794
|
|
|
768
795
|
const applySort = (q: AnyBuilder): AnyBuilder => {
|
|
769
796
|
let next = q
|
|
770
|
-
|
|
797
|
+
if (requiresPlaintextSort) return next
|
|
798
|
+
for (const s of resolvedSorts) {
|
|
771
799
|
const fieldName = String(s.field)
|
|
772
800
|
if (fieldName.startsWith('cf:')) {
|
|
773
801
|
const textExpr = this.buildCfTextExprSql(fieldName, indexSources)
|
|
@@ -845,7 +873,9 @@ export class HybridQueryEngine implements QueryEngine {
|
|
|
845
873
|
let dataBuilder = await applyQueryShape(dataRoot)
|
|
846
874
|
dataBuilder = applySelection(dataBuilder)
|
|
847
875
|
dataBuilder = applySort(dataBuilder)
|
|
848
|
-
|
|
876
|
+
if (!requiresPlaintextSort) {
|
|
877
|
+
dataBuilder = dataBuilder.limit(pageSize).offset((page - 1) * pageSize)
|
|
878
|
+
}
|
|
849
879
|
|
|
850
880
|
if (debugEnabled && sqlDebugEnabled) {
|
|
851
881
|
const compiled = dataBuilder.compile()
|
|
@@ -859,15 +889,11 @@ export class HybridQueryEngine implements QueryEngine {
|
|
|
859
889
|
if (debugEnabled) this.debug('query:complete', { entity, total, items: Array.isArray(itemsRaw) ? itemsRaw.length : 0 })
|
|
860
890
|
|
|
861
891
|
let items = itemsRaw as any[]
|
|
862
|
-
const encSvc = this.getEncryptionService()
|
|
863
892
|
const dekKeyCache = new Map<string | null, string | null>()
|
|
864
893
|
if (encSvc?.decryptEntityPayload) {
|
|
865
894
|
const decrypt = encSvc.decryptEntityPayload.bind(encSvc) as (
|
|
866
895
|
entityId: EntityId, payload: Record<string, unknown>, tenantId: string | null, organizationId: string | null,
|
|
867
896
|
) => Promise<Record<string, unknown>>
|
|
868
|
-
const fallbackOrgId =
|
|
869
|
-
opts.organizationId
|
|
870
|
-
?? (Array.isArray(opts.organizationIds) && opts.organizationIds.length === 1 ? opts.organizationIds[0] : null)
|
|
871
897
|
items = await Promise.all(
|
|
872
898
|
items.map(async (item) => {
|
|
873
899
|
try {
|
|
@@ -900,6 +926,10 @@ export class HybridQueryEngine implements QueryEngine {
|
|
|
900
926
|
}),
|
|
901
927
|
)
|
|
902
928
|
}
|
|
929
|
+
if (requiresPlaintextSort) {
|
|
930
|
+
items = sortRowsInMemory(items as Record<string, unknown>[], resolvedSorts)
|
|
931
|
+
.slice((page - 1) * pageSize, page * pageSize)
|
|
932
|
+
}
|
|
903
933
|
|
|
904
934
|
const typedItems = items as unknown as T[]
|
|
905
935
|
let result: QueryResult<T> = { items: typedItems, page, pageSize, total }
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
export const features = [
|
|
2
2
|
{ id: 'resources.view', title: 'View resources', module: 'resources' },
|
|
3
|
-
{
|
|
3
|
+
{
|
|
4
|
+
id: 'resources.manage_resources',
|
|
5
|
+
title: 'Manage resources',
|
|
6
|
+
module: 'resources',
|
|
7
|
+
dependsOn: ['resources.view'],
|
|
8
|
+
},
|
|
4
9
|
]
|
|
5
10
|
|
|
6
11
|
export default features
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
export const features = [
|
|
2
2
|
{ id: 'sync_excel.view', title: 'View sync_excel uploads and previews', module: 'sync_excel' },
|
|
3
|
-
{
|
|
3
|
+
{
|
|
4
|
+
id: 'sync_excel.run',
|
|
5
|
+
title: 'Upload CSV files and prepare imports',
|
|
6
|
+
module: 'sync_excel',
|
|
7
|
+
dependsOn: ['sync_excel.view'],
|
|
8
|
+
},
|
|
4
9
|
]
|
|
5
10
|
|
|
6
11
|
export default features
|