@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.
Files changed (35) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/currencies/acl.js +10 -5
  3. package/dist/modules/currencies/acl.js.map +2 -2
  4. package/dist/modules/directory/acl.js +12 -2
  5. package/dist/modules/directory/acl.js.map +2 -2
  6. package/dist/modules/directory/api/tenants/route.js +48 -34
  7. package/dist/modules/directory/api/tenants/route.js.map +2 -2
  8. package/dist/modules/entities/acl.js +12 -2
  9. package/dist/modules/entities/acl.js.map +2 -2
  10. package/dist/modules/integrations/api/[id]/credentials/route.js +20 -2
  11. package/dist/modules/integrations/api/[id]/credentials/route.js.map +2 -2
  12. package/dist/modules/integrations/lib/credentials-service.js +15 -25
  13. package/dist/modules/integrations/lib/credentials-service.js.map +2 -2
  14. package/dist/modules/planner/components/AvailabilityRulesEditor.js +39 -26
  15. package/dist/modules/planner/components/AvailabilityRulesEditor.js.map +2 -2
  16. package/dist/modules/planner/components/availabilityRulesEditorState.js +8 -0
  17. package/dist/modules/planner/components/availabilityRulesEditorState.js.map +7 -0
  18. package/dist/modules/query_index/acl.js +12 -2
  19. package/dist/modules/query_index/acl.js.map +2 -2
  20. package/dist/modules/resources/acl.js +6 -1
  21. package/dist/modules/resources/acl.js.map +2 -2
  22. package/dist/modules/sync_excel/acl.js +6 -1
  23. package/dist/modules/sync_excel/acl.js.map +2 -2
  24. package/package.json +7 -7
  25. package/src/modules/currencies/acl.ts +5 -0
  26. package/src/modules/directory/acl.ts +12 -2
  27. package/src/modules/directory/api/tenants/route.ts +55 -41
  28. package/src/modules/entities/acl.ts +12 -2
  29. package/src/modules/integrations/api/[id]/credentials/route.ts +21 -3
  30. package/src/modules/integrations/lib/credentials-service.ts +15 -33
  31. package/src/modules/planner/components/AvailabilityRulesEditor.tsx +40 -26
  32. package/src/modules/planner/components/availabilityRulesEditorState.ts +11 -0
  33. package/src/modules/query_index/acl.ts +12 -2
  34. package/src/modules/resources/acl.ts +6 -1
  35. 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
- { id: "query_index.reindex", title: "Trigger reindex", module: "query_index" },
4
- { id: "query_index.purge", title: "Purge index", module: "query_index" }
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 { id: 'query_index.reindex', title: 'Trigger reindex', module: 'query_index' },\n { id: 'query_index.purge', title: 'Purge index', module: 'query_index' },\n]\n\nexport default features\n"],
5
- "mappings": "AAAO,MAAM,WAAW;AAAA,EACtB,EAAE,IAAI,2BAA2B,OAAO,qBAAqB,QAAQ,cAAc;AAAA,EACnF,EAAE,IAAI,uBAAuB,OAAO,mBAAmB,QAAQ,cAAc;AAAA,EAC7E,EAAE,IAAI,qBAAqB,OAAO,eAAe,QAAQ,cAAc;AACzE;AAEA,IAAO,cAAQ;",
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
- { id: "resources.manage_resources", title: "Manage resources", module: "resources" }
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 { id: 'resources.manage_resources', title: 'Manage resources', module: 'resources' },\n]\n\nexport default features\n"],
5
- "mappings": "AAAO,MAAM,WAAW;AAAA,EACtB,EAAE,IAAI,kBAAkB,OAAO,kBAAkB,QAAQ,YAAY;AAAA,EACrE,EAAE,IAAI,8BAA8B,OAAO,oBAAoB,QAAQ,YAAY;AACrF;AAEA,IAAO,cAAQ;",
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
- { id: "sync_excel.run", title: "Upload CSV files and prepare imports", module: "sync_excel" }
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 { id: 'sync_excel.run', title: 'Upload CSV files and prepare imports', module: 'sync_excel' },\n]\n\nexport default features\n"],
5
- "mappings": "AAAO,MAAM,WAAW;AAAA,EACtB,EAAE,IAAI,mBAAmB,OAAO,wCAAwC,QAAQ,aAAa;AAAA,EAC7F,EAAE,IAAI,kBAAkB,OAAO,wCAAwC,QAAQ,aAAa;AAC9F;AAEA,IAAO,cAAQ;",
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.4095.1.9c790dc021",
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.4095.1.9c790dc021",
247
- "@open-mercato/shared": "0.6.4-develop.4095.1.9c790dc021",
248
- "@open-mercato/ui": "0.6.4-develop.4095.1.9c790dc021",
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.4095.1.9c790dc021",
254
- "@open-mercato/shared": "0.6.4-develop.4095.1.9c790dc021",
255
- "@open-mercato/ui": "0.6.4-develop.4095.1.9c790dc021",
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
- { id: 'directory.tenants.manage', title: 'Manage tenants', module: 'directory' },
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
- { id: 'directory.organizations.manage', title: 'Manage organizations', module: 'directory' },
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 filtered = cfFilterEntries.length
179
- ? all.filter((tenant) => {
180
- const rid = String(tenant.id)
181
- const payload = cfValues[rid] ?? {}
182
- return cfFilterEntries.every(([key, expected]) => {
183
- const value = payload[`cf_${key}`]
184
- if (expected && typeof expected === 'object' && !Array.isArray(expected)) {
185
- const maybeIn = (expected as { $in?: unknown[] }).$in
186
- if (Array.isArray(maybeIn)) {
187
- if (value === undefined || value === null) return false
188
- if (Array.isArray(value)) return value.some((val) => maybeIn.includes(val))
189
- return maybeIn.includes(value)
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
- return matchesValue(value, expected)
193
- })
199
+ }
200
+ return matchesValue(value, expected)
194
201
  })
195
- : all
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 total = filtered.length
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
- { id: 'entities.definitions.manage', title: 'Manage custom field definitions', module: 'entities' },
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
- { id: 'entities.records.manage', title: 'Manage records', module: 'entities' },
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 type { CredentialsService } from '../../../lib/credentials-service'
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
- const values = await credentialsService.resolve(integration.id, scope)
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
- await credentialsService.save(integration.id, payloadData.credentials, scope)
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 DERIVED_KEY_CONTEXT = 'integrations.credentials'
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
- return deriveDekFromSecret(resolveFallbackEncryptionSecret(), scope.tenantId)
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,