@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
@@ -0,0 +1,94 @@
1
+ import type { LogBindings, Logger } from './index'
2
+ import { parseBooleanToken } from '../boolean'
3
+ import { isLevelEnabled, type LogLevel } from './level'
4
+ import { formatBindings } from './transport.console'
5
+ import { OM_LOG_DESTINATION_ENV, isStderrDestinationToken } from './transport.server'
6
+
7
+ export const OM_LOG_PRETTY_ENV = 'OM_LOG_PRETTY'
8
+
9
+ type EnvSource = Record<string, string | undefined>
10
+
11
+ function readProcessEnv(): EnvSource {
12
+ return typeof process === 'undefined' ? {} : process.env
13
+ }
14
+
15
+ /** Resolve pretty mode from `OM_LOG_PRETTY`, defaulting to on outside production. */
16
+ export function resolvePrettyMode(env: EnvSource = readProcessEnv()): boolean {
17
+ const parsed = parseBooleanToken(env[OM_LOG_PRETTY_ENV])
18
+ if (parsed !== null) return parsed
19
+ return env.NODE_ENV !== 'production'
20
+ }
21
+
22
+ let cachedPrettyMode: boolean | null = null
23
+
24
+ export function isPrettyModeEnabled(): boolean {
25
+ if (cachedPrettyMode === null) cachedPrettyMode = resolvePrettyMode()
26
+ return cachedPrettyMode
27
+ }
28
+
29
+ /** Internal: clear the memoized pretty mode so tests can vary `OM_LOG_PRETTY`. */
30
+ export function resetLogPrettyCache(): void {
31
+ cachedPrettyMode = null
32
+ }
33
+
34
+ const LEVEL_LABELS: Record<LogLevel, string> = {
35
+ debug: 'DEBUG',
36
+ info: 'INFO ',
37
+ warn: 'WARN ',
38
+ error: 'ERROR',
39
+ }
40
+
41
+ const ANSI_RESET = '\x1b[0m'
42
+ const ANSI_DIM = '\x1b[2m'
43
+
44
+ const LEVEL_COLORS: Record<LogLevel, string> = {
45
+ debug: ANSI_DIM,
46
+ info: '\x1b[36m',
47
+ warn: '\x1b[33m',
48
+ error: '\x1b[31m',
49
+ }
50
+
51
+ type WritableLogStream = { write(chunk: string): unknown; isTTY?: boolean }
52
+
53
+ function resolveStream(): WritableLogStream {
54
+ const useStderr = isStderrDestinationToken(readProcessEnv()[OM_LOG_DESTINATION_ENV])
55
+ return useStderr ? process.stderr : process.stdout
56
+ }
57
+
58
+ function formatTimestamp(date: Date): string {
59
+ const hours = String(date.getHours()).padStart(2, '0')
60
+ const minutes = String(date.getMinutes()).padStart(2, '0')
61
+ const seconds = String(date.getSeconds()).padStart(2, '0')
62
+ const millis = String(date.getMilliseconds()).padStart(3, '0')
63
+ return `${hours}:${minutes}:${seconds}.${millis}`
64
+ }
65
+
66
+ export function createPrettyLogger(namespace: string, bindings: LogBindings = {}): Logger {
67
+ const emit = (level: LogLevel, msg: string, fields?: LogBindings): void => {
68
+ if (!isLevelEnabled(level)) return
69
+ const stream = resolveStream()
70
+ const { err, ...rest } = { ...bindings, ...fields }
71
+ let scope = namespace
72
+ if (typeof rest.component === 'string' && rest.component) {
73
+ scope = `${namespace}:${rest.component}`
74
+ delete rest.component
75
+ }
76
+ const tail = formatBindings(err instanceof Error || err === undefined ? rest : { ...rest, err })
77
+ const colored = stream.isTTY === true
78
+ const timestamp = formatTimestamp(new Date())
79
+ const timestampPart = colored ? `${ANSI_DIM}${timestamp}${ANSI_RESET}` : timestamp
80
+ const label = LEVEL_LABELS[level]
81
+ const levelPart = colored ? `${LEVEL_COLORS[level]}${label}${ANSI_RESET}` : label
82
+ let line = `${timestampPart} ${levelPart} [${scope}] ${msg}`
83
+ if (tail) line += ` ${tail}`
84
+ if (err instanceof Error) line += `\n${err.stack ?? err.message}`
85
+ stream.write(`${line}\n`)
86
+ }
87
+ return {
88
+ debug: (msg, fields) => emit('debug', msg, fields),
89
+ info: (msg, fields) => emit('info', msg, fields),
90
+ warn: (msg, fields) => emit('warn', msg, fields),
91
+ error: (msg, fields) => emit('error', msg, fields),
92
+ child: (childBindings) => createPrettyLogger(namespace, { ...bindings, ...childBindings }),
93
+ }
94
+ }
@@ -0,0 +1,125 @@
1
+ import type { LogBindings, Logger } from './index'
2
+ import { getLogLevel } from './level'
3
+ import { createConsoleLogger } from './transport.console'
4
+
5
+ const REDACT_PATHS = [
6
+ 'password',
7
+ '*.password',
8
+ 'token',
9
+ '*.token',
10
+ 'secret',
11
+ '*.secret',
12
+ 'authorization',
13
+ '*.authorization',
14
+ 'headers.authorization',
15
+ 'req.headers.authorization',
16
+ ]
17
+
18
+ const REDACT_CENSOR = '[Redacted]'
19
+
20
+ export const OM_LOG_DESTINATION_ENV = 'OM_LOG_DESTINATION'
21
+
22
+ type PinoBaseLogger = {
23
+ debug(fields: Record<string, unknown>, msg: string): void
24
+ info(fields: Record<string, unknown>, msg: string): void
25
+ warn(fields: Record<string, unknown>, msg: string): void
26
+ error(fields: Record<string, unknown>, msg: string): void
27
+ child(bindings: Record<string, unknown>): PinoBaseLogger
28
+ }
29
+
30
+ type PinoFactory = (options: Record<string, unknown>, destination?: unknown) => PinoBaseLogger
31
+
32
+ type NodeModuleBuiltin = {
33
+ createRequire(basePath: string): (id: string) => unknown
34
+ }
35
+
36
+ type ProcessWithBuiltins = {
37
+ cwd(): string
38
+ getBuiltinModule?(id: string): unknown
39
+ env?: Record<string, string | undefined>
40
+ stderr?: unknown
41
+ }
42
+
43
+ export function isStderrDestinationToken(raw: string | undefined): boolean {
44
+ return typeof raw === 'string' && raw.trim().toLowerCase() === 'stderr'
45
+ }
46
+
47
+ function resolveDestinationStream(nodeProcess: ProcessWithBuiltins): unknown {
48
+ return isStderrDestinationToken(nodeProcess.env?.[OM_LOG_DESTINATION_ENV])
49
+ ? nodeProcess.stderr
50
+ : undefined
51
+ }
52
+
53
+ let cachedRoot: PinoBaseLogger | null | undefined
54
+ let rootGeneration = 0
55
+
56
+ /**
57
+ * pino is loaded lazily via a runtime `require` obtained from
58
+ * `process.getBuiltinModule('node:module')` — never through a static import —
59
+ * so browser/edge bundles that reach this file never resolve pino or
60
+ * `node:module`. Any load failure falls back to the console transport.
61
+ */
62
+ function loadPinoRoot(): PinoBaseLogger | null {
63
+ if (cachedRoot !== undefined) return cachedRoot
64
+ try {
65
+ const nodeProcess = (typeof process === 'undefined' ? undefined : process) as
66
+ | ProcessWithBuiltins
67
+ | undefined
68
+ const moduleBuiltin = nodeProcess?.getBuiltinModule?.('node:module') as
69
+ | NodeModuleBuiltin
70
+ | undefined
71
+ if (!nodeProcess || !moduleBuiltin) {
72
+ cachedRoot = null
73
+ return cachedRoot
74
+ }
75
+ const requireFromApp = moduleBuiltin.createRequire(`${nodeProcess.cwd()}/package.json`)
76
+ const pinoExport = requireFromApp('pino') as PinoFactory | { default: PinoFactory }
77
+ const pinoFactory = typeof pinoExport === 'function' ? pinoExport : pinoExport.default
78
+ const destination = resolveDestinationStream(nodeProcess)
79
+ const options = {
80
+ level: getLogLevel(),
81
+ redact: { paths: REDACT_PATHS, censor: REDACT_CENSOR },
82
+ }
83
+ cachedRoot = destination ? pinoFactory(options, destination) : pinoFactory(options)
84
+ } catch {
85
+ cachedRoot = null
86
+ }
87
+ return cachedRoot
88
+ }
89
+
90
+ function wrapPinoLogger(pinoLogger: PinoBaseLogger): Logger {
91
+ return {
92
+ debug: (msg, fields) => pinoLogger.debug(fields ?? {}, msg),
93
+ info: (msg, fields) => pinoLogger.info(fields ?? {}, msg),
94
+ warn: (msg, fields) => pinoLogger.warn(fields ?? {}, msg),
95
+ error: (msg, fields) => pinoLogger.error(fields ?? {}, msg),
96
+ child: (childBindings) => wrapPinoLogger(pinoLogger.child(childBindings)),
97
+ }
98
+ }
99
+
100
+ export function createServerLogger(namespace: string, bindings: LogBindings = {}): Logger {
101
+ let delegate: Logger | null = null
102
+ let delegateGeneration = -1
103
+ const resolveDelegate = (): Logger => {
104
+ if (delegate && delegateGeneration === rootGeneration) return delegate
105
+ delegateGeneration = rootGeneration
106
+ const root = loadPinoRoot()
107
+ delegate = root
108
+ ? wrapPinoLogger(root.child({ name: namespace, ...bindings }))
109
+ : createConsoleLogger(namespace, bindings)
110
+ return delegate
111
+ }
112
+ return {
113
+ debug: (msg, fields) => resolveDelegate().debug(msg, fields),
114
+ info: (msg, fields) => resolveDelegate().info(msg, fields),
115
+ warn: (msg, fields) => resolveDelegate().warn(msg, fields),
116
+ error: (msg, fields) => resolveDelegate().error(msg, fields),
117
+ child: (childBindings) => createServerLogger(namespace, { ...bindings, ...childBindings }),
118
+ }
119
+ }
120
+
121
+ /** Internal: clear the cached pino root so tests can re-run transport selection. */
122
+ export function resetServerLoggerCache(): void {
123
+ cachedRoot = undefined
124
+ rootGeneration += 1
125
+ }
@@ -0,0 +1,17 @@
1
+ import type { Logger } from './index'
2
+ import { createConsoleLogger } from './transport.console'
3
+ import { createPrettyLogger, isPrettyModeEnabled } from './transport.pretty'
4
+ import { createServerLogger } from './transport.server'
5
+
6
+ function isNodeServerRuntime(): boolean {
7
+ if (typeof window !== 'undefined') return false
8
+ if (typeof process === 'undefined') return false
9
+ if (process.env.NEXT_RUNTIME === 'edge') return false
10
+ return true
11
+ }
12
+
13
+ /** Pick the runtime-appropriate transport: pretty or pino on the Node server, console elsewhere. */
14
+ export function selectTransport(namespace: string): Logger {
15
+ if (!isNodeServerRuntime()) return createConsoleLogger(namespace)
16
+ return isPrettyModeEnabled() ? createPrettyLogger(namespace) : createServerLogger(namespace)
17
+ }
@@ -7,6 +7,9 @@ import type {
7
7
  } from '@open-mercato/shared/modules/middleware/page'
