@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.
Files changed (324) hide show
  1. package/build.mjs +101 -0
  2. package/dist/index.js +1 -0
  3. package/dist/index.js.map +7 -0
  4. package/dist/lib/api/crud.js +47 -0
  5. package/dist/lib/api/crud.js.map +7 -0
  6. package/dist/lib/api/scoped.js +140 -0
  7. package/dist/lib/api/scoped.js.map +7 -0
  8. package/dist/lib/auth/jwt.js +34 -0
  9. package/dist/lib/auth/jwt.js.map +7 -0
  10. package/dist/lib/auth/server.js +157 -0
  11. package/dist/lib/auth/server.js.map +7 -0
  12. package/dist/lib/boolean.js +22 -0
  13. package/dist/lib/boolean.js.map +7 -0
  14. package/dist/lib/bootstrap/appResolver.js +43 -0
  15. package/dist/lib/bootstrap/appResolver.js.map +7 -0
  16. package/dist/lib/bootstrap/dynamicLoader.js +108 -0
  17. package/dist/lib/bootstrap/dynamicLoader.js.map +7 -0
  18. package/dist/lib/bootstrap/factory.js +59 -0
  19. package/dist/lib/bootstrap/factory.js.map +7 -0
  20. package/dist/lib/bootstrap/index.js +11 -0
  21. package/dist/lib/bootstrap/index.js.map +7 -0
  22. package/dist/lib/bootstrap/types.js +1 -0
  23. package/dist/lib/bootstrap/types.js.map +7 -0
  24. package/dist/lib/cache/segments.js +36 -0
  25. package/dist/lib/cache/segments.js.map +7 -0
  26. package/dist/lib/cli/progress.js +46 -0
  27. package/dist/lib/cli/progress.js.map +7 -0
  28. package/dist/lib/commands/command-bus.js +285 -0
  29. package/dist/lib/commands/command-bus.js.map +7 -0
  30. package/dist/lib/commands/customFieldSnapshots.js +66 -0
  31. package/dist/lib/commands/customFieldSnapshots.js.map +7 -0
  32. package/dist/lib/commands/helpers.js +98 -0
  33. package/dist/lib/commands/helpers.js.map +7 -0
  34. package/dist/lib/commands/index.js +8 -0
  35. package/dist/lib/commands/index.js.map +7 -0
  36. package/dist/lib/commands/operationMetadata.js +32 -0
  37. package/dist/lib/commands/operationMetadata.js.map +7 -0
  38. package/dist/lib/commands/registry.js +43 -0
  39. package/dist/lib/commands/registry.js.map +7 -0
  40. package/dist/lib/commands/scope.js +44 -0
  41. package/dist/lib/commands/scope.js.map +7 -0
  42. package/dist/lib/commands/types.js +8 -0
  43. package/dist/lib/commands/types.js.map +7 -0
  44. package/dist/lib/crud/cache-stats.js +98 -0
  45. package/dist/lib/crud/cache-stats.js.map +7 -0
  46. package/dist/lib/crud/cache.js +175 -0
  47. package/dist/lib/crud/cache.js.map +7 -0
  48. package/dist/lib/crud/custom-fields-client.js +52 -0
  49. package/dist/lib/crud/custom-fields-client.js.map +7 -0
  50. package/dist/lib/crud/custom-fields.js +467 -0
  51. package/dist/lib/crud/custom-fields.js.map +7 -0
  52. package/dist/lib/crud/errors.js +24 -0
  53. package/dist/lib/crud/errors.js.map +7 -0
  54. package/dist/lib/crud/exporters.js +154 -0
  55. package/dist/lib/crud/exporters.js.map +7 -0
  56. package/dist/lib/crud/factory.js +1311 -0
  57. package/dist/lib/crud/factory.js.map +7 -0
  58. package/dist/lib/crud/types.js +1 -0
  59. package/dist/lib/crud/types.js.map +7 -0
  60. package/dist/lib/custom-fields/normalize.js +36 -0
  61. package/dist/lib/custom-fields/normalize.js.map +7 -0
  62. package/dist/lib/data/engine.js +396 -0
  63. package/dist/lib/data/engine.js.map +7 -0
  64. package/dist/lib/db/escapeLikePattern.js +5 -0
  65. package/dist/lib/db/escapeLikePattern.js.map +7 -0
  66. package/dist/lib/db/mikro.js +82 -0
  67. package/dist/lib/db/mikro.js.map +7 -0
  68. package/dist/lib/di/container.js +94 -0
  69. package/dist/lib/di/container.js.map +7 -0
  70. package/dist/lib/email/send.js +12 -0
  71. package/dist/lib/email/send.js.map +7 -0
  72. package/dist/lib/encryption/aes.js +58 -0
  73. package/dist/lib/encryption/aes.js.map +7 -0
  74. package/dist/lib/encryption/customFieldValues.js +49 -0
  75. package/dist/lib/encryption/customFieldValues.js.map +7 -0
  76. package/dist/lib/encryption/entityFields.js +26 -0
  77. package/dist/lib/encryption/entityFields.js.map +7 -0
  78. package/dist/lib/encryption/entityIds.js +80 -0
  79. package/dist/lib/encryption/entityIds.js.map +7 -0
  80. package/dist/lib/encryption/find.js +45 -0
  81. package/dist/lib/encryption/find.js.map +7 -0
  82. package/dist/lib/encryption/indexDoc.js +69 -0
  83. package/dist/lib/encryption/indexDoc.js.map +7 -0
  84. package/dist/lib/encryption/kms.js +282 -0
  85. package/dist/lib/encryption/kms.js.map +7 -0
  86. package/dist/lib/encryption/subscriber.js +330 -0
  87. package/dist/lib/encryption/subscriber.js.map +7 -0
  88. package/dist/lib/encryption/tenantDataEncryptionService.js +252 -0
  89. package/dist/lib/encryption/tenantDataEncryptionService.js.map +7 -0
  90. package/dist/lib/encryption/toggles.js +18 -0
  91. package/dist/lib/encryption/toggles.js.map +7 -0
  92. package/dist/lib/entities/naming.js +9 -0
  93. package/dist/lib/entities/naming.js.map +7 -0
  94. package/dist/lib/entities/system-entities.js +43 -0
  95. package/dist/lib/entities/system-entities.js.map +7 -0
  96. package/dist/lib/frontend/organizationEvents.js +41 -0
  97. package/dist/lib/frontend/organizationEvents.js.map +7 -0
  98. package/dist/lib/frontend/useOrganizationScope.js +32 -0
  99. package/dist/lib/frontend/useOrganizationScope.js.map +7 -0
  100. package/dist/lib/hotkeys/index.js +128 -0
  101. package/dist/lib/hotkeys/index.js.map +7 -0
  102. package/dist/lib/i18n/app-dictionaries.js +17 -0
  103. package/dist/lib/i18n/app-dictionaries.js.map +7 -0
  104. package/dist/lib/i18n/config.js +7 -0
  105. package/dist/lib/i18n/config.js.map +7 -0
  106. package/dist/lib/i18n/context.js +50 -0
  107. package/dist/lib/i18n/context.js.map +7 -0
  108. package/dist/lib/i18n/server.js +68 -0
  109. package/dist/lib/i18n/server.js.map +7 -0
  110. package/dist/lib/i18n/translate.js +45 -0
  111. package/dist/lib/i18n/translate.js.map +7 -0
  112. package/dist/lib/indexers/error-log.js +82 -0
  113. package/dist/lib/indexers/error-log.js.map +7 -0
  114. package/dist/lib/indexers/status-log.js +80 -0
  115. package/dist/lib/indexers/status-log.js.map +7 -0
  116. package/dist/lib/lib/auth/jwt.js +34 -0
  117. package/dist/lib/lib/auth/jwt.js.map +7 -0
  118. package/dist/lib/lib/auth/server.js +77 -0
  119. package/dist/lib/lib/auth/server.js.map +7 -0
  120. package/dist/lib/lib/email/send.js +12 -0
  121. package/dist/lib/lib/email/send.js.map +7 -0
  122. package/dist/lib/lib/i18n/config.js +7 -0
  123. package/dist/lib/lib/i18n/config.js.map +7 -0
  124. package/dist/lib/lib/i18n/context.js +31 -0
  125. package/dist/lib/lib/i18n/context.js.map +7 -0
  126. package/dist/lib/lib/utils.js +9 -0
  127. package/dist/lib/lib/utils.js.map +7 -0
  128. package/dist/lib/location/countries.js +68 -0
  129. package/dist/lib/location/countries.js.map +7 -0
  130. package/dist/lib/modules/index.js +6 -0
  131. package/dist/lib/modules/index.js.map +7 -0
  132. package/dist/lib/modules/registry.js +18 -0
  133. package/dist/lib/modules/registry.js.map +7 -0
  134. package/dist/lib/openapi/crud.js +137 -0
  135. package/dist/lib/openapi/crud.js.map +7 -0
  136. package/dist/lib/openapi/generator.js +1131 -0
  137. package/dist/lib/openapi/generator.js.map +7 -0
  138. package/dist/lib/openapi/index.js +10 -0
  139. package/dist/lib/openapi/index.js.map +7 -0
  140. package/dist/lib/openapi/sanitize.js +110 -0
  141. package/dist/lib/openapi/sanitize.js.map +7 -0
  142. package/dist/lib/openapi/types.js +1 -0
  143. package/dist/lib/openapi/types.js.map +7 -0
  144. package/dist/lib/profiler/index.js +258 -0
  145. package/dist/lib/profiler/index.js.map +7 -0
  146. package/dist/lib/query/engine.js +729 -0
  147. package/dist/lib/query/engine.js.map +7 -0
  148. package/dist/lib/query/join-utils.js +195 -0
  149. package/dist/lib/query/join-utils.js.map +7 -0
  150. package/dist/lib/query/types.js +9 -0
  151. package/dist/lib/query/types.js.map +7 -0
  152. package/dist/lib/search/config.js +32 -0
  153. package/dist/lib/search/config.js.map +7 -0
  154. package/dist/lib/search/tokenize.js +34 -0
  155. package/dist/lib/search/tokenize.js.map +7 -0
  156. package/dist/lib/slugify.js +24 -0
  157. package/dist/lib/slugify.js.map +7 -0
  158. package/dist/lib/testing/bootstrap.js +51 -0
  159. package/dist/lib/testing/bootstrap.js.map +7 -0
  160. package/dist/lib/testing/index.js +17 -0
  161. package/dist/lib/testing/index.js.map +7 -0
  162. package/dist/lib/testing/renderWithProviders.js +15 -0
  163. package/dist/lib/testing/renderWithProviders.js.map +7 -0
  164. package/dist/lib/url.js +12 -0
  165. package/dist/lib/url.js.map +7 -0
  166. package/dist/lib/utils.js +13 -0
  167. package/dist/lib/utils.js.map +7 -0
  168. package/dist/lib/version.js +7 -0
  169. package/dist/lib/version.js.map +7 -0
  170. package/dist/modules/dashboard/widgets.js +1 -0
  171. package/dist/modules/dashboard/widgets.js.map +7 -0
  172. package/dist/modules/dsl.js +30 -0
  173. package/dist/modules/dsl.js.map +7 -0
  174. package/dist/modules/entities/kinds.js +22 -0
  175. package/dist/modules/entities/kinds.js.map +7 -0
  176. package/dist/modules/entities/options.js +26 -0
  177. package/dist/modules/entities/options.js.map +7 -0
  178. package/dist/modules/entities/validation.js +102 -0
  179. package/dist/modules/entities/validation.js.map +7 -0
  180. package/dist/modules/entities/validators.js +88 -0
  181. package/dist/modules/entities/validators.js.map +7 -0
  182. package/dist/modules/entities.js +1 -0
  183. package/dist/modules/entities.js.map +7 -0
  184. package/dist/modules/navigation/sidebarPreferences.js +50 -0
  185. package/dist/modules/navigation/sidebarPreferences.js.map +7 -0
  186. package/dist/modules/perspectives/types.js +1 -0
  187. package/dist/modules/perspectives/types.js.map +7 -0
  188. package/dist/modules/registry.js +96 -0
  189. package/dist/modules/registry.js.map +7 -0
  190. package/dist/modules/search.js +15 -0
  191. package/dist/modules/search.js.map +7 -0
  192. package/dist/modules/vector.js +1 -0
  193. package/dist/modules/vector.js.map +7 -0
  194. package/dist/modules/widgets/injection-loader.js +180 -0
  195. package/dist/modules/widgets/injection-loader.js.map +7 -0
  196. package/dist/modules/widgets/injection.js +1 -0
  197. package/dist/modules/widgets/injection.js.map +7 -0
  198. package/dist/security/features.js +23 -0
  199. package/dist/security/features.js.map +7 -0
  200. package/dist/types/pg.d.js +1 -0
  201. package/dist/types/pg.d.js.map +7 -0
  202. package/dist/types/react-email.d.js +1 -0
  203. package/dist/types/react-email.d.js.map +7 -0
  204. package/dist/types/resend.d.js +1 -0
  205. package/dist/types/resend.d.js.map +7 -0
  206. package/jest.config.cjs +22 -0
  207. package/package.json +88 -0
  208. package/src/index.ts +0 -0
  209. package/src/lib/api/__tests__/scoped.test.ts +38 -0
  210. package/src/lib/api/crud.ts +59 -0
  211. package/src/lib/api/scoped.ts +239 -0
  212. package/src/lib/auth/jwt.ts +39 -0
  213. package/src/lib/auth/server.ts +199 -0
  214. package/src/lib/boolean.ts +17 -0
  215. package/src/lib/bootstrap/appResolver.ts +85 -0
  216. package/src/lib/bootstrap/dynamicLoader.ts +177 -0
  217. package/src/lib/bootstrap/factory.ts +108 -0
  218. package/src/lib/bootstrap/index.ts +23 -0
  219. package/src/lib/bootstrap/types.ts +31 -0
  220. package/src/lib/cache/segments.ts +56 -0
  221. package/src/lib/cli/progress.ts +55 -0
  222. package/src/lib/commands/__tests__/command-bus.test.ts +84 -0
  223. package/src/lib/commands/__tests__/helpers.test.ts +42 -0
  224. package/src/lib/commands/command-bus.ts +349 -0
  225. package/src/lib/commands/customFieldSnapshots.ts +86 -0
  226. package/src/lib/commands/helpers.ts +143 -0
  227. package/src/lib/commands/index.ts +4 -0
  228. package/src/lib/commands/operationMetadata.ts +40 -0
  229. package/src/lib/commands/registry.ts +46 -0
  230. package/src/lib/commands/scope.ts +59 -0
  231. package/src/lib/commands/types.ts +63 -0
  232. package/src/lib/crud/__tests__/crud-factory.test.ts +333 -0
  233. package/src/lib/crud/__tests__/custom-fields.test.ts +150 -0
  234. package/src/lib/crud/cache-stats.ts +127 -0
  235. package/src/lib/crud/cache.ts +205 -0
  236. package/src/lib/crud/custom-fields-client.ts +54 -0
  237. package/src/lib/crud/custom-fields.ts +607 -0
  238. package/src/lib/crud/errors.ts +23 -0
  239. package/src/lib/crud/exporters.ts +188 -0
  240. package/src/lib/crud/factory.ts +1622 -0
  241. package/src/lib/crud/types.ts +29 -0
  242. package/src/lib/custom-fields/normalize.ts +45 -0
  243. package/src/lib/data/engine.ts +562 -0
  244. package/src/lib/db/escapeLikePattern.ts +2 -0
  245. package/src/lib/db/mikro.ts +100 -0
  246. package/src/lib/di/container.ts +105 -0
  247. package/src/lib/email/send.ts +18 -0
  248. package/src/lib/encryption/__tests__/customFieldValues.test.ts +63 -0
  249. package/src/lib/encryption/__tests__/indexDoc.test.ts +115 -0
  250. package/src/lib/encryption/aes.ts +64 -0
  251. package/src/lib/encryption/customFieldValues.ts +67 -0
  252. package/src/lib/encryption/entityFields.ts +39 -0
  253. package/src/lib/encryption/entityIds.ts +107 -0
  254. package/src/lib/encryption/find.ts +81 -0
  255. package/src/lib/encryption/indexDoc.ts +104 -0
  256. package/src/lib/encryption/kms.ts +337 -0
  257. package/src/lib/encryption/subscriber.ts +416 -0
  258. package/src/lib/encryption/tenantDataEncryptionService.ts +313 -0
  259. package/src/lib/encryption/toggles.ts +15 -0
  260. package/src/lib/entities/naming.ts +6 -0
  261. package/src/lib/entities/system-entities.ts +43 -0
  262. package/src/lib/frontend/organizationEvents.ts +55 -0
  263. package/src/lib/frontend/useOrganizationScope.ts +30 -0
  264. package/src/lib/hotkeys/index.ts +168 -0
  265. package/src/lib/i18n/app-dictionaries.ts +18 -0
  266. package/src/lib/i18n/config.ts +4 -0
  267. package/src/lib/i18n/context.tsx +66 -0
  268. package/src/lib/i18n/server.ts +74 -0
  269. package/src/lib/i18n/translate.ts +54 -0
  270. package/src/lib/indexers/error-log.ts +106 -0
  271. package/src/lib/indexers/status-log.ts +119 -0
  272. package/src/lib/lib/auth/jwt.ts +39 -0
  273. package/src/lib/lib/auth/server.ts +94 -0
  274. package/src/lib/lib/email/send.ts +18 -0
  275. package/src/lib/lib/i18n/config.ts +4 -0
  276. package/src/lib/lib/i18n/context.tsx +38 -0
  277. package/src/lib/lib/utils.ts +6 -0
  278. package/src/lib/location/countries.ts +97 -0
  279. package/src/lib/modules/index.ts +1 -0
  280. package/src/lib/modules/registry.ts +18 -0
  281. package/src/lib/openapi/crud.ts +218 -0
  282. package/src/lib/openapi/generator.ts +1311 -0
  283. package/src/lib/openapi/index.ts +4 -0
  284. package/src/lib/openapi/sanitize.ts +137 -0
  285. package/src/lib/openapi/types.ts +79 -0
  286. package/src/lib/profiler/index.ts +371 -0
  287. package/src/lib/query/__tests__/engine.test.ts +274 -0
  288. package/src/lib/query/engine.ts +837 -0
  289. package/src/lib/query/join-utils.ts +238 -0
  290. package/src/lib/query/types.ts +121 -0
  291. package/src/lib/search/config.ts +49 -0
  292. package/src/lib/search/tokenize.ts +45 -0
  293. package/src/lib/slugify.ts +28 -0
  294. package/src/lib/testing/bootstrap.ts +124 -0
  295. package/src/lib/testing/index.ts +15 -0
  296. package/src/lib/testing/renderWithProviders.tsx +31 -0
  297. package/src/lib/url.ts +12 -0
  298. package/src/lib/utils.ts +17 -0
  299. package/src/lib/version.ts +5 -0
  300. package/src/modules/__tests__/dsl.test.ts +35 -0
  301. package/src/modules/__tests__/registry.test.ts +300 -0
  302. package/src/modules/dashboard/widgets.ts +57 -0
  303. package/src/modules/dsl.ts +32 -0
  304. package/src/modules/entities/__tests__/validation.test.ts +52 -0
  305. package/src/modules/entities/kinds.ts +20 -0
  306. package/src/modules/entities/options.ts +36 -0
  307. package/src/modules/entities/validation.ts +118 -0
  308. package/src/modules/entities/validators.ts +93 -0
  309. package/src/modules/entities.ts +102 -0
  310. package/src/modules/navigation/sidebarPreferences.ts +62 -0
  311. package/src/modules/perspectives/types.ts +40 -0
  312. package/src/modules/registry.ts +249 -0
  313. package/src/modules/search.ts +325 -0
  314. package/src/modules/vector.ts +122 -0
  315. package/src/modules/widgets/__tests__/injection.test.ts +48 -0
  316. package/src/modules/widgets/injection-loader.ts +235 -0
  317. package/src/modules/widgets/injection.ts +120 -0
  318. package/src/security/features.ts +22 -0
  319. package/src/types/pg.d.ts +2 -0
  320. package/src/types/react-email.d.ts +2 -0
  321. package/src/types/resend.d.ts +2 -0
  322. package/tsconfig.build.json +11 -0
  323. package/tsconfig.json +9 -0
  324. 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 }