@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,3 +1,5 @@
1
+ import { createLogger } from "../logger/index.js";
2
+ const logger = createLogger("shared").child({ component: "redis" });
1
3
  function getRedisUrl(prefix) {
2
4
  if (prefix) {
3
5
  const prefixed = process.env[`${prefix}_REDIS_URL`];
@@ -28,7 +30,7 @@ function parseRedisUrl(url) {
28
30
  };
29
31
  } catch {
30
32
  const safeUrl = url.replace(/\/\/[^:]*:[^@]*@/, "//<redacted>@");
31
- console.warn(`[redis] Failed to parse URL "${safeUrl}", falling back to localhost:6379`);
33
+ logger.warn("Failed to parse Redis URL, falling back to localhost:6379", { url: safeUrl });
32
34
  return { host: "localhost", port: 6379 };
33
35
  }
34
36
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/redis/connection.ts"],
4
- "sourcesContent": ["/**\n * Shared Redis connection utilities.\n *\n * Every package that needs a Redis URL or parsed connection options\n * should import from here instead of reading env vars directly.\n *\n * The `prefix` parameter lets each subsystem define its own override:\n * getRedisUrl('QUEUE') \u2192 QUEUE_REDIS_URL > REDIS_URL > null\n * getRedisUrl('CACHE') \u2192 CACHE_REDIS_URL > REDIS_URL > null\n * getRedisUrl() \u2192 REDIS_URL > null\n *\n * Returns null when Redis is not configured so callers can explicitly\n * decide whether to fall back to a non-Redis strategy or fail loudly.\n * Use getRedisUrlOrThrow() when Redis is mandatory for the caller.\n */\n\nexport type ParsedRedisConnection = {\n host: string\n port: number\n password?: string\n db?: number\n tls?: Record<string, unknown>\n}\n\n/**\n * Resolve a Redis URL from environment variables.\n *\n * Priority: <PREFIX>_REDIS_URL \u2192 REDIS_URL \u2192 null\n *\n * Returns null when no env var is set. Callers MUST NOT assume a\n * localhost default \u2014 silently connecting to localhost masks missing\n * configuration and stalls on platforms where nothing listens there\n * (e.g., WSL2 without a local Redis).\n */\nexport function getRedisUrl(prefix?: string): string | null {\n if (prefix) {\n const prefixed = process.env[`${prefix}_REDIS_URL`]\n if (prefixed) return prefixed\n }\n return process.env.REDIS_URL || null\n}\n\n/**\n * Like getRedisUrl, but throws a descriptive error when Redis is not\n * configured. Use from code paths that require Redis (e.g. BullMQ\n * async queue, Redis cache strategy, scheduler service).\n */\nexport function getRedisUrlOrThrow(prefix?: string): string {\n const url = getRedisUrl(prefix)\n if (url) return url\n const which = prefix ? `${prefix}_REDIS_URL or REDIS_URL` : 'REDIS_URL'\n throw new Error(\n `Redis URL is not configured. Set ${which} in your environment to use a Redis-backed strategy.`\n )\n}\n\n/**\n * Parse a redis:// URL into a {host, port, password, db} object\n * suitable for BullMQ / ioredis structured connection options.\n *\n * @deprecated Prefer passing the full URL via `{ url: getRedisUrl(...) }` to\n * BullMQ/ioredis \u2014 this preserves rediss://, username, database, and query\n * params that structured parsing may lose. Kept for backward compatibility.\n */\nexport function parseRedisUrl(url: string): ParsedRedisConnection {\n try {\n const parsed = new URL(url)\n const dbStr = parsed.pathname ? parsed.pathname.slice(1) : ''\n const dbParsed = dbStr !== '' ? parseInt(dbStr, 10) : NaN\n const db = Number.isNaN(dbParsed) ? undefined : dbParsed\n return {\n host: parsed.hostname || 'localhost',\n port: parseInt(parsed.port, 10) || 6379,\n password: parsed.password || undefined,\n db,\n tls: parsed.protocol === 'rediss:' ? {} : undefined,\n }\n } catch {\n const safeUrl = url.replace(/\\/\\/[^:]*:[^@]*@/, '//<redacted>@')\n console.warn(`[redis] Failed to parse URL \"${safeUrl}\", falling back to localhost:6379`)\n return { host: 'localhost', port: 6379 }\n }\n}\n\n/**\n * Convenience: resolve the URL from env and parse it in one step.\n * Returns null when Redis is not configured.\n */\nexport function resolveRedisConnection(\n prefix?: string,\n): (ParsedRedisConnection & { url: string }) | null {\n const url = getRedisUrl(prefix)\n if (!url) return null\n return { url, ...parseRedisUrl(url) }\n}\n"],
5
- "mappings": "AAkCO,SAAS,YAAY,QAAgC;AAC1D,MAAI,QAAQ;AACV,UAAM,WAAW,QAAQ,IAAI,GAAG,MAAM,YAAY;AAClD,QAAI,SAAU,QAAO;AAAA,EACvB;AACA,SAAO,QAAQ,IAAI,aAAa;AAClC;AAOO,SAAS,mBAAmB,QAAyB;AAC1D,QAAM,MAAM,YAAY,MAAM;AAC9B,MAAI,IAAK,QAAO;AAChB,QAAM,QAAQ,SAAS,GAAG,MAAM,4BAA4B;AAC5D,QAAM,IAAI;AAAA,IACR,oCAAoC,KAAK;AAAA,EAC3C;AACF;AAUO,SAAS,cAAc,KAAoC;AAChE,MAAI;AACF,UAAM,SAAS,IAAI,IAAI,GAAG;AAC1B,UAAM,QAAQ,OAAO,WAAW,OAAO,SAAS,MAAM,CAAC,IAAI;AAC3D,UAAM,WAAW,UAAU,KAAK,SAAS,OAAO,EAAE,IAAI;AACtD,UAAM,KAAK,OAAO,MAAM,QAAQ,IAAI,SAAY;AAChD,WAAO;AAAA,MACL,MAAM,OAAO,YAAY;AAAA,MACzB,MAAM,SAAS,OAAO,MAAM,EAAE,KAAK;AAAA,MACnC,UAAU,OAAO,YAAY;AAAA,MAC7B;AAAA,MACA,KAAK,OAAO,aAAa,YAAY,CAAC,IAAI;AAAA,IAC5C;AAAA,EACF,QAAQ;AACN,UAAM,UAAU,IAAI,QAAQ,oBAAoB,eAAe;AAC/D,YAAQ,KAAK,gCAAgC,OAAO,mCAAmC;AACvF,WAAO,EAAE,MAAM,aAAa,MAAM,KAAK;AAAA,EACzC;AACF;AAMO,SAAS,uBACd,QACkD;AAClD,QAAM,MAAM,YAAY,MAAM;AAC9B,MAAI,CAAC,IAAK,QAAO;AACjB,SAAO,EAAE,KAAK,GAAG,cAAc,GAAG,EAAE;AACtC;",
4
+ "sourcesContent": ["import { createLogger } from '../logger'\n\nconst logger = createLogger('shared').child({ component: 'redis' })\n\n/**\n * Shared Redis connection utilities.\n *\n * Every package that needs a Redis URL or parsed connection options\n * should import from here instead of reading env vars directly.\n *\n * The `prefix` parameter lets each subsystem define its own override:\n * getRedisUrl('QUEUE') \u2192 QUEUE_REDIS_URL > REDIS_URL > null\n * getRedisUrl('CACHE') \u2192 CACHE_REDIS_URL > REDIS_URL > null\n * getRedisUrl() \u2192 REDIS_URL > null\n *\n * Returns null when Redis is not configured so callers can explicitly\n * decide whether to fall back to a non-Redis strategy or fail loudly.\n * Use getRedisUrlOrThrow() when Redis is mandatory for the caller.\n */\n\nexport type ParsedRedisConnection = {\n host: string\n port: number\n password?: string\n db?: number\n tls?: Record<string, unknown>\n}\n\n/**\n * Resolve a Redis URL from environment variables.\n *\n * Priority: <PREFIX>_REDIS_URL \u2192 REDIS_URL \u2192 null\n *\n * Returns null when no env var is set. Callers MUST NOT assume a\n * localhost default \u2014 silently connecting to localhost masks missing\n * configuration and stalls on platforms where nothing listens there\n * (e.g., WSL2 without a local Redis).\n */\nexport function getRedisUrl(prefix?: string): string | null {\n if (prefix) {\n const prefixed = process.env[`${prefix}_REDIS_URL`]\n if (prefixed) return prefixed\n }\n return process.env.REDIS_URL || null\n}\n\n/**\n * Like getRedisUrl, but throws a descriptive error when Redis is not\n * configured. Use from code paths that require Redis (e.g. BullMQ\n * async queue, Redis cache strategy, scheduler service).\n */\nexport function getRedisUrlOrThrow(prefix?: string): string {\n const url = getRedisUrl(prefix)\n if (url) return url\n const which = prefix ? `${prefix}_REDIS_URL or REDIS_URL` : 'REDIS_URL'\n throw new Error(\n `Redis URL is not configured. Set ${which} in your environment to use a Redis-backed strategy.`\n )\n}\n\n/**\n * Parse a redis:// URL into a {host, port, password, db} object\n * suitable for BullMQ / ioredis structured connection options.\n *\n * @deprecated Prefer passing the full URL via `{ url: getRedisUrl(...) }` to\n * BullMQ/ioredis \u2014 this preserves rediss://, username, database, and query\n * params that structured parsing may lose. Kept for backward compatibility.\n */\nexport function parseRedisUrl(url: string): ParsedRedisConnection {\n try {\n const parsed = new URL(url)\n const dbStr = parsed.pathname ? parsed.pathname.slice(1) : ''\n const dbParsed = dbStr !== '' ? parseInt(dbStr, 10) : NaN\n const db = Number.isNaN(dbParsed) ? undefined : dbParsed\n return {\n host: parsed.hostname || 'localhost',\n port: parseInt(parsed.port, 10) || 6379,\n password: parsed.password || undefined,\n db,\n tls: parsed.protocol === 'rediss:' ? {} : undefined,\n }\n } catch {\n const safeUrl = url.replace(/\\/\\/[^:]*:[^@]*@/, '//<redacted>@')\n logger.warn('Failed to parse Redis URL, falling back to localhost:6379', { url: safeUrl })\n return { host: 'localhost', port: 6379 }\n }\n}\n\n/**\n * Convenience: resolve the URL from env and parse it in one step.\n * Returns null when Redis is not configured.\n */\nexport function resolveRedisConnection(\n prefix?: string,\n): (ParsedRedisConnection & { url: string }) | null {\n const url = getRedisUrl(prefix)\n if (!url) return null\n return { url, ...parseRedisUrl(url) }\n}\n"],
5
+ "mappings": "AAAA,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,QAAQ,EAAE,MAAM,EAAE,WAAW,QAAQ,CAAC;AAoC3D,SAAS,YAAY,QAAgC;AAC1D,MAAI,QAAQ;AACV,UAAM,WAAW,QAAQ,IAAI,GAAG,MAAM,YAAY;AAClD,QAAI,SAAU,QAAO;AAAA,EACvB;AACA,SAAO,QAAQ,IAAI,aAAa;AAClC;AAOO,SAAS,mBAAmB,QAAyB;AAC1D,QAAM,MAAM,YAAY,MAAM;AAC9B,MAAI,IAAK,QAAO;AAChB,QAAM,QAAQ,SAAS,GAAG,MAAM,4BAA4B;AAC5D,QAAM,IAAI;AAAA,IACR,oCAAoC,KAAK;AAAA,EAC3C;AACF;AAUO,SAAS,cAAc,KAAoC;AAChE,MAAI;AACF,UAAM,SAAS,IAAI,IAAI,GAAG;AAC1B,UAAM,QAAQ,OAAO,WAAW,OAAO,SAAS,MAAM,CAAC,IAAI;AAC3D,UAAM,WAAW,UAAU,KAAK,SAAS,OAAO,EAAE,IAAI;AACtD,UAAM,KAAK,OAAO,MAAM,QAAQ,IAAI,SAAY;AAChD,WAAO;AAAA,MACL,MAAM,OAAO,YAAY;AAAA,MACzB,MAAM,SAAS,OAAO,MAAM,EAAE,KAAK;AAAA,MACnC,UAAU,OAAO,YAAY;AAAA,MAC7B;AAAA,MACA,KAAK,OAAO,aAAa,YAAY,CAAC,IAAI;AAAA,IAC5C;AAAA,EACF,QAAQ;AACN,UAAM,UAAU,IAAI,QAAQ,oBAAoB,eAAe;AAC/D,WAAO,KAAK,6DAA6D,EAAE,KAAK,QAAQ,CAAC;AACzF,WAAO,EAAE,MAAM,aAAa,MAAM,KAAK;AAAA,EACzC;AACF;AAMO,SAAS,uBACd,QACkD;AAClD,QAAM,MAAM,YAAY,MAAM;AAC9B,MAAI,CAAC,IAAK,QAAO;AACjB,SAAO,EAAE,KAAK,GAAG,cAAc,GAAG,EAAE;AACtC;",
6
6
  "names": []
7
7
  }