8
8
  import { CONTINUE_PAGE_MIDDLEWARE, matchPageMiddlewareTarget } from '@open-mercato/shared/modules/middleware/page'
9
9
  import { applyPageGuardOverridesToEntries } from '@open-mercato/shared/modules/overrides'
10
+ import { createLogger } from '../logger'
11
+
12
+ const logger = createLogger('shared').child({ component: 'middleware' })
10
13
 
11
14
  type ExecutePageMiddlewareArgs = {
12
15
  entries: PageMiddlewareRegistryEntry[]
@@ -49,7 +52,7 @@ export async function executePageMiddleware(args: ExecutePageMiddlewareArgs): Pr
49
52
  if (onError) {
50
53
  onError(error, { id: middleware.id, priority: middleware.priority })
51
54
  } else {
52
- console.error('[middleware:page] execution failed', { id: middleware.id, error })
55
+ logger.error('Page middleware execution failed', { id: middleware.id, err: error })
53
56
  }
54
57
  throw error
55
58
  }
@@ -1,4 +1,19 @@
1
1
  import type { Module } from '@open-mercato/shared/modules/registry'
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
  const GLOBAL_KEY = '__openMercatoModulesRegistry__'
4
19
 
@@ -85,31 +100,23 @@ describe('shared modules registry', () => {
85
100
 
86
101
  it('emits the HMR debug log on re-registration in development', () => {
87
102
  process.env.NODE_ENV = 'development'
88
- const debugSpy = jest.spyOn(console, 'debug').mockImplementation(() => {})
89
- try {
90
- const registry = loadRegistry()
91
- registry.registerModules(sampleModules)
92
- expect(debugSpy).not.toHaveBeenCalled()
93
- registry.registerModules(sampleModules)
94
- expect(debugSpy).toHaveBeenCalledWith(
95
- '[Bootstrap] Modules re-registered (this may occur during HMR)',
96
- )
97
- } finally {
98
- debugSpy.mockRestore()
99
- }
103
+ loggerDebug.mockClear()
104
+ const registry = loadRegistry()
105
+ registry.registerModules(sampleModules)
106
+ expect(loggerDebug).not.toHaveBeenCalled()
107
+ registry.registerModules(sampleModules)
108
+ expect(loggerDebug).toHaveBeenCalledWith(
109
+ 'Modules re-registered (this may occur during HMR)',
110
+ )
100
111
  })
101
112
 
102
113
  it('does not emit the HMR debug log when NODE_ENV is not development', () => {
103
114
  process.env.NODE_ENV = 'production'
104
- const debugSpy = jest.spyOn(console, 'debug').mockImplementation(() => {})
105
- try {
106
- const registry = loadRegistry()
107
- registry.registerModules(sampleModules)
108
- registry.registerModules(sampleModules)
109
- expect(debugSpy).not.toHaveBeenCalled()
110
- } finally {
111
- debugSpy.mockRestore()
112
- }
115
+ loggerDebug.mockClear()
116
+ const registry = loadRegistry()
117
+ registry.registerModules(sampleModules)
118
+ registry.registerModules(sampleModules)
119
+ expect(loggerDebug).not.toHaveBeenCalled()
113
120
  })
114
121
 
115
122
  it('does not let i18n-only registrations clobber runtime module contracts', () => {
@@ -1,6 +1,9 @@
1
1
  import type { Module } from '@open-mercato/shared/modules/registry'
2
2
  import { applyModuleOverridesToModules } from '@open-mercato/shared/modules/overrides'
3
3
  import { invalidateDictionaryCache } from '../i18n/dictionary-cache'
4
+ import { createLogger } from '../logger'
5
+
6
+ const logger = createLogger('shared').child({ component: 'modules-registry' })
4
7
 
5
8
  // Registration pattern for publishable packages.
6
9
  // Use globalThis to survive tsx/esbuild module duplication where the same
@@ -49,7 +52,7 @@ function mergeI18nModules(existing: Module[], incoming: Module[]): Module[] {
49
52
  export function registerModules(modules: Module[]) {
50
53
  const existing = getGlobalModules()
51
54
  if (existing !== null && process.env.NODE_ENV === 'development') {
52
- console.debug('[Bootstrap] Modules re-registered (this may occur during HMR)')
55
+ logger.debug('Modules re-registered (this may occur during HMR)')
53
56
  }
54
57
  const nextModules = applyModuleOverridesToModules(modules)
55
58
  const shouldMergeI18nOnly = existing !== null
@@ -1,4 +1,7 @@
1
1
  import { parseBooleanToken } from '../boolean'
2
+ import { createLogger } from '../logger'
3
+
4
+ const logger = createLogger('shared').child({ component: 'profiler' })
2
5
 
3
6
  const ROUND_PRECISION = 1000
4
7
  const NS_PER_MS = BigInt(1_000_000)
@@ -267,9 +270,8 @@ class ProfilerImpl implements Profiler {
267
270
  if (this.root.meta && Object.keys(this.root.meta).length > 0) {
268
271
  snapshot.meta = this.root.meta
269
272
  }
270
- const serialized = JSON.stringify(snapshot, null, 2)
271
273
  try {
272
- console.info(this.root.loggerLabel, serialized)
274
+ logger.info('Profiler snapshot', { label: this.root.loggerLabel, snapshot })
273
275
  } catch {
274
276
  // ignore logging failures
275
277
  }
@@ -14,6 +14,22 @@ import {
14
14
  runAfterQueryPipeline,
15
15
  } from '../query-extension-runner'
16
16
  import type { SyncQueryEventPayload } from '../sync-query-event-types'
17
+ import { createLogger } from '@open-mercato/shared/lib/logger'
18
+
19
+ jest.mock('@open-mercato/shared/lib/logger', () => {
20
+ const mocked = {
21
+ debug: jest.fn(),
22
+ info: jest.fn(),
23
+ warn: jest.fn(),
24
+ error: jest.fn(),
25
+ child: jest.fn(),
26
+ }
27
+ mocked.child.mockImplementation(() => mocked)
28
+ return { createLogger: jest.fn(() => mocked) }
29
+ })
30
+ const loggerError = createLogger('shared').error as jest.Mock
31
+ const loggerWarn = createLogger('shared').warn as jest.Mock
32
+
17
33
 
18
34
  const ctx = { resolve: jest.fn() }
19
35
 
@@ -204,7 +220,7 @@ describe('runBeforeQueryEvent', () => {
204
220
  })
205
221
 
206
222
  it('catches thrown errors and returns blocked', async () => {
207
- const consoleSpy = jest.spyOn(console, 'error').mockImplementation()
223
+ loggerError.mockClear()
208
224
  registerSyncSubscribers([
209
225
  makeQuerySub(
210
226
  'customers.person.querying',
@@ -217,7 +233,6 @@ describe('runBeforeQueryEvent', () => {
217
233
  const result = await runBeforeQueryEvent(payload, ctx)
218
234
  expect(result.ok).toBe(false)
219
235
  expect(result.errorStatus).toBe(500)
220
- consoleSpy.mockRestore()
221
236
  })
222
237
  })
223
238
 
@@ -266,7 +281,7 @@ describe('runAfterQueryEvent', () => {
266
281
  })
267
282
 
268
283
  it('rejects invalid modifiedResult shape', async () => {
269
- const consoleSpy = jest.spyOn(console, 'warn').mockImplementation()
284
+ loggerWarn.mockClear()
270
285
  registerSyncSubscribers([
271
286
  makeQuerySub(
272
287
  'customers.person.queried',
@@ -278,14 +293,14 @@ describe('runAfterQueryEvent', () => {
278
293
  const payload = makeQueriedPayload()
279
294
  const result = await runAfterQueryEvent(payload, ctx)
280
295
  expect(result.modifiedResult).toBeUndefined()
281
- expect(consoleSpy).toHaveBeenCalledWith(
296
+ expect(loggerWarn).toHaveBeenCalledWith(
282
297
  expect.stringContaining('invalid modifiedResult shape'),
298
+ expect.objectContaining({ subscriberId: expect.any(String) }),
283
299
  )
284
- consoleSpy.mockRestore()
285
300
  })
286
301
 
287
302
  it('continues on subscriber error', async () => {
288
- const consoleSpy = jest.spyOn(console, 'error').mockImplementation()
303
+ loggerError.mockClear()
289
304
  const handler2 = jest.fn().mockResolvedValue({
290
305
  modifiedResult: {
291
306
  items: [{ id: '1', transformed: true }],
@@ -307,7 +322,6 @@ describe('runAfterQueryEvent', () => {
307
322
  const result = await runAfterQueryEvent(payload, ctx)
308
323
  expect(handler2).toHaveBeenCalled()
309
324
  expect(result.modifiedResult).toBeDefined()
310
- consoleSpy.mockRestore()
311
325
  })
312
326
  })
313
327
 
@@ -22,6 +22,9 @@ import {
22
22
  } from '../crud/custom-field-definition-index'
23
23
  import { resolveEncryptedSortFields, resolveEncryptedSortMaxRows, sortRowsInMemory } from './encrypted-sort'
24
24
  import { mapWithConcurrency } from './bounded-decrypt'
25
+ import { createLogger } from '../logger'
26
+
27
+ const logger = createLogger('shared').child({ component: 'query' })
25
28
 
26
29
  const DECRYPT_CONCURRENCY = 8
27
30
 
@@ -138,11 +141,7 @@ export function resolveEntityTableName(em: EntityManager | undefined, entity: En
138
141
  }
139
142
 
140
143
  const fallback = pluralizeBaseName(rawName || '')
141
- console.warn(
142
- `[QueryEngine] Could not resolve entity "${entity}" via ORM metadata. ` +
143
- `Falling back to table name "${fallback}". ` +
144
- `Ensure the entity ID segment matches the class name convention.`
145
- )
144
+ logger.warn('Could not resolve entity via ORM metadata; falling back to table name — ensure the entity ID segment matches the class name convention', { entity, fallback })
146
145
  entityTableCache.set(entity, fallback)
147
146
  return fallback
148
147
  }
@@ -945,7 +944,7 @@ export class BasicQueryEngine implements QueryEngine {
945
944
  )
946
945
  return { ...item, ...decrypted }
947
946
  } catch (err) {
948
- console.error('QueryEngine: error decrypting entity payload', err)
947
+ logger.error('Error decrypting entity payload', { err })
949
948
  return item
950
949
  }
951
950
  }
@@ -1409,10 +1408,8 @@ export class BasicQueryEngine implements QueryEngine {
1409
1408
 
1410
1409
  private logSearchDebug(event: string, payload: Record<string, unknown>) {
1411
1410
  try {
1412
- console.info('[query:search]', event, JSON.stringify(payload))
1413
- } catch {
1414
- console.info('[query:search]', event, payload)
1415
- }
1411
+ logger.debug(event, payload)
1412
+ } catch {}
1416
1413
  }
1417
1414
 
1418
1415
  private resolveOrganizationScope(opts: QueryOptions): { ids: string[]; includeNull: boolean } | null {
@@ -18,6 +18,9 @@ import { getEnrichersForEntity } from '../crud/enricher-registry'
18
18
  import { applyResponseEnrichers, applyResponseEnricherToRecord } from '../crud/enricher-runner'
19
19
  import type { SyncQueryEventPayload, SyncQueryEventResult } from './sync-query-event-types'
20
20
  import type { QueryOptions, QueryResult } from './types'
21
+ import { createLogger } from '../logger'
22
+
23
+ const logger = createLogger('shared').child({ component: 'query-extensions' })
21
24
 
22
25
  // ---------------------------------------------------------------------------
23
26
  // Entity-to-event-ID helpers
@@ -96,10 +99,7 @@ export async function runBeforeQueryEvent(
96
99
  currentQuery = { ...currentQuery, ...queryResult.modifiedQuery }
97
100
  }
98
101
  } catch (error) {
99
- console.error(
100
- `[query-extension] before-query subscriber failed: ${subscriber.metadata.id}`,
101
- error,
102
- )
102
+ logger.error('Before-query subscriber failed', { subscriberId: subscriber.metadata.id, err: error })
103
103
  return {
104
104
  ok: false,
105
105
  errorMessage: `Subscriber ${subscriber.metadata.id} threw unexpectedly`,
@@ -148,16 +148,11 @@ export async function runAfterQueryEvent(
148
148
  if (isValidQueryResult(queryResult.modifiedResult)) {
149
149
  currentResult = queryResult.modifiedResult
150
150
  } else {
151
- console.warn(
152
- `[query-extension] after-query subscriber ${subscriber.metadata.id} returned invalid modifiedResult shape — ignored`,
153
- )
151
+ logger.warn('After-query subscriber returned invalid modifiedResult shape — ignored', { subscriberId: subscriber.metadata.id })
154
152
  }
155
153
  }
156
154
  } catch (error) {
157
- console.error(
158
- `[query-extension] after-query subscriber failed: ${subscriber.metadata.id}`,
159
- error,
160
- )
155
+ logger.error('After-query subscriber failed', { subscriberId: subscriber.metadata.id, err: error })
161
156
  }
162
157
  }
163
158
 
@@ -1,3 +1,7 @@
1
+ import { createLogger } from '../logger'
2
+
3
+ const logger = createLogger('shared').child({ component: 'redis' })
4
+
1
5
  /**
2
6
  * Shared Redis connection utilities.
3
7
  *
@@ -77,7 +81,7 @@ export function parseRedisUrl(url: string): ParsedRedisConnection {
77
81
  }
78
82
  } catch {
79
83
  const safeUrl = url.replace(/\/\/[^:]*:[^@]*@/, '//<redacted>@')
80
- console.warn(`[redis] Failed to parse URL "${safeUrl}", falling back to localhost:6379`)
84
+ logger.warn('Failed to parse Redis URL, falling back to localhost:6379', { url: safeUrl })
81
85
  return { host: 'localhost', port: 6379 }
82
86
  }
83
87
  }
package/src/lib/url.ts CHANGED
@@ -1,3 +1,7 @@
1
+ import { createLogger } from './logger'
2
+
3
+ const logger = createLogger('shared').child({ component: 'origin-check' })
4
+
1
5
  const DEFAULT_DEV_APP_URL = 'http://localhost:3000'
2
6
 
3
7
  type EnvLike = Record<string, string | undefined> & {
@@ -125,10 +129,11 @@ function logOriginDebugContext(
125
129
  nodeEnv?: string,
126
130
  ): void {
127
131
  if (level === 'warn' && nodeEnv === 'test') return
128
- const log = level === 'warn' ? console.warn : console.error
132
+ const log = (msg: string, fields: Record<string, unknown>) =>
133
+ level === 'warn' ? logger.warn(msg, fields) : logger.error(msg, fields)
129
134
 
130
135
  if (typeof input === 'string') {
131
- log('[origin-check] rejected string input', {
136
+ log('Origin check rejected string input', {
132
137
  requestUrl: input,
133
138
  rejectedOrigin: rejectedOrigin ?? null,
134
139
  allowedOrigins: Array.from(allowedOrigins),
@@ -137,14 +142,14 @@ function logOriginDebugContext(
137
142
  }
138
143
 
139
144
  if (!input) {
140
- log('[origin-check] rejected empty input', {
145
+ log('Origin check rejected empty input', {
141
146
  rejectedOrigin: rejectedOrigin ?? null,
142
147
  allowedOrigins: Array.from(allowedOrigins),
143
148
  })
144
149
  return
145
150
  }
146
151
 
147
- log('[origin-check] rejected request', {
152
+ log('Origin check rejected request', {
148
153
  requestUrl: input.url,
149
154
  host: input.headers.get('host'),
150
155
  forwardedHost: input.headers.get('x-forwarded-host'),
@@ -270,7 +275,7 @@ export function mapSecurityEmailUrlError(
270
275
  return { status: 400, body: { error: 'Invalid request origin' } }
271
276
  }
272
277
  if (error instanceof AppOriginConfigurationError) {
273
- console.error(`[${mapping.scope}] APP_URL is required in production`)
278
+ logger.error('APP_URL is required in production', { scope: mapping.scope })
274
279
  return { status: 500, body: { error: mapping.configMessage } }
275
280
  }
276
281
  return null
@@ -9,6 +9,7 @@
9
9
  * Spec: `.ai/specs/implemented/2026-05-04-modules-ts-unified-overrides.md`.
10
10
  */
11
11
  import {
12
+
12
13
  applyModuleOverridesFromEnabledModules,
13
14
  composeInjectionWidgetOverrides,
14
15
  composeSubscriberOverrides,
@@ -18,6 +19,20 @@ import {
18
19
  type ModuleEntryWithOverrides,
19
20
  type ModuleOverrideEntry,
20
21
  } from '../overrides'
22
+ import { createLogger } from '@open-mercato/shared/lib/logger'
23
+
24
+ jest.mock('@open-mercato/shared/lib/logger', () => {
25
+ const mocked = {
26
+ debug: jest.fn(),
27
+ info: jest.fn(),
28
+ warn: jest.fn(),
29
+ error: jest.fn(),
30
+ child: jest.fn(),
31
+ }
32
+ mocked.child.mockImplementation(() => mocked)
33
+ return { createLogger: jest.fn(() => mocked) }
34
+ })
35
+ const loggerWarn = createLogger('shared').warn as jest.Mock
21
36
 
22
37
  beforeEach(() => {
23
38
  resetModuleOverrideAppliersForTests()
@@ -77,7 +92,7 @@ describe('applyModuleOverridesFromEnabledModules', () => {
77
92
  })
78
93
 
79
94
  it('routes built-in domains to default appliers without unwired warnings', () => {
80
- const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {})
95
+ loggerWarn.mockClear()
81
96
 
82
97
  applyModuleOverridesFromEnabledModules([
83
98
  {
@@ -93,7 +108,7 @@ describe('applyModuleOverridesFromEnabledModules', () => {
93
108
  },
94
109
  ])
95
110
 
96
- const unwiredCalls = warnSpy.mock.calls.filter((args) =>
111
+ const unwiredCalls = loggerWarn.mock.calls.filter((args) =>
97
112
  typeof args[0] === 'string' && args[0].includes('not yet wired'),
98
113
  )
99
114
  expect(unwiredCalls).toHaveLength(0)
@@ -105,7 +120,6 @@ describe('applyModuleOverridesFromEnabledModules', () => {
105
120
  'example.todo.created.notify': null,
106
121
  })
107
122
 
108
- warnSpy.mockRestore()
109
123
  })
110
124
 
111
125
  it('does NOT consume the legacy `aiAgentOverrides` / `aiToolOverrides` keys', () => {