@open-mercato/shared 0.6.6-develop.6460.1.f94c74174c → 0.6.6-develop.6471.1.9ab5bdd79a

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 (164) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +1 -0
  3. package/dist/lib/commands/command-bus.js +4 -2
  4. package/dist/lib/commands/command-bus.js.map +2 -2
  5. package/dist/lib/commands/command-interceptor-runner.js +4 -2
  6. package/dist/lib/commands/command-interceptor-runner.js.map +2 -2
  7. package/dist/lib/commands/flush.js +3 -4
  8. package/dist/lib/commands/flush.js.map +2 -2
  9. package/dist/lib/commands/helpers.js +3 -1
  10. package/dist/lib/commands/helpers.js.map +2 -2
  11. package/dist/lib/commands/registry.js +5 -3
  12. package/dist/lib/commands/registry.js.map +2 -2
  13. package/dist/lib/commands/scope.js +5 -3
  14. package/dist/lib/commands/scope.js.map +2 -2
  15. package/dist/lib/crud/advanced-filter-integration.js +3 -1
  16. package/dist/lib/crud/advanced-filter-integration.js.map +2 -2
  17. package/dist/lib/crud/cache.js +3 -1
  18. package/dist/lib/crud/cache.js.map +2 -2
  19. package/dist/lib/crud/custom-fields.js +4 -2
  20. package/dist/lib/crud/custom-fields.js.map +2 -2
  21. package/dist/lib/crud/enricher-runner.js +6 -10
  22. package/dist/lib/crud/enricher-runner.js.map +2 -2
  23. package/dist/lib/crud/factory.js +10 -8
  24. package/dist/lib/crud/factory.js.map +2 -2
  25. package/dist/lib/crud/interceptor-registry.js +3 -3
  26. package/dist/lib/crud/interceptor-registry.js.map +2 -2
  27. package/dist/lib/crud/mutation-guard.js +4 -2
  28. package/dist/lib/crud/mutation-guard.js.map +2 -2
  29. package/dist/lib/crud/optimistic-lock.js +5 -6
  30. package/dist/lib/crud/optimistic-lock.js.map +2 -2
  31. package/dist/lib/crud/route-mutation-guard.js +3 -1
  32. package/dist/lib/crud/route-mutation-guard.js.map +2 -2
  33. package/dist/lib/crud/sync-event-runner.js +3 -1
  34. package/dist/lib/crud/sync-event-runner.js.map +2 -2
  35. package/dist/lib/data/engine.js +5 -5
  36. package/dist/lib/data/engine.js.map +2 -2
  37. package/dist/lib/db/mikro.js +5 -3
  38. package/dist/lib/db/mikro.js.map +2 -2
  39. package/dist/lib/di/container.js +3 -1
  40. package/dist/lib/di/container.js.map +2 -2
  41. package/dist/lib/encryption/aes.js +3 -1
  42. package/dist/lib/encryption/aes.js.map +2 -2
  43. package/dist/lib/encryption/entityFields.js +3 -1
  44. package/dist/lib/encryption/entityFields.js.map +2 -2
  45. package/dist/lib/encryption/entityIds.js +3 -1
  46. package/dist/lib/encryption/entityIds.js.map +2 -2
  47. package/dist/lib/encryption/kms.js +25 -32
  48. package/dist/lib/encryption/kms.js.map +2 -2
  49. package/dist/lib/encryption/subscriber.js +3 -1
  50. package/dist/lib/encryption/subscriber.js.map +2 -2
  51. package/dist/lib/encryption/tenantDataEncryptionService.js +3 -1
  52. package/dist/lib/encryption/tenantDataEncryptionService.js.map +2 -2
  53. package/dist/lib/indexers/error-log.js +4 -2
  54. package/dist/lib/indexers/error-log.js.map +2 -2
  55. package/dist/lib/indexers/status-log.js +5 -3
  56. package/dist/lib/indexers/status-log.js.map +2 -2
  57. package/dist/lib/logger/index.js +28 -0
  58. package/dist/lib/logger/index.js.map +7 -0
  59. package/dist/lib/logger/level.js +43 -0
  60. package/dist/lib/logger/level.js.map +7 -0
  61. package/dist/lib/logger/transport.console.js +39 -0
  62. package/dist/lib/logger/transport.console.js.map +7 -0
  63. package/dist/lib/logger/transport.js +17 -0
  64. package/dist/lib/logger/transport.js.map +7 -0
  65. package/dist/lib/logger/transport.pretty.js +85 -0
  66. package/dist/lib/logger/transport.pretty.js.map +7 -0
  67. package/dist/lib/logger/transport.server.js +85 -0
  68. package/dist/lib/logger/transport.server.js.map +7 -0
  69. package/dist/lib/middleware/page-executor.js +3 -1
  70. package/dist/lib/middleware/page-executor.js.map +2 -2
  71. package/dist/lib/modules/registry.js +3 -1
  72. package/dist/lib/modules/registry.js.map +2 -2
  73. package/dist/lib/profiler/index.js +3 -2
  74. package/dist/lib/profiler/index.js.map +2 -2
  75. package/dist/lib/query/engine.js +5 -6
  76. package/dist/lib/query/engine.js.map +2 -2
  77. package/dist/lib/query/query-extension-runner.js +5 -11
  78. package/dist/lib/query/query-extension-runner.js.map +2 -2
  79. package/dist/lib/redis/connection.js +3 -1
  80. package/dist/lib/redis/connection.js.map +2 -2
  81. package/dist/lib/url.js +7 -5
  82. package/dist/lib/url.js.map +2 -2
  83. package/dist/lib/version.js +1 -1
  84. package/dist/lib/version.js.map +1 -1
  85. package/dist/modules/analytics.js +3 -1
  86. package/dist/modules/analytics.js.map +2 -2
  87. package/dist/modules/dashboard/widgets.js +3 -1
  88. package/dist/modules/dashboard/widgets.js.map +2 -2
  89. package/dist/modules/events/factory.js +5 -3
  90. package/dist/modules/events/factory.js.map +2 -2
  91. package/dist/modules/overrides.js +16 -34
  92. package/dist/modules/overrides.js.map +2 -2
  93. package/dist/modules/registry.js +3 -1
  94. package/dist/modules/registry.js.map +2 -2
  95. package/dist/modules/search.js +3 -1
  96. package/dist/modules/search.js.map +2 -2
  97. package/dist/modules/widgets/injection-loader.js +6 -6
  98. package/dist/modules/widgets/injection-loader.js.map +2 -2
  99. package/dist/modules/widgets/injection-position.js +3 -1
  100. package/dist/modules/widgets/injection-position.js.map +2 -2
  101. package/package.json +7 -2
  102. package/src/lib/commands/__tests__/command-interceptor-runner.test.ts +19 -6
  103. package/src/lib/commands/__tests__/flush.test.ts +24 -15
  104. package/src/lib/commands/__tests__/normalizeAuthorUserId.test.ts +14 -2
  105. package/src/lib/commands/__tests__/registry.test.ts +17 -2
  106. package/src/lib/commands/__tests__/scope.test.ts +21 -9
  107. package/src/lib/commands/command-bus.ts +5 -2
  108. package/src/lib/commands/command-interceptor-runner.ts +5 -2
  109. package/src/lib/commands/flush.ts +4 -5
  110. package/src/lib/commands/helpers.ts +9 -2
  111. package/src/lib/commands/registry.ts +6 -3
  112. package/src/lib/commands/scope.ts +6 -3
  113. package/src/lib/crud/__tests__/optimistic-lock.test.ts +21 -6
  114. package/src/lib/crud/__tests__/route-mutation-guard.test.ts +17 -2
  115. package/src/lib/crud/__tests__/sync-event-runner.test.ts +17 -3
  116. package/src/lib/crud/advanced-filter-integration.ts +4 -1
  117. package/src/lib/crud/cache.ts +4 -1
  118. package/src/lib/crud/custom-fields.ts +5 -2
  119. package/src/lib/crud/enricher-runner.ts +7 -10
  120. package/src/lib/crud/factory.ts +11 -10
  121. package/src/lib/crud/interceptor-registry.ts +4 -3
  122. package/src/lib/crud/mutation-guard.ts +5 -2
  123. package/src/lib/crud/optimistic-lock.ts +6 -10
  124. package/src/lib/crud/route-mutation-guard.ts +4 -1
  125. package/src/lib/crud/sync-event-runner.ts +4 -1
  126. package/src/lib/data/__tests__/engine.event-validation.test.ts +26 -15
  127. package/src/lib/data/engine.ts +6 -6
  128. package/src/lib/db/mikro.ts +6 -3
  129. package/src/lib/di/container.ts +4 -1
  130. package/src/lib/encryption/aes.ts +4 -2
  131. package/src/lib/encryption/entityFields.ts +5 -1
  132. package/src/lib/encryption/entityIds.ts +4 -1
  133. package/src/lib/encryption/kms.ts +27 -33
  134. package/src/lib/encryption/subscriber.ts +4 -2
  135. package/src/lib/encryption/tenantDataEncryptionService.ts +4 -2
  136. package/src/lib/indexers/__tests__/status-log.test.ts +22 -10
  137. package/src/lib/indexers/error-log.ts +5 -2
  138. package/src/lib/indexers/status-log.ts +6 -3
  139. package/src/lib/logger/__tests__/logger.test.ts +539 -0
  140. package/src/lib/logger/index.ts +33 -0
  141. package/src/lib/logger/level.ts +50 -0
  142. package/src/lib/logger/transport.console.ts +41 -0
  143. package/src/lib/logger/transport.pretty.ts +94 -0
  144. package/src/lib/logger/transport.server.ts +125 -0
  145. package/src/lib/logger/transport.ts +17 -0
  146. package/src/lib/middleware/page-executor.ts +4 -1
  147. package/src/lib/modules/__tests__/registry.test.ts +28 -21
  148. package/src/lib/modules/registry.ts +4 -1
  149. package/src/lib/profiler/index.ts +4 -2
  150. package/src/lib/query/__tests__/query-extension-runner.test.ts +21 -7
  151. package/src/lib/query/engine.ts +7 -10
  152. package/src/lib/query/query-extension-runner.ts +6 -11
  153. package/src/lib/redis/connection.ts +5 -1
  154. package/src/lib/url.ts +10 -5
  155. package/src/modules/__tests__/overrides.test.ts +17 -3
  156. package/src/modules/__tests__/route-overrides.test.ts +29 -20
  157. package/src/modules/analytics.ts +5 -1
  158. package/src/modules/dashboard/widgets.ts +4 -1
  159. package/src/modules/events/factory.ts +6 -3
  160. package/src/modules/overrides.ts +18 -34
  161. package/src/modules/registry.ts +4 -1
  162. package/src/modules/search.ts +4 -1
  163. package/src/modules/widgets/injection-loader.ts +7 -6
  164. package/src/modules/widgets/injection-position.ts +5 -1
