@open-mercato/core 0.6.4-develop.4095.1.9c790dc021 → 0.6.4-develop.4110.1.836aafde58
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/.turbo/turbo-build.log +1 -1
- 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/planner/components/AvailabilityRulesEditor.js +39 -26
- package/dist/modules/planner/components/AvailabilityRulesEditor.js.map +2 -2
- package/dist/modules/planner/components/availabilityRulesEditorState.js +8 -0
- package/dist/modules/planner/components/availabilityRulesEditorState.js.map +7 -0
- package/dist/modules/query_index/acl.js +12 -2
- package/dist/modules/query_index/acl.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/planner/components/AvailabilityRulesEditor.tsx +40 -26
- package/src/modules/planner/components/availabilityRulesEditorState.ts +11 -0
- package/src/modules/query_index/acl.ts +12 -2
- package/src/modules/resources/acl.ts +6 -1
- package/src/modules/sync_excel/acl.ts +6 -1
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
function resolveRuleSetSelectValue(ruleSets, selectedRulesetId) {
|
|
2
|
+
if (!selectedRulesetId) return void 0;
|
|
3
|
+
return ruleSets.some((ruleSet) => ruleSet.id === selectedRulesetId) ? selectedRulesetId : void 0;
|
|
4
|
+
}
|
|
5
|
+
export {
|
|
6
|
+
resolveRuleSetSelectValue
|
|
7
|
+
};
|
|
8
|
+
//# sourceMappingURL=availabilityRulesEditorState.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/planner/components/availabilityRulesEditorState.ts"],
|
|
4
|
+
"sourcesContent": ["export type AvailabilityRuleSetOption = {\n id: string\n}\n\nexport function resolveRuleSetSelectValue(\n ruleSets: AvailabilityRuleSetOption[],\n selectedRulesetId: string | null | undefined,\n): string | undefined {\n if (!selectedRulesetId) return undefined\n return ruleSets.some((ruleSet) => ruleSet.id === selectedRulesetId) ? selectedRulesetId : undefined\n}\n"],
|
|
5
|
+
"mappings": "AAIO,SAAS,0BACd,UACA,mBACoB;AACpB,MAAI,CAAC,kBAAmB,QAAO;AAC/B,SAAO,SAAS,KAAK,CAAC,YAAY,QAAQ,OAAO,iBAAiB,IAAI,oBAAoB;AAC5F;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
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
|
var acl_default = features;
|
|
7
17
|
export {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/modules/query_index/acl.ts"],
|
|
4
|
-
"sourcesContent": ["export const features = [\n { id: 'query_index.status.view', title: 'View index status', module: 'query_index' },\n {
|
|
5
|
-
"mappings": "AAAO,MAAM,WAAW;AAAA,EACtB,EAAE,IAAI,2BAA2B,OAAO,qBAAqB,QAAQ,cAAc;AAAA,EACnF,
|
|
4
|
+
"sourcesContent": ["export const features = [\n { id: 'query_index.status.view', title: 'View index status', module: 'query_index' },\n {\n id: 'query_index.reindex',\n title: 'Trigger reindex',\n module: 'query_index',\n dependsOn: ['query_index.status.view'],\n },\n {\n id: 'query_index.purge',\n title: 'Purge index',\n module: 'query_index',\n dependsOn: ['query_index.status.view'],\n },\n]\n\nexport default features\n"],
|
|
5
|
+
"mappings": "AAAO,MAAM,WAAW;AAAA,EACtB,EAAE,IAAI,2BAA2B,OAAO,qBAAqB,QAAQ,cAAc;AAAA,EACnF;AAAA,IACE,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,WAAW,CAAC,yBAAyB;AAAA,EACvC;AAAA,EACA;AAAA,IACE,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,WAAW,CAAC,yBAAyB;AAAA,EACvC;AACF;AAEA,IAAO,cAAQ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -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.4110.1.836aafde58",
|
|
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.4110.1.836aafde58",
|
|
247
|
+
"@open-mercato/shared": "0.6.4-develop.4110.1.836aafde58",
|
|
248
|
+
"@open-mercato/ui": "0.6.4-develop.4110.1.836aafde58",
|
|
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.4110.1.836aafde58",
|
|
254
|
+
"@open-mercato/shared": "0.6.4-develop.4110.1.836aafde58",
|
|
255
|
+
"@open-mercato/ui": "0.6.4-develop.4110.1.836aafde58",
|
|
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,
|