@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,313 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql'
2
+ import type { CacheStrategy } from '@open-mercato/cache'
3
+ import { decryptWithAesGcm, encryptWithAesGcm, hashForLookup } from './aes'
4
+ import { createKmsService, type KmsService, type TenantDek } from './kms'
5
+ import { isTenantDataEncryptionEnabled, isEncryptionDebugEnabled } from './toggles'
6
+ import { EncryptionMap } from '@open-mercato/core/modules/entities/data/entities'
7
+
8
+ export type EncryptedFieldRule = {
9
+ field: string
10
+ hashField?: string | null
11
+ }
12
+
13
+ export type EncryptionMapRecord = {
14
+ entityId: string
15
+ fields: EncryptedFieldRule[]
16
+ }
17
+
18
+ type MapCacheKey = {
19
+ entityId: string
20
+ tenantId: string | null
21
+ organizationId: string | null
22
+ }
23
+
24
+ const MAP_MISS_TTL_MS = 5 * 60 * 1000
25
+
26
+ function cacheKey(key: MapCacheKey): string {
27
+ return [
28
+ 'encmap',
29
+ key.entityId.toLowerCase(),
30
+ key.tenantId ?? 'null',
31
+ key.organizationId ?? 'null',
32
+ ].join(':')
33
+ }
34
+
35
+ function debug(event: string, payload: Record<string, unknown>) {
36
+ if (!isEncryptionDebugEnabled()) return
37
+ try {
38
+ // eslint-disable-next-line no-console
39
+ console.debug(`${event} [tenant-encryption]`, payload)
40
+ } catch {
41
+ // ignore
42
+ }
43
+ }
44
+
45
+ const toSnakeCase = (value: string): string =>
46
+ value.replace(/([A-Z])/g, '_$1').replace(/__/g, '_').toLowerCase()
47
+
48
+ const toCamelCase = (value: string): string =>
49
+ value.replace(/_([a-z])/g, (_, c) => c.toUpperCase())
50
+
51
+ function findKey(obj: Record<string, unknown>, key: string): string | null {
52
+ const candidates = [key, toSnakeCase(key), toCamelCase(key)]
53
+ for (const candidate of candidates) {
54
+ if (Object.prototype.hasOwnProperty.call(obj, candidate)) return candidate
55
+ }
56
+ return null
57
+ }
58
+
59
+ function isEncryptedPayload(value: unknown): boolean {
60
+ if (typeof value !== 'string') return false
61
+ const parts = value.split(':')
62
+ return parts.length === 4 && parts[3] === 'v1'
63
+ }
64
+
65
+ export class TenantDataEncryptionService {
66
+ private static globalMemoryCache = new Map<string, EncryptionMapRecord>()
67
+ private static globalInflightMaps = new Map<string, Promise<EncryptionMapRecord | null>>()
68
+ private static globalDekCache = new Map<string, TenantDek>()
69
+ private static globalMissCache = new Map<string, number>()
70
+ private readonly kms: KmsService
71
+ private readonly cache?: CacheStrategy
72
+ private readonly memoryCache = TenantDataEncryptionService.globalMemoryCache
73
+ private readonly dekCache = TenantDataEncryptionService.globalDekCache
74
+ private readonly inflightMaps = TenantDataEncryptionService.globalInflightMaps
75
+ private readonly missCache = TenantDataEncryptionService.globalMissCache
76
+
77
+ constructor(
78
+ private em: EntityManager,
79
+ opts?: { cache?: CacheStrategy; kms?: KmsService }
80
+ ) {
81
+ this.cache = opts?.cache
82
+ this.kms = opts?.kms ?? createKmsService()
83
+ }
84
+
85
+ isEnabled(): boolean {
86
+ return isTenantDataEncryptionEnabled() && this.kms.isHealthy()
87
+ }
88
+
89
+ async getDek(tenantId: string | null | undefined): Promise<TenantDek | null> {
90
+ if (!tenantId) return null
91
+ const cached = this.dekCache.get(tenantId)
92
+ if (cached) return cached
93
+ const dek = await this.kms.getTenantDek(tenantId)
94
+ if (!dek) {
95
+ debug('🔎 dek.miss', { tenantId })
96
+ } else {
97
+ debug('✅ dek.hit', { tenantId })
98
+ }
99
+ if (dek) this.dekCache.set(tenantId, dek)
100
+ return dek
101
+ }
102
+
103
+ private async resolveDekForEncrypt(tenantId: string | null): Promise<TenantDek | null> {
104
+ const existing = await this.getDek(tenantId)
105
+ if (existing || !tenantId) return existing ?? null
106
+ if (typeof this.kms.createTenantDek !== 'function') return existing ?? null
107
+ const created = await this.kms.createTenantDek(tenantId)
108
+ if (created) this.dekCache.set(tenantId, created)
109
+ return created ?? null
110
+ }
111
+
112
+ async createDek(tenantId: string): Promise<TenantDek | null> {
113
+ const dek = await this.kms.createTenantDek(tenantId)
114
+ if (dek) this.dekCache.set(tenantId, dek)
115
+ return dek
116
+ }
117
+
118
+ private async fetchMap(key: MapCacheKey): Promise<EncryptionMapRecord | null> {
119
+ // Bypass ORM lifecycle hooks to avoid recursive decrypt loops by querying directly.
120
+ const conn: any = (this.em as any)?.getConnection?.()
121
+ if (!conn || typeof conn.execute !== 'function') return null
122
+ const sql = `
123
+ select entity_id, fields_json
124
+ from encryption_maps
125
+ where entity_id = ?
126
+ and tenant_id is not distinct from ?
127
+ and organization_id is not distinct from ?
128
+ and is_active = true
129
+ and deleted_at is null
130
+ limit 1
131
+ `
132
+ const rows = await conn.execute(sql, [key.entityId, key.tenantId ?? null, key.organizationId ?? null])
133
+ const row = Array.isArray(rows) && rows.length ? rows[0] : null
134
+ if (!row) return null
135
+ return {
136
+ entityId: row.entity_id || row.entityId || key.entityId,
137
+ fields: Array.isArray(row.fields_json)
138
+ ? (row.fields_json as EncryptedFieldRule[])
139
+ : Array.isArray(row.fieldsJson)
140
+ ? (row.fieldsJson as EncryptedFieldRule[])
141
+ : [],
142
+ }
143
+ }
144
+
145
+ private async getMap(key: MapCacheKey): Promise<EncryptionMapRecord | null> {
146
+ const shouldSkipLookup = (tag: string) => {
147
+ const expiresAt = this.missCache.get(tag)
148
+ if (!expiresAt) return false
149
+ if (expiresAt > Date.now()) return true
150
+ this.missCache.delete(tag)
151
+ return false
152
+ }
153
+ const recordMiss = (tag: string) => {
154
+ this.missCache.set(tag, Date.now() + MAP_MISS_TTL_MS)
155
+ }
156
+
157
+ const candidates: MapCacheKey[] = [
158
+ key,
159
+ { entityId: key.entityId, tenantId: key.tenantId ?? null, organizationId: null },
160
+ { entityId: key.entityId, tenantId: null, organizationId: null },
161
+ ]
162
+ for (const candidate of candidates) {
163
+ const tag = cacheKey(candidate)
164
+ if (shouldSkipLookup(tag)) continue
165
+ if (this.inflightMaps.has(tag)) {
166
+ const pending = this.inflightMaps.get(tag)!
167
+ const resolved = await pending
168
+ if (resolved) return resolved
169
+ }
170
+ const mem = this.memoryCache.get(tag)
171
+ if (mem) return mem
172
+ if (this.cache && typeof this.cache.get === 'function') {
173
+ const cached = await this.cache.get(tag)
174
+ if (cached) return cached as EncryptionMapRecord
175
+ }
176
+ const pending = this.fetchMap(candidate)
177
+ this.inflightMaps.set(tag, pending)
178
+ const loaded = await pending
179
+ this.inflightMaps.delete(tag)
180
+ if (!loaded) {
181
+ recordMiss(tag)
182
+ debug('🔍 encmap.miss', {
183
+ entityId: candidate.entityId,
184
+ tenantId: candidate.tenantId,
185
+ organizationId: candidate.organizationId,
186
+ })
187
+ continue
188
+ }
189
+ this.missCache.delete(tag)
190
+ this.memoryCache.set(tag, loaded)
191
+ if (this.cache && typeof this.cache.set === 'function') {
192
+ await this.cache.set(tag, loaded, { ttl: 300 })
193
+ }
194
+ return loaded
195
+ }
196
+ return null
197
+ }
198
+
199
+ async invalidateMap(entityId: string, tenantId: string | null, organizationId: string | null): Promise<void> {
200
+ const tag = cacheKey({ entityId, tenantId, organizationId })
201
+ this.memoryCache.delete(tag)
202
+ this.inflightMaps.delete(tag)
203
+ this.missCache.delete(tag)
204
+ if (this.cache && typeof (this.cache as any).delete === 'function') {
205
+ await (this.cache as any).delete(tag)
206
+ }
207
+ }
208
+
209
+ private encryptFields(
210
+ obj: Record<string, unknown>,
211
+ fields: EncryptedFieldRule[],
212
+ dek: TenantDek
213
+ ): Record<string, unknown> {
214
+ const clone: Record<string, unknown> = { ...obj }
215
+ for (const rule of fields) {
216
+ const key = findKey(clone, rule.field)
217
+ if (!key) continue
218
+ const value = clone[key]
219
+ if (value === null || value === undefined) continue
220
+ // Avoid double-encrypting already encrypted payloads
221
+ if (isEncryptedPayload(value)) continue
222
+ const serialized = typeof value === 'string' ? value : JSON.stringify(value)
223
+ const payload = encryptWithAesGcm(serialized, dek.key)
224
+ clone[key] = payload.value
225
+ if (rule.hashField) {
226
+ const hashKey = findKey(clone, rule.hashField) ?? rule.hashField
227
+ clone[hashKey] = hashForLookup(serialized)
228
+ }
229
+ }
230
+ return clone
231
+ }
232
+
233
+ private decryptFields(
234
+ obj: Record<string, unknown>,
235
+ fields: EncryptedFieldRule[],
236
+ dek: TenantDek
237
+ ): Record<string, unknown> {
238
+ const clone: Record<string, unknown> = { ...obj }
239
+ const maybeDecrypt = (payload: string): string | null => {
240
+ const first = decryptWithAesGcm(payload, dek.key)
241
+ if (first === null) return null
242
+ // Handle accidental double-encryption: if the first pass still looks like a v1 payload, try once more.
243
+ const parts = first.split(':')
244
+ if (parts.length === 4 && parts[3] === 'v1') {
245
+ const second = decryptWithAesGcm(first, dek.key)
246
+ return second ?? first
247
+ }
248
+ return first
249
+ }
250
+ for (const rule of fields) {
251
+ const key = findKey(clone, rule.field)
252
+ if (!key) continue
253
+ const value = clone[key]
254
+ if (typeof value !== 'string') continue
255
+ const decrypted = maybeDecrypt(value)
256
+ if (decrypted === null) continue
257
+ try {
258
+ clone[key] = JSON.parse(decrypted)
259
+ } catch {
260
+ clone[key] = decrypted
261
+ }
262
+ }
263
+ return clone
264
+ }
265
+
266
+ async encryptEntityPayload(
267
+ entityId: string,
268
+ payload: Record<string, unknown>,
269
+ tenantId: string | null | undefined,
270
+ organizationId?: string | null
271
+ ): Promise<Record<string, unknown>> {
272
+ if (!this.isEnabled()) {
273
+ debug('⚪️ encrypt.skip.disabled', { entityId, tenantId })
274
+ return payload
275
+ }
276
+ const dek = await this.resolveDekForEncrypt(tenantId ?? null)
277
+ if (!dek) {
278
+ debug('⚠️ encrypt.skip.no-dek', { entityId, tenantId })
279
+ return payload
280
+ }
281
+ const map = await this.getMap({ entityId, tenantId: tenantId ?? null, organizationId: organizationId ?? null })
282
+ if (!map || !map.fields?.length) {
283
+ debug('⚪️ encrypt.skip.no-map', { entityId, tenantId })
284
+ return payload
285
+ }
286
+ debug('🔒 encrypt_entity', { entityId, tenantId, organizationId, fields: map.fields.length })
287
+ return this.encryptFields(payload, map.fields, dek)
288
+ }
289
+
290
+ async decryptEntityPayload(
291
+ entityId: string,
292
+ payload: Record<string, unknown>,
293
+ tenantId: string | null | undefined,
294
+ organizationId?: string | null
295
+ ): Promise<Record<string, unknown>> {
296
+ if (!isTenantDataEncryptionEnabled()) {
297
+ debug('⚪️ decrypt.skip.disabled', { entityId, tenantId })
298
+ return payload
299
+ }
300
+ const dek = await this.getDek(tenantId ?? null)
301
+ if (!dek) {
302
+ debug('⚠️ decrypt.skip.no-dek', { entityId, tenantId })
303
+ return payload
304
+ }
305
+ const map = await this.getMap({ entityId, tenantId: tenantId ?? null, organizationId: organizationId ?? null })
306
+ if (!map || !map.fields?.length) {
307
+ debug('⚪️ decrypt.skip.no-map', { entityId, tenantId })
308
+ return payload
309
+ }
310
+ debug('🔓 decrypt_entity', { entityId, tenantId, organizationId, fields: map.fields.length })
311
+ return this.decryptFields(payload, map.fields, dek)
312
+ }
313
+ }
@@ -0,0 +1,15 @@
1
+ import { parseBooleanToken } from '../boolean'
2
+
3
+ export function isTenantDataEncryptionEnabled(): boolean {
4
+ const rawEnv = process.env.TENANT_DATA_ENCRYPTION
5
+ if (rawEnv === undefined) return true
6
+ const trimmed = rawEnv.trim()
7
+ if (!trimmed) return true
8
+ const parsed = parseBooleanToken(trimmed)
9
+ return parsed === null ? true : parsed
10
+ }
11
+
12
+ export function isEncryptionDebugEnabled(): boolean {
13
+ const parsed = parseBooleanToken(process.env.TENANT_DATA_ENCRYPTION_DEBUG ?? '')
14
+ return parsed === true
15
+ }
@@ -0,0 +1,6 @@
1
+ export function tableNameFromEntityId(entityId: string): string {
2
+ const [, name] = entityId.split(':')
3
+ if (!name) return ''
4
+ return name.endsWith('s') ? name : `${name}s`
5
+ }
6
+
@@ -0,0 +1,43 @@
1
+ const RESERVED_SYSTEM_ENTITY_TYPES = new Set<string>([
2
+ 'entities:custom_entity',
3
+ 'entities:custom_entity_storage',
4
+ 'entities:custom_field_def',
5
+ 'entities:custom_field_value',
6
+ 'query_index:entity_index_row',
7
+ 'query_index:entity_index_coverage',
8
+ 'query_index:search_token',
9
+ ])
10
+
11
+ export function isSystemEntitySelectable(entityId: string): boolean {
12
+ if (!entityId) return false
13
+ return !RESERVED_SYSTEM_ENTITY_TYPES.has(entityId)
14
+ }
15
+
16
+ export function flattenSystemEntityIds(
17
+ allEntities: Record<string, Record<string, string>>,
18
+ options?: { predicate?: (entityType: string) => boolean },
19
+ ): string[] {
20
+ if (!allEntities) return []
21
+ const predicate = options?.predicate || isSystemEntitySelectable
22
+ const seen = new Set<string>()
23
+ for (const bucket of Object.values(allEntities)) {
24
+ for (const id of Object.values(bucket ?? {})) {
25
+ if (typeof id !== 'string' || id.length === 0) continue
26
+ if (!predicate(id)) continue
27
+ seen.add(id)
28
+ }
29
+ }
30
+ return Array.from(seen).sort()
31
+ }
32
+
33
+ export function filterSelectableSystemEntityIds(entityIds: Iterable<string>): string[] {
34
+ const selected: string[] = []
35
+ for (const id of entityIds) {
36
+ if (isSystemEntitySelectable(id)) selected.push(id)
37
+ }
38
+ return selected
39
+ }
40
+
41
+ export function isReservedSystemEntityType(entityId: string): boolean {
42
+ return RESERVED_SYSTEM_ENTITY_TYPES.has(entityId)
43
+ }
@@ -0,0 +1,55 @@
1
+ export const ORGANIZATION_SCOPE_CHANGED_EVENT = 'om:organization-scope-changed'
2
+
3
+ export type OrganizationScopeChangedDetail = {
4
+ organizationId: string | null
5
+ tenantId: string | null
6
+ }
7
+
8
+ // Module-level state to track current scope and version
9
+ let currentScope: OrganizationScopeChangedDetail = {
10
+ organizationId: null,
11
+ tenantId: null
12
+ }
13
+ let currentVersion = 0
14
+
15
+ export function getCurrentOrganizationScope(): OrganizationScopeChangedDetail {
16
+ return { ...currentScope }
17
+ }
18
+
19
+ export function getCurrentOrganizationScopeVersion(): number {
20
+ return currentVersion
21
+ }
22
+
23
+ export function emitOrganizationScopeChanged(detail: OrganizationScopeChangedDetail): void {
24
+ if (typeof window === 'undefined' || typeof CustomEvent === 'undefined') return
25
+
26
+ // Detect actual changes
27
+ const hasChanged =
28
+ currentScope.organizationId !== detail.organizationId ||
29
+ currentScope.tenantId !== detail.tenantId
30
+
31
+ // Update module-level state
32
+ currentScope = { ...detail }
33
+
34
+ // Increment version only if actual change detected
35
+ if (hasChanged) {
36
+ currentVersion++
37
+ }
38
+
39
+ // Emit event
40
+ window.dispatchEvent(new CustomEvent<OrganizationScopeChangedDetail>(ORGANIZATION_SCOPE_CHANGED_EVENT, { detail }))
41
+ }
42
+
43
+ export function subscribeOrganizationScopeChanged(
44
+ handler: (detail: OrganizationScopeChangedDetail) => void
45
+ ): () => void {
46
+ if (typeof window === 'undefined') return () => {}
47
+ const listener = (event: Event) => {
48
+ const detail = (event as CustomEvent<OrganizationScopeChangedDetail>).detail ?? { organizationId: null, tenantId: null }
49
+ handler(detail)
50
+ }
51
+ window.addEventListener(ORGANIZATION_SCOPE_CHANGED_EVENT, listener as EventListener)
52
+ return () => {
53
+ window.removeEventListener(ORGANIZATION_SCOPE_CHANGED_EVENT, listener as EventListener)
54
+ }
55
+ }
@@ -0,0 +1,30 @@
1
+ "use client"
2
+ import * as React from 'react'
3
+ import {
4
+ subscribeOrganizationScopeChanged,
5
+ getCurrentOrganizationScope,
6
+ getCurrentOrganizationScopeVersion,
7
+ type OrganizationScopeChangedDetail
8
+ } from './organizationEvents'
9
+
10
+ export function useOrganizationScopeVersion(): number {
11
+ const [version, setVersion] = React.useState(getCurrentOrganizationScopeVersion)
12
+ React.useEffect(() => {
13
+ return subscribeOrganizationScopeChanged(() => {
14
+ setVersion(getCurrentOrganizationScopeVersion())
15
+ })
16
+ }, [])
17
+ return version
18
+ }
19
+
20
+ export function useOrganizationScopeDetail(): OrganizationScopeChangedDetail {
21
+ const [detail, setDetail] = React.useState<OrganizationScopeChangedDetail>(
22
+ getCurrentOrganizationScope
23
+ )
24
+ React.useEffect(() => {
25
+ return subscribeOrganizationScopeChanged((next) => {
26
+ setDetail(next)
27
+ })
28
+ }, [])
29
+ return detail
30
+ }
@@ -0,0 +1,168 @@
1
+ "use client"
2
+
3
+ type HotkeyRegistration = {
4
+ combos: Set<string>
5
+ callback: (event: KeyboardEvent) => void
6
+ debounce: number
7
+ lastTriggered: number
8
+ }
9
+
10
+ const scopes = new Map<string, Set<HotkeyRegistration>>()
11
+
12
+ const MODIFIER_SYNONYMS: Record<string, string> = {
13
+ cmd: 'meta',
14
+ command: 'meta',
15
+ option: 'alt',
16
+ opt: 'alt',
17
+ control: 'ctrl',
18
+ ctrl: 'ctrl',
19
+ shift: 'shift',
20
+ meta: 'meta',
21
+ alt: 'alt',
22
+ }
23
+
24
+ let listenersAttached = false
25
+
26
+ const THIRTY_FPS_INTERVAL = 1000 / 30
27
+
28
+ function normalizeToken(token: string): string {
29
+ const trimmed = token.trim().toLowerCase()
30
+ if (!trimmed) return ''
31
+ if (trimmed in MODIFIER_SYNONYMS) return MODIFIER_SYNONYMS[trimmed]
32
+ if (trimmed === 'return') return 'enter'
33
+ if (trimmed === 'space') return ' '
34
+ return trimmed
35
+ }
36
+
37
+ function serializeCombination(tokens: string[]): string {
38
+ const filtered = tokens.filter(Boolean)
39
+ filtered.sort()
40
+ return filtered.join('+')
41
+ }
42
+
43
+ function parseHotkeys(hotkeys: string): Set<string> {
44
+ return new Set(
45
+ hotkeys
46
+ .split(/\s+/)
47
+ .map((combo) =>
48
+ serializeCombination(
49
+ combo
50
+ .split('+')
51
+ .map(normalizeToken)
52
+ .filter(Boolean),
53
+ ),
54
+ )
55
+ .filter(Boolean),
56
+ )
57
+ }
58
+
59
+ function createRegistration(
60
+ hotkeys: string,
61
+ callback: (event: KeyboardEvent) => void,
62
+ debounce: number,
63
+ ): HotkeyRegistration {
64
+ const combos = parseHotkeys(hotkeys)
65
+ return {
66
+ combos,
67
+ callback,
68
+ debounce: Math.max(debounce, THIRTY_FPS_INTERVAL),
69
+ lastTriggered: 0,
70
+ }
71
+ }
72
+
73
+ function activeCombination(event: KeyboardEvent): string | null {
74
+ const keys: string[] = []
75
+ if (event.metaKey) keys.push('meta')
76
+ if (event.ctrlKey) keys.push('ctrl')
77
+ if (event.altKey) keys.push('alt')
78
+ if (event.shiftKey) keys.push('shift')
79
+
80
+ const key = normalizeToken(event.key)
81
+ if (key && !(key in MODIFIER_SYNONYMS)) {
82
+ keys.push(key.length === 1 ? key : normalizeToken(key))
83
+ } else if (keys.length === 0) {
84
+ // Ignore pure modifier presses
85
+ return null
86
+ }
87
+
88
+ return serializeCombination(keys)
89
+ }
90
+
91
+ function handleKeydown(event: KeyboardEvent) {
92
+ if (event.defaultPrevented) return
93
+ const combo = activeCombination(event)
94
+ if (!combo) return
95
+
96
+ const now = Date.now()
97
+ scopes.forEach((registrations) => {
98
+ registrations.forEach((registration) => {
99
+ if (!registration.combos.has(combo)) return
100
+ if (event.repeat && now - registration.lastTriggered < registration.debounce) return
101
+ if (now - registration.lastTriggered < registration.debounce) return
102
+ registration.lastTriggered = now
103
+ registration.callback(event)
104
+ })
105
+ })
106
+ }
107
+
108
+ function handleKeyup() {
109
+ // No-op placeholder for future extensibility (mirrors API pattern from article reference)
110
+ }
111
+
112
+ function ensureListeners() {
113
+ if (listenersAttached) return
114
+ if (typeof document === 'undefined') return
115
+ document.addEventListener('keydown', handleKeydown)
116
+ document.addEventListener('keyup', handleKeyup)
117
+ listenersAttached = true
118
+ }
119
+
120
+ function detachListenersIfIdle() {
121
+ if (!listenersAttached) return
122
+ if (scopes.size > 0) return
123
+ if (typeof document === 'undefined') return
124
+ document.removeEventListener('keydown', handleKeydown)
125
+ document.removeEventListener('keyup', handleKeyup)
126
+ listenersAttached = false
127
+ }
128
+
129
+ export function registerHotkey(
130
+ hotkeys: string,
131
+ scopeName: string,
132
+ callback: (event: KeyboardEvent) => void,
133
+ debounceTimeInMilliseconds = 150,
134
+ ) {
135
+ if (typeof document === 'undefined') {
136
+ return {
137
+ bind() {},
138
+ unbind() {},
139
+ }
140
+ }
141
+
142
+ const registration = createRegistration(hotkeys, callback, debounceTimeInMilliseconds)
143
+ let scope = scopes.get(scopeName)
144
+
145
+ const bind = () => {
146
+ if (!scope) {
147
+ scope = new Set()
148
+ scopes.set(scopeName, scope)
149
+ }
150
+ scope.add(registration)
151
+ ensureListeners()
152
+ }
153
+
154
+ const unbind = () => {
155
+ const existingScope = scopes.get(scopeName)
156
+ if (existingScope) {
157
+ existingScope.delete(registration)
158
+ if (existingScope.size === 0) {
159
+ scopes.delete(scopeName)
160
+ }
161
+ }
162
+ detachListenersIfIdle()
163
+ }
164
+
165
+ bind()
166
+
167
+ return { bind, unbind }
168
+ }
@@ -0,0 +1,18 @@
1
+ import type { Locale } from './config'
2
+
3
+ type DictionaryLoader = (locale: Locale) => Promise<Record<string, unknown>>
4
+
5
+ let _appDictionaryLoader: DictionaryLoader | null = null
6
+
7
+ export function registerAppDictionaryLoader(loader: DictionaryLoader): void {
8
+ _appDictionaryLoader = loader
9
+ }
10
+
11
+ export async function loadAppDictionary(locale: Locale): Promise<Record<string, unknown>> {
12
+ if (!_appDictionaryLoader) return {}
13
+ try {
14
+ return await _appDictionaryLoader(locale)
15
+ } catch {
16
+ return {}
17
+ }
18
+ }
@@ -0,0 +1,4 @@
1
+ export type Locale = 'en' | 'pl' | 'es' | 'de'
2
+
3
+ export const locales: Locale[] = ['en', 'pl', 'es', 'de']
4
+ export const defaultLocale: Locale = 'en'