@@ -1,4 +1,19 @@
1
1
  import { withAtomicFlush } from '../flush'
2
+ import { createLogger } from '@open-mercato/shared/lib/logger'
3
+
4
+ jest.mock('@open-mercato/shared/lib/logger', () => {
5
+ const mocked = {
6
+ debug: jest.fn(),
7
+ info: jest.fn(),
8
+ warn: jest.fn(),
9
+ error: jest.fn(),
10
+ child: jest.fn(),
11
+ }
12
+ mocked.child.mockImplementation(() => mocked)
13
+ return { createLogger: jest.fn(() => mocked) }
14
+ })
15
+ const loggerWarn = createLogger('shared').warn as jest.Mock
16
+
2
17
 
3
18
  type FakeEntityManager = {
4
19
  flush: jest.Mock<Promise<void>, []>
@@ -258,11 +273,10 @@ describe('withAtomicFlush', () => {
258
273
  // after its own flush). The guard must persist it instead of letting the
259
274
  // transaction commit the work-in-progress silently.
260
275
  const em = createUowEm([{ entity: 'lingering' }], { inTransaction: false })
261
- const warn = jest.spyOn(console, 'warn').mockImplementation(() => {})
276
+ loggerWarn.mockClear()
262
277
  try {
263
278
  await withAtomicFlush(em as any, [() => {}], { transaction: true, label: 'demo.command' })
264
279
  } finally {
265
- warn.mockRestore()
266
280
  }
267
281
 
268
282
  // 1 per-phase flush + 1 defensive guard flush, all inside the same transaction.
@@ -275,36 +289,31 @@ describe('withAtomicFlush', () => {
275
289
  const em = createUowEm([{ a: 1 }, { b: 2 }])
276
290
  const previousEnv = process.env.NODE_ENV
277
291
  process.env.NODE_ENV = 'development'
278
- const warn = jest.spyOn(console, 'warn').mockImplementation(() => {})
279
- let warnCallCount = 0
280
- let warnMessage = ''
292
+ loggerWarn.mockClear()
281
293
  try {
282
294
  await withAtomicFlush(em as any, [() => {}], { label: 'sales.update_shipment' })
283
- // Capture BEFORE mockRestore — restore() resets mock.calls.
284
- warnCallCount = warn.mock.calls.length
285
- warnMessage = String(warn.mock.calls[0]?.[0] ?? '')
286
295
  } finally {
287
- warn.mockRestore()
288
296
  process.env.NODE_ENV = previousEnv
289
297
  }
290
298
 
291
- expect(warnCallCount).toBe(1)
292
- expect(warnMessage).toContain('sales.update_shipment')
293
- expect(warnMessage).toContain('2 pending change-set(s)')
299
+ expect(loggerWarn).toHaveBeenCalledTimes(1)
300
+ expect(loggerWarn).toHaveBeenCalledWith(
301
+ expect.stringContaining('flushed defensively'),
302
+ expect.objectContaining({ label: 'sales.update_shipment', pendingCount: 2 }),
303
+ )
294
304
  })
295
305
 
296
306
  it('does NOT flush again when the UnitOfWork is clean at the boundary', async () => {
297
307
  const em = createUowEm([])
298
- const warn = jest.spyOn(console, 'warn').mockImplementation(() => {})
308
+ loggerWarn.mockClear()
299
309
  try {
300
310
  await withAtomicFlush(em as any, [() => {}, () => {}])
301
311
  } finally {
302
- warn.mockRestore()
303
312
  }
304
313
 
305
314
  // 2 phases → 2 per-phase flushes; the clean guard adds nothing.
306
315
  expect(em.flush).toHaveBeenCalledTimes(2)
307
- expect(warn).not.toHaveBeenCalled()
316
+ expect(loggerWarn).not.toHaveBeenCalled()
308
317
  })
309
318
 
310
319
  it('never throws when the UnitOfWork probe itself fails', async () => {
@@ -3,8 +3,20 @@ import { normalizeAuthorUserId } from '@open-mercato/shared/lib/commands/helpers
3
3
  describe('normalizeAuthorUserId', () => {
4
4
  const validUuid = 'a1b2c3d4-e5f6-1a2b-9c3d-4e5f6a7b8c9d'
5
5
 
6
- it('returns explicit authorUserId when provided', () => {
7
- expect(normalizeAuthorUserId('explicit-id', { sub: validUuid })).toBe('explicit-id')
6
+ it('ignores explicit authorUserId for normal authenticated callers', () => {
7
+ const spoofedUuid = 'b1b2c3d4-e5f6-1a2b-9c3d-4e5f6a7b8c9d'
8
+
9
+ expect(normalizeAuthorUserId(spoofedUuid, { sub: validUuid })).toBe(validUuid)
10
+ })
11
+
12
+ it('honors explicit authorUserId for super admins', () => {
13
+ const delegatedUuid = 'b1b2c3d4-e5f6-1a2b-9c3d-4e5f6a7b8c9d'
14
+
15
+ expect(normalizeAuthorUserId(delegatedUuid, { sub: validUuid, isSuperAdmin: true })).toBe(delegatedUuid)
16
+ })
17
+
18
+ it('rejects non-UUID explicit authorUserId values for super admins', () => {
19
+ expect(normalizeAuthorUserId('not-a-uuid', { sub: validUuid, isSuperAdmin: true })).toBe(validUuid)
8
20
  })
9
21
 
10
22
  it('returns null when no explicit ID and auth is null', () => {
@@ -1,4 +1,19 @@
1
1
  import { commandRegistry, registerCommand, registerCommandLoaders } from '@open-mercato/shared/lib/commands'
2
+ import { createLogger } from '@open-mercato/shared/lib/logger'
3
+
4
+ jest.mock('@open-mercato/shared/lib/logger', () => {
5
+ const mocked = {
6
+ debug: jest.fn(),
7
+ info: jest.fn(),
8
+ warn: jest.fn(),
9
+ error: jest.fn(),
10
+ child: jest.fn(),
11
+ }
12
+ mocked.child.mockImplementation(() => mocked)
13
+ return { createLogger: jest.fn(() => mocked) }
14
+ })
15
+ const loggerDebug = createLogger('shared').debug as jest.Mock
16
+
2
17
 
3
18
  describe('command registry registration', () => {
4
19
  const originalNodeEnv = process.env.NODE_ENV
@@ -27,7 +42,7 @@ describe('command registry registration', () => {
27
42
 
28
43
  it('overwrites duplicate command ids in development to tolerate HMR re-evaluation', () => {
29
44
  process.env.NODE_ENV = 'development'
30
- const debugSpy = jest.spyOn(console, 'debug').mockImplementation(() => {})
45
+ loggerDebug.mockClear()
31
46
  const firstExecute = jest.fn(async () => 'first')
32
47
  const secondExecute = jest.fn(async () => 'second')
33
48
 
@@ -42,7 +57,7 @@ describe('command registry registration', () => {
42
57
  })
43
58
 
44
59
  expect(commandRegistry.get('test.command.hmr')?.execute).toBe(secondExecute)
45
- expect(debugSpy).toHaveBeenCalledWith('[Bootstrap] Commands re-registered (this may occur during HMR)')
60
+ expect(loggerDebug).toHaveBeenCalledWith('Commands re-registered (this may occur during HMR)')
46
61
  })
47
62
 
48
63
  it('loads a command handler on demand from a registered loader', async () => {
@@ -1,6 +1,21 @@
1
1
  import { ensureOrganizationScope, ensureTenantScope } from '@open-mercato/shared/lib/commands/scope'
2
2
  import type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'
3
3
  import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
4
+ import { createLogger } from '@open-mercato/shared/lib/logger'
5
+
6
+ jest.mock('@open-mercato/shared/lib/logger', () => {
7
+ const mocked = {
8
+ debug: jest.fn(),
9
+ info: jest.fn(),
10
+ warn: jest.fn(),
11
+ error: jest.fn(),
12
+ child: jest.fn(),
13
+ }
14
+ mocked.child.mockImplementation(() => mocked)
15
+ return { createLogger: jest.fn(() => mocked) }
16
+ })
17
+ const loggerWarn = createLogger('shared').warn as jest.Mock
18
+
4
19
 
5
20
  type ScopeShape = NonNullable<CommandRuntimeContext['organizationScope']>
6
21
 
@@ -154,17 +169,14 @@ describe('ensureOrganizationScope', () => {
154
169
  describe('fail-open-by-omission hardening (#2441)', () => {
155
170
  const originalNodeEnv = process.env.NODE_ENV
156
171
  const originalStrict = process.env.OM_ENFORCE_ORG_SCOPE_STRICT
157
- let warnSpy: jest.SpyInstance
158
-
159
172
  beforeEach(() => {
160
173
  // The scope loggers suppress output under NODE_ENV==='test'; flip it so the
161
174
  // observability signal becomes assertable, then restore in afterEach.
162
175
  process.env.NODE_ENV = 'development'
163
- warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {})
176
+ loggerWarn.mockClear()
164
177
  })
165
178
 
166
179
  afterEach(() => {
167
- warnSpy.mockRestore()
168
180
  if (originalNodeEnv === undefined) delete process.env.NODE_ENV
169
181
  else process.env.NODE_ENV = originalNodeEnv
170
182
  if (originalStrict === undefined) delete process.env.OM_ENFORCE_ORG_SCOPE_STRICT
@@ -175,8 +187,8 @@ describe('ensureOrganizationScope', () => {
175
187
  delete process.env.OM_ENFORCE_ORG_SCOPE_STRICT
176
188
  const ctx = buildCtx({ orgId: null, selectedOrganizationId: null, organizationScope: null })
177
189
  expect(() => ensureOrganizationScope(ctx, 'org-b')).not.toThrow()
178
- expect(warnSpy).toHaveBeenCalledWith(
179
- '[scope] Unscoped organization command executed without organization context',
190
+ expect(loggerWarn).toHaveBeenCalledWith(
191
+ 'Unscoped organization command executed without organization context',
180
192
  expect.objectContaining({ targetOrganizationId: 'org-b', strictEnforcement: false })
181
193
  )
182
194
  })
@@ -185,8 +197,8 @@ describe('ensureOrganizationScope', () => {
185
197
  process.env.OM_ENFORCE_ORG_SCOPE_STRICT = 'true'
186
198
  const ctx = buildCtx({ orgId: null, selectedOrganizationId: null, organizationScope: null })
187
199
  expect(() => ensureOrganizationScope(ctx, 'org-b')).toThrow(CrudHttpError)
188
- expect(warnSpy).toHaveBeenCalledWith(
189
- '[scope] Unscoped organization command executed without organization context',
200
+ expect(loggerWarn).toHaveBeenCalledWith(
201
+ 'Unscoped organization command executed without organization context',
190
202
  expect.objectContaining({ targetOrganizationId: 'org-b', strictEnforcement: true })
191
203
  )
192
204
  })
@@ -195,7 +207,7 @@ describe('ensureOrganizationScope', () => {
195
207
  delete process.env.OM_ENFORCE_ORG_SCOPE_STRICT
196
208
  const ctx = buildCtx({ orgId: null, selectedOrganizationId: null, organizationScope: null })
197
209
  expect(() => ensureOrganizationScope(ctx, '')).not.toThrow()
198
- expect(warnSpy).not.toHaveBeenCalled()
210
+ expect(loggerWarn).not.toHaveBeenCalled()
199
211
  })
200
212
  })
201
213
  })
@@ -30,6 +30,9 @@ import {
30
30
  } from './command-interceptor-runner'
31
31
  import type { CommandInterceptorContext } from './command-interceptor'
32
32
  import { CommandInterceptorError } from './errors'
33
+ import { createLogger } from '../logger'
34
+
35
+ const logger = createLogger('shared').child({ component: 'commands' })
33
36
 
34
37
  const SKIPPED_ACTION_LOG_RESOURCE_KINDS = new Set<string>([
35
38
  'audit_logs.access',
@@ -624,7 +627,7 @@ export class CommandBus {
624
627
  } catch (err) {
625
628
  if (isCrudCacheDebugEnabled()) {
626
629
  try {
627
- console.debug('[crud][cache] execute-invalidation failed', { commandId, err })
630
+ logger.debug('Cache execute-invalidation failed', { commandId, err })
628
631
  } catch {}
629
632
  }
630
633
  }
@@ -656,7 +659,7 @@ export class CommandBus {
656
659
  } catch (err) {
657
660
  if (isCrudCacheDebugEnabled()) {
658
661
  try {
659
- console.debug('[crud][cache] undo-invalidation failed', { commandId: log.commandId, err })
662
+ logger.debug('Cache undo-invalidation failed', { commandId: log.commandId, err })
660
663
  } catch {}
661
664
  }
662
665
  }
@@ -4,6 +4,9 @@ import type {
4
4
  CommandInterceptorUndoContext,
5
5
  } from './command-interceptor'
6
6
  import { hasAllFeatures } from '../../security/features'
7
+ import { createLogger } from '../logger'
8
+
9
+ const logger = createLogger('shared').child({ component: 'commands' })
7
10
 
8
11
  // ---------------------------------------------------------------------------
9
12
  // Command pattern matching
@@ -116,7 +119,7 @@ export async function runCommandInterceptorsAfter(
116
119
  currentResult = { ...(currentResult as Record<string, unknown>), ...afterResult.modifiedResult }
117
120
  }
118
121
  } catch (error) {
119
- console.error(`[command-interceptor] afterExecute failed: ${interceptor.id}`, error)
122
+ logger.error('Command interceptor afterExecute failed', { interceptorId: interceptor.id, err: error })
120
123
  }
121
124
  }
122
125
 
@@ -185,7 +188,7 @@ export async function runCommandInterceptorsAfterUndo(
185
188
  { ...context, commandId, metadata: metadataByInterceptor.get(interceptor.id) },
186
189
  )
187
190
  } catch (error) {
188
- console.error(`[command-interceptor] afterUndo failed: ${interceptor.id}`, error)
191
+ logger.error('Command interceptor afterUndo failed', { interceptorId: interceptor.id, err: error })
189
192
  }
190
193
  }
191
194
  }
@@ -1,5 +1,8 @@
1
1
  import type { EntityManager } from '@mikro-orm/postgresql'
2
2
  import type { IsolationLevel } from '@mikro-orm/core'
3
+ import { createLogger } from '../logger'
4
+
5
+ const logger = createLogger('shared').child({ component: 'commands' })
3
6
 
4
7
  /**
5
8
  * Options controlling how {@link withAtomicFlush} executes its phases.
@@ -72,11 +75,7 @@ async function flushPendingChangesGuard(
72
75
  if (pendingCount > 0) {
73
76
  await em.flush()
74
77
  if (process.env.NODE_ENV !== 'production') {
75
- const where = label ? ` (${label})` : ''
76
- console.warn(
77
- `[withAtomicFlush]${where}: ${pendingCount} pending change-set(s) remained at the commit boundary and were flushed defensively. ` +
78
- 'A phase mutated a managed entity after its flush boundary — split the mutation and any dependent read/sync into separate phases so the change is never at risk of being dropped.',
79
- )
78
+ logger.warn('withAtomicFlush: pending change-sets remained at the commit boundary and were flushed defensively — a phase mutated a managed entity after its flush boundary; split the mutation and any dependent read/sync into separate phases', { label: label ?? null, pendingCount })
80
79
  }
81
80
  }
82
81
  }
@@ -170,9 +170,16 @@ const AUTHOR_UUID_REGEX = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[8
170
170
 
171
171
  export function normalizeAuthorUserId(
172
172
  explicitAuthorUserId: string | undefined | null,
173
- auth: { isApiKey?: boolean; sub?: string | null } | undefined | null
173
+ auth: { isApiKey?: boolean; isSuperAdmin?: boolean; sub?: string | null } | undefined | null
174
174
  ): string | null {
175
- if (explicitAuthorUserId) return explicitAuthorUserId
175
+ if (
176
+ explicitAuthorUserId &&
177
+ auth?.isSuperAdmin === true &&
178
+ auth.isApiKey !== true &&
179
+ AUTHOR_UUID_REGEX.test(explicitAuthorUserId)
180
+ ) {
181
+ return explicitAuthorUserId
182
+ }
176
183
  const authSub = auth?.isApiKey ? null : auth?.sub ?? null
177
184
  if (!authSub) return null
178
185
  return AUTHOR_UUID_REGEX.test(authSub) ? authSub : null
@@ -1,4 +1,7 @@
1
1
  import type { CommandHandler } from './types'
2
+ import { createLogger } from '../logger'
3
+
4
+ const logger = createLogger('shared').child({ component: 'commands' })
2
5
 
3
6
  export type CommandLoader = {
4
7
  id?: string | null
@@ -21,7 +24,7 @@ class CommandRegistry {
21
24
  if (this.handlers.has(handler.id)) {
22
25
  if (process.env.NODE_ENV === 'development') {
23
26
  if (!this.didWarnAboutDevelopmentReregistration) {
24
- console.debug('[Bootstrap] Commands re-registered (this may occur during HMR)')
27
+ logger.debug('Commands re-registered (this may occur during HMR)')
25
28
  this.didWarnAboutDevelopmentReregistration = true
26
29
  }
27
30
  this.handlers.set(handler.id, handler)
@@ -42,7 +45,7 @@ class CommandRegistry {
42
45
  throw new Error(`Duplicate command loader registration for id ${loader.id}`)
43
46
  }
44
47
  if (this.loadersById.has(loader.id) && process.env.NODE_ENV === 'development' && !this.didWarnAboutDevelopmentLoaderReregistration) {
45
- console.debug('[Bootstrap] Command loaders re-registered (this may occur during HMR)')
48
+ logger.debug('Command loaders re-registered (this may occur during HMR)')
46
49
  this.didWarnAboutDevelopmentLoaderReregistration = true
47
50
  }
48
51
  this.loadersById.set(loader.id, loader)
@@ -55,7 +58,7 @@ class CommandRegistry {
55
58
  throw new Error(`Duplicate command loader registration for key ${key}`)
56
59
  }
57
60
  if (existing.has(key) && process.env.NODE_ENV === 'development' && !this.didWarnAboutDevelopmentLoaderReregistration) {
58
- console.debug('[Bootstrap] Command loaders re-registered (this may occur during HMR)')
61
+ logger.debug('Command loaders re-registered (this may occur during HMR)')
59
62
  this.didWarnAboutDevelopmentLoaderReregistration = true
60
63
  }
61
64
  existing.set(key, loader)
@@ -3,6 +3,9 @@ import type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'
3
3
  import { isOrganizationAccessAllowed } from '@open-mercato/shared/lib/auth/organizationAccess'
4
4
  import { parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'
5
5
  import { env } from 'process'
6
+ import { createLogger } from '../logger'
7
+
8
+ const logger = createLogger('shared').child({ component: 'scope' })
6
9
 
7
10
  function buildScopeLogContext(ctx: CommandRuntimeContext) {
8
11
  const requestInfo =
@@ -46,7 +49,7 @@ function logScopeViolation(
46
49
  ): void {
47
50
  try {
48
51
  if (env.NODE_ENV !== 'test') {
49
- console.warn('[scope] Forbidden organization scope mismatch detected', {
52
+ logger.warn('Forbidden organization scope mismatch detected', {
50
53
  expectedId: expected,
51
54
  actualId: actual,
52
55
  ...buildScopeLogContext(ctx),
@@ -60,7 +63,7 @@ function logScopeViolation(
60
63
  function logUnscopedOrganizationAccess(ctx: CommandRuntimeContext, organizationId: string): void {
61
64
  try {
62
65
  if (env.NODE_ENV !== 'test') {
63
- console.warn('[scope] Unscoped organization command executed without organization context', {
66
+ logger.warn('Unscoped organization command executed without organization context', {
64
67
  targetOrganizationId: organizationId,
65
68
  strictEnforcement: isStrictOrganizationScopeEnforced(),
66
69
  ...buildScopeLogContext(ctx),
@@ -78,7 +81,7 @@ function logTenantScopeViolation(
78
81
  ): void {
79
82
  try {
80
83
  if (env.NODE_ENV !== 'test') {
81
- console.warn('[scope] Forbidden tenant scope mismatch detected', {
84
+ logger.warn('Forbidden tenant scope mismatch detected', {
82
85
  expectedTenantId,
83
86
  actualTenantId,
84
87
  ...buildScopeLogContext(ctx),
@@ -10,6 +10,21 @@ import {
10
10
  OPTIMISTIC_LOCK_HEADER_NAME,
11
11
  } from '../optimistic-lock-headers'
12
12
  import type { CrudMutationGuardValidateInput } from '../mutation-guard'
13
+ import { createLogger } from '@open-mercato/shared/lib/logger'
14
+
15
+ jest.mock('@open-mercato/shared/lib/logger', () => {
16
+ const mocked = {
17
+ debug: jest.fn(),
18
+ info: jest.fn(),
19
+ warn: jest.fn(),
20
+ error: jest.fn(),
21
+ child: jest.fn(),
22
+ }
23
+ mocked.child.mockImplementation(() => mocked)
24
+ return { createLogger: jest.fn(() => mocked) }
25
+ })
26
+ const loggerError = createLogger('shared').error as jest.Mock
27
+
13
28
 
14
29
  describe('parseOptimisticLockEnv', () => {
15
30
  it('returns mode=all when unset (default ON)', () => {
@@ -476,7 +491,7 @@ describe('createGenericOptimisticLockReader', () => {
476
491
  throw new Error('column "deleted_at" does not exist')
477
492
  },
478
493
  } as never
479
- const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {})
494
+ loggerError.mockClear()
480
495
  try {
481
496
  const result = await reader(em, {
482
497
  resourceKind: 'customers.tag',
@@ -487,12 +502,12 @@ describe('createGenericOptimisticLockReader', () => {
487
502
  // Fail-open control flow preserved: a query error must never 500 the mutation.
488
503
  expect(result).toBeNull()
489
504
  // ...but a misconfig must be visible, naming the affected resourceKind.
490
- expect(errorSpy).toHaveBeenCalledTimes(1)
491
- const [message] = errorSpy.mock.calls[0]
492
- expect(String(message)).toContain('customers.tag')
493
- expect(String(message)).toContain('[optimistic-lock]')
505
+ expect(loggerError).toHaveBeenCalledTimes(1)
506
+ const [message, fields] = loggerError.mock.calls[0]
507
+ expect(String(message)).toContain('optimistic locking is DISABLED')
508
+ expect(fields).toEqual(expect.objectContaining({ resourceKind: 'customers.tag' }))
494
509
  } finally {
495
- errorSpy.mockRestore()
510
+ loggerError.mockClear()
496
511
  }
497
512
  })
498
513
 
@@ -2,6 +2,21 @@ import type { AwilixContainer } from 'awilix'
2
2
  import { registerMutationGuards } from '../mutation-guard-store'
3
3
  import type { MutationGuard } from '../mutation-guard-registry'
4
4
  import { runRouteMutationGuards, toRegistryMutationOperation } from '../route-mutation-guard'
5
+ import { createLogger } from '@open-mercato/shared/lib/logger'
6
+
7
+ jest.mock('@open-mercato/shared/lib/logger', () => {
8
+ const mocked = {
9
+ debug: jest.fn(),
10
+ info: jest.fn(),
11
+ warn: jest.fn(),
12
+ error: jest.fn(),
13
+ child: jest.fn(),
14
+ }
15
+ mocked.child.mockImplementation(() => mocked)
16
+ return { createLogger: jest.fn(() => mocked) }
17
+ })
18
+ const loggerError = createLogger('shared').error as jest.Mock
19
+
5
20
 
6
21
  type Registrations = Record<string, unknown>
7
22
 
@@ -146,7 +161,7 @@ describe('runRouteMutationGuards', () => {
146
161
  })
147
162
 
148
163
  it('swallows afterSuccess callback errors so a committed write still succeeds', async () => {
149
- const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {})
164
+ loggerError.mockClear()
150
165
  const guard = makeGuard({
151
166
  id: 'throwing-after-guard',
152
167
  validate: jest.fn().mockResolvedValue({ ok: true, shouldRunAfterSuccess: true }),
@@ -164,7 +179,7 @@ describe('runRouteMutationGuards', () => {
164
179
  expect(result.ok).toBe(true)
165
180
  if (!result.ok) throw new Error('expected passed result')
166
181
  await expect(result.runAfterSuccess()).resolves.toBeUndefined()
167
- expect(consoleError).toHaveBeenCalled()
182
+ expect(loggerError).toHaveBeenCalledWith('Mutation guard afterSuccess failed', expect.objectContaining({ guardId: 'throwing-after-guard' }))
168
183
  })
169
184
 
170
185
  it('skips feature-gated guards when the caller lacks the feature', async () => {
@@ -1,6 +1,21 @@
1
1
  import { matchesEventPattern, collectSyncSubscribers, runSyncBeforeEvent, runSyncAfterEvent } from '../sync-event-runner'
2
2
  import type { SyncSubscriberEntry } from '../sync-subscriber-store'
3
3
  import type { SyncCrudEventPayload } from '../sync-event-types'
4
+ import { createLogger } from '@open-mercato/shared/lib/logger'
5
+
6
+ jest.mock('@open-mercato/shared/lib/logger', () => {
7
+ const mocked = {
8
+ debug: jest.fn(),
9
+ info: jest.fn(),
10
+ warn: jest.fn(),
11
+ error: jest.fn(),
12
+ child: jest.fn(),
13
+ }
14
+ mocked.child.mockImplementation(() => mocked)
15
+ return { createLogger: jest.fn(() => mocked) }
16
+ })
17
+ const loggerError = createLogger('shared').error as jest.Mock
18
+
4
19
 
5
20
  describe('matchesEventPattern', () => {
6
21
  it('matches exact event ID', () => {
@@ -158,10 +173,9 @@ describe('runSyncAfterEvent', () => {
158
173
  handler: jest.fn().mockResolvedValue(undefined),
159
174
  }
160
175
 
161
- const consoleSpy = jest.spyOn(console, 'error').mockImplementation()
176
+ loggerError.mockClear()
162
177
  await runSyncAfterEvent([s1, s2], basePayload, ctx)
163
178
  expect(s2.handler).toHaveBeenCalled()
164
- expect(consoleSpy).toHaveBeenCalled()
165
- consoleSpy.mockRestore()
179
+ expect(loggerError).toHaveBeenCalled()
166
180
  })
167
181
  })
@@ -5,6 +5,9 @@ import {
5
5
  convertAdvancedFilterToWhere,
6
6
  } from '../query/advanced-filter'
7
7
  import { compileTreeToWhere, type AdvancedFilterTree } from '../query/advanced-filter-tree'
8
+ import { createLogger } from '../logger'
9
+
10
+ const logger = createLogger('shared').child({ component: 'crud' })
8
11
 
9
12
  function splitOrClauses(filters: Record<string, unknown>): {
10
13
  directFilters: Record<string, unknown>
@@ -142,7 +145,7 @@ export function consumeAdvancedFilterState(query: Record<string, unknown>): Adva
142
145
  if (!g[sentinel]) {
143
146
  g[sentinel] = true
144
147
  // eslint-disable-next-line no-console
145
- console.warn('[advanced-filter] legacy v1 URL detected; auto-upgraded to v2 tree')
148
+ logger.warn('Legacy v1 advanced-filter URL detected; auto-upgraded to v2 tree')
146
149
  }
147
150
  }
148
151
  }
@@ -1,6 +1,9 @@
1
1
  import type { AwilixContainer } from 'awilix'
2
2
  import { runWithCacheTenant, type CacheStrategy } from '@open-mercato/cache'
3
3
  import { parseBooleanToken } from '../boolean'
4
+ import { createLogger } from '../logger'
5
+
6
+ const logger = createLogger('shared').child({ component: 'crud-cache' })
4
7
 
5
8
  export type CrudCacheIdentifiers = {
6
9
  id?: string | null
@@ -25,7 +28,7 @@ export function isCrudCacheDebugEnabled(): boolean {
25
28
  export function debugCrudCache(event: string, context: Record<string, unknown>) {
26
29
  if (!isCrudCacheDebugEnabled()) return
27
30
  try {
28
- console.debug('[crud][cache]', event, context)
31
+ logger.debug(event, context)
29
32
  } catch {}
30
33
  }
31
34
 
@@ -6,6 +6,7 @@ import type { TenantDataEncryptionService } from '../encryption/tenantDataEncryp
6
6
  import { decryptCustomFieldValue, resolveTenantEncryptionService } from '../encryption/customFieldValues'
7
7
  import { parseBooleanToken } from '../boolean'
8
8
  import { extractCustomFieldEntries } from './custom-fields-client'
9
+ import { createLogger } from '../logger'
9
10
  import {
10
11
  buildCustomFieldDefinitionIndexFromRows,
11
12
  normalizeDefinitionKey,
@@ -16,6 +17,8 @@ import {
16
17
  type CustomFieldDefinitionSummary,
17
18
  } from './custom-field-definition-index'
18
19
 
20
+ const logger = createLogger('shared').child({ component: 'crud' })
21
+
19
22
  export type { CustomFieldDefinitionSummary, CustomFieldDefinitionIndex } from './custom-field-definition-index'
20
23
 
21
24
  export type CustomFieldSelectors = {
@@ -416,7 +419,7 @@ export async function loadCustomFieldDefinitionIndex(opts: LoadCustomFieldDefini
416
419
  return restored
417
420
  }
418
421
  } catch (err) {
419
- console.warn('[crud:cf-def-cache] read failed', err)
422
+ logger.warn('Custom-field definition cache read failed', { err })
420
423
  }
421
424
  }
422
425
 
@@ -433,7 +436,7 @@ export async function loadCustomFieldDefinitionIndex(opts: LoadCustomFieldDefini
433
436
  tags: buildCfDefIndexCacheTags({ tenantId, entityIds }),
434
437
  })
435
438
  } catch (err) {
436
- console.warn('[crud:cf-def-cache] write failed', err)
439
+ logger.warn('Custom-field definition cache write failed', { err })
437
440
  }
438
441
  }
439
442
  if (requestBucket) requestBucket.set(cacheKey, index)
@@ -14,6 +14,9 @@ import type {
14
14
  } from './response-enricher'
15
15
  import { getEnrichersForEntity } from './enricher-registry'
16
16
  import { logEnricherTiming } from '../umes/enricher-timing'
17
+ import { createLogger } from '../logger'
18
+
19
+ const logger = createLogger('shared').child({ component: 'umes' })
17
20
 
18
21
  const DEFAULT_TIMEOUT = 2000
19
22
  const SLOW_WARN_MS = 100
@@ -263,13 +266,9 @@ export async function applyResponseEnrichers<T extends Record<string, unknown>>(
263
266
 
264
267
  const elapsedMs = Date.now() - startTime
265
268
  if (elapsedMs > SLOW_ERROR_MS) {
266
- console.error(
267
- `[UMES] Enricher ${enricher.id} took ${elapsedMs}ms (threshold: ${SLOW_ERROR_MS}ms)`,
268
- )
269
+ logger.error('Enricher exceeded slow threshold', { enricherId: enricher.id, elapsedMs, thresholdMs: SLOW_ERROR_MS })
269
270
  } else if (elapsedMs > SLOW_WARN_MS) {
270
- console.warn(
271
- `[UMES] Enricher ${enricher.id} took ${elapsedMs}ms (threshold: ${SLOW_WARN_MS}ms)`,
272
- )
271
+ logger.warn('Enricher exceeded slow threshold', { enricherId: enricher.id, elapsedMs, thresholdMs: SLOW_WARN_MS })
273
272
  }
274
273
  logEnricherTiming(enricher.id, entry.moduleId, targetEntity, elapsedMs)
275
274
 
@@ -289,8 +288,7 @@ export async function applyResponseEnrichers<T extends Record<string, unknown>>(
289
288
  throw err
290
289
  }
291
290
 
292
- const message = err instanceof Error ? err.message : String(err)
293
- console.warn(`[UMES] Enricher ${enricher.id} failed: ${message}`)
291
+ logger.warn('Enricher failed', { enricherId: enricher.id, err })
294
292
  enricherErrors.push(enricher.id)
295
293
 
296
294
  if (enricher.fallback) {
@@ -376,8 +374,7 @@ export async function applyResponseEnricherToRecord<T extends Record<string, unk
376
374
  throw err
377
375
  }
378
376
 
379
- const message = err instanceof Error ? err.message : String(err)
380
- console.warn(`[UMES] Enricher ${enricher.id} failed: ${message}`)
377
+ logger.warn('Enricher failed', { enricherId: enricher.id, err })
381
378
  enricherErrors.push(enricher.id)
382
379
 
383
380
  if (enricher.fallback) {