@open-mercato/shared 0.4.2-canary-c02407ff85
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/build.mjs +101 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +7 -0
- package/dist/lib/api/crud.js +47 -0
- package/dist/lib/api/crud.js.map +7 -0
- package/dist/lib/api/scoped.js +140 -0
- package/dist/lib/api/scoped.js.map +7 -0
- package/dist/lib/auth/jwt.js +34 -0
- package/dist/lib/auth/jwt.js.map +7 -0
- package/dist/lib/auth/server.js +157 -0
- package/dist/lib/auth/server.js.map +7 -0
- package/dist/lib/boolean.js +22 -0
- package/dist/lib/boolean.js.map +7 -0
- package/dist/lib/bootstrap/appResolver.js +43 -0
- package/dist/lib/bootstrap/appResolver.js.map +7 -0
- package/dist/lib/bootstrap/dynamicLoader.js +108 -0
- package/dist/lib/bootstrap/dynamicLoader.js.map +7 -0
- package/dist/lib/bootstrap/factory.js +59 -0
- package/dist/lib/bootstrap/factory.js.map +7 -0
- package/dist/lib/bootstrap/index.js +11 -0
- package/dist/lib/bootstrap/index.js.map +7 -0
- package/dist/lib/bootstrap/types.js +1 -0
- package/dist/lib/bootstrap/types.js.map +7 -0
- package/dist/lib/cache/segments.js +36 -0
- package/dist/lib/cache/segments.js.map +7 -0
- package/dist/lib/cli/progress.js +46 -0
- package/dist/lib/cli/progress.js.map +7 -0
- package/dist/lib/commands/command-bus.js +285 -0
- package/dist/lib/commands/command-bus.js.map +7 -0
- package/dist/lib/commands/customFieldSnapshots.js +66 -0
- package/dist/lib/commands/customFieldSnapshots.js.map +7 -0
- package/dist/lib/commands/helpers.js +98 -0
- package/dist/lib/commands/helpers.js.map +7 -0
- package/dist/lib/commands/index.js +8 -0
- package/dist/lib/commands/index.js.map +7 -0
- package/dist/lib/commands/operationMetadata.js +32 -0
- package/dist/lib/commands/operationMetadata.js.map +7 -0
- package/dist/lib/commands/registry.js +43 -0
- package/dist/lib/commands/registry.js.map +7 -0
- package/dist/lib/commands/scope.js +44 -0
- package/dist/lib/commands/scope.js.map +7 -0
- package/dist/lib/commands/types.js +8 -0
- package/dist/lib/commands/types.js.map +7 -0
- package/dist/lib/crud/cache-stats.js +98 -0
- package/dist/lib/crud/cache-stats.js.map +7 -0
- package/dist/lib/crud/cache.js +175 -0
- package/dist/lib/crud/cache.js.map +7 -0
- package/dist/lib/crud/custom-fields-client.js +52 -0
- package/dist/lib/crud/custom-fields-client.js.map +7 -0
- package/dist/lib/crud/custom-fields.js +467 -0
- package/dist/lib/crud/custom-fields.js.map +7 -0
- package/dist/lib/crud/errors.js +24 -0
- package/dist/lib/crud/errors.js.map +7 -0
- package/dist/lib/crud/exporters.js +154 -0
- package/dist/lib/crud/exporters.js.map +7 -0
- package/dist/lib/crud/factory.js +1311 -0
- package/dist/lib/crud/factory.js.map +7 -0
- package/dist/lib/crud/types.js +1 -0
- package/dist/lib/crud/types.js.map +7 -0
- package/dist/lib/custom-fields/normalize.js +36 -0
- package/dist/lib/custom-fields/normalize.js.map +7 -0
- package/dist/lib/data/engine.js +396 -0
- package/dist/lib/data/engine.js.map +7 -0
- package/dist/lib/db/escapeLikePattern.js +5 -0
- package/dist/lib/db/escapeLikePattern.js.map +7 -0
- package/dist/lib/db/mikro.js +82 -0
- package/dist/lib/db/mikro.js.map +7 -0
- package/dist/lib/di/container.js +94 -0
- package/dist/lib/di/container.js.map +7 -0
- package/dist/lib/email/send.js +12 -0
- package/dist/lib/email/send.js.map +7 -0
- package/dist/lib/encryption/aes.js +58 -0
- package/dist/lib/encryption/aes.js.map +7 -0
- package/dist/lib/encryption/customFieldValues.js +49 -0
- package/dist/lib/encryption/customFieldValues.js.map +7 -0
- package/dist/lib/encryption/entityFields.js +26 -0
- package/dist/lib/encryption/entityFields.js.map +7 -0
- package/dist/lib/encryption/entityIds.js +80 -0
- package/dist/lib/encryption/entityIds.js.map +7 -0
- package/dist/lib/encryption/find.js +45 -0
- package/dist/lib/encryption/find.js.map +7 -0
- package/dist/lib/encryption/indexDoc.js +69 -0
- package/dist/lib/encryption/indexDoc.js.map +7 -0
- package/dist/lib/encryption/kms.js +282 -0
- package/dist/lib/encryption/kms.js.map +7 -0
- package/dist/lib/encryption/subscriber.js +330 -0
- package/dist/lib/encryption/subscriber.js.map +7 -0
- package/dist/lib/encryption/tenantDataEncryptionService.js +252 -0
- package/dist/lib/encryption/tenantDataEncryptionService.js.map +7 -0
- package/dist/lib/encryption/toggles.js +18 -0
- package/dist/lib/encryption/toggles.js.map +7 -0
- package/dist/lib/entities/naming.js +9 -0
- package/dist/lib/entities/naming.js.map +7 -0
- package/dist/lib/entities/system-entities.js +43 -0
- package/dist/lib/entities/system-entities.js.map +7 -0
- package/dist/lib/frontend/organizationEvents.js +41 -0
- package/dist/lib/frontend/organizationEvents.js.map +7 -0
- package/dist/lib/frontend/useOrganizationScope.js +32 -0
- package/dist/lib/frontend/useOrganizationScope.js.map +7 -0
- package/dist/lib/hotkeys/index.js +128 -0
- package/dist/lib/hotkeys/index.js.map +7 -0
- package/dist/lib/i18n/app-dictionaries.js +17 -0
- package/dist/lib/i18n/app-dictionaries.js.map +7 -0
- package/dist/lib/i18n/config.js +7 -0
- package/dist/lib/i18n/config.js.map +7 -0
- package/dist/lib/i18n/context.js +50 -0
- package/dist/lib/i18n/context.js.map +7 -0
- package/dist/lib/i18n/server.js +68 -0
- package/dist/lib/i18n/server.js.map +7 -0
- package/dist/lib/i18n/translate.js +45 -0
- package/dist/lib/i18n/translate.js.map +7 -0
- package/dist/lib/indexers/error-log.js +82 -0
- package/dist/lib/indexers/error-log.js.map +7 -0
- package/dist/lib/indexers/status-log.js +80 -0
- package/dist/lib/indexers/status-log.js.map +7 -0
- package/dist/lib/lib/auth/jwt.js +34 -0
- package/dist/lib/lib/auth/jwt.js.map +7 -0
- package/dist/lib/lib/auth/server.js +77 -0
- package/dist/lib/lib/auth/server.js.map +7 -0
- package/dist/lib/lib/email/send.js +12 -0
- package/dist/lib/lib/email/send.js.map +7 -0
- package/dist/lib/lib/i18n/config.js +7 -0
- package/dist/lib/lib/i18n/config.js.map +7 -0
- package/dist/lib/lib/i18n/context.js +31 -0
- package/dist/lib/lib/i18n/context.js.map +7 -0
- package/dist/lib/lib/utils.js +9 -0
- package/dist/lib/lib/utils.js.map +7 -0
- package/dist/lib/location/countries.js +68 -0
- package/dist/lib/location/countries.js.map +7 -0
- package/dist/lib/modules/index.js +6 -0
- package/dist/lib/modules/index.js.map +7 -0
- package/dist/lib/modules/registry.js +18 -0
- package/dist/lib/modules/registry.js.map +7 -0
- package/dist/lib/openapi/crud.js +137 -0
- package/dist/lib/openapi/crud.js.map +7 -0
- package/dist/lib/openapi/generator.js +1131 -0
- package/dist/lib/openapi/generator.js.map +7 -0
- package/dist/lib/openapi/index.js +10 -0
- package/dist/lib/openapi/index.js.map +7 -0
- package/dist/lib/openapi/sanitize.js +110 -0
- package/dist/lib/openapi/sanitize.js.map +7 -0
- package/dist/lib/openapi/types.js +1 -0
- package/dist/lib/openapi/types.js.map +7 -0
- package/dist/lib/profiler/index.js +258 -0
- package/dist/lib/profiler/index.js.map +7 -0
- package/dist/lib/query/engine.js +729 -0
- package/dist/lib/query/engine.js.map +7 -0
- package/dist/lib/query/join-utils.js +195 -0
- package/dist/lib/query/join-utils.js.map +7 -0
- package/dist/lib/query/types.js +9 -0
- package/dist/lib/query/types.js.map +7 -0
- package/dist/lib/search/config.js +32 -0
- package/dist/lib/search/config.js.map +7 -0
- package/dist/lib/search/tokenize.js +34 -0
- package/dist/lib/search/tokenize.js.map +7 -0
- package/dist/lib/slugify.js +24 -0
- package/dist/lib/slugify.js.map +7 -0
- package/dist/lib/testing/bootstrap.js +51 -0
- package/dist/lib/testing/bootstrap.js.map +7 -0
- package/dist/lib/testing/index.js +17 -0
- package/dist/lib/testing/index.js.map +7 -0
- package/dist/lib/testing/renderWithProviders.js +15 -0
- package/dist/lib/testing/renderWithProviders.js.map +7 -0
- package/dist/lib/url.js +12 -0
- package/dist/lib/url.js.map +7 -0
- package/dist/lib/utils.js +13 -0
- package/dist/lib/utils.js.map +7 -0
- package/dist/lib/version.js +7 -0
- package/dist/lib/version.js.map +7 -0
- package/dist/modules/dashboard/widgets.js +1 -0
- package/dist/modules/dashboard/widgets.js.map +7 -0
- package/dist/modules/dsl.js +30 -0
- package/dist/modules/dsl.js.map +7 -0
- package/dist/modules/entities/kinds.js +22 -0
- package/dist/modules/entities/kinds.js.map +7 -0
- package/dist/modules/entities/options.js +26 -0
- package/dist/modules/entities/options.js.map +7 -0
- package/dist/modules/entities/validation.js +102 -0
- package/dist/modules/entities/validation.js.map +7 -0
- package/dist/modules/entities/validators.js +88 -0
- package/dist/modules/entities/validators.js.map +7 -0
- package/dist/modules/entities.js +1 -0
- package/dist/modules/entities.js.map +7 -0
- package/dist/modules/navigation/sidebarPreferences.js +50 -0
- package/dist/modules/navigation/sidebarPreferences.js.map +7 -0
- package/dist/modules/perspectives/types.js +1 -0
- package/dist/modules/perspectives/types.js.map +7 -0
- package/dist/modules/registry.js +96 -0
- package/dist/modules/registry.js.map +7 -0
- package/dist/modules/search.js +15 -0
- package/dist/modules/search.js.map +7 -0
- package/dist/modules/vector.js +1 -0
- package/dist/modules/vector.js.map +7 -0
- package/dist/modules/widgets/injection-loader.js +180 -0
- package/dist/modules/widgets/injection-loader.js.map +7 -0
- package/dist/modules/widgets/injection.js +1 -0
- package/dist/modules/widgets/injection.js.map +7 -0
- package/dist/security/features.js +23 -0
- package/dist/security/features.js.map +7 -0
- package/dist/types/pg.d.js +1 -0
- package/dist/types/pg.d.js.map +7 -0
- package/dist/types/react-email.d.js +1 -0
- package/dist/types/react-email.d.js.map +7 -0
- package/dist/types/resend.d.js +1 -0
- package/dist/types/resend.d.js.map +7 -0
- package/jest.config.cjs +22 -0
- package/package.json +88 -0
- package/src/index.ts +0 -0
- package/src/lib/api/__tests__/scoped.test.ts +38 -0
- package/src/lib/api/crud.ts +59 -0
- package/src/lib/api/scoped.ts +239 -0
- package/src/lib/auth/jwt.ts +39 -0
- package/src/lib/auth/server.ts +199 -0
- package/src/lib/boolean.ts +17 -0
- package/src/lib/bootstrap/appResolver.ts +85 -0
- package/src/lib/bootstrap/dynamicLoader.ts +177 -0
- package/src/lib/bootstrap/factory.ts +108 -0
- package/src/lib/bootstrap/index.ts +23 -0
- package/src/lib/bootstrap/types.ts +31 -0
- package/src/lib/cache/segments.ts +56 -0
- package/src/lib/cli/progress.ts +55 -0
- package/src/lib/commands/__tests__/command-bus.test.ts +84 -0
- package/src/lib/commands/__tests__/helpers.test.ts +42 -0
- package/src/lib/commands/command-bus.ts +349 -0
- package/src/lib/commands/customFieldSnapshots.ts +86 -0
- package/src/lib/commands/helpers.ts +143 -0
- package/src/lib/commands/index.ts +4 -0
- package/src/lib/commands/operationMetadata.ts +40 -0
- package/src/lib/commands/registry.ts +46 -0
- package/src/lib/commands/scope.ts +59 -0
- package/src/lib/commands/types.ts +63 -0
- package/src/lib/crud/__tests__/crud-factory.test.ts +333 -0
- package/src/lib/crud/__tests__/custom-fields.test.ts +150 -0
- package/src/lib/crud/cache-stats.ts +127 -0
- package/src/lib/crud/cache.ts +205 -0
- package/src/lib/crud/custom-fields-client.ts +54 -0
- package/src/lib/crud/custom-fields.ts +607 -0
- package/src/lib/crud/errors.ts +23 -0
- package/src/lib/crud/exporters.ts +188 -0
- package/src/lib/crud/factory.ts +1622 -0
- package/src/lib/crud/types.ts +29 -0
- package/src/lib/custom-fields/normalize.ts +45 -0
- package/src/lib/data/engine.ts +562 -0
- package/src/lib/db/escapeLikePattern.ts +2 -0
- package/src/lib/db/mikro.ts +100 -0
- package/src/lib/di/container.ts +105 -0
- package/src/lib/email/send.ts +18 -0
- package/src/lib/encryption/__tests__/customFieldValues.test.ts +63 -0
- package/src/lib/encryption/__tests__/indexDoc.test.ts +115 -0
- package/src/lib/encryption/aes.ts +64 -0
- package/src/lib/encryption/customFieldValues.ts +67 -0
- package/src/lib/encryption/entityFields.ts +39 -0
- package/src/lib/encryption/entityIds.ts +107 -0
- package/src/lib/encryption/find.ts +81 -0
- package/src/lib/encryption/indexDoc.ts +104 -0
- package/src/lib/encryption/kms.ts +337 -0
- package/src/lib/encryption/subscriber.ts +416 -0
- package/src/lib/encryption/tenantDataEncryptionService.ts +313 -0
- package/src/lib/encryption/toggles.ts +15 -0
- package/src/lib/entities/naming.ts +6 -0
- package/src/lib/entities/system-entities.ts +43 -0
- package/src/lib/frontend/organizationEvents.ts +55 -0
- package/src/lib/frontend/useOrganizationScope.ts +30 -0
- package/src/lib/hotkeys/index.ts +168 -0
- package/src/lib/i18n/app-dictionaries.ts +18 -0
- package/src/lib/i18n/config.ts +4 -0
- package/src/lib/i18n/context.tsx +66 -0
- package/src/lib/i18n/server.ts +74 -0
- package/src/lib/i18n/translate.ts +54 -0
- package/src/lib/indexers/error-log.ts +106 -0
- package/src/lib/indexers/status-log.ts +119 -0
- package/src/lib/lib/auth/jwt.ts +39 -0
- package/src/lib/lib/auth/server.ts +94 -0
- package/src/lib/lib/email/send.ts +18 -0
- package/src/lib/lib/i18n/config.ts +4 -0
- package/src/lib/lib/i18n/context.tsx +38 -0
- package/src/lib/lib/utils.ts +6 -0
- package/src/lib/location/countries.ts +97 -0
- package/src/lib/modules/index.ts +1 -0
- package/src/lib/modules/registry.ts +18 -0
- package/src/lib/openapi/crud.ts +218 -0
- package/src/lib/openapi/generator.ts +1311 -0
- package/src/lib/openapi/index.ts +4 -0
- package/src/lib/openapi/sanitize.ts +137 -0
- package/src/lib/openapi/types.ts +79 -0
- package/src/lib/profiler/index.ts +371 -0
- package/src/lib/query/__tests__/engine.test.ts +274 -0
- package/src/lib/query/engine.ts +837 -0
- package/src/lib/query/join-utils.ts +238 -0
- package/src/lib/query/types.ts +121 -0
- package/src/lib/search/config.ts +49 -0
- package/src/lib/search/tokenize.ts +45 -0
- package/src/lib/slugify.ts +28 -0
- package/src/lib/testing/bootstrap.ts +124 -0
- package/src/lib/testing/index.ts +15 -0
- package/src/lib/testing/renderWithProviders.tsx +31 -0
- package/src/lib/url.ts +12 -0
- package/src/lib/utils.ts +17 -0
- package/src/lib/version.ts +5 -0
- package/src/modules/__tests__/dsl.test.ts +35 -0
- package/src/modules/__tests__/registry.test.ts +300 -0
- package/src/modules/dashboard/widgets.ts +57 -0
- package/src/modules/dsl.ts +32 -0
- package/src/modules/entities/__tests__/validation.test.ts +52 -0
- package/src/modules/entities/kinds.ts +20 -0
- package/src/modules/entities/options.ts +36 -0
- package/src/modules/entities/validation.ts +118 -0
- package/src/modules/entities/validators.ts +93 -0
- package/src/modules/entities.ts +102 -0
- package/src/modules/navigation/sidebarPreferences.ts +62 -0
- package/src/modules/perspectives/types.ts +40 -0
- package/src/modules/registry.ts +249 -0
- package/src/modules/search.ts +325 -0
- package/src/modules/vector.ts +122 -0
- package/src/modules/widgets/__tests__/injection.test.ts +48 -0
- package/src/modules/widgets/injection-loader.ts +235 -0
- package/src/modules/widgets/injection.ts +120 -0
- package/src/security/features.ts +22 -0
- package/src/types/pg.d.ts +2 -0
- package/src/types/react-email.d.ts +2 -0
- package/src/types/resend.d.ts +2 -0
- package/tsconfig.build.json +11 -0
- package/tsconfig.json +9 -0
- package/watch.mjs +6 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
EntityManager,
|
|
3
|
+
EntityName,
|
|
4
|
+
FilterQuery,
|
|
5
|
+
FindOneOptions,
|
|
6
|
+
FindOptions,
|
|
7
|
+
} from '@mikro-orm/postgresql'
|
|
8
|
+
import { decryptEntitiesWithFallbackScope } from './subscriber'
|
|
9
|
+
import type { TenantDataEncryptionService } from './tenantDataEncryptionService'
|
|
10
|
+
|
|
11
|
+
export type DecryptionScope = {
|
|
12
|
+
tenantId?: string | null
|
|
13
|
+
organizationId?: string | null
|
|
14
|
+
encryptionService?: TenantDataEncryptionService | null
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type AnyFindOptions<Entity extends object, Hint extends string = any> = FindOptions<Entity, Hint, any, any>
|
|
18
|
+
type AnyFindOneOptions<Entity extends object, Hint extends string = any> = FindOneOptions<Entity, Hint, any, any>
|
|
19
|
+
|
|
20
|
+
export async function findWithDecryption<Entity extends object, Hint extends string = any>(
|
|
21
|
+
em: EntityManager,
|
|
22
|
+
entityName: EntityName<Entity>,
|
|
23
|
+
where: FilterQuery<Entity>,
|
|
24
|
+
options?: AnyFindOptions<Entity, Hint>,
|
|
25
|
+
scope?: DecryptionScope,
|
|
26
|
+
): Promise<Entity[]> {
|
|
27
|
+
const records = (await em.find<Entity, Hint, any, any>(entityName as any, where as any, options as any)) as any as
|
|
28
|
+
| Entity[]
|
|
29
|
+
| undefined
|
|
30
|
+
if (!Array.isArray(records) || records.length === 0) return records ?? []
|
|
31
|
+
await decryptEntitiesWithFallbackScope(records, {
|
|
32
|
+
em,
|
|
33
|
+
tenantId: scope?.tenantId ?? null,
|
|
34
|
+
organizationId: scope?.organizationId ?? null,
|
|
35
|
+
encryptionService: scope?.encryptionService ?? null,
|
|
36
|
+
})
|
|
37
|
+
return records
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function findOneWithDecryption<Entity extends object, Hint extends string = any>(
|
|
41
|
+
em: EntityManager,
|
|
42
|
+
entityName: EntityName<Entity>,
|
|
43
|
+
where: FilterQuery<Entity>,
|
|
44
|
+
options?: AnyFindOneOptions<Entity, Hint>,
|
|
45
|
+
scope?: DecryptionScope,
|
|
46
|
+
): Promise<Entity | null> {
|
|
47
|
+
const record = (await em.findOne<Entity, Hint, any, any>(entityName as any, where as any, options as any)) as any as
|
|
48
|
+
| Entity
|
|
49
|
+
| null
|
|
50
|
+
if (!record) return record
|
|
51
|
+
await decryptEntitiesWithFallbackScope(record, {
|
|
52
|
+
em,
|
|
53
|
+
tenantId: scope?.tenantId ?? null,
|
|
54
|
+
organizationId: scope?.organizationId ?? null,
|
|
55
|
+
encryptionService: scope?.encryptionService ?? null,
|
|
56
|
+
})
|
|
57
|
+
return record
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function findAndCountWithDecryption<Entity extends object, Hint extends string = any>(
|
|
61
|
+
em: EntityManager,
|
|
62
|
+
entityName: EntityName<Entity>,
|
|
63
|
+
where: FilterQuery<Entity>,
|
|
64
|
+
options?: AnyFindOptions<Entity, Hint>,
|
|
65
|
+
scope?: DecryptionScope,
|
|
66
|
+
): Promise<[Entity[], number]> {
|
|
67
|
+
const [recordsRaw, count] = await em.findAndCount<Entity, Hint, any, any>(
|
|
68
|
+
entityName as any,
|
|
69
|
+
where as any,
|
|
70
|
+
options as any,
|
|
71
|
+
) as any as [Entity[] | undefined, number]
|
|
72
|
+
const records = Array.isArray(recordsRaw) ? recordsRaw : []
|
|
73
|
+
if (!records.length) return [records, count]
|
|
74
|
+
await decryptEntitiesWithFallbackScope(records, {
|
|
75
|
+
em,
|
|
76
|
+
tenantId: scope?.tenantId ?? null,
|
|
77
|
+
organizationId: scope?.organizationId ?? null,
|
|
78
|
+
encryptionService: scope?.encryptionService ?? null,
|
|
79
|
+
})
|
|
80
|
+
return [records, count]
|
|
81
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { decryptCustomFieldValue } from './customFieldValues'
|
|
2
|
+
import type { TenantDataEncryptionService } from './tenantDataEncryptionService'
|
|
3
|
+
|
|
4
|
+
export type IndexDocScope = {
|
|
5
|
+
tenantId: string | null
|
|
6
|
+
organizationId?: string | null
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async function decryptValue(
|
|
10
|
+
value: unknown,
|
|
11
|
+
scope: IndexDocScope,
|
|
12
|
+
service: TenantDataEncryptionService | null,
|
|
13
|
+
cache?: Map<string | null, string | null>,
|
|
14
|
+
): Promise<unknown> {
|
|
15
|
+
if (Array.isArray(value)) {
|
|
16
|
+
return Promise.all(value.map((entry) => decryptCustomFieldValue(entry, scope.tenantId, service, cache)))
|
|
17
|
+
}
|
|
18
|
+
return decryptCustomFieldValue(value, scope.tenantId, service, cache)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function decryptIndexDocCustomFields(
|
|
22
|
+
doc: Record<string, unknown>,
|
|
23
|
+
scope: IndexDocScope,
|
|
24
|
+
service: TenantDataEncryptionService | null,
|
|
25
|
+
cache?: Map<string | null, string | null>,
|
|
26
|
+
): Promise<Record<string, unknown>> {
|
|
27
|
+
// HybridQueryEngine aliases cf keys as `cf_<key>` (sanitized), while index docs use `cf:<key>`.
|
|
28
|
+
// Support both shapes to keep decryption consistent across query paths.
|
|
29
|
+
const keys = Object.keys(doc).filter((key) => key.startsWith('cf:') || key.startsWith('cf_'))
|
|
30
|
+
if (!keys.length) return doc
|
|
31
|
+
|
|
32
|
+
const working: Record<string, unknown> = { ...doc }
|
|
33
|
+
await Promise.all(
|
|
34
|
+
keys.map(async (key) => {
|
|
35
|
+
try {
|
|
36
|
+
working[key] = await decryptValue(working[key], scope, service, cache)
|
|
37
|
+
} catch {
|
|
38
|
+
// ignore; keep original value
|
|
39
|
+
}
|
|
40
|
+
}),
|
|
41
|
+
)
|
|
42
|
+
return working
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function decryptIndexDocForSearch(
|
|
46
|
+
entityId: string,
|
|
47
|
+
doc: Record<string, unknown>,
|
|
48
|
+
scope: IndexDocScope,
|
|
49
|
+
service: TenantDataEncryptionService | null,
|
|
50
|
+
cache?: Map<string | null, string | null>,
|
|
51
|
+
): Promise<Record<string, unknown>> {
|
|
52
|
+
if (!service || typeof service.decryptEntityPayload !== 'function') {
|
|
53
|
+
return decryptIndexDocCustomFields(doc, scope, service, cache)
|
|
54
|
+
}
|
|
55
|
+
if (service.isEnabled?.() === false) {
|
|
56
|
+
return decryptIndexDocCustomFields(doc, scope, service, cache)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let working: Record<string, unknown> = doc
|
|
60
|
+
const decryptEntity = async (targetEntityId: string) => {
|
|
61
|
+
const decrypted = await service.decryptEntityPayload(
|
|
62
|
+
targetEntityId,
|
|
63
|
+
working,
|
|
64
|
+
scope.tenantId ?? null,
|
|
65
|
+
scope.organizationId ?? null,
|
|
66
|
+
)
|
|
67
|
+
working = { ...working, ...decrypted }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
await decryptEntity(entityId)
|
|
71
|
+
if (entityId === 'customers:customer_person_profile' || entityId === 'customers:customer_company_profile') {
|
|
72
|
+
await decryptEntity('customers:customer_entity')
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return decryptIndexDocCustomFields(working, scope, service, cache)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function encryptIndexDocForStorage(
|
|
79
|
+
entityId: string,
|
|
80
|
+
doc: Record<string, unknown>,
|
|
81
|
+
scope: IndexDocScope,
|
|
82
|
+
service: TenantDataEncryptionService | null,
|
|
83
|
+
): Promise<Record<string, unknown>> {
|
|
84
|
+
if (!service || typeof service.encryptEntityPayload !== 'function') return doc
|
|
85
|
+
if (service.isEnabled?.() === false) return doc
|
|
86
|
+
|
|
87
|
+
let working: Record<string, unknown> = doc
|
|
88
|
+
const encryptEntity = async (targetEntityId: string) => {
|
|
89
|
+
const encrypted = await service.encryptEntityPayload(
|
|
90
|
+
targetEntityId,
|
|
91
|
+
working,
|
|
92
|
+
scope.tenantId ?? null,
|
|
93
|
+
scope.organizationId ?? null,
|
|
94
|
+
)
|
|
95
|
+
working = { ...working, ...encrypted }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
await encryptEntity(entityId)
|
|
99
|
+
if (entityId === 'customers:customer_person_profile' || entityId === 'customers:customer_company_profile') {
|
|
100
|
+
await encryptEntity('customers:customer_entity')
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return working
|
|
104
|
+
}
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import crypto from 'node:crypto'
|
|
2
|
+
import { generateDek, hashForLookup } from './aes'
|
|
3
|
+
import { isEncryptionDebugEnabled, isTenantDataEncryptionEnabled } from './toggles'
|
|
4
|
+
|
|
5
|
+
export type TenantDek = {
|
|
6
|
+
tenantId: string
|
|
7
|
+
key: string // base64
|
|
8
|
+
fetchedAt: number
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface KmsService {
|
|
12
|
+
getTenantDek(tenantId: string): Promise<TenantDek | null>
|
|
13
|
+
createTenantDek(tenantId: string): Promise<TenantDek | null>
|
|
14
|
+
isHealthy(): boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
class FallbackKmsService implements KmsService {
|
|
18
|
+
private notified = false
|
|
19
|
+
constructor(
|
|
20
|
+
private readonly primary: KmsService,
|
|
21
|
+
private readonly fallback: KmsService | null,
|
|
22
|
+
private readonly onFallback?: () => void,
|
|
23
|
+
) {}
|
|
24
|
+
|
|
25
|
+
isHealthy(): boolean {
|
|
26
|
+
return this.primary.isHealthy() || Boolean(this.fallback?.isHealthy?.())
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
private notifyFallback() {
|
|
30
|
+
if (this.notified) return
|
|
31
|
+
this.notified = true
|
|
32
|
+
this.onFallback?.()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private async fromPrimary<T>(op: () => Promise<T | null>): Promise<T | null> {
|
|
36
|
+
try {
|
|
37
|
+
return await op()
|
|
38
|
+
} catch (err) {
|
|
39
|
+
console.warn('⚠️ [encryption][kms] Primary KMS failed, will try fallback', {
|
|
40
|
+
error: (err as Error)?.message || String(err),
|
|
41
|
+
})
|
|
42
|
+
return null
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async getTenantDek(tenantId: string): Promise<TenantDek | null> {
|
|
47
|
+
if (this.primary.isHealthy()) {
|
|
48
|
+
const dek = await this.fromPrimary(() => this.primary.getTenantDek(tenantId))
|
|
49
|
+
if (dek) return dek
|
|
50
|
+
}
|
|
51
|
+
if (this.fallback?.isHealthy()) {
|
|
52
|
+
this.notifyFallback()
|
|
53
|
+
return this.fallback.getTenantDek(tenantId)
|
|
54
|
+
}
|
|
55
|
+
return null
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async createTenantDek(tenantId: string): Promise<TenantDek | null> {
|
|
59
|
+
if (this.primary.isHealthy()) {
|
|
60
|
+
const dek = await this.fromPrimary(() => this.primary.createTenantDek(tenantId))
|
|
61
|
+
if (dek) return dek
|
|
62
|
+
}
|
|
63
|
+
if (this.fallback?.isHealthy()) {
|
|
64
|
+
this.notifyFallback()
|
|
65
|
+
return this.fallback.createTenantDek(tenantId)
|
|
66
|
+
}
|
|
67
|
+
return null
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
type VaultClientOpts = {
|
|
72
|
+
vaultAddr?: string
|
|
73
|
+
vaultToken?: string
|
|
74
|
+
mountPath?: string
|
|
75
|
+
ttlMs?: number
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
type VaultReadResponse = {
|
|
79
|
+
data?: { data?: { key?: string; version?: number }; metadata?: Record<string, unknown> }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function normalizeEnv(value: string | undefined): string {
|
|
83
|
+
if (!value) return ''
|
|
84
|
+
return value.trim().replace(/^['"]|['"]$/g, '')
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
type DerivedSecret = { secret: string; source: 'explicit' | 'dev-default'; envName: string }
|
|
88
|
+
|
|
89
|
+
function resolveDerivedKeySecret(): DerivedSecret | null {
|
|
90
|
+
const candidates: Array<{ value: string | null; envName: string }> = [
|
|
91
|
+
{ value: process.env.TENANT_DATA_ENCRYPTION_FALLBACK_KEY ?? null, envName: 'TENANT_DATA_ENCRYPTION_FALLBACK_KEY' },
|
|
92
|
+
{ value: process.env.TENANT_DATA_ENCRYPTION_KEY ?? null, envName: 'TENANT_DATA_ENCRYPTION_KEY' },
|
|
93
|
+
{ value: process.env.AUTH_SECRET ?? null, envName: 'AUTH_SECRET' },
|
|
94
|
+
{ value: process.env.NEXTAUTH_SECRET ?? null, envName: 'NEXTAUTH_SECRET' },
|
|
95
|
+
]
|
|
96
|
+
for (const raw of candidates) {
|
|
97
|
+
const normalized = normalizeEnv(raw.value ?? undefined)
|
|
98
|
+
if (normalized) return { secret: normalized, source: 'explicit', envName: raw.envName }
|
|
99
|
+
}
|
|
100
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
101
|
+
return { secret: 'om-dev-tenant-encryption', source: 'dev-default', envName: 'DEV_DEFAULT' }
|
|
102
|
+
}
|
|
103
|
+
return null
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export class NoopKmsService implements KmsService {
|
|
107
|
+
isHealthy(): boolean { return !isTenantDataEncryptionEnabled() }
|
|
108
|
+
async getTenantDek(): Promise<TenantDek | null> { return null }
|
|
109
|
+
async createTenantDek(): Promise<TenantDek | null> { return null }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
class DerivedKmsService implements KmsService {
|
|
113
|
+
private root: Buffer
|
|
114
|
+
constructor(secret: string) {
|
|
115
|
+
// Derive a stable root key from the provided secret so derived tenant keys are deterministic
|
|
116
|
+
this.root = crypto.createHash('sha256').update(secret).digest()
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
isHealthy(): boolean {
|
|
120
|
+
return true
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private deriveKey(tenantId: string): string {
|
|
124
|
+
const iterations = 310_000
|
|
125
|
+
const keyLength = 32
|
|
126
|
+
const derived = crypto.pbkdf2Sync(this.root, tenantId, iterations, keyLength, 'sha512')
|
|
127
|
+
return derived.toString('base64')
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async getTenantDek(tenantId: string): Promise<TenantDek | null> {
|
|
131
|
+
if (!tenantId) return null
|
|
132
|
+
return { tenantId, key: this.deriveKey(tenantId), fetchedAt: Date.now() }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async createTenantDek(tenantId: string): Promise<TenantDek | null> {
|
|
136
|
+
return this.getTenantDek(tenantId)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export class HashicorpVaultKmsService implements KmsService {
|
|
141
|
+
private cache = new Map<string, TenantDek>()
|
|
142
|
+
private readonly vaultAddr: string
|
|
143
|
+
private readonly vaultToken: string
|
|
144
|
+
private readonly mountPath: string
|
|
145
|
+
private readonly ttlMs: number
|
|
146
|
+
private healthy = true
|
|
147
|
+
private readonly debugEnabled: boolean
|
|
148
|
+
private static loggedInit = false
|
|
149
|
+
|
|
150
|
+
constructor(opts: VaultClientOpts = {}) {
|
|
151
|
+
this.vaultAddr = normalizeEnv(opts.vaultAddr || process.env.VAULT_ADDR || '')
|
|
152
|
+
this.vaultToken = normalizeEnv(opts.vaultToken || process.env.VAULT_TOKEN || '')
|
|
153
|
+
this.mountPath = (opts.mountPath || process.env.VAULT_KV_PATH || 'secret/data').replace(/\/+$/, '')
|
|
154
|
+
this.ttlMs = opts.ttlMs ?? 15 * 60 * 1000
|
|
155
|
+
this.debugEnabled = isEncryptionDebugEnabled()
|
|
156
|
+
if (!this.vaultAddr || !this.vaultToken) {
|
|
157
|
+
this.healthy = false
|
|
158
|
+
if (this.debugEnabled) {
|
|
159
|
+
console.warn('⚠️ [encryption][kms] Vault misconfigured (missing VAULT_ADDR or VAULT_TOKEN)')
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (this.healthy && !HashicorpVaultKmsService.loggedInit && this.debugEnabled) {
|
|
163
|
+
HashicorpVaultKmsService.loggedInit = true
|
|
164
|
+
if(this.debugEnabled) {
|
|
165
|
+
console.info('🔐 [encryption][kms] Hashicorp Vault KMS enabled')
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
isHealthy(): boolean {
|
|
171
|
+
return this.healthy
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private now(): number {
|
|
175
|
+
return Date.now()
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private cacheHit(tenantId: string): TenantDek | null {
|
|
179
|
+
const entry = this.cache.get(tenantId)
|
|
180
|
+
if (!entry) return null
|
|
181
|
+
if (this.now() - entry.fetchedAt > this.ttlMs) {
|
|
182
|
+
this.cache.delete(tenantId)
|
|
183
|
+
return null
|
|
184
|
+
}
|
|
185
|
+
return entry
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private async readVault(path: string): Promise<VaultReadResponse | null> {
|
|
189
|
+
if (!this.vaultAddr || !this.vaultToken) {
|
|
190
|
+
this.healthy = false
|
|
191
|
+
return null
|
|
192
|
+
}
|
|
193
|
+
try {
|
|
194
|
+
const res = await fetch(`${this.vaultAddr}/v1/${path}`, {
|
|
195
|
+
method: 'GET',
|
|
196
|
+
headers: { 'X-Vault-Token': this.vaultToken },
|
|
197
|
+
})
|
|
198
|
+
if (!res.ok) {
|
|
199
|
+
this.healthy = res.status < 500
|
|
200
|
+
console.warn('⚠️ [encryption][kms] Vault read failed', { path, status: res.status })
|
|
201
|
+
return null
|
|
202
|
+
}
|
|
203
|
+
if (this.debugEnabled) {
|
|
204
|
+
console.info('🔍 [encryption][kms] Vault read ok', { path })
|
|
205
|
+
}
|
|
206
|
+
return (await res.json()) as VaultReadResponse
|
|
207
|
+
} catch (err) {
|
|
208
|
+
this.healthy = false
|
|
209
|
+
console.warn('⚠️ [encryption][kms] Vault read error', { path, error: (err as Error)?.message || String(err) })
|
|
210
|
+
return null
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
private async writeVault(path: string, key: string): Promise<boolean> {
|
|
215
|
+
if (!this.vaultAddr || !this.vaultToken) {
|
|
216
|
+
this.healthy = false
|
|
217
|
+
return false
|
|
218
|
+
}
|
|
219
|
+
try {
|
|
220
|
+
const res = await fetch(`${this.vaultAddr}/v1/${path}`, {
|
|
221
|
+
|
|
222
|
+
method: 'POST',
|
|
223
|
+
headers: {
|
|
224
|
+
'X-Vault-Token': this.vaultToken,
|
|
225
|
+
'Content-Type': 'application/json',
|
|
226
|
+
},
|
|
227
|
+
body: JSON.stringify({ data: { key } }),
|
|
228
|
+
})
|
|
229
|
+
this.healthy = res.ok
|
|
230
|
+
if (!res.ok) {
|
|
231
|
+
console.warn('⚠️ [encryption][kms] Vault write failed', { path, status: res.status })
|
|
232
|
+
}
|
|
233
|
+
return res.ok
|
|
234
|
+
} catch (err) {
|
|
235
|
+
this.healthy = false
|
|
236
|
+
console.warn('⚠️ [encryption][kms] Vault write error', { path, error: (err as Error)?.message || String(err) })
|
|
237
|
+
return false
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private buildKeyPath(tenantId: string): string {
|
|
242
|
+
const suffix = `tenant_key_${tenantId}`
|
|
243
|
+
const normalizedMount = this.mountPath.replace(/^\/+/, '')
|
|
244
|
+
return `${normalizedMount}/${suffix}`
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
private remember(entry: TenantDek): TenantDek {
|
|
248
|
+
this.cache.set(entry.tenantId, entry)
|
|
249
|
+
return entry
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async getTenantDek(tenantId: string): Promise<TenantDek | null> {
|
|
253
|
+
const cached = this.cacheHit(tenantId)
|
|
254
|
+
if (cached) return cached
|
|
255
|
+
const path = this.buildKeyPath(tenantId)
|
|
256
|
+
const res = await this.readVault(path)
|
|
257
|
+
const key = res?.data?.data?.key
|
|
258
|
+
if (!key) {
|
|
259
|
+
console.warn('⚠️ [encryption][kms] No tenant DEK found in Vault', { tenantId, path })
|
|
260
|
+
return null
|
|
261
|
+
}
|
|
262
|
+
const dek: TenantDek = { tenantId, key, fetchedAt: this.now() }
|
|
263
|
+
return this.remember(dek)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async createTenantDek(tenantId: string): Promise<TenantDek | null> {
|
|
267
|
+
const key = generateDek()
|
|
268
|
+
const path = this.buildKeyPath(tenantId)
|
|
269
|
+
const ok = await this.writeVault(path, key)
|
|
270
|
+
if (ok) {
|
|
271
|
+
console.info('🔑 [encryption][kms] Stored tenant DEK in Vault', { tenantId, path })
|
|
272
|
+
} else {
|
|
273
|
+
console.warn('⚠️ [encryption][kms] Failed to store tenant DEK in Vault', { tenantId, path })
|
|
274
|
+
}
|
|
275
|
+
if (!ok) return null
|
|
276
|
+
return this.remember({ tenantId, key, fetchedAt: this.now() })
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
let loggedDerivedKeyFallbackBanner = false
|
|
281
|
+
|
|
282
|
+
function logDerivedKeyFallbackBanner(opts: DerivedSecret): void {
|
|
283
|
+
if (process.env.NODE_ENV === 'test' || loggedDerivedKeyFallbackBanner) return
|
|
284
|
+
loggedDerivedKeyFallbackBanner = true
|
|
285
|
+
const redBg = '\x1b[41m'
|
|
286
|
+
const white = '\x1b[97m'
|
|
287
|
+
const reset = '\x1b[0m'
|
|
288
|
+
const width = 110
|
|
289
|
+
const border = `${redBg}${white}${'━'.repeat(width)}${reset}`
|
|
290
|
+
const isProduction = process.env.NODE_ENV === 'production'
|
|
291
|
+
const sourceLine =
|
|
292
|
+
opts.source === 'explicit' ? `Source: ${opts.envName}` : 'Source: dev default secret (do NOT use in production)'
|
|
293
|
+
const body = [
|
|
294
|
+
'🚨 Using derived tenant encryption keys (Vault unavailable / no DEK)',
|
|
295
|
+
sourceLine,
|
|
296
|
+
isProduction ? 'Secret: [redacted in production]' : `Secret: ${opts.secret}`,
|
|
297
|
+
'Persist this secret securely. Without it, encrypted tenant data cannot be recovered after restart.',
|
|
298
|
+
]
|
|
299
|
+
console.warn(border)
|
|
300
|
+
for (const line of body) {
|
|
301
|
+
const padded = line.padEnd(width - 2, ' ')
|
|
302
|
+
console.warn(`${redBg}${white} ${padded} ${reset}`)
|
|
303
|
+
}
|
|
304
|
+
console.warn(border)
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export function createKmsService(): KmsService {
|
|
308
|
+
if (!isTenantDataEncryptionEnabled()) return new NoopKmsService()
|
|
309
|
+
const primary = new HashicorpVaultKmsService()
|
|
310
|
+
|
|
311
|
+
const derived = resolveDerivedKeySecret()
|
|
312
|
+
const fallback = derived ? new DerivedKmsService(derived.secret) : null
|
|
313
|
+
const notifyFallback = derived
|
|
314
|
+
? () => {
|
|
315
|
+
logDerivedKeyFallbackBanner(derived)
|
|
316
|
+
}
|
|
317
|
+
: undefined
|
|
318
|
+
|
|
319
|
+
if (!primary.isHealthy()) {
|
|
320
|
+
if (fallback) {
|
|
321
|
+
notifyFallback?.()
|
|
322
|
+
return fallback
|
|
323
|
+
}
|
|
324
|
+
console.warn(
|
|
325
|
+
'⚠️ [encryption][kms] Vault not healthy or misconfigured (missing VAULT_ADDR/VAULT_TOKEN) and no fallback secret provided; falling back to noop KMS',
|
|
326
|
+
)
|
|
327
|
+
return new NoopKmsService()
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (fallback) {
|
|
331
|
+
return new FallbackKmsService(primary, fallback, notifyFallback)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return primary
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export { hashForLookup }
|