@open-mercato/shared 0.6.7-develop.6860.1.f796b22e80 → 0.6.7

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 (199) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +6 -57
  3. package/build.mjs +4 -2
  4. package/dist/lib/auth/server.js +2 -15
  5. package/dist/lib/auth/server.js.map +2 -2
  6. package/dist/lib/bootstrap/dynamicLoader.js +43 -117
  7. package/dist/lib/bootstrap/dynamicLoader.js.map +2 -2
  8. package/dist/lib/commands/command-bus.js +1 -5
  9. package/dist/lib/commands/command-bus.js.map +2 -2
  10. package/dist/lib/commands/command-interceptor-runner.js +2 -2
  11. package/dist/lib/commands/command-interceptor-runner.js.map +2 -2
  12. package/dist/lib/crud/enricher-runner.js +11 -2
  13. package/dist/lib/crud/enricher-runner.js.map +2 -2
  14. package/dist/lib/crud/factory.js +4 -63
  15. package/dist/lib/crud/factory.js.map +2 -2
  16. package/dist/lib/crud/interceptor-runner.js +2 -2
  17. package/dist/lib/crud/interceptor-runner.js.map +2 -2
  18. package/dist/lib/crud/mutation-guard-registry.js +2 -2
  19. package/dist/lib/crud/mutation-guard-registry.js.map +2 -2
  20. package/dist/lib/crud/types.js +0 -4
  21. package/dist/lib/crud/types.js.map +3 -3
  22. package/dist/lib/data/engine.js +11 -37
  23. package/dist/lib/data/engine.js.map +2 -2
  24. package/dist/lib/db/mikro.js +12 -3
  25. package/dist/lib/db/mikro.js.map +2 -2
  26. package/dist/lib/db/pg-errors.js +0 -42
  27. package/dist/lib/db/pg-errors.js.map +2 -2
  28. package/dist/lib/di/container.js +6 -48
  29. package/dist/lib/di/container.js.map +2 -2
  30. package/dist/lib/email/send.js +0 -56
  31. package/dist/lib/email/send.js.map +2 -2
  32. package/dist/lib/encryption/subscriber.js +10 -2
  33. package/dist/lib/encryption/subscriber.js.map +2 -2
  34. package/dist/lib/encryption/tenantDataEncryptionService.js +17 -52
  35. package/dist/lib/encryption/tenantDataEncryptionService.js.map +2 -2
  36. package/dist/lib/i18n/config.js +1 -1
  37. package/dist/lib/i18n/config.js.map +2 -2
  38. package/dist/lib/i18n/server.js +4 -4
  39. package/dist/lib/i18n/server.js.map +3 -3
  40. package/dist/lib/logger/index.js +3 -47
  41. package/dist/lib/logger/index.js.map +3 -3
  42. package/dist/lib/modules/registry.js +1 -5
  43. package/dist/lib/modules/registry.js.map +2 -2
  44. package/dist/lib/query/engine.js +40 -54
  45. package/dist/lib/query/engine.js.map +2 -2
  46. package/dist/lib/query/types.js.map +1 -1
  47. package/dist/lib/redis/connection.js +2 -7
  48. package/dist/lib/redis/connection.js.map +2 -2
  49. package/dist/lib/search/config.js +2 -61
  50. package/dist/lib/search/config.js.map +2 -2
  51. package/dist/lib/search/tokenize.js +11 -23
  52. package/dist/lib/search/tokenize.js.map +2 -2
  53. package/dist/lib/version.js +1 -1
  54. package/dist/lib/version.js.map +1 -1
  55. package/dist/lib/webhooks/index.js +1 -13
  56. package/dist/lib/webhooks/index.js.map +2 -2
  57. package/dist/lib/webhooks/verify.js +7 -12
  58. package/dist/lib/webhooks/verify.js.map +2 -2
  59. package/dist/modules/analytics.js.map +2 -2
  60. package/dist/modules/overrides.js +1 -50
  61. package/dist/modules/overrides.js.map +2 -2
  62. package/dist/modules/payment_gateways/types.js +1 -8
  63. package/dist/modules/payment_gateways/types.js.map +2 -2
  64. package/dist/modules/registry.js +0 -1
  65. package/dist/modules/registry.js.map +2 -2
  66. package/dist/security/enabledModulesRegistry.js +11 -56
  67. package/dist/security/enabledModulesRegistry.js.map +2 -2
  68. package/jest.config.cjs +1 -2
  69. package/package.json +9 -19
  70. package/src/lib/ai/llm-provider.ts +0 -33
  71. package/src/lib/auth/__tests__/server.test.ts +0 -23
  72. package/src/lib/auth/server.ts +2 -23
  73. package/src/lib/bootstrap/__tests__/dynamicLoader.commandInterceptors.test.ts +8 -63
  74. package/src/lib/bootstrap/__tests__/dynamicLoader.tsconfig.test.ts +1 -21
  75. package/src/lib/bootstrap/dynamicLoader.ts +65 -213
  76. package/src/lib/commands/__tests__/command-interceptor-runner.test.ts +0 -28
  77. package/src/lib/commands/command-bus.ts +1 -5
  78. package/src/lib/commands/command-interceptor-runner.ts +2 -2
  79. package/src/lib/crud/__tests__/crud-factory.test.ts +0 -298
  80. package/src/lib/crud/__tests__/mutation-guard-registry.test.ts +0 -26
  81. package/src/lib/crud/enricher-runner.ts +11 -2
  82. package/src/lib/crud/factory.ts +1 -69
  83. package/src/lib/crud/interceptor-runner.ts +2 -2
  84. package/src/lib/crud/mutation-guard-registry.ts +2 -2
  85. package/src/lib/crud/types.ts +0 -3
  86. package/src/lib/data/__tests__/engine.bulk-suppress.test.ts +3 -18
  87. package/src/lib/data/engine.ts +16 -50
  88. package/src/lib/db/__tests__/mikro.test.ts +25 -0
  89. package/src/lib/db/mikro.ts +38 -13
  90. package/src/lib/db/pg-errors.ts +0 -58
  91. package/src/lib/di/container.ts +8 -72
  92. package/src/lib/email/__tests__/send.test.ts +1 -37
  93. package/src/lib/email/send.ts +0 -79
  94. package/src/lib/encryption/__tests__/subscriber.test.ts +2 -54
  95. package/src/lib/encryption/__tests__/tenantDataEncryptionService.test.ts +0 -58
  96. package/src/lib/encryption/subscriber.ts +14 -2
  97. package/src/lib/encryption/tenantDataEncryptionService.ts +18 -64
  98. package/src/lib/i18n/config.ts +2 -2
  99. package/src/lib/i18n/server.ts +2 -6
  100. package/src/lib/logger/__tests__/logger.test.ts +0 -77
  101. package/src/lib/logger/index.ts +1 -59
  102. package/src/lib/modules/registry.ts +0 -10
  103. package/src/lib/query/__tests__/engine.test.ts +13 -65
  104. package/src/lib/query/engine.ts +58 -75
  105. package/src/lib/query/types.ts +5 -4
  106. package/src/lib/redis/__tests__/connection.test.ts +0 -16
  107. package/src/lib/redis/connection.ts +7 -10
  108. package/src/lib/search/__tests__/config.test.ts +0 -177
  109. package/src/lib/search/config.ts +8 -106
  110. package/src/lib/search/tokenize.ts +11 -33
  111. package/src/lib/webhooks/__tests__/verify.test.ts +1 -20
  112. package/src/lib/webhooks/index.ts +1 -19
  113. package/src/lib/webhooks/verify.ts +9 -17
  114. package/src/modules/__tests__/registry.test.ts +2 -10
  115. package/src/modules/analytics.ts +0 -7
  116. package/src/modules/customer-auth.ts +0 -1
  117. package/src/modules/encryption.ts +0 -3
  118. package/src/modules/navigation/backendChrome.ts +0 -15
  119. package/src/modules/overrides.ts +0 -103
  120. package/src/modules/payment_gateways/types.ts +0 -41
  121. package/src/modules/registry.ts +0 -3
  122. package/src/security/enabledModulesRegistry.ts +12 -64
  123. package/dist/lib/ai/safety-identifier.js +0 -32
  124. package/dist/lib/ai/safety-identifier.js.map +0 -7
  125. package/dist/lib/auth/organizationScope.js +0 -34
  126. package/dist/lib/auth/organizationScope.js.map +0 -7
  127. package/dist/lib/bootstrap/clientOnlyModules.js +0 -55
  128. package/dist/lib/bootstrap/clientOnlyModules.js.map +0 -7
  129. package/dist/lib/data/consistency.js +0 -19
  130. package/dist/lib/data/consistency.js.map +0 -7
  131. package/dist/lib/db/entityMetadata.js +0 -26
  132. package/dist/lib/db/entityMetadata.js.map +0 -7
  133. package/dist/lib/logger/extension.js +0 -29
  134. package/dist/lib/logger/extension.js.map +0 -7
  135. package/dist/lib/modules/surfaceFingerprint.js +0 -47
  136. package/dist/lib/modules/surfaceFingerprint.js.map +0 -7
  137. package/dist/lib/query/ciphertext-search-warning.js +0 -45
  138. package/dist/lib/query/ciphertext-search-warning.js.map +0 -7
  139. package/dist/lib/search/auto-indexing.js +0 -14
  140. package/dist/lib/search/auto-indexing.js.map +0 -7
  141. package/dist/lib/search/availability.js +0 -107
  142. package/dist/lib/search/availability.js.map +0 -7
  143. package/dist/lib/search/tokenLookup.js +0 -46
  144. package/dist/lib/search/tokenLookup.js.map +0 -7
  145. package/dist/lib/telemetry/runtime.js +0 -35
  146. package/dist/lib/telemetry/runtime.js.map +0 -7
  147. package/dist/lib/webhooks/body.js +0 -57
  148. package/dist/lib/webhooks/body.js.map +0 -7
  149. package/dist/lib/webhooks/inbound-types.js +0 -1
  150. package/dist/lib/webhooks/inbound-types.js.map +0 -7
  151. package/dist/modules/widgets/extension-points.js +0 -96
  152. package/dist/modules/widgets/extension-points.js.map +0 -7
  153. package/dist/security/featurePolicy.js +0 -62
  154. package/dist/security/featurePolicy.js.map +0 -7
  155. package/scripts/versionSource.cjs +0 -65
  156. package/src/lib/__tests__/versionSource.test.ts +0 -46
  157. package/src/lib/ai/__tests__/llm-provider-contract.test.ts +0 -59
  158. package/src/lib/ai/__tests__/safety-identifier.test.ts +0 -71
  159. package/src/lib/ai/safety-identifier.ts +0 -73
  160. package/src/lib/auth/__tests__/organizationScope.test.ts +0 -107
  161. package/src/lib/auth/organizationScope.ts +0 -65
  162. package/src/lib/bootstrap/__tests__/clientOnlyModules.test.ts +0 -189
  163. package/src/lib/bootstrap/__tests__/dynamicLoader.aliasResolver.test.ts +0 -98
  164. package/src/lib/bootstrap/__tests__/dynamicLoader.appDi.test.ts +0 -211
  165. package/src/lib/bootstrap/__tests__/dynamicLoader.generatedCacheRecovery.test.ts +0 -192
  166. package/src/lib/bootstrap/clientOnlyModules.ts +0 -85
  167. package/src/lib/crud/__tests__/crud-factory.cache-user-scope.test.ts +0 -171
  168. package/src/lib/data/__tests__/consistency.test.ts +0 -38
  169. package/src/lib/data/consistency.ts +0 -17
  170. package/src/lib/db/__tests__/entityMetadata.test.ts +0 -38
  171. package/src/lib/db/__tests__/pg-errors.test.ts +0 -48
  172. package/src/lib/db/entityMetadata.ts +0 -43
  173. package/src/lib/di/__tests__/container-app-di-absent.test.ts +0 -155
  174. package/src/lib/di/__tests__/container-app-di.test.ts +0 -173
  175. package/src/lib/di/__tests__/registrar-error-log.test.ts +0 -154
  176. package/src/lib/i18n/__tests__/config.test.ts +0 -8
  177. package/src/lib/i18n/__tests__/server-dictionary-cache.test.ts +0 -30
  178. package/src/lib/i18n/__tests__/server-unbootstrapped.test.ts +0 -29
  179. package/src/lib/logger/extension.ts +0 -59
  180. package/src/lib/modules/__tests__/surfaceFingerprint.test.ts +0 -122
  181. package/src/lib/modules/surfaceFingerprint.ts +0 -87
  182. package/src/lib/query/__tests__/ciphertext-search-warning.test.ts +0 -178
  183. package/src/lib/query/ciphertext-search-warning.ts +0 -95
  184. package/src/lib/search/__tests__/availability.test.ts +0 -211
  185. package/src/lib/search/__tests__/tokenLookup.test.ts +0 -206
  186. package/src/lib/search/__tests__/tokenize.test.ts +0 -49
  187. package/src/lib/search/auto-indexing.ts +0 -22
  188. package/src/lib/search/availability.ts +0 -232
  189. package/src/lib/search/tokenLookup.ts +0 -133
  190. package/src/lib/telemetry/runtime.ts +0 -72
  191. package/src/lib/webhooks/__tests__/body.test.ts +0 -95
  192. package/src/lib/webhooks/body.ts +0 -70
  193. package/src/lib/webhooks/inbound-types.ts +0 -125
  194. package/src/modules/__tests__/nav-group-order-override.test.ts +0 -183
  195. package/src/modules/payment_gateways/__tests__/types.test.ts +0 -37
  196. package/src/modules/widgets/__tests__/extension-points.test.ts +0 -101
  197. package/src/modules/widgets/extension-points.ts +0 -524
  198. package/src/security/__tests__/featurePolicy.test.ts +0 -166
  199. package/src/security/featurePolicy.ts +0 -89