package/dist/lib/url.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { createLogger } from "./logger/index.js";
2
+ const logger = createLogger("shared").child({ component: "origin-check" });
1
3
  const DEFAULT_DEV_APP_URL = "http://localhost:3000";
2
4
  class AppOriginConfigurationError extends Error {
3
5
  constructor(message) {
@@ -83,9 +85,9 @@ function requestOrigins(input) {
83
85
  }
84
86
  function logOriginDebugContext(input, allowedOrigins, rejectedOrigin, level = "error", nodeEnv) {
85
87
  if (level === "warn" && nodeEnv === "test") return;
86
- const log = level === "warn" ? console.warn : console.error;
88
+ const log = (msg, fields) => level === "warn" ? logger.warn(msg, fields) : logger.error(msg, fields);
87
89
  if (typeof input === "string") {
88
- log("[origin-check] rejected string input", {
90
+ log("Origin check rejected string input", {
89
91
  requestUrl: input,
90
92
  rejectedOrigin: rejectedOrigin ?? null,
91
93
  allowedOrigins: Array.from(allowedOrigins)
@@ -93,13 +95,13 @@ function logOriginDebugContext(input, allowedOrigins, rejectedOrigin, level = "e
93
95
  return;
94
96
  }
95
97
  if (!input) {
96
- log("[origin-check] rejected empty input", {
98
+ log("Origin check rejected empty input", {
97
99
  rejectedOrigin: rejectedOrigin ?? null,
98
100
  allowedOrigins: Array.from(allowedOrigins)
99
101
  });
100
102
  return;
101
103
  }
102
- log("[origin-check] rejected request", {
104
+ log("Origin check rejected request", {
103
105
  requestUrl: input.url,
104
106
  host: input.headers.get("host"),
105
107
  forwardedHost: input.headers.get("x-forwarded-host"),
@@ -202,7 +204,7 @@ function mapSecurityEmailUrlError(error, mapping) {
202
204
  return { status: 400, body: { error: "Invalid request origin" } };
203
205
  }
204
206
  if (error instanceof AppOriginConfigurationError) {
205
- console.error(`[${mapping.scope}] APP_URL is required in production`);
207
+ logger.error("APP_URL is required in production", { scope: mapping.scope });
206
208
  return { status: 500, body: { error: mapping.configMessage } };
207
209
  }
208
210
  return null;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/url.ts"],
4
- "sourcesContent": ["const DEFAULT_DEV_APP_URL = 'http://localhost:3000'\n\ntype EnvLike = Record<string, string | undefined> & {\n APP_URL?: string\n NEXT_PUBLIC_APP_URL?: string\n APP_ALLOWED_ORIGINS?: string\n NODE_ENV?: string\n}\ntype RequestInput = Request | string | undefined\n\nexport type SecurityEmailUrlErrorMapping = {\n scope: string\n configMessage: string\n}\n\nexport class AppOriginConfigurationError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'AppOriginConfigurationError'\n }\n}\n\nexport class AppOriginRejectedError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'AppOriginRejectedError'\n }\n}\n\nfunction parseBaseUrl(raw: string | undefined): URL | null {\n const value = raw?.trim()\n if (!value) return null\n try {\n const url = new URL(value)\n url.hash = ''\n url.search = ''\n return url\n } catch {\n return null\n }\n}\n\nfunction normalizeBaseUrl(raw: string | undefined): string | null {\n const url = parseBaseUrl(raw)\n return url ? url.toString().replace(/\\/$/, '') : null\n}\n\nfunction normalizeOrigin(raw: string | undefined): string | null {\n const url = parseBaseUrl(raw)\n return url ? url.origin : null\n}\n\nfunction readCsv(value: string | undefined): string[] {\n return (value ?? '')\n .split(',')\n .map((item) => item.trim())\n .filter((item) => item.length > 0)\n}\n\nfunction readAllowedOrigins(env: EnvLike): Set<string> {\n const origins = new Set<string>()\n for (const raw of [env.APP_URL, env.NEXT_PUBLIC_APP_URL, ...readCsv(env.APP_ALLOWED_ORIGINS)]) {\n const origin = normalizeOrigin(raw)\n if (origin) origins.add(origin)\n }\n return origins\n}\n\nfunction readFirstHeaderValue(value: string | null): string | null {\n const first = value?.split(',')[0]?.trim()\n return first && first.length > 0 ? first : null\n}\n\nfunction originFromHost(protocol: string, rawHost: string | null): string | null {\n const host = readFirstHeaderValue(rawHost)\n if (!host) return null\n return normalizeOrigin(`${protocol}//${host}`)\n}\n\ntype RequestOriginCandidates = {\n urlOrigin: string | null\n headerOrigins: Set<string>\n}\n\nfunction readRequestOriginCandidates(input: RequestInput): RequestOriginCandidates {\n const candidates: RequestOriginCandidates = {\n urlOrigin: null,\n headerOrigins: new Set<string>(),\n }\n if (!input) return candidates\n\n const requestUrl = typeof input === 'string' ? input : input.url\n let parsedUrl: URL\n try {\n parsedUrl = new URL(requestUrl)\n } catch {\n return candidates\n }\n\n candidates.urlOrigin = normalizeOrigin(parsedUrl.origin)\n if (typeof input === 'string') return candidates\n\n const forwardedProto = readFirstHeaderValue(input.headers.get('x-forwarded-proto')) ?? parsedUrl.protocol.replace(/:$/, '')\n const protocol = forwardedProto.endsWith(':') ? forwardedProto : `${forwardedProto}:`\n const hostOrigin = originFromHost(protocol, input.headers.get('host'))\n const forwardedHostOrigin = originFromHost(protocol, input.headers.get('x-forwarded-host'))\n if (hostOrigin) candidates.headerOrigins.add(hostOrigin)\n if (forwardedHostOrigin) candidates.headerOrigins.add(forwardedHostOrigin)\n return candidates\n}\n\nfunction requestOrigins(input: RequestInput): Set<string> {\n const origins = new Set<string>()\n const { urlOrigin, headerOrigins } = readRequestOriginCandidates(input)\n if (urlOrigin) origins.add(urlOrigin)\n for (const origin of headerOrigins) origins.add(origin)\n return origins\n}\n\nfunction logOriginDebugContext(\n input: RequestInput,\n allowedOrigins: Set<string>,\n rejectedOrigin?: string,\n level: 'error' | 'warn' = 'error',\n nodeEnv?: string,\n): void {\n if (level === 'warn' && nodeEnv === 'test') return\n const log = level === 'warn' ? console.warn : console.error\n\n if (typeof input === 'string') {\n log('[origin-check] rejected string input', {\n requestUrl: input,\n rejectedOrigin: rejectedOrigin ?? null,\n allowedOrigins: Array.from(allowedOrigins),\n })\n return\n }\n\n if (!input) {\n log('[origin-check] rejected empty input', {\n rejectedOrigin: rejectedOrigin ?? null,\n allowedOrigins: Array.from(allowedOrigins),\n })\n return\n }\n\n log('[origin-check] rejected request', {\n requestUrl: input.url,\n host: input.headers.get('host'),\n forwardedHost: input.headers.get('x-forwarded-host'),\n forwardedProto: input.headers.get('x-forwarded-proto'),\n derivedOrigins: Array.from(requestOrigins(input)),\n rejectedOrigin: rejectedOrigin ?? null,\n allowedOrigins: Array.from(allowedOrigins),\n })\n}\n\nfunction isLoopbackHostname(hostname: string): boolean {\n return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1'\n}\n\nfunction isLoopbackOrigin(origin: string): boolean {\n try {\n return isLoopbackHostname(new URL(origin).hostname)\n } catch {\n return false\n }\n}\n\nfunction normalizeOriginPort(url: URL): string {\n if (url.port) return url.port\n if (url.protocol === 'https:') return '443'\n if (url.protocol === 'http:') return '80'\n return ''\n}\n\nfunction isEquivalentLoopbackOrigin(origin: string, allowedOrigin: string): boolean {\n try {\n const candidateUrl = new URL(origin)\n const allowedUrl = new URL(allowedOrigin)\n if (!isLoopbackHostname(candidateUrl.hostname) || !isLoopbackHostname(allowedUrl.hostname)) {\n return false\n }\n return normalizeOriginPort(candidateUrl) === normalizeOriginPort(allowedUrl)\n } catch {\n return false\n }\n}\n\nfunction shouldAllowLoopbackOrigin(origin: string, allowedOrigins: Set<string>, env: EnvLike): boolean {\n if (!isLoopbackOrigin(origin)) return false\n for (const allowedOrigin of allowedOrigins) {\n if (!isLoopbackOrigin(allowedOrigin)) continue\n if (env.NODE_ENV !== 'production') return true\n if (isEquivalentLoopbackOrigin(origin, allowedOrigin)) return true\n }\n return false\n}\n\nexport function assertAllowedAppOrigin(input: RequestInput, env: EnvLike = process.env): void {\n const allowedOrigins = readAllowedOrigins(env)\n if (allowedOrigins.size === 0) {\n if (env.NODE_ENV === 'production') {\n logOriginDebugContext(input, allowedOrigins)\n throw new AppOriginConfigurationError('APP_URL must be configured in production')\n }\n return\n }\n const { urlOrigin, headerOrigins } = readRequestOriginCandidates(input)\n const hasAllowedHeaderOrigin = Array.from(headerOrigins).some((origin) => allowedOrigins.has(origin))\n\n for (const origin of headerOrigins) {\n if (allowedOrigins.has(origin)) continue\n if (shouldAllowLoopbackOrigin(origin, allowedOrigins, env)) continue\n logOriginDebugContext(input, allowedOrigins, origin, 'warn', env.NODE_ENV)\n throw new AppOriginRejectedError('Request origin is not allowed')\n }\n\n if (!urlOrigin) return\n if (allowedOrigins.has(urlOrigin)) return\n if (shouldAllowLoopbackOrigin(urlOrigin, allowedOrigins, env)) return\n if (isLoopbackOrigin(urlOrigin) && hasAllowedHeaderOrigin) return\n\n logOriginDebugContext(input, allowedOrigins, urlOrigin, 'warn', env.NODE_ENV)\n throw new AppOriginRejectedError('Request origin is not allowed')\n}\n\nexport function resolveRequestOrigin(req: Request): string {\n const url = new URL(req.url)\n const proto = req.headers.get('x-forwarded-proto') || url.protocol.replace(':', '')\n const host = req.headers.get('x-forwarded-host') || req.headers.get('host') || url.host\n return `${proto}://${host}`\n}\n\nexport function getAppBaseUrl(req: Request): string {\n return (\n process.env.NEXT_PUBLIC_APP_URL ||\n process.env.APP_URL ||\n resolveRequestOrigin(req)\n )\n}\n\nexport function toAbsoluteUrl(req: Request, path: string): string {\n return new URL(path, getAppBaseUrl(req)).toString()\n}\n\nexport function getSecurityEmailBaseUrl(input?: RequestInput, env: EnvLike = process.env): string {\n const configuredAppUrl = normalizeBaseUrl(env.APP_URL)\n if (!configuredAppUrl) {\n if (env.NODE_ENV === 'production') {\n throw new AppOriginConfigurationError('APP_URL must be configured in production')\n }\n return DEFAULT_DEV_APP_URL\n }\n\n assertAllowedAppOrigin(input, env)\n return configuredAppUrl\n}\n\nexport function toSecurityEmailUrl(input: RequestInput, path: string, env: EnvLike = process.env): string {\n const base = getSecurityEmailBaseUrl(input, env)\n return `${base}/${path.replace(/^\\/+/, '')}`\n}\n\nexport function mapSecurityEmailUrlError(\n error: unknown,\n mapping: SecurityEmailUrlErrorMapping,\n): { status: number; body: { error: string } } | null {\n if (error instanceof AppOriginRejectedError) {\n return { status: 400, body: { error: 'Invalid request origin' } }\n }\n if (error instanceof AppOriginConfigurationError) {\n console.error(`[${mapping.scope}] APP_URL is required in production`)\n return { status: 500, body: { error: mapping.configMessage } }\n }\n return null\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { createLogger } from './logger'\n\nconst logger = createLogger('shared').child({ component: 'origin-check' })\n\nconst DEFAULT_DEV_APP_URL = 'http://localhost:3000'\n\ntype EnvLike = Record<string, string | undefined> & {\n APP_URL?: string\n NEXT_PUBLIC_APP_URL?: string\n APP_ALLOWED_ORIGINS?: string\n NODE_ENV?: string\n}\ntype RequestInput = Request | string | undefined\n\nexport type SecurityEmailUrlErrorMapping = {\n scope: string\n configMessage: string\n}\n\nexport class AppOriginConfigurationError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'AppOriginConfigurationError'\n }\n}\n\nexport class AppOriginRejectedError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'AppOriginRejectedError'\n }\n}\n\nfunction parseBaseUrl(raw: string | undefined): URL | null {\n const value = raw?.trim()\n if (!value) return null\n try {\n const url = new URL(value)\n url.hash = ''\n url.search = ''\n return url\n } catch {\n return null\n }\n}\n\nfunction normalizeBaseUrl(raw: string | undefined): string | null {\n const url = parseBaseUrl(raw)\n return url ? url.toString().replace(/\\/$/, '') : null\n}\n\nfunction normalizeOrigin(raw: string | undefined): string | null {\n const url = parseBaseUrl(raw)\n return url ? url.origin : null\n}\n\nfunction readCsv(value: string | undefined): string[] {\n return (value ?? '')\n .split(',')\n .map((item) => item.trim())\n .filter((item) => item.length > 0)\n}\n\nfunction readAllowedOrigins(env: EnvLike): Set<string> {\n const origins = new Set<string>()\n for (const raw of [env.APP_URL, env.NEXT_PUBLIC_APP_URL, ...readCsv(env.APP_ALLOWED_ORIGINS)]) {\n const origin = normalizeOrigin(raw)\n if (origin) origins.add(origin)\n }\n return origins\n}\n\nfunction readFirstHeaderValue(value: string | null): string | null {\n const first = value?.split(',')[0]?.trim()\n return first && first.length > 0 ? first : null\n}\n\nfunction originFromHost(protocol: string, rawHost: string | null): string | null {\n const host = readFirstHeaderValue(rawHost)\n if (!host) return null\n return normalizeOrigin(`${protocol}//${host}`)\n}\n\ntype RequestOriginCandidates = {\n urlOrigin: string | null\n headerOrigins: Set<string>\n}\n\nfunction readRequestOriginCandidates(input: RequestInput): RequestOriginCandidates {\n const candidates: RequestOriginCandidates = {\n urlOrigin: null,\n headerOrigins: new Set<string>(),\n }\n if (!input) return candidates\n\n const requestUrl = typeof input === 'string' ? input : input.url\n let parsedUrl: URL\n try {\n parsedUrl = new URL(requestUrl)\n } catch {\n return candidates\n }\n\n candidates.urlOrigin = normalizeOrigin(parsedUrl.origin)\n if (typeof input === 'string') return candidates\n\n const forwardedProto = readFirstHeaderValue(input.headers.get('x-forwarded-proto')) ?? parsedUrl.protocol.replace(/:$/, '')\n const protocol = forwardedProto.endsWith(':') ? forwardedProto : `${forwardedProto}:`\n const hostOrigin = originFromHost(protocol, input.headers.get('host'))\n const forwardedHostOrigin = originFromHost(protocol, input.headers.get('x-forwarded-host'))\n if (hostOrigin) candidates.headerOrigins.add(hostOrigin)\n if (forwardedHostOrigin) candidates.headerOrigins.add(forwardedHostOrigin)\n return candidates\n}\n\nfunction requestOrigins(input: RequestInput): Set<string> {\n const origins = new Set<string>()\n const { urlOrigin, headerOrigins } = readRequestOriginCandidates(input)\n if (urlOrigin) origins.add(urlOrigin)\n for (const origin of headerOrigins) origins.add(origin)\n return origins\n}\n\nfunction logOriginDebugContext(\n input: RequestInput,\n allowedOrigins: Set<string>,\n rejectedOrigin?: string,\n level: 'error' | 'warn' = 'error',\n nodeEnv?: string,\n): void {\n if (level === 'warn' && nodeEnv === 'test') return\n const log = (msg: string, fields: Record<string, unknown>) =>\n level === 'warn' ? logger.warn(msg, fields) : logger.error(msg, fields)\n\n if (typeof input === 'string') {\n log('Origin check rejected string input', {\n requestUrl: input,\n rejectedOrigin: rejectedOrigin ?? null,\n allowedOrigins: Array.from(allowedOrigins),\n })\n return\n }\n\n if (!input) {\n log('Origin check rejected empty input', {\n rejectedOrigin: rejectedOrigin ?? null,\n allowedOrigins: Array.from(allowedOrigins),\n })\n return\n }\n\n log('Origin check rejected request', {\n requestUrl: input.url,\n host: input.headers.get('host'),\n forwardedHost: input.headers.get('x-forwarded-host'),\n forwardedProto: input.headers.get('x-forwarded-proto'),\n derivedOrigins: Array.from(requestOrigins(input)),\n rejectedOrigin: rejectedOrigin ?? null,\n allowedOrigins: Array.from(allowedOrigins),\n })\n}\n\nfunction isLoopbackHostname(hostname: string): boolean {\n return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1'\n}\n\nfunction isLoopbackOrigin(origin: string): boolean {\n try {\n return isLoopbackHostname(new URL(origin).hostname)\n } catch {\n return false\n }\n}\n\nfunction normalizeOriginPort(url: URL): string {\n if (url.port) return url.port\n if (url.protocol === 'https:') return '443'\n if (url.protocol === 'http:') return '80'\n return ''\n}\n\nfunction isEquivalentLoopbackOrigin(origin: string, allowedOrigin: string): boolean {\n try {\n const candidateUrl = new URL(origin)\n const allowedUrl = new URL(allowedOrigin)\n if (!isLoopbackHostname(candidateUrl.hostname) || !isLoopbackHostname(allowedUrl.hostname)) {\n return false\n }\n return normalizeOriginPort(candidateUrl) === normalizeOriginPort(allowedUrl)\n } catch {\n return false\n }\n}\n\nfunction shouldAllowLoopbackOrigin(origin: string, allowedOrigins: Set<string>, env: EnvLike): boolean {\n if (!isLoopbackOrigin(origin)) return false\n for (const allowedOrigin of allowedOrigins) {\n if (!isLoopbackOrigin(allowedOrigin)) continue\n if (env.NODE_ENV !== 'production') return true\n if (isEquivalentLoopbackOrigin(origin, allowedOrigin)) return true\n }\n return false\n}\n\nexport function assertAllowedAppOrigin(input: RequestInput, env: EnvLike = process.env): void {\n const allowedOrigins = readAllowedOrigins(env)\n if (allowedOrigins.size === 0) {\n if (env.NODE_ENV === 'production') {\n logOriginDebugContext(input, allowedOrigins)\n throw new AppOriginConfigurationError('APP_URL must be configured in production')\n }\n return\n }\n const { urlOrigin, headerOrigins } = readRequestOriginCandidates(input)\n const hasAllowedHeaderOrigin = Array.from(headerOrigins).some((origin) => allowedOrigins.has(origin))\n\n for (const origin of headerOrigins) {\n if (allowedOrigins.has(origin)) continue\n if (shouldAllowLoopbackOrigin(origin, allowedOrigins, env)) continue\n logOriginDebugContext(input, allowedOrigins, origin, 'warn', env.NODE_ENV)\n throw new AppOriginRejectedError('Request origin is not allowed')\n }\n\n if (!urlOrigin) return\n if (allowedOrigins.has(urlOrigin)) return\n if (shouldAllowLoopbackOrigin(urlOrigin, allowedOrigins, env)) return\n if (isLoopbackOrigin(urlOrigin) && hasAllowedHeaderOrigin) return\n\n logOriginDebugContext(input, allowedOrigins, urlOrigin, 'warn', env.NODE_ENV)\n throw new AppOriginRejectedError('Request origin is not allowed')\n}\n\nexport function resolveRequestOrigin(req: Request): string {\n const url = new URL(req.url)\n const proto = req.headers.get('x-forwarded-proto') || url.protocol.replace(':', '')\n const host = req.headers.get('x-forwarded-host') || req.headers.get('host') || url.host\n return `${proto}://${host}`\n}\n\nexport function getAppBaseUrl(req: Request): string {\n return (\n process.env.NEXT_PUBLIC_APP_URL ||\n process.env.APP_URL ||\n resolveRequestOrigin(req)\n )\n}\n\nexport function toAbsoluteUrl(req: Request, path: string): string {\n return new URL(path, getAppBaseUrl(req)).toString()\n}\n\nexport function getSecurityEmailBaseUrl(input?: RequestInput, env: EnvLike = process.env): string {\n const configuredAppUrl = normalizeBaseUrl(env.APP_URL)\n if (!configuredAppUrl) {\n if (env.NODE_ENV === 'production') {\n throw new AppOriginConfigurationError('APP_URL must be configured in production')\n }\n return DEFAULT_DEV_APP_URL\n }\n\n assertAllowedAppOrigin(input, env)\n return configuredAppUrl\n}\n\nexport function toSecurityEmailUrl(input: RequestInput, path: string, env: EnvLike = process.env): string {\n const base = getSecurityEmailBaseUrl(input, env)\n return `${base}/${path.replace(/^\\/+/, '')}`\n}\n\nexport function mapSecurityEmailUrlError(\n error: unknown,\n mapping: SecurityEmailUrlErrorMapping,\n): { status: number; body: { error: string } } | null {\n if (error instanceof AppOriginRejectedError) {\n return { status: 400, body: { error: 'Invalid request origin' } }\n }\n if (error instanceof AppOriginConfigurationError) {\n logger.error('APP_URL is required in production', { scope: mapping.scope })\n return { status: 500, body: { error: mapping.configMessage } }\n }\n return null\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,4 +1,4 @@
1
- const APP_VERSION = "0.6.6-develop.6460.1.f94c74174c";
1
+ const APP_VERSION = "0.6.6-develop.6471.1.9ab5bdd79a";
2
2
  const appVersion = APP_VERSION;
3
3
  export {
4
4
  APP_VERSION,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/version.ts"],
4
- "sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.6.6-develop.6460.1.f94c74174c'\nexport const appVersion = APP_VERSION\n"],
4
+ "sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.6.6-develop.6471.1.9ab5bdd79a'\nexport const appVersion = APP_VERSION\n"],
5
5
  "mappings": "AACO,MAAM,cAAc;AACpB,MAAM,aAAa;",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,9 @@
1
+ import { createLogger } from "../lib/logger/index.js";
2
+ const logger = createLogger("shared").child({ component: "analytics-registry" });
1
3
  let _analyticsModuleConfigs = null;
2
4
  function registerAnalyticsModuleConfigs(configs) {
3
5
  if (_analyticsModuleConfigs !== null && process.env.NODE_ENV === "development") {
4
- console.debug("[Bootstrap] Analytics module configs re-registered (this may occur during HMR)");
6
+ logger.debug("Analytics module configs re-registered (this may occur during HMR)");
5
7
  }
6
8
  _analyticsModuleConfigs = configs;
7
9
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/modules/analytics.ts"],
4
- "sourcesContent": ["export type AggregateFunction = 'count' | 'sum' | 'avg' | 'min' | 'max'\n\nexport type DateGranularity = 'day' | 'week' | 'month' | 'quarter' | 'year'\n\nexport type AnalyticsFieldType = 'numeric' | 'text' | 'uuid' | 'timestamp' | 'jsonb'\n\nexport type AnalyticsEntityTypeConfig = {\n tableName: string\n schema?: string\n dateField: string\n defaultScopeFields: string[]\n}\n\nexport type AnalyticsFieldMapping = {\n dbColumn: string\n type: AnalyticsFieldType\n}\n\nexport type AnalyticsLabelResolverConfig = {\n table: string\n idColumn: string\n labelColumn: string\n}\n\nexport type AnalyticsEntityConfig = {\n entityId: string\n requiredFeatures: string[]\n entityConfig: AnalyticsEntityTypeConfig\n fieldMappings: Record<string, AnalyticsFieldMapping>\n labelResolvers?: Record<string, AnalyticsLabelResolverConfig>\n}\n\nexport type AnalyticsModuleConfig = {\n entities: AnalyticsEntityConfig[]\n}\n\nlet _analyticsModuleConfigs: AnalyticsModuleConfig[] | null = null\n\nexport function registerAnalyticsModuleConfigs(configs: AnalyticsModuleConfig[]): void {\n if (_analyticsModuleConfigs !== null && process.env.NODE_ENV === 'development') {\n console.debug('[Bootstrap] Analytics module configs re-registered (this may occur during HMR)')\n }\n _analyticsModuleConfigs = configs\n}\n\nexport function getAnalyticsModuleConfigs(): AnalyticsModuleConfig[] {\n return _analyticsModuleConfigs ?? []\n}\n\nexport function getAllAnalyticsEntityConfigs(): AnalyticsEntityConfig[] {\n const configs = getAnalyticsModuleConfigs()\n return configs.flatMap((moduleConfig) => moduleConfig.entities)\n}\n\nexport function getAnalyticsEntityConfig(entityId: string): AnalyticsEntityConfig | null {\n const entities = getAllAnalyticsEntityConfigs()\n return entities.find((e) => e.entityId === entityId) ?? null\n}\n"],
5
- "mappings": "AAoCA,IAAI,0BAA0D;AAEvD,SAAS,+BAA+B,SAAwC;AACrF,MAAI,4BAA4B,QAAQ,QAAQ,IAAI,aAAa,eAAe;AAC9E,YAAQ,MAAM,gFAAgF;AAAA,EAChG;AACA,4BAA0B;AAC5B;AAEO,SAAS,4BAAqD;AACnE,SAAO,2BAA2B,CAAC;AACrC;AAEO,SAAS,+BAAwD;AACtE,QAAM,UAAU,0BAA0B;AAC1C,SAAO,QAAQ,QAAQ,CAAC,iBAAiB,aAAa,QAAQ;AAChE;AAEO,SAAS,yBAAyB,UAAgD;AACvF,QAAM,WAAW,6BAA6B;AAC9C,SAAO,SAAS,KAAK,CAAC,MAAM,EAAE,aAAa,QAAQ,KAAK;AAC1D;",
4
+ "sourcesContent": ["import { createLogger } from '../lib/logger'\n\nconst logger = createLogger('shared').child({ component: 'analytics-registry' })\n\nexport type AggregateFunction = 'count' | 'sum' | 'avg' | 'min' | 'max'\n\nexport type DateGranularity = 'day' | 'week' | 'month' | 'quarter' | 'year'\n\nexport type AnalyticsFieldType = 'numeric' | 'text' | 'uuid' | 'timestamp' | 'jsonb'\n\nexport type AnalyticsEntityTypeConfig = {\n tableName: string\n schema?: string\n dateField: string\n defaultScopeFields: string[]\n}\n\nexport type AnalyticsFieldMapping = {\n dbColumn: string\n type: AnalyticsFieldType\n}\n\nexport type AnalyticsLabelResolverConfig = {\n table: string\n idColumn: string\n labelColumn: string\n}\n\nexport type AnalyticsEntityConfig = {\n entityId: string\n requiredFeatures: string[]\n entityConfig: AnalyticsEntityTypeConfig\n fieldMappings: Record<string, AnalyticsFieldMapping>\n labelResolvers?: Record<string, AnalyticsLabelResolverConfig>\n}\n\nexport type AnalyticsModuleConfig = {\n entities: AnalyticsEntityConfig[]\n}\n\nlet _analyticsModuleConfigs: AnalyticsModuleConfig[] | null = null\n\nexport function registerAnalyticsModuleConfigs(configs: AnalyticsModuleConfig[]): void {\n if (_analyticsModuleConfigs !== null && process.env.NODE_ENV === 'development') {\n logger.debug('Analytics module configs re-registered (this may occur during HMR)')\n }\n _analyticsModuleConfigs = configs\n}\n\nexport function getAnalyticsModuleConfigs(): AnalyticsModuleConfig[] {\n return _analyticsModuleConfigs ?? []\n}\n\nexport function getAllAnalyticsEntityConfigs(): AnalyticsEntityConfig[] {\n const configs = getAnalyticsModuleConfigs()\n return configs.flatMap((moduleConfig) => moduleConfig.entities)\n}\n\nexport function getAnalyticsEntityConfig(entityId: string): AnalyticsEntityConfig | null {\n const entities = getAllAnalyticsEntityConfigs()\n return entities.find((e) => e.entityId === entityId) ?? null\n}\n"],
5
+ "mappings": "AAAA,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,QAAQ,EAAE,MAAM,EAAE,WAAW,qBAAqB,CAAC;AAsC/E,IAAI,0BAA0D;AAEvD,SAAS,+BAA+B,SAAwC;AACrF,MAAI,4BAA4B,QAAQ,QAAQ,IAAI,aAAa,eAAe;AAC9E,WAAO,MAAM,oEAAoE;AAAA,EACnF;AACA,4BAA0B;AAC5B;AAEO,SAAS,4BAAqD;AACnE,SAAO,2BAA2B,CAAC;AACrC;AAEO,SAAS,+BAAwD;AACtE,QAAM,UAAU,0BAA0B;AAC1C,SAAO,QAAQ,QAAQ,CAAC,iBAAiB,aAAa,QAAQ;AAChE;AAEO,SAAS,yBAAyB,UAAgD;AACvF,QAAM,WAAW,6BAA6B;AAC9C,SAAO,SAAS,KAAK,CAAC,MAAM,EAAE,aAAa,QAAQ,KAAK;AAC1D;",
6
6
  "names": []
7
7
  }
@@ -1,4 +1,6 @@
1
1
  import React from "react";
2
+ import { createLogger } from "../../lib/logger/index.js";
3
+ const logger = createLogger("dashboard").child({ component: "widgets" });
2
4
  function lazyDashboardWidget(loader) {
3
5
  let cached = null;
4
6
  let pending = null;
@@ -23,7 +25,7 @@ function lazyDashboardWidget(loader) {
23
25
  }).catch((err) => {
24
26
  if (!cancelled) {
25
27
  try {
26
- console.error("Failed to load dashboard widget component", err);
28
+ logger.error("Failed to load dashboard widget component", { err });
27
29
  } catch {
28
30
  }
29
31
  setTick((value) => value + 1);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/modules/dashboard/widgets.ts"],
4
- "sourcesContent": ["import React, { type ComponentType } from 'react'\n\nexport type DashboardWidgetSize = 'sm' | 'md' | 'lg'\n\nexport type DashboardWidgetMetadata = {\n id: string\n title: string\n description?: string\n subtitle?: string\n features?: string[]\n defaultSize?: DashboardWidgetSize\n defaultSettings?: unknown\n defaultEnabled?: boolean\n tags?: string[]\n category?: string\n icon?: string\n supportsRefresh?: boolean\n}\n\nexport type DashboardLayoutItem = {\n id: string\n widgetId: string\n order: number\n priority?: number\n size?: DashboardWidgetSize\n settings?: unknown\n}\n\nexport type DashboardWidgetRenderMode = 'view' | 'settings'\n\nexport type DashboardWidgetRenderContext = {\n userId: string\n tenantId?: string | null\n organizationId?: string | null\n userName?: string | null\n userEmail?: string | null\n userLabel?: string | null\n}\n\nexport type DashboardWidgetComponentProps<TSettings = unknown> = {\n mode: DashboardWidgetRenderMode\n layout: DashboardLayoutItem\n settings: TSettings\n context: DashboardWidgetRenderContext\n onSettingsChange: (next: TSettings) => void\n refreshToken: number\n onRefreshStateChange?: (refreshing: boolean) => void\n}\n\nexport type DashboardWidgetModule<TSettings = unknown> = {\n metadata: DashboardWidgetMetadata\n Widget: ComponentType<DashboardWidgetComponentProps<TSettings>>\n hydrateSettings?: (raw: unknown) => TSettings\n dehydrateSettings?: (settings: TSettings) => unknown\n}\n\nexport type DashboardWidgetRenderProps<TSettings = unknown> = DashboardWidgetComponentProps<TSettings>\n\ntype DashboardWidgetLoader<TSettings> = () => Promise<\n | { default: ComponentType<DashboardWidgetComponentProps<TSettings>> }\n | ComponentType<DashboardWidgetComponentProps<TSettings>>\n>\n\nexport function lazyDashboardWidget<TSettings>(\n loader: DashboardWidgetLoader<TSettings>,\n): ComponentType<DashboardWidgetComponentProps<TSettings>> {\n let cached: ComponentType<DashboardWidgetComponentProps<TSettings>> | null = null\n let pending: Promise<void> | null = null\n\n const load = () => {\n if (cached) return Promise.resolve()\n if (!pending) {\n pending = loader()\n .then((mod) => {\n cached = (mod as { default?: ComponentType<DashboardWidgetComponentProps<TSettings>> }).default ??\n (mod as ComponentType<DashboardWidgetComponentProps<TSettings>>)\n })\n .catch((err) => {\n pending = null\n throw err\n })\n }\n return pending\n }\n\n const LazyWidget: ComponentType<DashboardWidgetComponentProps<TSettings>> = (props) => {\n const [, setTick] = React.useState(0)\n React.useEffect(() => {\n let cancelled = false\n void load()\n .then(() => {\n if (!cancelled) setTick((value) => value + 1)\n })\n .catch((err) => {\n if (!cancelled) {\n try {\n console.error('Failed to load dashboard widget component', err)\n } catch {}\n setTick((value) => value + 1)\n }\n })\n return () => {\n cancelled = true\n }\n }, [])\n if (!cached) return null\n return React.createElement(cached, props)\n }\n\n return LazyWidget\n}\n"],
5
- "mappings": "AAAA,OAAO,WAAmC;AA+DnC,SAAS,oBACd,QACyD;AACzD,MAAI,SAAyE;AAC7E,MAAI,UAAgC;AAEpC,QAAM,OAAO,MAAM;AACjB,QAAI,OAAQ,QAAO,QAAQ,QAAQ;AACnC,QAAI,CAAC,SAAS;AACZ,gBAAU,OAAO,EACd,KAAK,CAAC,QAAQ;AACb,iBAAU,IAA8E,WACrF;AAAA,MACL,CAAC,EACA,MAAM,CAAC,QAAQ;AACd,kBAAU;AACV,cAAM;AAAA,MACR,CAAC;AAAA,IACL;AACA,WAAO;AAAA,EACT;AAEA,QAAM,aAAsE,CAAC,UAAU;AACrF,UAAM,CAAC,EAAE,OAAO,IAAI,MAAM,SAAS,CAAC;AACpC,UAAM,UAAU,MAAM;AACpB,UAAI,YAAY;AAChB,WAAK,KAAK,EACP,KAAK,MAAM;AACV,YAAI,CAAC,UAAW,SAAQ,CAAC,UAAU,QAAQ,CAAC;AAAA,MAC9C,CAAC,EACA,MAAM,CAAC,QAAQ;AACd,YAAI,CAAC,WAAW;AACd,cAAI;AACF,oBAAQ,MAAM,6CAA6C,GAAG;AAAA,UAChE,QAAQ;AAAA,UAAC;AACT,kBAAQ,CAAC,UAAU,QAAQ,CAAC;AAAA,QAC9B;AAAA,MACF,CAAC;AACH,aAAO,MAAM;AACX,oBAAY;AAAA,MACd;AAAA,IACF,GAAG,CAAC,CAAC;AACL,QAAI,CAAC,OAAQ,QAAO;AACpB,WAAO,MAAM,cAAc,QAAQ,KAAK;AAAA,EAC1C;AAEA,SAAO;AACT;",
4
+ "sourcesContent": ["import React, { type ComponentType } from 'react'\nimport { createLogger } from '../../lib/logger'\n\nconst logger = createLogger('dashboard').child({ component: 'widgets' })\n\nexport type DashboardWidgetSize = 'sm' | 'md' | 'lg'\n\nexport type DashboardWidgetMetadata = {\n id: string\n title: string\n description?: string\n subtitle?: string\n features?: string[]\n defaultSize?: DashboardWidgetSize\n defaultSettings?: unknown\n defaultEnabled?: boolean\n tags?: string[]\n category?: string\n icon?: string\n supportsRefresh?: boolean\n}\n\nexport type DashboardLayoutItem = {\n id: string\n widgetId: string\n order: number\n priority?: number\n size?: DashboardWidgetSize\n settings?: unknown\n}\n\nexport type DashboardWidgetRenderMode = 'view' | 'settings'\n\nexport type DashboardWidgetRenderContext = {\n userId: string\n tenantId?: string | null\n organizationId?: string | null\n userName?: string | null\n userEmail?: string | null\n userLabel?: string | null\n}\n\nexport type DashboardWidgetComponentProps<TSettings = unknown> = {\n mode: DashboardWidgetRenderMode\n layout: DashboardLayoutItem\n settings: TSettings\n context: DashboardWidgetRenderContext\n onSettingsChange: (next: TSettings) => void\n refreshToken: number\n onRefreshStateChange?: (refreshing: boolean) => void\n}\n\nexport type DashboardWidgetModule<TSettings = unknown> = {\n metadata: DashboardWidgetMetadata\n Widget: ComponentType<DashboardWidgetComponentProps<TSettings>>\n hydrateSettings?: (raw: unknown) => TSettings\n dehydrateSettings?: (settings: TSettings) => unknown\n}\n\nexport type DashboardWidgetRenderProps<TSettings = unknown> = DashboardWidgetComponentProps<TSettings>\n\ntype DashboardWidgetLoader<TSettings> = () => Promise<\n | { default: ComponentType<DashboardWidgetComponentProps<TSettings>> }\n | ComponentType<DashboardWidgetComponentProps<TSettings>>\n>\n\nexport function lazyDashboardWidget<TSettings>(\n loader: DashboardWidgetLoader<TSettings>,\n): ComponentType<DashboardWidgetComponentProps<TSettings>> {\n let cached: ComponentType<DashboardWidgetComponentProps<TSettings>> | null = null\n let pending: Promise<void> | null = null\n\n const load = () => {\n if (cached) return Promise.resolve()\n if (!pending) {\n pending = loader()\n .then((mod) => {\n cached = (mod as { default?: ComponentType<DashboardWidgetComponentProps<TSettings>> }).default ??\n (mod as ComponentType<DashboardWidgetComponentProps<TSettings>>)\n })\n .catch((err) => {\n pending = null\n throw err\n })\n }\n return pending\n }\n\n const LazyWidget: ComponentType<DashboardWidgetComponentProps<TSettings>> = (props) => {\n const [, setTick] = React.useState(0)\n React.useEffect(() => {\n let cancelled = false\n void load()\n .then(() => {\n if (!cancelled) setTick((value) => value + 1)\n })\n .catch((err) => {\n if (!cancelled) {\n try {\n logger.error('Failed to load dashboard widget component', { err })\n } catch {}\n setTick((value) => value + 1)\n }\n })\n return () => {\n cancelled = true\n }\n }, [])\n if (!cached) return null\n return React.createElement(cached, props)\n }\n\n return LazyWidget\n}\n"],
5
+ "mappings": "AAAA,OAAO,WAAmC;AAC1C,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,WAAW,EAAE,MAAM,EAAE,WAAW,UAAU,CAAC;AA+DhE,SAAS,oBACd,QACyD;AACzD,MAAI,SAAyE;AAC7E,MAAI,UAAgC;AAEpC,QAAM,OAAO,MAAM;AACjB,QAAI,OAAQ,QAAO,QAAQ,QAAQ;AACnC,QAAI,CAAC,SAAS;AACZ,gBAAU,OAAO,EACd,KAAK,CAAC,QAAQ;AACb,iBAAU,IAA8E,WACrF;AAAA,MACL,CAAC,EACA,MAAM,CAAC,QAAQ;AACd,kBAAU;AACV,cAAM;AAAA,MACR,CAAC;AAAA,IACL;AACA,WAAO;AAAA,EACT;AAEA,QAAM,aAAsE,CAAC,UAAU;AACrF,UAAM,CAAC,EAAE,OAAO,IAAI,MAAM,SAAS,CAAC;AACpC,UAAM,UAAU,MAAM;AACpB,UAAI,YAAY;AAChB,WAAK,KAAK,EACP,KAAK,MAAM;AACV,YAAI,CAAC,UAAW,SAAQ,CAAC,UAAU,QAAQ,CAAC;AAAA,MAC9C,CAAC,EACA,MAAM,CAAC,QAAQ;AACd,YAAI,CAAC,WAAW;AACd,cAAI;AACF,mBAAO,MAAM,6CAA6C,EAAE,IAAI,CAAC;AAAA,UACnE,QAAQ;AAAA,UAAC;AACT,kBAAQ,CAAC,UAAU,QAAQ,CAAC;AAAA,QAC9B;AAAA,MACF,CAAC;AACH,aAAO,MAAM;AACX,oBAAY;AAAA,MACd;AAAA,IACF,GAAG,CAAC,CAAC;AACL,QAAI,CAAC,OAAQ,QAAO;AACpB,WAAO,MAAM,cAAc,QAAQ,KAAK;AAAA,EAC1C;AAEA,SAAO;AACT;",
6
6
  "names": []
7
7
  }
@@ -1,3 +1,5 @@
1
+ import { createLogger } from "../../lib/logger/index.js";
2
+ const logger = createLogger("events").child({ component: "factory" });
1
3
  const GLOBAL_EVENT_BUS_KEY = "__openMercatoGlobalEventBus__";
2
4
  let globalEventBus = null;
3
5
  function setGlobalEventBus(bus) {
@@ -46,7 +48,7 @@ function isPortalBroadcastEvent(eventId) {
46
48
  let _registeredEventConfigs = null;
47
49
  function registerEventModuleConfigs(configs) {
48
50
  if (_registeredEventConfigs !== null && process.env.NODE_ENV === "development") {
49
- console.debug("[Bootstrap] Event module configs re-registered (this may occur during HMR)");
51
+ logger.debug("Event module configs re-registered (this may occur during HMR)");
50
52
  }
51
53
  _registeredEventConfigs = configs;
52
54
  for (const config of configs) {
@@ -74,12 +76,12 @@ function createModuleEvents(options) {
74
76
  if (strict) {
75
77
  throw new Error(message);
76
78
  } else {
77
- console.error(message);
79
+ logger.error("Module tried to emit undeclared event \u2014 add it to the module events.ts first", { moduleId, eventId });
78
80
  }
79
81
  }
80
82
  const eventBus = getGlobalEventBus();
81
83
  if (!eventBus) {
82
- console.warn(`[events] Event bus not available, cannot emit "${eventId}"`);
84
+ logger.warn("Event bus not available, cannot emit event", { eventId });
83
85
  return;
84
86
  }
85
87
  await eventBus.emit(eventId, payload, emitOptions);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/modules/events/factory.ts"],
4
- "sourcesContent": ["/**\n * Event Module Factory\n *\n * Provides factory functions for creating type-safe event configurations.\n */\n\nimport type {\n EventDefinition,\n EventModuleConfig,\n EventPayload,\n EmitOptions,\n CreateModuleEventsOptions,\n ModuleEventEmitter,\n} from './types'\n\n// =============================================================================\n// Global Event Bus Reference\n// =============================================================================\n\n/**\n * Type for the global event bus interface\n */\ninterface GlobalEventBus {\n emit(event: string, payload: unknown, options?: EmitOptions): Promise<void>\n}\n\nconst GLOBAL_EVENT_BUS_KEY = '__openMercatoGlobalEventBus__'\n\n// Global event bus reference (set during bootstrap)\nlet globalEventBus: GlobalEventBus | null = null\n\n/**\n * Set the global event bus instance.\n * Called during app bootstrap to wire up event emission.\n */\nexport function setGlobalEventBus(bus: GlobalEventBus): void {\n globalEventBus = bus\n try {\n ;(globalThis as Record<string, unknown>)[GLOBAL_EVENT_BUS_KEY] = bus\n } catch {\n // ignore global assignment failures\n }\n}\n\n/**\n * Get the global event bus instance.\n * Returns null if not yet bootstrapped.\n */\nexport function getGlobalEventBus(): GlobalEventBus | null {\n try {\n const sharedBus = (globalThis as Record<string, unknown>)[GLOBAL_EVENT_BUS_KEY]\n if (sharedBus && typeof sharedBus === 'object' && typeof (sharedBus as GlobalEventBus).emit === 'function') {\n return sharedBus as GlobalEventBus\n }\n } catch {\n // ignore global read failures\n }\n return globalEventBus\n}\n\n// =============================================================================\n// Event Registry for Validation\n// =============================================================================\n\n// Global set of all declared event IDs for runtime validation\nconst allDeclaredEventIds = new Set<string>()\n\n// Global registry of all declared events with their full definitions\nconst allDeclaredEvents: EventDefinition[] = []\n\nfunction addDeclaredEvent(event: EventDefinition): void {\n allDeclaredEventIds.add(event.id)\n // Avoid duplicates if createModuleEvents/registerEventModuleConfigs is called multiple times (e.g., HMR)\n if (!allDeclaredEvents.find(e => e.id === event.id)) {\n allDeclaredEvents.push(event)\n }\n}\n\n/**\n * Check if an event ID has been declared by any module.\n * Used for runtime validation to ensure only declared events are emitted.\n */\nexport function isEventDeclared(eventId: string): boolean {\n return allDeclaredEventIds.has(eventId)\n}\n\n/**\n * Get all declared event IDs.\n * Useful for debugging and introspection.\n */\nexport function getAllDeclaredEventIds(): string[] {\n return Array.from(allDeclaredEventIds)\n}\n\n/**\n * Get all declared events with their full definitions.\n * Used by the API to return available events for workflow triggers.\n */\nexport function getDeclaredEvents(): EventDefinition[] {\n return [...allDeclaredEvents]\n}\n\n/**\n * Check if an event has clientBroadcast enabled.\n * Used by the SSE endpoint to filter events for the DOM Event Bridge.\n */\nexport function isBroadcastEvent(eventId: string): boolean {\n const event = allDeclaredEvents.find(e => e.id === eventId)\n return event?.clientBroadcast === true\n}\n\n/**\n * Check if an event has portalBroadcast enabled.\n * Used by the portal SSE endpoint to filter events for the Portal Event Bridge.\n */\nexport function isPortalBroadcastEvent(eventId: string): boolean {\n const event = allDeclaredEvents.find(e => e.id === eventId)\n return event?.portalBroadcast === true\n}\n\n// =============================================================================\n// Bootstrap Registration (similar to searchModuleConfigs pattern)\n// =============================================================================\n\nlet _registeredEventConfigs: EventModuleConfig[] | null = null\n\n/**\n * Register event module configurations globally.\n * Called during app bootstrap with configs from events.generated.ts.\n */\nexport function registerEventModuleConfigs(configs: EventModuleConfig[]): void {\n if (_registeredEventConfigs !== null && process.env.NODE_ENV === 'development') {\n console.debug('[Bootstrap] Event module configs re-registered (this may occur during HMR)')\n }\n _registeredEventConfigs = configs\n for (const config of configs) {\n for (const event of config.events) {\n addDeclaredEvent(event)\n }\n }\n}\n\n/**\n * Get registered event module configurations.\n * Returns empty array if not registered.\n */\nexport function getEventModuleConfigs(): EventModuleConfig[] {\n return _registeredEventConfigs ?? []\n}\n\n// =============================================================================\n// Factory Function\n// =============================================================================\n\n/**\n * Creates a type-safe event configuration for a module.\n *\n * Usage in module events.ts:\n * ```typescript\n * import { createModuleEvents } from '@open-mercato/shared/modules/events'\n *\n * const events = [\n * { id: 'customers.people.created', label: 'Person Created', category: 'crud' },\n * { id: 'customers.people.updated', label: 'Person Updated', category: 'crud' },\n * ] as const\n *\n * export const eventsConfig = createModuleEvents({\n * moduleId: 'customers',\n * events,\n * })\n *\n * // Export the typed emit function for use in commands\n * export const emitCustomersEvent = eventsConfig.emit\n *\n * // Export event IDs as a type for external use\n * export type CustomersEventId = typeof events[number]['id']\n *\n * export default eventsConfig\n * ```\n *\n * TypeScript will enforce that only declared event IDs can be emitted:\n * ```typescript\n * // \u2705 This compiles - event is declared\n * emitCustomersEvent('customers.people.created', { id: '123', tenantId: 'abc' })\n *\n * // \u274C TypeScript error - event not declared\n * emitCustomersEvent('customers.people.exploded', { id: '123' })\n * ```\n */\nexport function createModuleEvents<\n const TEvents extends readonly { id: string }[],\n TEventIds extends TEvents[number]['id'] = TEvents[number]['id']\n>(options: CreateModuleEventsOptions<TEventIds>): EventModuleConfig<TEventIds> {\n const { moduleId, events, strict = false } = options\n\n // Build set of valid event IDs for runtime validation\n const validEventIds = new Set(events.map(e => e.id))\n\n // Build full event definitions with module added\n const fullEvents: EventDefinition[] = events.map(e => ({\n ...e,\n module: moduleId,\n }))\n\n // Register all event IDs and definitions in the global registry.\n for (const event of fullEvents) {\n addDeclaredEvent(event)\n }\n\n /**\n * The emit function - validates events and delegates to the global event bus\n */\n const emit = async (\n eventId: TEventIds,\n payload: EventPayload,\n emitOptions?: EmitOptions\n ): Promise<void> => {\n // Runtime validation - event must be declared\n if (!validEventIds.has(eventId)) {\n const message =\n `[events] Module \"${moduleId}\" tried to emit undeclared event \"${eventId}\". ` +\n `Add it to the module's events.ts file first.`\n\n if (strict) {\n throw new Error(message)\n } else {\n console.error(message)\n // In non-strict mode, still emit but with warning\n }\n }\n\n // Get event bus from global reference\n const eventBus = getGlobalEventBus()\n if (!eventBus) {\n console.warn(`[events] Event bus not available, cannot emit \"${eventId}\"`)\n return\n }\n\n await eventBus.emit(eventId, payload, emitOptions)\n }\n\n return {\n moduleId,\n events: fullEvents,\n emit: emit as unknown as ModuleEventEmitter<TEventIds>,\n }\n}\n"],
5
- "mappings": "AA0BA,MAAM,uBAAuB;AAG7B,IAAI,iBAAwC;AAMrC,SAAS,kBAAkB,KAA2B;AAC3D,mBAAiB;AACjB,MAAI;AACF;AAAC,IAAC,WAAuC,oBAAoB,IAAI;AAAA,EACnE,QAAQ;AAAA,EAER;AACF;AAMO,SAAS,oBAA2C;AACzD,MAAI;AACF,UAAM,YAAa,WAAuC,oBAAoB;AAC9E,QAAI,aAAa,OAAO,cAAc,YAAY,OAAQ,UAA6B,SAAS,YAAY;AAC1G,aAAO;AAAA,IACT;AAAA,EACF,QAAQ;AAAA,EAER;AACA,SAAO;AACT;AAOA,MAAM,sBAAsB,oBAAI,IAAY;AAG5C,MAAM,oBAAuC,CAAC;AAE9C,SAAS,iBAAiB,OAA8B;AACtD,sBAAoB,IAAI,MAAM,EAAE;AAEhC,MAAI,CAAC,kBAAkB,KAAK,OAAK,EAAE,OAAO,MAAM,EAAE,GAAG;AACnD,sBAAkB,KAAK,KAAK;AAAA,EAC9B;AACF;AAMO,SAAS,gBAAgB,SAA0B;AACxD,SAAO,oBAAoB,IAAI,OAAO;AACxC;AAMO,SAAS,yBAAmC;AACjD,SAAO,MAAM,KAAK,mBAAmB;AACvC;AAMO,SAAS,oBAAuC;AACrD,SAAO,CAAC,GAAG,iBAAiB;AAC9B;AAMO,SAAS,iBAAiB,SAA0B;AACzD,QAAM,QAAQ,kBAAkB,KAAK,OAAK,EAAE,OAAO,OAAO;AAC1D,SAAO,OAAO,oBAAoB;AACpC;AAMO,SAAS,uBAAuB,SAA0B;AAC/D,QAAM,QAAQ,kBAAkB,KAAK,OAAK,EAAE,OAAO,OAAO;AAC1D,SAAO,OAAO,oBAAoB;AACpC;AAMA,IAAI,0BAAsD;AAMnD,SAAS,2BAA2B,SAAoC;AAC7E,MAAI,4BAA4B,QAAQ,QAAQ,IAAI,aAAa,eAAe;AAC9E,YAAQ,MAAM,4EAA4E;AAAA,EAC5F;AACA,4BAA0B;AAC1B,aAAW,UAAU,SAAS;AAC5B,eAAW,SAAS,OAAO,QAAQ;AACjC,uBAAiB,KAAK;AAAA,IACxB;AAAA,EACF;AACF;AAMO,SAAS,wBAA6C;AAC3D,SAAO,2BAA2B,CAAC;AACrC;AAyCO,SAAS,mBAGd,SAA6E;AAC7E,QAAM,EAAE,UAAU,QAAQ,SAAS,MAAM,IAAI;AAG7C,QAAM,gBAAgB,IAAI,IAAI,OAAO,IAAI,OAAK,EAAE,EAAE,CAAC;AAGnD,QAAM,aAAgC,OAAO,IAAI,QAAM;AAAA,IACrD,GAAG;AAAA,IACH,QAAQ;AAAA,EACV,EAAE;AAGF,aAAW,SAAS,YAAY;AAC9B,qBAAiB,KAAK;AAAA,EACxB;AAKA,QAAM,OAAO,OACX,SACA,SACA,gBACkB;AAElB,QAAI,CAAC,cAAc,IAAI,OAAO,GAAG;AAC/B,YAAM,UACJ,oBAAoB,QAAQ,qCAAqC,OAAO;AAG1E,UAAI,QAAQ;AACV,cAAM,IAAI,MAAM,OAAO;AAAA,MACzB,OAAO;AACL,gBAAQ,MAAM,OAAO;AAAA,MAEvB;AAAA,IACF;AAGA,UAAM,WAAW,kBAAkB;AACnC,QAAI,CAAC,UAAU;AACb,cAAQ,KAAK,kDAAkD,OAAO,GAAG;AACzE;AAAA,IACF;AAEA,UAAM,SAAS,KAAK,SAAS,SAAS,WAAW;AAAA,EACnD;AAEA,SAAO;AAAA,IACL;AAAA,IACA,QAAQ;AAAA,IACR;AAAA,EACF;AACF;",
4
+ "sourcesContent": ["/**\n * Event Module Factory\n *\n * Provides factory functions for creating type-safe event configurations.\n */\n\nimport { createLogger } from '../../lib/logger'\nimport type {\n EventDefinition,\n EventModuleConfig,\n EventPayload,\n EmitOptions,\n CreateModuleEventsOptions,\n ModuleEventEmitter,\n} from './types'\n\nconst logger = createLogger('events').child({ component: 'factory' })\n\n// =============================================================================\n// Global Event Bus Reference\n// =============================================================================\n\n/**\n * Type for the global event bus interface\n */\ninterface GlobalEventBus {\n emit(event: string, payload: unknown, options?: EmitOptions): Promise<void>\n}\n\nconst GLOBAL_EVENT_BUS_KEY = '__openMercatoGlobalEventBus__'\n\n// Global event bus reference (set during bootstrap)\nlet globalEventBus: GlobalEventBus | null = null\n\n/**\n * Set the global event bus instance.\n * Called during app bootstrap to wire up event emission.\n */\nexport function setGlobalEventBus(bus: GlobalEventBus): void {\n globalEventBus = bus\n try {\n ;(globalThis as Record<string, unknown>)[GLOBAL_EVENT_BUS_KEY] = bus\n } catch {\n // ignore global assignment failures\n }\n}\n\n/**\n * Get the global event bus instance.\n * Returns null if not yet bootstrapped.\n */\nexport function getGlobalEventBus(): GlobalEventBus | null {\n try {\n const sharedBus = (globalThis as Record<string, unknown>)[GLOBAL_EVENT_BUS_KEY]\n if (sharedBus && typeof sharedBus === 'object' && typeof (sharedBus as GlobalEventBus).emit === 'function') {\n return sharedBus as GlobalEventBus\n }\n } catch {\n // ignore global read failures\n }\n return globalEventBus\n}\n\n// =============================================================================\n// Event Registry for Validation\n// =============================================================================\n\n// Global set of all declared event IDs for runtime validation\nconst allDeclaredEventIds = new Set<string>()\n\n// Global registry of all declared events with their full definitions\nconst allDeclaredEvents: EventDefinition[] = []\n\nfunction addDeclaredEvent(event: EventDefinition): void {\n allDeclaredEventIds.add(event.id)\n // Avoid duplicates if createModuleEvents/registerEventModuleConfigs is called multiple times (e.g., HMR)\n if (!allDeclaredEvents.find(e => e.id === event.id)) {\n allDeclaredEvents.push(event)\n }\n}\n\n/**\n * Check if an event ID has been declared by any module.\n * Used for runtime validation to ensure only declared events are emitted.\n */\nexport function isEventDeclared(eventId: string): boolean {\n return allDeclaredEventIds.has(eventId)\n}\n\n/**\n * Get all declared event IDs.\n * Useful for debugging and introspection.\n */\nexport function getAllDeclaredEventIds(): string[] {\n return Array.from(allDeclaredEventIds)\n}\n\n/**\n * Get all declared events with their full definitions.\n * Used by the API to return available events for workflow triggers.\n */\nexport function getDeclaredEvents(): EventDefinition[] {\n return [...allDeclaredEvents]\n}\n\n/**\n * Check if an event has clientBroadcast enabled.\n * Used by the SSE endpoint to filter events for the DOM Event Bridge.\n */\nexport function isBroadcastEvent(eventId: string): boolean {\n const event = allDeclaredEvents.find(e => e.id === eventId)\n return event?.clientBroadcast === true\n}\n\n/**\n * Check if an event has portalBroadcast enabled.\n * Used by the portal SSE endpoint to filter events for the Portal Event Bridge.\n */\nexport function isPortalBroadcastEvent(eventId: string): boolean {\n const event = allDeclaredEvents.find(e => e.id === eventId)\n return event?.portalBroadcast === true\n}\n\n// =============================================================================\n// Bootstrap Registration (similar to searchModuleConfigs pattern)\n// =============================================================================\n\nlet _registeredEventConfigs: EventModuleConfig[] | null = null\n\n/**\n * Register event module configurations globally.\n * Called during app bootstrap with configs from events.generated.ts.\n */\nexport function registerEventModuleConfigs(configs: EventModuleConfig[]): void {\n if (_registeredEventConfigs !== null && process.env.NODE_ENV === 'development') {\n logger.debug('Event module configs re-registered (this may occur during HMR)')\n }\n _registeredEventConfigs = configs\n for (const config of configs) {\n for (const event of config.events) {\n addDeclaredEvent(event)\n }\n }\n}\n\n/**\n * Get registered event module configurations.\n * Returns empty array if not registered.\n */\nexport function getEventModuleConfigs(): EventModuleConfig[] {\n return _registeredEventConfigs ?? []\n}\n\n// =============================================================================\n// Factory Function\n// =============================================================================\n\n/**\n * Creates a type-safe event configuration for a module.\n *\n * Usage in module events.ts:\n * ```typescript\n * import { createModuleEvents } from '@open-mercato/shared/modules/events'\n *\n * const events = [\n * { id: 'customers.people.created', label: 'Person Created', category: 'crud' },\n * { id: 'customers.people.updated', label: 'Person Updated', category: 'crud' },\n * ] as const\n *\n * export const eventsConfig = createModuleEvents({\n * moduleId: 'customers',\n * events,\n * })\n *\n * // Export the typed emit function for use in commands\n * export const emitCustomersEvent = eventsConfig.emit\n *\n * // Export event IDs as a type for external use\n * export type CustomersEventId = typeof events[number]['id']\n *\n * export default eventsConfig\n * ```\n *\n * TypeScript will enforce that only declared event IDs can be emitted:\n * ```typescript\n * // \u2705 This compiles - event is declared\n * emitCustomersEvent('customers.people.created', { id: '123', tenantId: 'abc' })\n *\n * // \u274C TypeScript error - event not declared\n * emitCustomersEvent('customers.people.exploded', { id: '123' })\n * ```\n */\nexport function createModuleEvents<\n const TEvents extends readonly { id: string }[],\n TEventIds extends TEvents[number]['id'] = TEvents[number]['id']\n>(options: CreateModuleEventsOptions<TEventIds>): EventModuleConfig<TEventIds> {\n const { moduleId, events, strict = false } = options\n\n // Build set of valid event IDs for runtime validation\n const validEventIds = new Set(events.map(e => e.id))\n\n // Build full event definitions with module added\n const fullEvents: EventDefinition[] = events.map(e => ({\n ...e,\n module: moduleId,\n }))\n\n // Register all event IDs and definitions in the global registry.\n for (const event of fullEvents) {\n addDeclaredEvent(event)\n }\n\n /**\n * The emit function - validates events and delegates to the global event bus\n */\n const emit = async (\n eventId: TEventIds,\n payload: EventPayload,\n emitOptions?: EmitOptions\n ): Promise<void> => {\n // Runtime validation - event must be declared\n if (!validEventIds.has(eventId)) {\n const message =\n `[events] Module \"${moduleId}\" tried to emit undeclared event \"${eventId}\". ` +\n `Add it to the module's events.ts file first.`\n\n if (strict) {\n throw new Error(message)\n } else {\n logger.error('Module tried to emit undeclared event \u2014 add it to the module events.ts first', { moduleId, eventId })\n // In non-strict mode, still emit but with warning\n }\n }\n\n // Get event bus from global reference\n const eventBus = getGlobalEventBus()\n if (!eventBus) {\n logger.warn('Event bus not available, cannot emit event', { eventId })\n return\n }\n\n await eventBus.emit(eventId, payload, emitOptions)\n }\n\n return {\n moduleId,\n events: fullEvents,\n emit: emit as unknown as ModuleEventEmitter<TEventIds>,\n }\n}\n"],
5
+ "mappings": "AAMA,SAAS,oBAAoB;AAU7B,MAAM,SAAS,aAAa,QAAQ,EAAE,MAAM,EAAE,WAAW,UAAU,CAAC;AAapE,MAAM,uBAAuB;AAG7B,IAAI,iBAAwC;AAMrC,SAAS,kBAAkB,KAA2B;AAC3D,mBAAiB;AACjB,MAAI;AACF;AAAC,IAAC,WAAuC,oBAAoB,IAAI;AAAA,EACnE,QAAQ;AAAA,EAER;AACF;AAMO,SAAS,oBAA2C;AACzD,MAAI;AACF,UAAM,YAAa,WAAuC,oBAAoB;AAC9E,QAAI,aAAa,OAAO,cAAc,YAAY,OAAQ,UAA6B,SAAS,YAAY;AAC1G,aAAO;AAAA,IACT;AAAA,EACF,QAAQ;AAAA,EAER;AACA,SAAO;AACT;AAOA,MAAM,sBAAsB,oBAAI,IAAY;AAG5C,MAAM,oBAAuC,CAAC;AAE9C,SAAS,iBAAiB,OAA8B;AACtD,sBAAoB,IAAI,MAAM,EAAE;AAEhC,MAAI,CAAC,kBAAkB,KAAK,OAAK,EAAE,OAAO,MAAM,EAAE,GAAG;AACnD,sBAAkB,KAAK,KAAK;AAAA,EAC9B;AACF;AAMO,SAAS,gBAAgB,SAA0B;AACxD,SAAO,oBAAoB,IAAI,OAAO;AACxC;AAMO,SAAS,yBAAmC;AACjD,SAAO,MAAM,KAAK,mBAAmB;AACvC;AAMO,SAAS,oBAAuC;AACrD,SAAO,CAAC,GAAG,iBAAiB;AAC9B;AAMO,SAAS,iBAAiB,SAA0B;AACzD,QAAM,QAAQ,kBAAkB,KAAK,OAAK,EAAE,OAAO,OAAO;AAC1D,SAAO,OAAO,oBAAoB;AACpC;AAMO,SAAS,uBAAuB,SAA0B;AAC/D,QAAM,QAAQ,kBAAkB,KAAK,OAAK,EAAE,OAAO,OAAO;AAC1D,SAAO,OAAO,oBAAoB;AACpC;AAMA,IAAI,0BAAsD;AAMnD,SAAS,2BAA2B,SAAoC;AAC7E,MAAI,4BAA4B,QAAQ,QAAQ,IAAI,aAAa,eAAe;AAC9E,WAAO,MAAM,gEAAgE;AAAA,EAC/E;AACA,4BAA0B;AAC1B,aAAW,UAAU,SAAS;AAC5B,eAAW,SAAS,OAAO,QAAQ;AACjC,uBAAiB,KAAK;AAAA,IACxB;AAAA,EACF;AACF;AAMO,SAAS,wBAA6C;AAC3D,SAAO,2BAA2B,CAAC;AACrC;AAyCO,SAAS,mBAGd,SAA6E;AAC7E,QAAM,EAAE,UAAU,QAAQ,SAAS,MAAM,IAAI;AAG7C,QAAM,gBAAgB,IAAI,IAAI,OAAO,IAAI,OAAK,EAAE,EAAE,CAAC;AAGnD,QAAM,aAAgC,OAAO,IAAI,QAAM;AAAA,IACrD,GAAG;AAAA,IACH,QAAQ;AAAA,EACV,EAAE;AAGF,aAAW,SAAS,YAAY;AAC9B,qBAAiB,KAAK;AAAA,EACxB;AAKA,QAAM,OAAO,OACX,SACA,SACA,gBACkB;AAElB,QAAI,CAAC,cAAc,IAAI,OAAO,GAAG;AAC/B,YAAM,UACJ,oBAAoB,QAAQ,qCAAqC,OAAO;AAG1E,UAAI,QAAQ;AACV,cAAM,IAAI,MAAM,OAAO;AAAA,MACzB,OAAO;AACL,eAAO,MAAM,qFAAgF,EAAE,UAAU,QAAQ,CAAC;AAAA,MAEpH;AAAA,IACF;AAGA,UAAM,WAAW,kBAAkB;AACnC,QAAI,CAAC,UAAU;AACb,aAAO,KAAK,8CAA8C,EAAE,QAAQ,CAAC;AACrE;AAAA,IACF;AAEA,UAAM,SAAS,KAAK,SAAS,SAAS,WAAW;AAAA,EACnD;AAEA,SAAO;AAAA,IACL;AAAA,IACA,QAAQ;AAAA,IACR;AAAA,EACF;AACF;",
6
6
  "names": []
7
7
  }
@@ -48,15 +48,15 @@ function applyModuleOverridesFromEnabledModules(modules) {
48
48
  if (!warnedUnwiredDomains.has(domain)) {
49
49
  warnedUnwiredDomains.add(domain);
50
50
  const moduleIds = Array.from(new Set(entries.map((e) => e.moduleId))).join(", ");
51
- console.warn(
52
- `[Module Overrides] Domain "${domain}" not yet wired \u2014 entry.overrides.${domain} for module(s) [${moduleIds}] was ignored. ${TRACKING_ISSUE_HINT}`
53
- );
51
+ logger.warn("Override domain not yet wired \u2014 entry ignored", { domain, moduleIds, hint: TRACKING_ISSUE_HINT });
54
52
  }
55
53
  continue;
56
54
  }
57
55
  applier(entries);
58
56
  }
59
57
  }
58
+ import { createLogger } from "../lib/logger/index.js";
59
+ const logger = createLogger("shared").child({ component: "module-overrides" });
60
60
  const pageRouteOverrideStore = { modules: {}, programmatic: {} };
61
61
  const subscriberOverrideStore = { modules: {}, programmatic: {} };
62
62
  const workerOverrideStore = { modules: {}, programmatic: {} };
@@ -78,7 +78,7 @@ function normalizeIdOverrideKey(key, label) {
78
78
  if (typeof key !== "string") return null;
79
79
  const trimmed = key.trim();
80
80
  if (trimmed) return trimmed;
81
- console.warn(`[Module Overrides] Skipping malformed ${label} key "${key}" \u2014 expected a non-empty string.`);
81
+ logger.warn("Skipping malformed override key \u2014 expected a non-empty string", { label, key });
82
82
  return null;
83
83
  }
84
84
  function clearStore(store) {
@@ -121,17 +121,13 @@ function applyArrayOverrides(items, overrides, options) {
121
121
  const replacement = overrides[id];
122
122
  if (replacement === null) continue;
123
123
  if (options.isReplacement && !options.isReplacement(replacement)) {
124
- console.warn(
125
- `[Module Overrides] Skipping malformed ${options.label} override for "${id}" \u2014 replacement has the wrong shape.`
126
- );
124
+ logger.warn("Skipping malformed override \u2014 replacement has the wrong shape", { label: options.label, id });
127
125
  result.push(item);
128
126
  continue;
129
127
  }
130
128
  const replacementId = options.getId(replacement);
131
129
  if (replacementId !== id) {
132
- console.warn(
133
- `[Module Overrides] Skipping malformed ${options.label} override for "${id}" \u2014 replacement id must match the override key.`
134
- );
130
+ logger.warn("Skipping malformed override \u2014 replacement id must match the override key", { label: options.label, id });
135
131
  result.push(item);
136
132
  continue;
137
133
  }
@@ -142,7 +138,7 @@ function applyArrayOverrides(items, overrides, options) {
142
138
  function warnStaleOverrides(label, overrides, consumed) {
143
139
  for (const key of Object.keys(overrides)) {
144
140
  if (!consumed.has(key)) {
145
- console.warn(`[Module Overrides] ${label} override "${key}" did not match any registered entry \u2014 override skipped.`);
141
+ logger.warn("Override did not match any registered entry \u2014 override skipped", { label, key });
146
142
  }
147
143
  }
148
144
  }
@@ -232,9 +228,7 @@ function applyApiRouteOverrides(overrides) {
232
228
  for (const [rawKey, value] of Object.entries(overrides)) {
233
229
  const key = normalizeApiRouteOverrideKey(rawKey);
234
230
  if (!key) {
235
- console.warn(
236
- `[Module Overrides] Skipping malformed routes.api key "${rawKey}" \u2014 expected "METHOD /api/path".`
237
- );
231
+ logger.warn('Skipping malformed routes.api key \u2014 expected "METHOD /api/path"', { key: rawKey });
238
232
  continue;
239
233
  }
240
234
  programmaticApiRouteOverrides[key] = value;
@@ -403,9 +397,7 @@ function applyApiOverridesToManifests(routes, overrides) {
403
397
  methodOverrides.set(method, value);
404
398
  remainingMethods.push(method);
405
399
  } else {
406
- console.warn(
407
- `[Module Overrides] Skipping malformed routes.api override for "${key}" \u2014 expected { handler, metadata? } or null.`
408
- );
400
+ logger.warn("Skipping malformed routes.api override \u2014 expected { handler, metadata? } or null", { key });
409
401
  remainingMethods.push(method);
410
402
  }
411
403
  }
@@ -433,9 +425,7 @@ function applyApiOverridesToManifests(routes, overrides) {
433
425
  }
434
426
  for (const key of overrideKeys) {
435
427
  if (!consumedKeys.has(key)) {
436
- console.warn(
437
- `[Module Overrides] routes.api override "${key}" did not match any registered API route \u2014 override skipped.`
438
- );
428
+ logger.warn("routes.api override did not match any registered API route \u2014 override skipped", { key });
439
429
  }
440
430
  }
441
431
  return result;
@@ -446,9 +436,7 @@ function applyPageOverridesToManifests(routes, overrides, kind) {
446
436
  for (const [rawKey, value] of Object.entries(overrides)) {
447
437
  const key = normalizePageRouteOverrideKey(rawKey);
448
438
  if (!key) {
449
- console.warn(
450
- `[Module Overrides] Skipping malformed routes.pages key "${rawKey}" \u2014 expected "/backend/path" or "/frontend/path".`
451
- );
439
+ logger.warn('Skipping malformed routes.pages key \u2014 expected "/backend/path" or "/frontend/path"', { key: rawKey });
452
440
  continue;
453
441
  }
454
442
  normalizedOverrides[key] = value;
@@ -468,18 +456,14 @@ function applyPageOverridesToManifests(routes, overrides, kind) {
468
456
  const override = normalizedOverrides[key];
469
457
  if (override === null) continue;
470
458
  if (!override || typeof override !== "object") {
471
- console.warn(
472
- `[Module Overrides] Skipping malformed routes.pages override for "${key}" \u2014 expected { load?, Component?, metadata? } or null.`
473
- );
459
+ logger.warn("Skipping malformed routes.pages override \u2014 expected { load?, Component?, metadata? } or null", { key });
474
460
  result.push(entry);
475
461
  continue;
476
462
  }
477
463
  const hasLoad = typeof override.load === "function";
478
464
  const hasComponent = typeof override.Component === "function";
479
465
  if (!hasLoad && !hasComponent && override.metadata === void 0) {
480
- console.warn(
481
- `[Module Overrides] Skipping malformed routes.pages override for "${key}" \u2014 expected a loader, component, metadata, or null.`
482
- );
466
+ logger.warn("Skipping malformed routes.pages override \u2014 expected a loader, component, metadata, or null", { key });
483
467
  result.push(entry);
484
468
  continue;
485
469
  }
@@ -581,13 +565,13 @@ function applyComponentOverridesToEntries(entries, overrides = composeComponentO
581
565
  const value = overrides[key];
582
566
  if (value === null) continue;
583
567
  if (!isComponentOverrideValue(value)) {
584
- console.warn(`[Module Overrides] Skipping malformed widgets.components override for "${key}" \u2014 expected ComponentOverride, ComponentOverride[], or null.`);
568
+ logger.warn("Skipping malformed widgets.components override \u2014 expected ComponentOverride, ComponentOverride[], or null", { key });
585
569
  continue;
586
570
  }
587
571
  const values = Array.isArray(value) ? value : [value];
588
572
  for (const override of values) {
589
573
  if (override.target.componentId !== key) {
590
- console.warn(`[Module Overrides] Skipping malformed widgets.components override for "${key}" \u2014 target.componentId must match the override key.`);
574
+ logger.warn("Skipping malformed widgets.components override \u2014 target.componentId must match the override key", { key });
591
575
  continue;
592
576
  }
593
577
  additions.push(override);
@@ -758,9 +742,7 @@ function routesOverridesApplier(entries) {
758
742
  for (const [rawKey, value] of Object.entries(api)) {
759
743
  const key = normalizeApiRouteOverrideKey(rawKey);
760
744
  if (!key) {
761
- console.warn(
762
- `[Module Overrides] Skipping malformed routes.api key "${rawKey}" \u2014 expected "METHOD /api/path".`
763
- );
745
+ logger.warn('Skipping malformed routes.api key \u2014 expected "METHOD /api/path"', { key: rawKey });
764
746
  continue;
765
747
  }
766
748
  modulesConfigApiRouteOverrides[key] = value;