@open-mercato/shared 0.6.7-develop.6842.1.8cd8a8883c → 0.6.7-develop.6859.1.e4727b568a
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/AGENTS.md +2 -0
- package/dist/lib/auth/server.js +15 -2
- package/dist/lib/auth/server.js.map +2 -2
- package/dist/lib/crud/factory.js +10 -0
- package/dist/lib/crud/factory.js.map +2 -2
- package/dist/lib/db/entityMetadata.js +26 -0
- package/dist/lib/db/entityMetadata.js.map +7 -0
- package/dist/lib/db/pg-errors.js +42 -0
- package/dist/lib/db/pg-errors.js.map +2 -2
- package/dist/lib/encryption/subscriber.js +2 -10
- package/dist/lib/encryption/subscriber.js.map +2 -2
- package/dist/lib/encryption/tenantDataEncryptionService.js +37 -15
- package/dist/lib/encryption/tenantDataEncryptionService.js.map +2 -2
- package/dist/lib/version.js +1 -1
- package/dist/lib/version.js.map +1 -1
- package/dist/modules/registry.js +1 -0
- package/dist/modules/registry.js.map +2 -2
- package/package.json +2 -2
- package/src/lib/auth/__tests__/server.test.ts +23 -0
- package/src/lib/auth/server.ts +23 -2
- package/src/lib/crud/__tests__/crud-factory.test.ts +12 -0
- package/src/lib/crud/factory.ts +13 -0
- package/src/lib/db/__tests__/entityMetadata.test.ts +38 -0
- package/src/lib/db/__tests__/pg-errors.test.ts +48 -0
- package/src/lib/db/entityMetadata.ts +43 -0
- package/src/lib/db/pg-errors.ts +58 -0
- package/src/lib/encryption/__tests__/subscriber.test.ts +54 -2
- package/src/lib/encryption/__tests__/tenantDataEncryptionService.test.ts +58 -0
- package/src/lib/encryption/subscriber.ts +2 -14
- package/src/lib/encryption/tenantDataEncryptionService.ts +48 -16
- package/src/modules/__tests__/registry.test.ts +10 -2
- package/src/modules/encryption.ts +3 -0
- package/src/modules/registry.ts +1 -0
|
@@ -6,6 +6,7 @@ import { isTenantDataEncryptionEnabled } from './toggles'
|
|
|
6
6
|
import { isEncryptionDebugEnabled } from './toggles'
|
|
7
7
|
import { resolveTenantEncryptionService } from './customFieldValues'
|
|
8
8
|
import { createLogger } from '../logger'
|
|
9
|
+
import { listEntityMetadataFromRegistry } from '../db/entityMetadata'
|
|
9
10
|
|
|
10
11
|
const logger = createLogger('shared').child({ component: 'encryption' })
|
|
11
12
|
|
|
@@ -85,13 +86,8 @@ export class TenantEncryptionSubscriber implements EventSubscriber<any> {
|
|
|
85
86
|
try { return registry.find?.(ctor) } catch {}
|
|
86
87
|
try { return registry.get?.(name) } catch {}
|
|
87
88
|
try { return registry.get?.(ctor) } catch {}
|
|
88
|
-
const all =
|
|
89
|
-
(typeof registry.getAll === 'function' && registry.getAll()) ||
|
|
90
|
-
(Array.isArray((registry as any).metadata) ? (registry as any).metadata : undefined) ||
|
|
91
|
-
(registry as any).metadata ||
|
|
92
|
-
{}
|
|
93
89
|
try {
|
|
94
|
-
const entries =
|
|
90
|
+
const entries = listEntityMetadataFromRegistry(registry)
|
|
95
91
|
const match = entries.find(
|
|
96
92
|
(m: any) =>
|
|
97
93
|
m?.className === name ||
|
|
@@ -224,10 +220,6 @@ export class TenantEncryptionSubscriber implements EventSubscriber<any> {
|
|
|
224
220
|
return
|
|
225
221
|
}
|
|
226
222
|
const { tenantId, organizationId } = resolveScope(target)
|
|
227
|
-
if (!tenantId) {
|
|
228
|
-
debug('⚪️ subscriber.skip', { reason: 'no-tenant', entityId })
|
|
229
|
-
return
|
|
230
|
-
}
|
|
231
223
|
const encrypted = await this.service.encryptEntityPayload(entityId, target, tenantId, organizationId)
|
|
232
224
|
const metaProps: Record<string, unknown> = resolvedMeta?.properties && typeof resolvedMeta.properties === 'object'
|
|
233
225
|
? resolvedMeta.properties
|
|
@@ -325,10 +317,6 @@ export class TenantEncryptionSubscriber implements EventSubscriber<any> {
|
|
|
325
317
|
const { tenantId, organizationId } = resolveScope(target)
|
|
326
318
|
const scopedTenantId = tenantId ?? fallbackScope?.tenantId ?? null
|
|
327
319
|
const scopedOrgId = organizationId ?? fallbackScope?.organizationId ?? null
|
|
328
|
-
if (!scopedTenantId) {
|
|
329
|
-
debug('⚪️ subscriber.skip', { reason: 'no-tenant', entityId })
|
|
330
|
-
return
|
|
331
|
-
}
|
|
332
320
|
// Capture pending (un-flushed) changes BEFORE decrypt mutates the target. Re-baselining a
|
|
333
321
|
// managed entity that a command already mutated would clear its dirty changeset and silently
|
|
334
322
|
// drop the pending write (e.g. an undo handler that mutates an entity, then loads a related
|
|
@@ -4,6 +4,7 @@ import { decryptWithAesGcm, encryptWithAesGcm, hashForLookup } from './aes'
|
|
|
4
4
|
import { createKmsService, type KmsService, type TenantDek } from './kms'
|
|
5
5
|
import { isTenantDataEncryptionEnabled, isEncryptionDebugEnabled } from './toggles'
|
|
6
6
|
import { createLogger } from '../logger'
|
|
7
|
+
import type { EncryptionKeyScope, ModuleEncryptionMap } from '../../modules/encryption'
|
|
7
8
|
|
|
8
9
|
const logger = createLogger('shared').child({ component: 'tenant-encryption' })
|
|
9
10
|
|
|
@@ -14,6 +15,7 @@ export type EncryptedFieldRule = {
|
|
|
14
15
|
|
|
15
16
|
export type EncryptionMapRecord = {
|
|
16
17
|
entityId: string
|
|
18
|
+
keyScope?: EncryptionKeyScope
|
|
17
19
|
fields: EncryptedFieldRule[]
|
|
18
20
|
}
|
|
19
21
|
|
|
@@ -154,13 +156,23 @@ export class TenantDataEncryptionService {
|
|
|
154
156
|
private readonly inflightDeks = TenantDataEncryptionService.globalInflightDeks
|
|
155
157
|
private readonly inflightMaps = TenantDataEncryptionService.globalInflightMaps
|
|
156
158
|
private readonly missCache = TenantDataEncryptionService.globalMissCache
|
|
159
|
+
private readonly systemDefaultMaps: Map<string, ModuleEncryptionMap>
|
|
157
160
|
|
|
158
161
|
constructor(
|
|
159
162
|
private em: EntityManager,
|
|
160
|
-
opts?: {
|
|
163
|
+
opts?: {
|
|
164
|
+
cache?: CacheStrategy
|
|
165
|
+
kms?: KmsService
|
|
166
|
+
defaultEncryptionMaps?: readonly ModuleEncryptionMap[]
|
|
167
|
+
}
|
|
161
168
|
) {
|
|
162
169
|
this.cache = opts?.cache
|
|
163
170
|
this.kms = opts?.kms ?? createKmsService()
|
|
171
|
+
this.systemDefaultMaps = new Map(
|
|
172
|
+
(opts?.defaultEncryptionMaps ?? [])
|
|
173
|
+
.filter((map) => map.keyScope === 'system')
|
|
174
|
+
.map((map) => [map.entityId, map]),
|
|
175
|
+
)
|
|
164
176
|
}
|
|
165
177
|
|
|
166
178
|
isEnabled(): boolean {
|
|
@@ -239,6 +251,24 @@ export class TenantDataEncryptionService {
|
|
|
239
251
|
}
|
|
240
252
|
}
|
|
241
253
|
|
|
254
|
+
private applySystemDefault(record: EncryptionMapRecord | null, entityId: string): EncryptionMapRecord | null {
|
|
255
|
+
const declared = this.systemDefaultMaps.get(entityId)
|
|
256
|
+
if (!declared) return record
|
|
257
|
+
const fields: EncryptedFieldRule[] = declared.fields.map((field) => ({
|
|
258
|
+
field: field.field,
|
|
259
|
+
hashField: field.hashField ?? null,
|
|
260
|
+
}))
|
|
261
|
+
const declaredFields = new Set(fields.map((field) => field.field))
|
|
262
|
+
for (const field of record?.fields ?? []) {
|
|
263
|
+
if (!declaredFields.has(field.field)) fields.push(field)
|
|
264
|
+
}
|
|
265
|
+
return {
|
|
266
|
+
entityId,
|
|
267
|
+
keyScope: 'system',
|
|
268
|
+
fields,
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
242
272
|
private async getMap(key: MapCacheKey): Promise<EncryptionMapRecord | null> {
|
|
243
273
|
const shouldSkipLookup = (tag: string) => {
|
|
244
274
|
const expiresAt = this.missCache.get(tag)
|
|
@@ -262,13 +292,13 @@ export class TenantDataEncryptionService {
|
|
|
262
292
|
if (this.inflightMaps.has(tag)) {
|
|
263
293
|
const pending = this.inflightMaps.get(tag)!
|
|
264
294
|
const resolved = await pending
|
|
265
|
-
if (resolved) return resolved
|
|
295
|
+
if (resolved) return this.applySystemDefault(resolved, key.entityId)
|
|
266
296
|
}
|
|
267
297
|
const mem = this.memoryCache.get(tag)
|
|
268
|
-
if (mem) return mem
|
|
298
|
+
if (mem) return this.applySystemDefault(mem, key.entityId)
|
|
269
299
|
if (this.cache && typeof this.cache.get === 'function') {
|
|
270
300
|
const cached = await this.cache.get(tag)
|
|
271
|
-
if (cached) return cached as EncryptionMapRecord
|
|
301
|
+
if (cached) return this.applySystemDefault(cached as EncryptionMapRecord, key.entityId)
|
|
272
302
|
}
|
|
273
303
|
const pending = this.fetchMap(candidate)
|
|
274
304
|
this.inflightMaps.set(tag, pending)
|
|
@@ -288,9 +318,9 @@ export class TenantDataEncryptionService {
|
|
|
288
318
|
if (this.cache && typeof this.cache.set === 'function') {
|
|
289
319
|
await this.cache.set(tag, loaded, { ttl: 300 })
|
|
290
320
|
}
|
|
291
|
-
return loaded
|
|
321
|
+
return this.applySystemDefault(loaded, key.entityId)
|
|
292
322
|
}
|
|
293
|
-
return null
|
|
323
|
+
return this.applySystemDefault(null, key.entityId)
|
|
294
324
|
}
|
|
295
325
|
|
|
296
326
|
private async fetchAllOrganizationFieldNames(entityId: string, tenantId: string | null): Promise<string[]> {
|
|
@@ -435,16 +465,17 @@ export class TenantDataEncryptionService {
|
|
|
435
465
|
debug('⚪️ encrypt.skip.disabled', { entityId, tenantId })
|
|
436
466
|
return payload
|
|
437
467
|
}
|
|
438
|
-
const dek = await this.resolveDekForEncrypt(tenantId ?? null)
|
|
439
|
-
if (!dek) {
|
|
440
|
-
debug('⚠️ encrypt.skip.no-dek', { entityId, tenantId })
|
|
441
|
-
return payload
|
|
442
|
-
}
|
|
443
468
|
const map = await this.getMap({ entityId, tenantId: tenantId ?? null, organizationId: organizationId ?? null })
|
|
444
469
|
if (!map || !map.fields?.length) {
|
|
445
470
|
debug('⚪️ encrypt.skip.no-map', { entityId, tenantId })
|
|
446
471
|
return payload
|
|
447
472
|
}
|
|
473
|
+
const keyId = map.keyScope === 'system' ? `system:${entityId}` : tenantId ?? null
|
|
474
|
+
const dek = await this.resolveDekForEncrypt(keyId)
|
|
475
|
+
if (!dek) {
|
|
476
|
+
debug('⚠️ encrypt.skip.no-dek', { entityId, tenantId, keyScope: map.keyScope ?? 'tenant' })
|
|
477
|
+
return payload
|
|
478
|
+
}
|
|
448
479
|
debug('🔒 encrypt_entity', { entityId, tenantId, organizationId, fields: map.fields.length })
|
|
449
480
|
return this.encryptFields(payload, map.fields, dek)
|
|
450
481
|
}
|
|
@@ -459,16 +490,17 @@ export class TenantDataEncryptionService {
|
|
|
459
490
|
debug('⚪️ decrypt.skip.disabled', { entityId, tenantId })
|
|
460
491
|
return payload
|
|
461
492
|
}
|
|
462
|
-
const dek = await this.getDek(tenantId ?? null)
|
|
463
|
-
if (!dek) {
|
|
464
|
-
debug('⚠️ decrypt.skip.no-dek', { entityId, tenantId })
|
|
465
|
-
return payload
|
|
466
|
-
}
|
|
467
493
|
const map = await this.getMap({ entityId, tenantId: tenantId ?? null, organizationId: organizationId ?? null })
|
|
468
494
|
if (!map || !map.fields?.length) {
|
|
469
495
|
debug('⚪️ decrypt.skip.no-map', { entityId, tenantId })
|
|
470
496
|
return payload
|
|
471
497
|
}
|
|
498
|
+
const keyId = map.keyScope === 'system' ? `system:${entityId}` : tenantId ?? null
|
|
499
|
+
const dek = await this.getDek(keyId)
|
|
500
|
+
if (!dek) {
|
|
501
|
+
debug('⚠️ decrypt.skip.no-dek', { entityId, tenantId, keyScope: map.keyScope ?? 'tenant' })
|
|
502
|
+
return payload
|
|
503
|
+
}
|
|
472
504
|
debug('🔓 decrypt_entity', { entityId, tenantId, organizationId, fields: map.fields.length })
|
|
473
505
|
return this.decryptFields(payload, map.fields, dek)
|
|
474
506
|
}
|
|
@@ -94,7 +94,11 @@ describe('CLI Modules Registry', () => {
|
|
|
94
94
|
{
|
|
95
95
|
id: 'auth',
|
|
96
96
|
defaultEncryptionMaps: [
|
|
97
|
-
{
|
|
97
|
+
{
|
|
98
|
+
entityId: 'auth:user',
|
|
99
|
+
keyScope: 'system',
|
|
100
|
+
fields: [{ field: 'email', hashField: 'email_hash' }],
|
|
101
|
+
},
|
|
98
102
|
],
|
|
99
103
|
},
|
|
100
104
|
{
|
|
@@ -106,7 +110,11 @@ describe('CLI Modules Registry', () => {
|
|
|
106
110
|
])
|
|
107
111
|
|
|
108
112
|
expect(maps).toEqual([
|
|
109
|
-
{
|
|
113
|
+
{
|
|
114
|
+
entityId: 'auth:user',
|
|
115
|
+
keyScope: 'system',
|
|
116
|
+
fields: [{ field: 'email', hashField: 'email_hash' }],
|
|
117
|
+
},
|
|
110
118
|
{ entityId: 'customers:customer_comment', fields: [{ field: 'body', hashField: null }] },
|
|
111
119
|
])
|
|
112
120
|
})
|
|
@@ -3,7 +3,10 @@ export type ModuleEncryptionFieldRule = {
|
|
|
3
3
|
hashField?: string | null
|
|
4
4
|
}
|
|
5
5
|
|
|
6
|
+
export type EncryptionKeyScope = 'tenant' | 'system'
|
|
7
|
+
|
|
6
8
|
export type ModuleEncryptionMap = {
|
|
7
9
|
entityId: string
|
|
10
|
+
keyScope?: EncryptionKeyScope
|
|
8
11
|
fields: ModuleEncryptionFieldRule[]
|
|
9
12
|
}
|
package/src/modules/registry.ts
CHANGED
|
@@ -515,6 +515,7 @@ export function getDefaultEncryptionMaps(modules: Module[]): import('./encryptio
|
|
|
515
515
|
moduleId: mod.id,
|
|
516
516
|
map: {
|
|
517
517
|
entityId: entry.entityId,
|
|
518
|
+
...(entry.keyScope ? { keyScope: entry.keyScope } : {}),
|
|
518
519
|
fields: entry.fields.map((field) => ({
|
|
519
520
|
field: field.field,
|
|
520
521
|
hashField: field.hashField ?? null,
|