@@ -196,64 +196,6 @@ describe('TenantDataEncryptionService.encryptFields (issue #2720)', () => {
196
196
  })
197
197
  })
198
198
 
199
- describe('TenantDataEncryptionService system-scoped maps', () => {
200
- const originalToggle = process.env.TENANT_DATA_ENCRYPTION
201
-
202
- beforeEach(() => {
203
- process.env.TENANT_DATA_ENCRYPTION = 'yes'
204
- })
205
-
206
- afterEach(() => {
207
- if (originalToggle === undefined) delete process.env.TENANT_DATA_ENCRYPTION
208
- else process.env.TENANT_DATA_ENCRYPTION = originalToggle
209
- })
210
-
211
- it('encrypts tenant-less payloads with a stable entity-scoped KMS key', async () => {
212
- const entityId = 'onboarding:onboarding_request'
213
- const systemKeyId = `system:${entityId}`
214
- const getTenantDek = jest.fn(async (keyId: string) => (
215
- keyId === systemKeyId
216
- ? { tenantId: keyId, key: fixedKey, fetchedAt: new Date() }
217
- : null
218
- ))
219
- const service = new TenantDataEncryptionService(
220
- {
221
- getConnection: () => ({ execute: jest.fn(async () => []) }),
222
- } as never,
223
- {
224
- kms: {
225
- getTenantDek,
226
- createTenantDek: jest.fn(async () => null),
227
- isHealthy: () => true,
228
- },
229
- defaultEncryptionMaps: [
230
- {
231
- entityId,
232
- keyScope: 'system',
233
- fields: [
234
- { field: 'email', hashField: 'email_hash' },
235
- { field: 'password_hash' },
236
- ],
237
- },
238
- ],
239
- } as never,
240
- )
241
-
242
- const encrypted = await service.encryptEntityPayload(entityId, {
243
- email: 'person@example.com',
244
- email_hash: null,
245
- password_hash: 'bcrypt-value',
246
- }, null, null)
247
-
248
- expect(getTenantDek).toHaveBeenCalledWith(systemKeyId)
249
- expect(encrypted.email).not.toBe('person@example.com')
250
- expect(encrypted.password_hash).not.toBe('bcrypt-value')
251
- expect(encrypted.email_hash).toBe(hashForLookup('person@example.com'))
252
- expect(decryptWithAesGcm(encrypted.email as string, fixedKey)).toBe('person@example.com')
253
- expect(decryptWithAesGcm(encrypted.password_hash as string, fixedKey)).toBe('bcrypt-value')
254
- })
255
- })
256
-
257
199
  describe('TenantDataEncryptionService.getEncryptedFieldNames', () => {
258
200
  it('returns active encryption-map field names for query planning', async () => {
259
201
  const service = new TenantDataEncryptionService({} as never)
@@ -6,7 +6,6 @@ import { isTenantDataEncryptionEnabled } from './toggles'
6
6
  import { isEncryptionDebugEnabled } from './toggles'
7
7
  import { resolveTenantEncryptionService } from './customFieldValues'
8
8
  import { createLogger } from '../logger'
9
- import { listEntityMetadataFromRegistry } from '../db/entityMetadata'
10
9
 
11
10
  const logger = createLogger('shared').child({ component: 'encryption' })
12
11
 
@@ -86,8 +85,13 @@ export class TenantEncryptionSubscriber implements EventSubscriber<any> {
86
85
  try { return registry.find?.(ctor) } catch {}
87
86
  try { return registry.get?.(name) } catch {}
88
87
  try { return registry.get?.(ctor) } catch {}
88
+ const all =
89
+ (typeof registry.getAll === 'function' && registry.getAll()) ||
90
+ (Array.isArray((registry as any).metadata) ? (registry as any).metadata : undefined) ||
91
+ (registry as any).metadata ||
92
+ {}
89
93
  try {
90
- const entries = listEntityMetadataFromRegistry(registry)
94
+ const entries = Array.isArray(all) ? all : Object.values<any>(all)
91
95
  const match = entries.find(
92
96
  (m: any) =>
93
97
  m?.className === name ||
@@ -220,6 +224,10 @@ export class TenantEncryptionSubscriber implements EventSubscriber<any> {
220
224
  return
221
225
  }
222
226
  const { tenantId, organizationId } = resolveScope(target)
227
+ if (!tenantId) {
228
+ debug('⚪️ subscriber.skip', { reason: 'no-tenant', entityId })
229
+ return
230
+ }
223
231
  const encrypted = await this.service.encryptEntityPayload(entityId, target, tenantId, organizationId)
224
232
  const metaProps: Record<string, unknown> = resolvedMeta?.properties && typeof resolvedMeta.properties === 'object'
225
233
  ? resolvedMeta.properties
@@ -317,6 +325,10 @@ export class TenantEncryptionSubscriber implements EventSubscriber<any> {
317
325
  const { tenantId, organizationId } = resolveScope(target)
318
326
  const scopedTenantId = tenantId ?? fallbackScope?.tenantId ?? null
319
327
  const scopedOrgId = organizationId ?? fallbackScope?.organizationId ?? null
328
+ if (!scopedTenantId) {
329
+ debug('⚪️ subscriber.skip', { reason: 'no-tenant', entityId })
330
+ return
331
+ }
320
332
  // Capture pending (un-flushed) changes BEFORE decrypt mutates the target. Re-baselining a
321
333
  // managed entity that a command already mutated would clear its dirty changeset and silently
322
334
  // drop the pending write (e.g. an undo handler that mutates an entity, then loads a related
@@ -4,7 +4,6 @@ import { decryptWithAesGcm, encryptWithAesGcm, hashForLookup } from './aes'
4
4
  import { createKmsService, type KmsService, type TenantDek } from './kms'
5
5
  import { isTenantDataEncryptionEnabled, isEncryptionDebugEnabled } from './toggles'
6
6
  import { createLogger } from '../logger'
7
- import type { EncryptionKeyScope, ModuleEncryptionMap } from '../../modules/encryption'
8
7
 
9
8
  const logger = createLogger('shared').child({ component: 'tenant-encryption' })
10
9
 
@@ -15,7 +14,6 @@ export type EncryptedFieldRule = {
15
14
 
16
15
  export type EncryptionMapRecord = {
17
16
  entityId: string
18
- keyScope?: EncryptionKeyScope
19
17
  fields: EncryptedFieldRule[]
20
18
  }
21
19
 
@@ -156,23 +154,13 @@ export class TenantDataEncryptionService {
156
154
  private readonly inflightDeks = TenantDataEncryptionService.globalInflightDeks
157
155
  private readonly inflightMaps = TenantDataEncryptionService.globalInflightMaps
158
156
  private readonly missCache = TenantDataEncryptionService.globalMissCache
159
- private readonly systemDefaultMaps: Map<string, ModuleEncryptionMap>
160
157
 
161
158
  constructor(
162
159
  private em: EntityManager,
163
- opts?: {
164
- cache?: CacheStrategy
165
- kms?: KmsService
166
- defaultEncryptionMaps?: readonly ModuleEncryptionMap[]
167
- }
160
+ opts?: { cache?: CacheStrategy; kms?: KmsService }
168
161
  ) {
169
162
  this.cache = opts?.cache
170
163
  this.kms = opts?.kms ?? createKmsService()
171
- this.systemDefaultMaps = new Map(
172
- (opts?.defaultEncryptionMaps ?? [])
173
- .filter((map) => map.keyScope === 'system')
174
- .map((map) => [map.entityId, map]),
175
- )
176
164
  }
177
165
 
178
166
  isEnabled(): boolean {
@@ -251,24 +239,6 @@ export class TenantDataEncryptionService {
251
239
  }
252
240
  }
253
241
 
254
- private applySystemDefault(record: EncryptionMapRecord | null, entityId: string): EncryptionMapRecord | null {
255
- const declared = this.systemDefaultMaps.get(entityId)
256
- if (!declared) return record
257
- const fields: EncryptedFieldRule[] = declared.fields.map((field) => ({
258
- field: field.field,
259
- hashField: field.hashField ?? null,
260
- }))
261
- const declaredFields = new Set(fields.map((field) => field.field))
262
- for (const field of record?.fields ?? []) {
263
- if (!declaredFields.has(field.field)) fields.push(field)
264
- }
265
- return {
266
- entityId,
267
- keyScope: 'system',
268
- fields,
269
- }
270
- }
271
-
272
242
  private async getMap(key: MapCacheKey): Promise<EncryptionMapRecord | null> {
273
243
  const shouldSkipLookup = (tag: string) => {
274
244
  const expiresAt = this.missCache.get(tag)
@@ -292,13 +262,13 @@ export class TenantDataEncryptionService {
292
262
  if (this.inflightMaps.has(tag)) {
293
263
  const pending = this.inflightMaps.get(tag)!
294
264
  const resolved = await pending
295
- if (resolved) return this.applySystemDefault(resolved, key.entityId)
265
+ if (resolved) return resolved
296
266
  }
297
267
  const mem = this.memoryCache.get(tag)
298
- if (mem) return this.applySystemDefault(mem, key.entityId)
268
+ if (mem) return mem
299
269
  if (this.cache && typeof this.cache.get === 'function') {
300
270
  const cached = await this.cache.get(tag)
301
- if (cached) return this.applySystemDefault(cached as EncryptionMapRecord, key.entityId)
271
+ if (cached) return cached as EncryptionMapRecord
302
272
  }
303
273
  const pending = this.fetchMap(candidate)
304
274
  this.inflightMaps.set(tag, pending)
@@ -318,9 +288,9 @@ export class TenantDataEncryptionService {
318
288
  if (this.cache && typeof this.cache.set === 'function') {
319
289
  await this.cache.set(tag, loaded, { ttl: 300 })
320
290
  }
321
- return this.applySystemDefault(loaded, key.entityId)
291
+ return loaded
322
292
  }
323
- return this.applySystemDefault(null, key.entityId)
293
+ return null
324
294
  }
325
295
 
326
296
  private async fetchAllOrganizationFieldNames(entityId: string, tenantId: string | null): Promise<string[]> {
@@ -366,26 +336,12 @@ export class TenantDataEncryptionService {
366
336
  this.kms.invalidateDek?.(tenantId)
367
337
  }
368
338
 
369
- /**
370
- * Lists the fields an encryption map marks as encrypted at rest.
371
- *
372
- * `isEnabled()` folds the environment toggle together with KMS health, so by default an
373
- * unhealthy KMS reports "nothing is encrypted" — which is safe for write paths but wrong for
374
- * readers that must decide whether a stored column holds ciphertext. `ignoreRuntimeHealth`
375
- * answers the on-disk question instead: it consults the map even when the KMS cannot currently
376
- * resolve a DEK, so callers can fail closed rather than treat ciphertext as plaintext (#4622).
377
- */
378
339
  async getEncryptedFieldNames(
379
340
  entityId: string,
380
341
  tenantId: string | null | undefined,
381
- organizationId?: string | null,
382
- options?: { ignoreRuntimeHealth?: boolean }
342
+ organizationId?: string | null
383
343
  ): Promise<string[]> {
384
- if (options?.ignoreRuntimeHealth) {
385
- if (!isTenantDataEncryptionEnabled()) return []
386
- } else if (!this.isEnabled()) {
387
- return []
388
- }
344
+ if (!this.isEnabled()) return []
389
345
  const map = await this.getMap({ entityId, tenantId: tenantId ?? null, organizationId: organizationId ?? null })
390
346
  const fields = new Set(normalizeEncryptedFieldNames(map?.fields))
391
347
  if (organizationId == null) {
@@ -465,17 +421,16 @@ export class TenantDataEncryptionService {
465
421
  debug('⚪️ encrypt.skip.disabled', { entityId, tenantId })
466
422
  return payload
467
423
  }
424
+ const dek = await this.resolveDekForEncrypt(tenantId ?? null)
425
+ if (!dek) {
426
+ debug('⚠️ encrypt.skip.no-dek', { entityId, tenantId })
427
+ return payload
428
+ }
468
429
  const map = await this.getMap({ entityId, tenantId: tenantId ?? null, organizationId: organizationId ?? null })
469
430
  if (!map || !map.fields?.length) {
470
431
  debug('⚪️ encrypt.skip.no-map', { entityId, tenantId })
471
432
  return payload
472
433
  }
473
- const keyId = map.keyScope === 'system' ? `system:${entityId}` : tenantId ?? null
474
- const dek = await this.resolveDekForEncrypt(keyId)
475
- if (!dek) {
476
- debug('⚠️ encrypt.skip.no-dek', { entityId, tenantId, keyScope: map.keyScope ?? 'tenant' })
477
- return payload
478
- }
479
434
  debug('🔒 encrypt_entity', { entityId, tenantId, organizationId, fields: map.fields.length })
480
435
  return this.encryptFields(payload, map.fields, dek)
481
436
  }
@@ -490,17 +445,16 @@ export class TenantDataEncryptionService {
490
445
  debug('⚪️ decrypt.skip.disabled', { entityId, tenantId })
491
446
  return payload
492
447
  }
448
+ const dek = await this.getDek(tenantId ?? null)
449
+ if (!dek) {
450
+ debug('⚠️ decrypt.skip.no-dek', { entityId, tenantId })
451
+ return payload
452
+ }
493
453
  const map = await this.getMap({ entityId, tenantId: tenantId ?? null, organizationId: organizationId ?? null })
494
454
  if (!map || !map.fields?.length) {
495
455
  debug('⚪️ decrypt.skip.no-map', { entityId, tenantId })
496
456
  return payload
497
457
  }
498
- const keyId = map.keyScope === 'system' ? `system:${entityId}` : tenantId ?? null
499
- const dek = await this.getDek(keyId)
500
- if (!dek) {
501
- debug('⚠️ decrypt.skip.no-dek', { entityId, tenantId, keyScope: map.keyScope ?? 'tenant' })
502
- return payload
503
- }
504
458
  debug('🔓 decrypt_entity', { entityId, tenantId, organizationId, fields: map.fields.length })
505
459
  return this.decryptFields(payload, map.fields, dek)
506
460
  }
@@ -1,4 +1,4 @@
1
- export type Locale = 'en' | 'pl' | 'es' | 'de' | 'ko'
1
+ export type Locale = 'en' | 'pl' | 'es' | 'de'
2
2
 
3
- export const locales: Locale[] = ['en', 'pl', 'es', 'de', 'ko']
3
+ export const locales: Locale[] = ['en', 'pl', 'es', 'de']
4
4
  export const defaultLocale: Locale = 'en'
@@ -2,7 +2,7 @@ import { defaultLocale, locales, type Locale } from './config'
2
2
  import type { Dict } from './context'
3
3
  import { resolveForcedLocale, resolveLocaleFromAcceptLanguage } from './locale'
4
4
  import { createFallbackTranslator, createTranslator } from './translate'
5
- import { tryGetModules } from '../modules/registry'
5
+ import { getModules } from '../modules/registry'
6
6
  import { loadAppDictionary } from './app-dictionaries'
7
7
  import { getCachedDictionary, setCachedDictionary } from './dictionary-cache'
8
8
 
@@ -61,11 +61,7 @@ export async function loadDictionary(locale: Locale): Promise<Dict> {
61
61
  // Load from registry instead of @/ import (works in standalone packages)
62
62
  const baseRaw = await loadAppDictionary(locale)
63
63
  const merged: Dict = { ...flattenDictionary(baseRaw) }
64
- // Route handlers translate their responses, so they resolve a dictionary even
65
- // when they are exercised in isolation without a bootstrapped registry. The
66
- // app dictionary alone is the right degraded answer there — `registerModules`
67
- // invalidates this cache, so a later bootstrap still gets the merged result.
68
- const modules = tryGetModules() ?? []
64
+ const modules = getModules()
69
65
  for (const m of modules) {
70
66
  const dict = m.translations?.[locale]
71
67
  if (dict) Object.assign(merged, flattenDictionary(dict))
@@ -3,9 +3,7 @@ import {
3
3
  createLogger,
4
4
  getLogLevel,
5
5
  isLevelEnabled,
6
- registerLoggerExtension,
7
6
  resetLogLevelCache,
8
- resetLoggerExtension,
9
7
  resetLoggerRegistry,
10
8
  } from '../index'
11
9
  import { resolveLevel, type LogLevel } from '../level'
@@ -67,7 +65,6 @@ function resetLoggerState(): void {
67
65
  resetLoggerRegistry()
68
66
  resetServerLoggerCache()
69
67
  resetLogPrettyCache()
70
- resetLoggerExtension()
71
68
  }
72
69
 
73
70
  function forcePinoTransport(): void {
@@ -244,80 +241,6 @@ describe('structured logging facade', () => {
244
241
  })
245
242
  })
246
243
 
247
- describe('process-wide logger extension', () => {
248
- beforeEach(() => {
249
- forcePinoTransport()
250
- })
251
-
252
- it('keeps one local line and observes one correlated record with child bindings', () => {
253
- const fake = createFakePinoHarness()
254
- mockPinoLoader(fake.factory)
255
- const records: unknown[] = []
256
- registerLoggerExtension({
257
- enrich: () => ({ trace_id: 'trace-1', span_id: 'span-1' }),
258
- emit: (record) => records.push(record),
259
- })
260
-
261
- createLogger('orders')
262
- .child({ module: 'sales' })
263
- .info('Order placed', { orderId: 'o-1' })
264
-
265
- expect(fake.calls).toEqual([{
266
- level: 'info',
267
- args: [{
268
- orderId: 'o-1',
269
- trace_id: 'trace-1',
270
- span_id: 'span-1',
271
- }, 'Order placed'],
272
- }])
273
- expect(records).toHaveLength(1)
274
- expect(records[0]).toMatchObject({
275
- level: 'info',
276
- namespace: 'orders',
277
- message: 'Order placed',
278
- fields: {
279
- module: 'sales',
280
- orderId: 'o-1',
281
- trace_id: 'trace-1',
282
- span_id: 'span-1',
283
- },
284
- })
285
- })
286
-
287
- it('applies the shared level gate to local and remote output', () => {
288
- process.env.OM_LOG_LEVEL = 'warn'
289
- resetLogLevelCache()
290
- const fake = createFakePinoHarness()
291
- mockPinoLoader(fake.factory)
292
- const emit = jest.fn()
293
- registerLoggerExtension({ emit })
294
- const logger = createLogger('gated-extension')
295
-
296
- logger.info('quiet')
297
- logger.warn('visible')
298
-
299
- expect(fake.calls.map((call) => call.level)).toEqual(['warn'])
300
- expect(emit).toHaveBeenCalledTimes(1)
301
- expect(emit.mock.calls[0][0]).toMatchObject({ level: 'warn', message: 'visible' })
302
- })
303
-
304
- it('isolates extension failures from application logging', () => {
305
- const fake = createFakePinoHarness()
306
- mockPinoLoader(fake.factory)
307
- registerLoggerExtension({
308
- enrich: () => {
309
- throw new Error('context failed')
310
- },
311
- emit: () => {
312
- throw new Error('sink failed')
313
- },
314
- })
315
-
316
- expect(() => createLogger('resilient-extension').error('Still local')).not.toThrow()
317
- expect(fake.calls).toEqual([{ level: 'error', args: [{}, 'Still local'] }])
318
- })
319
- })
320
-
321
244
  describe('isomorphism', () => {
322
245
  beforeEach(() => {
323
246
  forcePinoTransport()
@@ -1,17 +1,9 @@
1
1
  import { selectTransport } from './transport'
2
- import { getLoggerExtension } from './extension'
3
- import { isLevelEnabled, type LogLevel } from './level'
4
2
 
5
3
  export type { LogLevel } from './level'
6
4
  export { getLogLevel, isLevelEnabled, resetLogLevelCache, OM_LOG_LEVEL_ENV } from './level'
7
5
  export { resetServerLoggerCache, OM_LOG_DESTINATION_ENV } from './transport.server'
8
6
  export { resetLogPrettyCache, OM_LOG_PRETTY_ENV } from './transport.pretty'
9
- export {
10
- getLoggerExtension,
11
- registerLoggerExtension,
12
- resetLoggerExtension,
13
- } from './extension'
14
- export type { LoggerExtension, LoggerExtensionRecord } from './extension'
15
7
 
16
8
  export type LogBindings = Record<string, unknown>
17
9
 
@@ -26,61 +18,11 @@ export interface Logger {
26
18
 
27
19
  const loggerRegistry = new Map<string, Logger>()
28
20
 
29
- function createExtendedLogger(
30
- namespace: string,
31
- transport: Logger,
32
- bindings: LogBindings = {},
33
- ): Logger {
34
- const emit = (
35
- level: LogLevel,
36
- msg: string,
37
- fields?: LogBindings,
38
- ): void => {
39
- if (!isLevelEnabled(level)) return
40
-
41
- const extension = getLoggerExtension()
42
- let context: LogBindings = {}
43
- try {
44
- context = extension?.enrich?.() ?? {}
45
- } catch {
46
- // Observability must never alter application behavior.
47
- }
48
-
49
- const mergedFields = { ...fields, ...context }
50
- transport[level](msg, mergedFields)
51
-
52
- try {
53
- extension?.emit?.({
54
- level,
55
- namespace,
56
- message: msg,
57
- fields: { ...bindings, ...mergedFields },
58
- time: Date.now(),
59
- })
60
- } catch {
61
- // Remote logging is best-effort; the local line has already been written.
62
- }
63
- }
64
-
65
- return {
66
- debug: (msg, fields) => emit('debug', msg, fields),
67
- info: (msg, fields) => emit('info', msg, fields),
68
- warn: (msg, fields) => emit('warn', msg, fields),
69
- error: (msg, fields) => emit('error', msg, fields),
70
- child: (childBindings) =>
71
- createExtendedLogger(
72
- namespace,
73
- transport.child(childBindings),
74
- { ...bindings, ...childBindings },
75
- ),
76
- }
77
- }
78
-
79
21
  /** Create (or reuse) a namespaced logger. `namespace` is attached as `name`. */
80
22
  export function createLogger(namespace: string): Logger {
81
23
  const existing = loggerRegistry.get(namespace)
82
24
  if (existing) return existing
83
- const created = createExtendedLogger(namespace, selectTransport(namespace))
25
+ const created = selectTransport(namespace)
84
26
  loggerRegistry.set(namespace, created)
85
27
  return created
86
28
  }
@@ -69,13 +69,3 @@ export function getModules(): Module[] {
69
69
  }
70
70
  return modules
71
71
  }
72
-
73
- /**
74
- * Non-throwing counterpart of `getModules()` for call sites that have a
75
- * meaningful degraded behavior when bootstrap has not run — route unit tests
76
- * exercise a single handler without `registerModules()`, and a hard throw there
77
- * turns an unrelated assertion into a bootstrap error.
78
- */
79
- export function tryGetModules(): Module[] | null {
80
- return getGlobalModules()
81
- }
@@ -1,13 +1,6 @@
1
1
  import { BasicQueryEngine } from '../engine'
2
2
  import { SortDir } from '../types'
3
3
  import { registerModules } from '../../i18n/server'
4
- import { clearSearchTokenPresenceCache } from '../../search/availability'
5
-
6
- // The token-presence answer is cached process-wide (TTL); without clearing it,
7
- // probe-count assertions would observe hits from earlier tests in this file.
8
- beforeEach(() => {
9
- clearSearchTokenPresenceCache()
10
- })
11
4
 
12
5
  // Mock modules with one entity extension
13
6
  const mockModules = [
@@ -362,8 +355,6 @@ describe('BasicQueryEngine (Kysely)', () => {
362
355
  const fakeDb = createFakeKysely({
363
356
  customer_entities: [],
364
357
  customer_people: [],
365
- search_tokens: [{ one: 1 }],
366
- 'information_schema.tables': [{ table_name: 'search_tokens' }],
367
358
  'information_schema.columns': [
368
359
  { table_name: 'customer_entities', column_name: 'tenant_id' },
369
360
  { table_name: 'customer_people', column_name: 'id' },
@@ -371,6 +362,8 @@ describe('BasicQueryEngine (Kysely)', () => {
371
362
  ],
372
363
  })
373
364
  const engine = new BasicQueryEngine({} as any, () => fakeDb as any)
365
+ jest.spyOn(engine as any, 'tableExists').mockResolvedValue(true)
366
+ jest.spyOn(engine as any, 'hasSearchTokens').mockResolvedValue(true)
374
367
  const applySearchTokensSpy = jest.spyOn(engine as any, 'applySearchTokens')
375
368
 
376
369
  await engine.query('customers:customer_entity', {
@@ -416,6 +409,7 @@ describe('BasicQueryEngine (Kysely)', () => {
416
409
  ],
417
410
  })
418
411
  const engine = new BasicQueryEngine({} as any, () => fakeDb as any)
412
+ jest.spyOn(engine as any, 'tableExists').mockResolvedValue(false)
419
413
  const applySearchTokensSpy = jest.spyOn(engine as any, 'applySearchTokens')
420
414
 
421
415
  await engine.query('customers:customer_entity', {
@@ -451,10 +445,10 @@ describe('BasicQueryEngine (Kysely)', () => {
451
445
  test('uses search tokens for index document fields on base entities', async () => {
452
446
  const fakeDb = createFakeKysely({
453
447
  todos: [],
454
- search_tokens: [{ one: 1 }],
455
- 'information_schema.tables': [{ table_name: 'search_tokens' }],
456
448
  })
457
449
  const engine = new BasicQueryEngine({} as any, () => fakeDb as any)
450
+ const tableExistsSpy = jest.spyOn(engine as any, 'tableExists').mockResolvedValue(true)
451
+ const hasSearchTokensSpy = jest.spyOn(engine as any, 'hasSearchTokens').mockResolvedValue(true)
458
452
  const applySearchTokensSpy = jest.spyOn(engine as any, 'applySearchTokens')
459
453
 
460
454
  await engine.query('example:todo', {
@@ -467,15 +461,12 @@ describe('BasicQueryEngine (Kysely)', () => {
467
461
  page: { page: 1, pageSize: 10 },
468
462
  })
469
463
 
470
- const calls = fakeDb._calls as Array<{ _ops: { table: string; wheres: unknown[][] } }>
471
- const tableProbe = calls.find((call) =>
472
- call._ops.table === 'information_schema.tables' &&
473
- call._ops.wheres.some((where) => where[0] === 'table_name' && where[2] === 'search_tokens'))
474
- expect(tableProbe).toBeTruthy()
475
- const tokenProbe = calls.find((call) =>
476
- call._ops.table === 'search_tokens' &&
477
- call._ops.wheres.some((where) => where[0] === 'entity_type' && where[2] === 'example:todo'))
478
- expect(tokenProbe).toBeTruthy()
464
+ expect(tableExistsSpy).toHaveBeenCalledWith('search_tokens')
465
+ expect(hasSearchTokensSpy).toHaveBeenCalledWith(
466
+ 'example:todo',
467
+ 't1',
468
+ expect.objectContaining({ ids: ['org1'] }),
469
+ )
479
470
  expect(applySearchTokensSpy).toHaveBeenCalledWith(
480
471
  expect.anything(),
481
472
  expect.objectContaining({
@@ -497,6 +488,7 @@ describe('BasicQueryEngine (Kysely)', () => {
497
488
  ],
498
489
  })
499
490
  const engine = new BasicQueryEngine({} as any, () => fakeDb as any)
491
+ const hasSearchTokensSpy = jest.spyOn(engine as any, 'hasSearchTokens').mockResolvedValue(true)
500
492
  const applySearchTokensSpy = jest.spyOn(engine as any, 'applySearchTokens')
501
493
 
502
494
  await engine.query('example:todo', {
@@ -509,8 +501,7 @@ describe('BasicQueryEngine (Kysely)', () => {
509
501
  page: { page: 1, pageSize: 10 },
510
502
  })
511
503
 
512
- const calls = fakeDb._calls as Array<{ _ops: { table: string } }>
513
- expect(calls.some((call) => call._ops.table === 'search_tokens')).toBe(false)
504
+ expect(hasSearchTokensSpy).not.toHaveBeenCalled()
514
505
  expect(applySearchTokensSpy).not.toHaveBeenCalled()
515
506
  const baseCall = fakeDb._calls.find((builder: any) => builder._ops.table === 'todos')
516
507
  expect(baseCall?._ops.wheres).toContainEqual(['todos.search_text', 'ilike', '%avision%'])
@@ -942,47 +933,4 @@ describe('BasicQueryEngine (Kysely)', () => {
942
933
  expect(baseCall._ops.limits).toBe(10)
943
934
  expect(baseCall._ops.offsets).toBe(10)
944
935
  })
945
-
946
- describe('search_tokens coverage probe (#4723 parity)', () => {
947
- type ProbeDbLog = { _calls: Array<{ _ops: { table: string } }> }
948
-
949
- const countProbes = (fakeDb: ProbeDbLog): number =>
950
- fakeDb._calls.filter((call) => call._ops.table === 'search_tokens').length
951
-
952
- const buildEngine = (fakeDb: unknown): BasicQueryEngine => new BasicQueryEngine(
953
- {} as ConstructorParameters<typeof BasicQueryEngine>[0],
954
- (() => fakeDb) as unknown as NonNullable<ConstructorParameters<typeof BasicQueryEngine>[1]>,
955
- )
956
-
957
- const buildDb = () => createFakeKysely({
958
- users: [],
959
- 'information_schema.tables': [{ table_name: 'search_tokens' }],
960
- })
961
-
962
- test('is skipped when the query carries no like/ilike filter', async () => {
963
- const fakeDb = buildDb()
964
- const engine = buildEngine(fakeDb)
965
-
966
- await engine.query('auth:user', {
967
- tenantId: 't1',
968
- organizationId: 'org1',
969
- filters: { is_active: { $eq: true } },
970
- })
971
-
972
- expect(countProbes(fakeDb)).toBe(0)
973
- })
974
-
975
- test('still runs when the query actually searches', async () => {
976
- const fakeDb = buildDb()
977
- const engine = buildEngine(fakeDb)
978
-
979
- await engine.query('auth:user', {
980
- tenantId: 't1',
981
- organizationId: 'org1',
982
- filters: { email: { $ilike: '%abc%' } },
983
- })
984
-
985
- expect(countProbes(fakeDb)).toBeGreaterThan(0)
986
- })
987
- })
988
936
  })