logixia 1.12.6 → 1.13.3

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 (154) hide show
  1. package/README.md +19 -4
  2. package/dist/{async-context-CwBNMm2E.mjs → async-context-BzJrbfQs.mjs} +8 -4
  3. package/dist/async-context-BzJrbfQs.mjs.map +1 -0
  4. package/dist/{async-context-CSHWoHMB.js → async-context-DyQQYq3I.js} +8 -4
  5. package/dist/async-context-DyQQYq3I.js.map +1 -0
  6. package/dist/{azure-monitor.transport-CG7ClkYj.d.ts → azure-monitor.transport-BDFO4F-i.d.ts} +2 -2
  7. package/dist/{azure-monitor.transport-CG7ClkYj.d.ts.map → azure-monitor.transport-BDFO4F-i.d.ts.map} +1 -1
  8. package/dist/{azure-monitor.transport-Cm74kXZS.d.mts → azure-monitor.transport-D5wQEH04.d.mts} +2 -2
  9. package/dist/{azure-monitor.transport-Cm74kXZS.d.mts.map → azure-monitor.transport-D5wQEH04.d.mts.map} +1 -1
  10. package/dist/azure.d.mts +2 -2
  11. package/dist/azure.d.ts +2 -2
  12. package/dist/cli/index.js +98 -52
  13. package/dist/cloudwatch.d.mts +2 -2
  14. package/dist/cloudwatch.d.ts +2 -2
  15. package/dist/cloudwatch.js +2 -1
  16. package/dist/cloudwatch.mjs +2 -1
  17. package/dist/{cloudwatch.transport-CGHY4buE.d.mts → cloudwatch.transport-B00st6Xv.d.ts} +9 -2
  18. package/dist/cloudwatch.transport-B00st6Xv.d.ts.map +1 -0
  19. package/dist/{cloudwatch.transport-pNBNWJLt.js → cloudwatch.transport-BO1ahxw-.js} +26 -19
  20. package/dist/cloudwatch.transport-BO1ahxw-.js.map +1 -0
  21. package/dist/{cloudwatch.transport-DK_8cKd3.d.ts → cloudwatch.transport-Blc7Gtxt.d.mts} +9 -2
  22. package/dist/cloudwatch.transport-Blc7Gtxt.d.mts.map +1 -0
  23. package/dist/{cloudwatch.transport-yBwm4OeO.mjs → cloudwatch.transport-DZ6ies7g.mjs} +26 -19
  24. package/dist/cloudwatch.transport-DZ6ies7g.mjs.map +1 -0
  25. package/dist/{coerce.utils-BN980Y9V.js → coerce.utils-B-LilukN.js} +1 -1
  26. package/dist/{coerce.utils-BN980Y9V.js.map → coerce.utils-B-LilukN.js.map} +1 -1
  27. package/dist/{coerce.utils-Dxv4_ikC.mjs → coerce.utils-Cu6IUTgX.mjs} +1 -1
  28. package/dist/{coerce.utils-Dxv4_ikC.mjs.map → coerce.utils-Cu6IUTgX.mjs.map} +1 -1
  29. package/dist/correlation.d.mts +31 -6
  30. package/dist/correlation.d.mts.map +1 -1
  31. package/dist/correlation.d.ts +31 -6
  32. package/dist/correlation.d.ts.map +1 -1
  33. package/dist/correlation.js +45 -20
  34. package/dist/correlation.js.map +1 -1
  35. package/dist/correlation.mjs +45 -20
  36. package/dist/correlation.mjs.map +1 -1
  37. package/dist/entry-fields-BJkuPnIg.mjs +30 -0
  38. package/dist/entry-fields-BJkuPnIg.mjs.map +1 -0
  39. package/dist/entry-fields-BvOLFlJ4.js +36 -0
  40. package/dist/entry-fields-BvOLFlJ4.js.map +1 -0
  41. package/dist/gcp.d.mts +2 -2
  42. package/dist/gcp.d.ts +2 -2
  43. package/dist/gcp.js +2 -1
  44. package/dist/gcp.mjs +2 -1
  45. package/dist/{gcp.transport-SHdtc1n0.js → gcp.transport-CHV8MGgz.js} +5 -5
  46. package/dist/gcp.transport-CHV8MGgz.js.map +1 -0
  47. package/dist/{gcp.transport-BJuNRFe_.mjs → gcp.transport-DBX5TPIy.mjs} +5 -5
  48. package/dist/gcp.transport-DBX5TPIy.mjs.map +1 -0
  49. package/dist/{gcp.transport-BOhkVKlI.d.ts → gcp.transport-m4fVw0b1.d.ts} +2 -2
  50. package/dist/gcp.transport-m4fVw0b1.d.ts.map +1 -0
  51. package/dist/{gcp.transport-Bi6CUqZR.d.mts → gcp.transport-zzNyIoih.d.mts} +2 -2
  52. package/dist/gcp.transport-zzNyIoih.d.mts.map +1 -0
  53. package/dist/{index-BPKwwERo.d.mts → index-BPwEisy4.d.mts} +2 -2
  54. package/dist/{index-BPKwwERo.d.mts.map → index-BPwEisy4.d.mts.map} +1 -1
  55. package/dist/{index-C1X-TE5I.d.ts → index-nZEeCr3X.d.ts} +2 -2
  56. package/dist/{index-C1X-TE5I.d.ts.map → index-nZEeCr3X.d.ts.map} +1 -1
  57. package/dist/index.d.mts +63 -98
  58. package/dist/index.d.mts.map +1 -1
  59. package/dist/index.d.ts +63 -98
  60. package/dist/index.d.ts.map +1 -1
  61. package/dist/index.js +167 -443
  62. package/dist/index.js.map +1 -1
  63. package/dist/index.mjs +98 -377
  64. package/dist/index.mjs.map +1 -1
  65. package/dist/middleware.d.mts +11 -4
  66. package/dist/middleware.d.mts.map +1 -1
  67. package/dist/middleware.d.ts +11 -4
  68. package/dist/middleware.d.ts.map +1 -1
  69. package/dist/middleware.js +148 -24
  70. package/dist/middleware.js.map +1 -1
  71. package/dist/middleware.mjs +148 -24
  72. package/dist/middleware.mjs.map +1 -1
  73. package/dist/nest.d.mts +4 -4
  74. package/dist/nest.d.ts +4 -4
  75. package/dist/nest.js +20 -11
  76. package/dist/nest.mjs +7 -5
  77. package/dist/{logitron-logger.module-BsDZR5BH.js → nestjs-extras-DnJ6vrIC.js} +562 -444
  78. package/dist/nestjs-extras-DnJ6vrIC.js.map +1 -0
  79. package/dist/{logitron-logger.module-Bs3XYZgF.mjs → nestjs-extras-ImbuDhs1.mjs} +505 -349
  80. package/dist/nestjs-extras-ImbuDhs1.mjs.map +1 -0
  81. package/dist/{logitron-logger.module-zDKVd_o0.d.mts → nestjs-extras-OkC796py.d.mts} +113 -6
  82. package/dist/nestjs-extras-OkC796py.d.mts.map +1 -0
  83. package/dist/{logitron-logger.module-CxT1F6Cm.d.ts → nestjs-extras-mKrHE9S6.d.ts} +113 -6
  84. package/dist/nestjs-extras-mKrHE9S6.d.ts.map +1 -0
  85. package/dist/{search-njTI2hdo.js → search-B0MH1SJJ.js} +23 -10
  86. package/dist/search-B0MH1SJJ.js.map +1 -0
  87. package/dist/{search-DujhR94O.d.ts → search-BfBzDIT9.d.mts} +2 -3
  88. package/dist/search-BfBzDIT9.d.mts.map +1 -0
  89. package/dist/{search-CQOo-Iq6.d.mts → search-CHUZPl0h.d.ts} +2 -3
  90. package/dist/search-CHUZPl0h.d.ts.map +1 -0
  91. package/dist/{search-CFQRISUv.mjs → search-DjXSn6v5.mjs} +23 -10
  92. package/dist/search-DjXSn6v5.mjs.map +1 -0
  93. package/dist/search.d.mts +3 -3
  94. package/dist/search.d.ts +3 -3
  95. package/dist/search.js +2 -2
  96. package/dist/search.mjs +2 -2
  97. package/dist/testing.d.mts +2 -2
  98. package/dist/testing.d.ts +2 -2
  99. package/dist/trace.utils-CvXgaiQT.mjs +278 -0
  100. package/dist/trace.utils-CvXgaiQT.mjs.map +1 -0
  101. package/dist/trace.utils-DpBOLDtq.js +358 -0
  102. package/dist/trace.utils-DpBOLDtq.js.map +1 -0
  103. package/dist/{transport.manager-D6KL3t9V.mjs → transport.manager-CpdVHEGP.mjs} +365 -86
  104. package/dist/transport.manager-CpdVHEGP.mjs.map +1 -0
  105. package/dist/{transport.manager-BivyUTtU.js → transport.manager-Crau3Rle.js} +370 -85
  106. package/dist/transport.manager-Crau3Rle.js.map +1 -0
  107. package/dist/{transport.types-B5vcyv2k.d.mts → transport.types-CtNr3D7S.d.mts} +19 -2
  108. package/dist/transport.types-CtNr3D7S.d.mts.map +1 -0
  109. package/dist/{transport.types-BuY84TwO.d.ts → transport.types-DqVLZTyf.d.ts} +19 -2
  110. package/dist/transport.types-DqVLZTyf.d.ts.map +1 -0
  111. package/dist/transports.d.mts +38 -13
  112. package/dist/transports.d.mts.map +1 -1
  113. package/dist/transports.d.ts +38 -13
  114. package/dist/transports.d.ts.map +1 -1
  115. package/dist/transports.js +6 -5
  116. package/dist/transports.mjs +6 -5
  117. package/dist/worker-entry.js +307 -55
  118. package/dist/worker.d.mts +2 -2
  119. package/dist/worker.d.ts +2 -2
  120. package/dist/worker.js +1 -1
  121. package/dist/worker.mjs +1 -1
  122. package/dist/{worker.transport-BP8Q28Z2.d.mts → worker.transport-Bgv9heDI.d.mts} +2 -2
  123. package/dist/{worker.transport-BP8Q28Z2.d.mts.map → worker.transport-Bgv9heDI.d.mts.map} +1 -1
  124. package/dist/{worker.transport-mKXgNK0L.d.ts → worker.transport-Bj-i0P9V.d.ts} +2 -2
  125. package/dist/{worker.transport-mKXgNK0L.d.ts.map → worker.transport-Bj-i0P9V.d.ts.map} +1 -1
  126. package/dist/{worker.transport-CfH6z37Z.js → worker.transport-JP8YHBuU.js} +1 -1
  127. package/dist/{worker.transport-CfH6z37Z.js.map → worker.transport-JP8YHBuU.js.map} +1 -1
  128. package/dist/{worker.transport-BhMyvqQ2.mjs → worker.transport-otwhJ_Et.mjs} +1 -1
  129. package/dist/{worker.transport-BhMyvqQ2.mjs.map → worker.transport-otwhJ_Et.mjs.map} +1 -1
  130. package/package.json +10 -6
  131. package/dist/.tsbuildinfo +0 -1
  132. package/dist/async-context-CSHWoHMB.js.map +0 -1
  133. package/dist/async-context-CwBNMm2E.mjs.map +0 -1
  134. package/dist/cloudwatch.transport-CGHY4buE.d.mts.map +0 -1
  135. package/dist/cloudwatch.transport-DK_8cKd3.d.ts.map +0 -1
  136. package/dist/cloudwatch.transport-pNBNWJLt.js.map +0 -1
  137. package/dist/cloudwatch.transport-yBwm4OeO.mjs.map +0 -1
  138. package/dist/gcp.transport-BJuNRFe_.mjs.map +0 -1
  139. package/dist/gcp.transport-BOhkVKlI.d.ts.map +0 -1
  140. package/dist/gcp.transport-Bi6CUqZR.d.mts.map +0 -1
  141. package/dist/gcp.transport-SHdtc1n0.js.map +0 -1
  142. package/dist/logitron-logger.module-Bs3XYZgF.mjs.map +0 -1
  143. package/dist/logitron-logger.module-BsDZR5BH.js.map +0 -1
  144. package/dist/logitron-logger.module-CxT1F6Cm.d.ts.map +0 -1
  145. package/dist/logitron-logger.module-zDKVd_o0.d.mts.map +0 -1
  146. package/dist/search-CFQRISUv.mjs.map +0 -1
  147. package/dist/search-CQOo-Iq6.d.mts.map +0 -1
  148. package/dist/search-DujhR94O.d.ts.map +0 -1
  149. package/dist/search-njTI2hdo.js.map +0 -1
  150. package/dist/transport.manager-BivyUTtU.js.map +0 -1
  151. package/dist/transport.manager-D6KL3t9V.mjs.map +0 -1
  152. package/dist/transport.types-B5vcyv2k.d.mts.map +0 -1
  153. package/dist/transport.types-BuY84TwO.d.ts.map +0 -1
  154. package/scripts/postinstall.js +0 -61
package/README.md CHANGED
@@ -1291,6 +1291,15 @@ The filter returns a consistent structured shape on every error:
1291
1291
  }
1292
1292
  ```
1293
1293
 
1294
+ The `debug` block (stack trace, service, duration) is **off by default** and is never sent to clients unless you opt in, for example in local development only:
1295
+
1296
+ ```typescript
1297
+ new LogixiaExceptionFilter(undefined, undefined, {
1298
+ exposeDebug: process.env.NODE_ENV === 'development',
1299
+ // exposeCause: true, // also include the underlying error message (may contain internal detail)
1300
+ });
1301
+ ```
1302
+
1294
1303
  And every log line carries `method`, `url`, `status`, `request_id` as structured fields — not a plain string:
1295
1304
 
1296
1305
  ```
@@ -1316,22 +1325,28 @@ logixia ships this as a dedicated `logixia/correlation` sub-package. It uses the
1316
1325
  ```typescript
1317
1326
  import { correlationMiddleware } from 'logixia/correlation';
1318
1327
 
1319
- // Zero-config — reads X-Correlation-ID / X-Request-ID from the incoming request.
1320
- // Generates a UUID v4 if no header is present.
1328
+ // Zero-config — reads X-Correlation-ID from the incoming request.
1329
+ // Generates a UUID v4 if the header is missing or invalid.
1321
1330
  // Sets X-Correlation-ID on the response.
1322
1331
  app.use(correlationMiddleware());
1323
1332
 
1324
1333
  // Custom config:
1325
1334
  app.use(
1326
1335
  correlationMiddleware({
1327
- header: 'X-Correlation-ID', // header to read / write. Default: 'X-Correlation-ID'
1328
- generateId: () => crypto.randomUUID(),
1336
+ header: 'X-Correlation-ID', // header to read / write (matched case-insensitively). Default: 'x-correlation-id'
1337
+ generate: () => crypto.randomUUID(), // used when no usable incoming ID. Default: crypto.randomUUID()
1329
1338
  trustIncoming: true, // honour the header from the client. Default: true
1330
1339
  setResponseHeader: true, // echo the ID back in the response. Default: true
1331
1340
  })
1332
1341
  );
1333
1342
  ```
1334
1343
 
1344
+ An incoming ID is only used if it is at most 128 characters long and contains only
1345
+ `A-Z`, `a-z`, `0-9`, `.`, `_`, `:` and `-`. Anything else (newlines, spaces, or base64-style
1346
+ `+`, `/`, `=`) is replaced with a freshly generated ID, so client-supplied text can't end up in
1347
+ every log line. Set `trustIncoming: false` to ignore the incoming header entirely and always
1348
+ generate a new ID. `correlationFastifyHook()` takes the same options.
1349
+
1335
1350
  ### Correlation Fastify hook
1336
1351
 
1337
1352
  ```typescript
@@ -1,3 +1,4 @@
1
+ import { d as sanitizeIncomingId } from "./trace.utils-CvXgaiQT.mjs";
1
2
  import { randomUUID } from "node:crypto";
2
3
  import { AsyncLocalStorage } from "node:async_hooks";
3
4
 
@@ -21,7 +22,10 @@ const LogixiaContext = {
21
22
  set(fields) {
22
23
  const store = _storage.getStore();
23
24
  if (!store) return;
24
- Object.assign(store, fields);
25
+ for (const key of Object.keys(fields)) {
26
+ if (key === "__proto__" || key === "constructor" || key === "prototype") continue;
27
+ store[key] = fields[key];
28
+ }
25
29
  },
26
30
  getStorage() {
27
31
  return _storage;
@@ -40,7 +44,7 @@ const LogixiaContext = {
40
44
  function createExpressContextMiddleware(options = {}) {
41
45
  const { enrich, traceIdHeader = "x-trace-id" } = options;
42
46
  return function logixiaContextMiddleware(req, _res, next) {
43
- const base = { traceId: (req["headers"] ?? {})[traceIdHeader] ?? randomShortId() };
47
+ const base = { traceId: sanitizeIncomingId((req["headers"] ?? {})[traceIdHeader.toLowerCase()]) ?? randomShortId() };
44
48
  LogixiaContext.run({
45
49
  ...base,
46
50
  ...enrich ? enrich(req) : {}
@@ -55,7 +59,7 @@ function createExpressContextMiddleware(options = {}) {
55
59
  function createFastifyContextHook(options = {}) {
56
60
  const { enrich, traceIdHeader = "x-trace-id" } = options;
57
61
  return function logixiaFastifyHook(request, _reply, done) {
58
- const base = { traceId: (request["headers"] ?? {})[traceIdHeader] ?? request["id"] ?? randomShortId() };
62
+ const base = { traceId: sanitizeIncomingId((request["headers"] ?? {})[traceIdHeader.toLowerCase()]) ?? sanitizeIncomingId(request["id"]) ?? randomShortId() };
59
63
  LogixiaContext.run({
60
64
  ...base,
61
65
  ...enrich ? enrich(request) : {}
@@ -65,4 +69,4 @@ function createFastifyContextHook(options = {}) {
65
69
 
66
70
  //#endregion
67
71
  export { createExpressContextMiddleware as n, createFastifyContextHook as r, LogixiaContext as t };
68
- //# sourceMappingURL=async-context-CwBNMm2E.mjs.map
72
+ //# sourceMappingURL=async-context-BzJrbfQs.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"async-context-BzJrbfQs.mjs","names":["base: LogContext"],"sources":["../src/context/async-context.ts"],"sourcesContent":["/**\n * LogixiaContext — Zero-boilerplate AsyncLocalStorage context propagation.\n *\n * Lets every `logger.info()` call inside a context automatically pick up stored\n * fields (requestId, userId, tenantId, …) without passing them through every function.\n *\n * Works across `Promise.all`, `setTimeout`, and event emitters — anything that\n * inherits the async execution context.\n *\n * @example\n * ```ts\n * import { LogixiaContext } from 'logixia';\n *\n * // Wrap a request handler:\n * app.use((req, res, next) => {\n * LogixiaContext.run({ requestId: req.id, userId: req.user?.id }, next);\n * });\n *\n * // Anywhere deeper in the call tree:\n * async function processOrder(orderId: string) {\n * const ctx = LogixiaContext.get(); // { requestId, userId, orderId }\n * logger.info('Processing order', { orderId, ...ctx });\n * }\n * ```\n */\n\nimport { AsyncLocalStorage } from 'node:async_hooks';\nimport { randomUUID } from 'node:crypto';\n\nimport { sanitizeIncomingId } from '../utils/trace.utils';\n\n/** Generates an 8-character random ID using Node's crypto module. */\nfunction randomShortId(): string {\n return randomUUID().slice(0, 8);\n}\n\nexport interface LogContext {\n traceId?: string;\n spanId?: string;\n userId?: string;\n tenantId?: string;\n sessionId?: string;\n [key: string]: unknown;\n}\n\n// Module-level singleton so all loggers share the same ALS store.\nconst _storage = new AsyncLocalStorage<LogContext>();\n\nexport const LogixiaContext = {\n /**\n * Run `callback` inside an async context that carries `store` fields.\n * All loggers queried within `callback` (and its async descendants) will\n * automatically see these fields.\n *\n * @example\n * ```ts\n * LogixiaContext.run({ requestId: 'abc', userId: '42' }, async () => {\n * await processOrder(); // sees requestId + userId in every log\n * });\n * ```\n */\n run<T>(store: LogContext, callback: () => T): T {\n const parent = _storage.getStore() ?? {};\n return _storage.run({ ...parent, ...store }, callback);\n },\n\n /**\n * Return the context fields active in the current async scope.\n * Returns `undefined` when called outside any `LogixiaContext.run()`.\n */\n get(): LogContext | undefined {\n return _storage.getStore();\n },\n\n /**\n * Merge `fields` into the **existing** context for the current scope.\n * If called outside a `run()` context this is a no-op (logs a dev warning).\n */\n set(fields: Partial<LogContext>): void {\n const store = _storage.getStore();\n if (!store) {\n // Silently ignore — avoids crashing in code that may call setContext\n // before a request context is established (e.g. background jobs).\n return;\n }\n for (const key of Object.keys(fields)) {\n if (key === '__proto__' || key === 'constructor' || key === 'prototype') continue;\n store[key] = fields[key];\n }\n },\n\n /**\n * Retrieve the underlying `AsyncLocalStorage` instance.\n * Useful for advanced use-cases like custom middleware.\n */\n getStorage(): AsyncLocalStorage<LogContext> {\n return _storage;\n },\n};\n\n/**\n * Create an Express/Connect-compatible middleware that wraps each request in\n * a `LogixiaContext.run()` scope populated with common request fields.\n *\n * @example\n * ```ts\n * import { createExpressContextMiddleware } from 'logixia';\n * app.use(createExpressContextMiddleware());\n * ```\n */\nexport function createExpressContextMiddleware(\n options: {\n /** Extract additional fields from the request. */\n enrich?: (req: Record<string, unknown>) => Partial<LogContext>;\n /** Header to read the traceId from. Default: 'x-trace-id'. */\n traceIdHeader?: string;\n } = {}\n) {\n const { enrich, traceIdHeader = 'x-trace-id' } = options;\n\n return function logixiaContextMiddleware(\n req: Record<string, unknown>,\n _res: unknown,\n next: () => void\n ): void {\n const headers = (req['headers'] ?? {}) as Record<string, string | undefined>;\n const traceId = sanitizeIncomingId(headers[traceIdHeader.toLowerCase()]) ?? randomShortId();\n const base: LogContext = { traceId };\n LogixiaContext.run({ ...base, ...(enrich ? enrich(req) : {}) }, next);\n };\n}\n\n/**\n * Create a Fastify lifecycle hook that wraps each request in a context scope.\n *\n * Register with `fastify.addHook('onRequest', createFastifyContextHook())`.\n */\nexport function createFastifyContextHook(\n options: {\n enrich?: (request: Record<string, unknown>) => Partial<LogContext>;\n traceIdHeader?: string;\n } = {}\n) {\n const { enrich, traceIdHeader = 'x-trace-id' } = options;\n\n return function logixiaFastifyHook(\n request: Record<string, unknown>,\n _reply: unknown,\n done: () => void\n ): void {\n const headers = (request['headers'] ?? {}) as Record<string, string | undefined>;\n const traceId =\n sanitizeIncomingId(headers[traceIdHeader.toLowerCase()]) ??\n sanitizeIncomingId(request['id']) ??\n randomShortId();\n const base: LogContext = { traceId };\n LogixiaContext.run({ ...base, ...(enrich ? enrich(request) : {}) }, done);\n };\n}\n"],"mappings":";;;;;;AAgCA,SAAS,gBAAwB;AAC/B,QAAO,YAAY,CAAC,MAAM,GAAG,EAAE;;AAajC,MAAM,WAAW,IAAI,mBAA+B;AAEpD,MAAa,iBAAiB;CAa5B,IAAO,OAAmB,UAAsB;EAC9C,MAAM,SAAS,SAAS,UAAU,IAAI,EAAE;AACxC,SAAO,SAAS,IAAI;GAAE,GAAG;GAAQ,GAAG;GAAO,EAAE,SAAS;;CAOxD,MAA8B;AAC5B,SAAO,SAAS,UAAU;;CAO5B,IAAI,QAAmC;EACrC,MAAM,QAAQ,SAAS,UAAU;AACjC,MAAI,CAAC,MAGH;AAEF,OAAK,MAAM,OAAO,OAAO,KAAK,OAAO,EAAE;AACrC,OAAI,QAAQ,eAAe,QAAQ,iBAAiB,QAAQ,YAAa;AACzE,SAAM,OAAO,OAAO;;;CAQxB,aAA4C;AAC1C,SAAO;;CAEV;;;;;;;;;;;AAYD,SAAgB,+BACd,UAKI,EAAE,EACN;CACA,MAAM,EAAE,QAAQ,gBAAgB,iBAAiB;AAEjD,QAAO,SAAS,yBACd,KACA,MACA,MACM;EAGN,MAAMA,OAAmB,EAAE,SADX,oBADC,IAAI,cAAc,EAAE,EACM,cAAc,aAAa,EAAE,IAAI,eAAe,EACvD;AACpC,iBAAe,IAAI;GAAE,GAAG;GAAM,GAAI,SAAS,OAAO,IAAI,GAAG,EAAE;GAAG,EAAE,KAAK;;;;;;;;AASzE,SAAgB,yBACd,UAGI,EAAE,EACN;CACA,MAAM,EAAE,QAAQ,gBAAgB,iBAAiB;AAEjD,QAAO,SAAS,mBACd,SACA,QACA,MACM;EAMN,MAAMA,OAAmB,EAAE,SAHzB,oBAFe,QAAQ,cAAc,EAAE,EAEZ,cAAc,aAAa,EAAE,IACxD,mBAAmB,QAAQ,MAAM,IACjC,eAAe,EACmB;AACpC,iBAAe,IAAI;GAAE,GAAG;GAAM,GAAI,SAAS,OAAO,QAAQ,GAAG,EAAE;GAAG,EAAE,KAAK"}
@@ -1,4 +1,5 @@
1
1
  const require_chunk = require('./chunk-nOFOJqeH.js');
2
+ const require_trace_utils = require('./trace.utils-DpBOLDtq.js');
2
3
  let node_crypto = require("node:crypto");
3
4
  node_crypto = require_chunk.__toESM(node_crypto);
4
5
  let node_async_hooks = require("node:async_hooks");
@@ -24,7 +25,10 @@ const LogixiaContext = {
24
25
  set(fields) {
25
26
  const store = _storage.getStore();
26
27
  if (!store) return;
27
- Object.assign(store, fields);
28
+ for (const key of Object.keys(fields)) {
29
+ if (key === "__proto__" || key === "constructor" || key === "prototype") continue;
30
+ store[key] = fields[key];
31
+ }
28
32
  },
29
33
  getStorage() {
30
34
  return _storage;
@@ -43,7 +47,7 @@ const LogixiaContext = {
43
47
  function createExpressContextMiddleware(options = {}) {
44
48
  const { enrich, traceIdHeader = "x-trace-id" } = options;
45
49
  return function logixiaContextMiddleware(req, _res, next) {
46
- const base = { traceId: (req["headers"] ?? {})[traceIdHeader] ?? randomShortId() };
50
+ const base = { traceId: require_trace_utils.sanitizeIncomingId((req["headers"] ?? {})[traceIdHeader.toLowerCase()]) ?? randomShortId() };
47
51
  LogixiaContext.run({
48
52
  ...base,
49
53
  ...enrich ? enrich(req) : {}
@@ -58,7 +62,7 @@ function createExpressContextMiddleware(options = {}) {
58
62
  function createFastifyContextHook(options = {}) {
59
63
  const { enrich, traceIdHeader = "x-trace-id" } = options;
60
64
  return function logixiaFastifyHook(request, _reply, done) {
61
- const base = { traceId: (request["headers"] ?? {})[traceIdHeader] ?? request["id"] ?? randomShortId() };
65
+ const base = { traceId: require_trace_utils.sanitizeIncomingId((request["headers"] ?? {})[traceIdHeader.toLowerCase()]) ?? require_trace_utils.sanitizeIncomingId(request["id"]) ?? randomShortId() };
62
66
  LogixiaContext.run({
63
67
  ...base,
64
68
  ...enrich ? enrich(request) : {}
@@ -85,4 +89,4 @@ Object.defineProperty(exports, 'createFastifyContextHook', {
85
89
  return createFastifyContextHook;
86
90
  }
87
91
  });
88
- //# sourceMappingURL=async-context-CSHWoHMB.js.map
92
+ //# sourceMappingURL=async-context-DyQQYq3I.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"async-context-DyQQYq3I.js","names":["AsyncLocalStorage","base: LogContext","sanitizeIncomingId"],"sources":["../src/context/async-context.ts"],"sourcesContent":["/**\n * LogixiaContext — Zero-boilerplate AsyncLocalStorage context propagation.\n *\n * Lets every `logger.info()` call inside a context automatically pick up stored\n * fields (requestId, userId, tenantId, …) without passing them through every function.\n *\n * Works across `Promise.all`, `setTimeout`, and event emitters — anything that\n * inherits the async execution context.\n *\n * @example\n * ```ts\n * import { LogixiaContext } from 'logixia';\n *\n * // Wrap a request handler:\n * app.use((req, res, next) => {\n * LogixiaContext.run({ requestId: req.id, userId: req.user?.id }, next);\n * });\n *\n * // Anywhere deeper in the call tree:\n * async function processOrder(orderId: string) {\n * const ctx = LogixiaContext.get(); // { requestId, userId, orderId }\n * logger.info('Processing order', { orderId, ...ctx });\n * }\n * ```\n */\n\nimport { AsyncLocalStorage } from 'node:async_hooks';\nimport { randomUUID } from 'node:crypto';\n\nimport { sanitizeIncomingId } from '../utils/trace.utils';\n\n/** Generates an 8-character random ID using Node's crypto module. */\nfunction randomShortId(): string {\n return randomUUID().slice(0, 8);\n}\n\nexport interface LogContext {\n traceId?: string;\n spanId?: string;\n userId?: string;\n tenantId?: string;\n sessionId?: string;\n [key: string]: unknown;\n}\n\n// Module-level singleton so all loggers share the same ALS store.\nconst _storage = new AsyncLocalStorage<LogContext>();\n\nexport const LogixiaContext = {\n /**\n * Run `callback` inside an async context that carries `store` fields.\n * All loggers queried within `callback` (and its async descendants) will\n * automatically see these fields.\n *\n * @example\n * ```ts\n * LogixiaContext.run({ requestId: 'abc', userId: '42' }, async () => {\n * await processOrder(); // sees requestId + userId in every log\n * });\n * ```\n */\n run<T>(store: LogContext, callback: () => T): T {\n const parent = _storage.getStore() ?? {};\n return _storage.run({ ...parent, ...store }, callback);\n },\n\n /**\n * Return the context fields active in the current async scope.\n * Returns `undefined` when called outside any `LogixiaContext.run()`.\n */\n get(): LogContext | undefined {\n return _storage.getStore();\n },\n\n /**\n * Merge `fields` into the **existing** context for the current scope.\n * If called outside a `run()` context this is a no-op (logs a dev warning).\n */\n set(fields: Partial<LogContext>): void {\n const store = _storage.getStore();\n if (!store) {\n // Silently ignore — avoids crashing in code that may call setContext\n // before a request context is established (e.g. background jobs).\n return;\n }\n for (const key of Object.keys(fields)) {\n if (key === '__proto__' || key === 'constructor' || key === 'prototype') continue;\n store[key] = fields[key];\n }\n },\n\n /**\n * Retrieve the underlying `AsyncLocalStorage` instance.\n * Useful for advanced use-cases like custom middleware.\n */\n getStorage(): AsyncLocalStorage<LogContext> {\n return _storage;\n },\n};\n\n/**\n * Create an Express/Connect-compatible middleware that wraps each request in\n * a `LogixiaContext.run()` scope populated with common request fields.\n *\n * @example\n * ```ts\n * import { createExpressContextMiddleware } from 'logixia';\n * app.use(createExpressContextMiddleware());\n * ```\n */\nexport function createExpressContextMiddleware(\n options: {\n /** Extract additional fields from the request. */\n enrich?: (req: Record<string, unknown>) => Partial<LogContext>;\n /** Header to read the traceId from. Default: 'x-trace-id'. */\n traceIdHeader?: string;\n } = {}\n) {\n const { enrich, traceIdHeader = 'x-trace-id' } = options;\n\n return function logixiaContextMiddleware(\n req: Record<string, unknown>,\n _res: unknown,\n next: () => void\n ): void {\n const headers = (req['headers'] ?? {}) as Record<string, string | undefined>;\n const traceId = sanitizeIncomingId(headers[traceIdHeader.toLowerCase()]) ?? randomShortId();\n const base: LogContext = { traceId };\n LogixiaContext.run({ ...base, ...(enrich ? enrich(req) : {}) }, next);\n };\n}\n\n/**\n * Create a Fastify lifecycle hook that wraps each request in a context scope.\n *\n * Register with `fastify.addHook('onRequest', createFastifyContextHook())`.\n */\nexport function createFastifyContextHook(\n options: {\n enrich?: (request: Record<string, unknown>) => Partial<LogContext>;\n traceIdHeader?: string;\n } = {}\n) {\n const { enrich, traceIdHeader = 'x-trace-id' } = options;\n\n return function logixiaFastifyHook(\n request: Record<string, unknown>,\n _reply: unknown,\n done: () => void\n ): void {\n const headers = (request['headers'] ?? {}) as Record<string, string | undefined>;\n const traceId =\n sanitizeIncomingId(headers[traceIdHeader.toLowerCase()]) ??\n sanitizeIncomingId(request['id']) ??\n randomShortId();\n const base: LogContext = { traceId };\n LogixiaContext.run({ ...base, ...(enrich ? enrich(request) : {}) }, done);\n };\n}\n"],"mappings":";;;;;;;;;AAgCA,SAAS,gBAAwB;AAC/B,qCAAmB,CAAC,MAAM,GAAG,EAAE;;AAajC,MAAM,WAAW,IAAIA,oCAA+B;AAEpD,MAAa,iBAAiB;CAa5B,IAAO,OAAmB,UAAsB;EAC9C,MAAM,SAAS,SAAS,UAAU,IAAI,EAAE;AACxC,SAAO,SAAS,IAAI;GAAE,GAAG;GAAQ,GAAG;GAAO,EAAE,SAAS;;CAOxD,MAA8B;AAC5B,SAAO,SAAS,UAAU;;CAO5B,IAAI,QAAmC;EACrC,MAAM,QAAQ,SAAS,UAAU;AACjC,MAAI,CAAC,MAGH;AAEF,OAAK,MAAM,OAAO,OAAO,KAAK,OAAO,EAAE;AACrC,OAAI,QAAQ,eAAe,QAAQ,iBAAiB,QAAQ,YAAa;AACzE,SAAM,OAAO,OAAO;;;CAQxB,aAA4C;AAC1C,SAAO;;CAEV;;;;;;;;;;;AAYD,SAAgB,+BACd,UAKI,EAAE,EACN;CACA,MAAM,EAAE,QAAQ,gBAAgB,iBAAiB;AAEjD,QAAO,SAAS,yBACd,KACA,MACA,MACM;EAGN,MAAMC,OAAmB,EAAE,SADXC,wCADC,IAAI,cAAc,EAAE,EACM,cAAc,aAAa,EAAE,IAAI,eAAe,EACvD;AACpC,iBAAe,IAAI;GAAE,GAAG;GAAM,GAAI,SAAS,OAAO,IAAI,GAAG,EAAE;GAAG,EAAE,KAAK;;;;;;;;AASzE,SAAgB,yBACd,UAGI,EAAE,EACN;CACA,MAAM,EAAE,QAAQ,gBAAgB,iBAAiB;AAEjD,QAAO,SAAS,mBACd,SACA,QACA,MACM;EAMN,MAAMD,OAAmB,EAAE,SAHzBC,wCAFe,QAAQ,cAAc,EAAE,EAEZ,cAAc,aAAa,EAAE,IACxDA,uCAAmB,QAAQ,MAAM,IACjC,eAAe,EACmB;AACpC,iBAAe,IAAI;GAAE,GAAG;GAAM,GAAI,SAAS,OAAO,QAAQ,GAAG,EAAE;GAAG,EAAE,KAAK"}
@@ -1,4 +1,4 @@
1
- import { p as TransportLogEntry, s as IAsyncTransport } from "./transport.types-BuY84TwO.js";
1
+ import { p as TransportLogEntry, s as IAsyncTransport } from "./transport.types-DqVLZTyf.js";
2
2
 
3
3
  //#region src/transports/azure-monitor.transport.d.ts
4
4
 
@@ -59,4 +59,4 @@ declare class AzureMonitorTransport implements IAsyncTransport {
59
59
  }
60
60
  //#endregion
61
61
  export { AzureMonitorTransportConfig as n, AzureMonitorTransport as t };
62
- //# sourceMappingURL=azure-monitor.transport-CG7ClkYj.d.ts.map
62
+ //# sourceMappingURL=azure-monitor.transport-BDFO4F-i.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"azure-monitor.transport-CG7ClkYj.d.ts","names":[],"sources":["../src/transports/azure-monitor.transport.ts"],"sourcesContent":[],"mappings":";;;;UAkDiB,2BAAA;;;;;;;;;;;;;;;;;;;;;;;;;;cAwCJ,qBAAA,YAAiC;;;;;;;;;;;;;;;;sBAmBxB;eAwBP;WAKJ;;;;;;WASM"}
1
+ {"version":3,"file":"azure-monitor.transport-BDFO4F-i.d.ts","names":[],"sources":["../src/transports/azure-monitor.transport.ts"],"sourcesContent":[],"mappings":";;;;UAkDiB,2BAAA;;;;;;;;;;;;;;;;;;;;;;;;;;cAwCJ,qBAAA,YAAiC;;;;;;;;;;;;;;;;sBAmBxB;eAwBP;WAKJ;;;;;;WASM"}
@@ -1,4 +1,4 @@
1
- import { p as TransportLogEntry, s as IAsyncTransport } from "./transport.types-B5vcyv2k.mjs";
1
+ import { p as TransportLogEntry, s as IAsyncTransport } from "./transport.types-CtNr3D7S.mjs";
2
2
 
3
3
  //#region src/transports/azure-monitor.transport.d.ts
4
4
 
@@ -59,4 +59,4 @@ declare class AzureMonitorTransport implements IAsyncTransport {
59
59
  }
60
60
  //#endregion
61
61
  export { AzureMonitorTransportConfig as n, AzureMonitorTransport as t };
62
- //# sourceMappingURL=azure-monitor.transport-Cm74kXZS.d.mts.map
62
+ //# sourceMappingURL=azure-monitor.transport-D5wQEH04.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"azure-monitor.transport-Cm74kXZS.d.mts","names":[],"sources":["../src/transports/azure-monitor.transport.ts"],"sourcesContent":[],"mappings":";;;;UAkDiB,2BAAA;;;;;;;;;;;;;;;;;;;;;;;;;;cAwCJ,qBAAA,YAAiC;;;;;;;;;;;;;;;;sBAmBxB;eAwBP;WAKJ;;;;;;WASM"}
1
+ {"version":3,"file":"azure-monitor.transport-D5wQEH04.d.mts","names":[],"sources":["../src/transports/azure-monitor.transport.ts"],"sourcesContent":[],"mappings":";;;;UAkDiB,2BAAA;;;;;;;;;;;;;;;;;;;;;;;;;;cAwCJ,qBAAA,YAAiC;;;;;;;;;;;;;;;;sBAmBxB;eAwBP;WAKJ;;;;;;WASM"}
package/dist/azure.d.mts CHANGED
@@ -1,3 +1,3 @@
1
- import "./transport.types-B5vcyv2k.mjs";
2
- import { n as AzureMonitorTransportConfig, t as AzureMonitorTransport } from "./azure-monitor.transport-Cm74kXZS.mjs";
1
+ import "./transport.types-CtNr3D7S.mjs";
2
+ import { n as AzureMonitorTransportConfig, t as AzureMonitorTransport } from "./azure-monitor.transport-D5wQEH04.mjs";
3
3
  export { AzureMonitorTransport, AzureMonitorTransportConfig };
package/dist/azure.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- import "./transport.types-BuY84TwO.js";
2
- import { n as AzureMonitorTransportConfig, t as AzureMonitorTransport } from "./azure-monitor.transport-CG7ClkYj.js";
1
+ import "./transport.types-DqVLZTyf.js";
2
+ import { n as AzureMonitorTransportConfig, t as AzureMonitorTransport } from "./azure-monitor.transport-BDFO4F-i.js";
3
3
  export { AzureMonitorTransport, AzureMonitorTransportConfig };
package/dist/cli/index.js CHANGED
@@ -31,6 +31,25 @@ commander = __toESM(commander);
31
31
  let picocolors = require("picocolors");
32
32
  picocolors = __toESM(picocolors);
33
33
 
34
+ //#region src/utils/csv.utils.ts
35
+ const CSV_FORMULA_PREFIX = /^[=+\-@\t\r]/;
36
+ /**
37
+ * Render one CSV field. String values that a spreadsheet would treat as a
38
+ * formula (leading `=`, `+`, `-`, `@`, tab or CR) are prefixed with `'`.
39
+ * Objects are JSON-encoded. Fields containing a comma, quote, CR or LF are quoted;
40
+ * `alwaysQuote` quotes every field. `null` / `undefined` render as empty.
41
+ */
42
+ function csvField(value, alwaysQuote = false) {
43
+ if (value === void 0 || value === null) return alwaysQuote ? "\"\"" : "";
44
+ let str;
45
+ if (typeof value === "string") str = CSV_FORMULA_PREFIX.test(value) ? `'${value}` : value;
46
+ else if (typeof value === "object") str = JSON.stringify(value);
47
+ else str = String(value);
48
+ if (alwaysQuote || /[",\r\n]/.test(str)) return `"${str.replace(/"/g, "\"\"")}"`;
49
+ return str;
50
+ }
51
+
52
+ //#endregion
34
53
  //#region src/cli/utils.ts
35
54
  /**
36
55
  * Render a single cell value. Uses a null/undefined check rather than `||` so
@@ -38,12 +57,33 @@ picocolors = __toESM(picocolors);
38
57
  * `statusCode=0` field must not silently display as blank.
39
58
  */
40
59
  function cellText(value) {
41
- return value === null || value === void 0 ? "" : String(value);
60
+ return value === null || value === void 0 ? "" : sanitizeInline(String(value));
61
+ }
62
+ const TERMINAL_UNSAFE = /[\u0000-\u0008\u000b-\u001f\u007f-\u009f\u2028\u2029]/g;
63
+ function escapeChar(ch) {
64
+ const code = ch.charCodeAt(0);
65
+ return code <= 255 ? `\\x${code.toString(16).padStart(2, "0")}` : `\\u${code.toString(16).padStart(4, "0")}`;
66
+ }
67
+ /**
68
+ * Make untrusted log text safe to print: control characters (ESC, CR, BEL, C1
69
+ * codes, ...) are replaced by a visible `\xNN` escape. Tabs and newlines are kept
70
+ * so multi-line output the CLI formats itself is unaffected.
71
+ */
72
+ function sanitizeForTerminal(text) {
73
+ return text.replace(TERMINAL_UNSAFE, escapeChar);
74
+ }
75
+ /** Like {@link sanitizeForTerminal} but also escapes newlines and tabs (single-line cells). */
76
+ function sanitizeInline(text) {
77
+ return sanitizeForTerminal(text).replace(/\n/g, "\\n").replace(/\t/g, "\\t");
78
+ }
79
+ /** Escape regular-expression metacharacters so `text` matches literally. */
80
+ function escapeRegExp(text) {
81
+ return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
42
82
  }
43
83
  function formatAsTable(rows, columns) {
44
- const colWidths = columns.map((c) => Math.max(c.length, ...rows.map((r) => cellText(r[c]).length)));
84
+ const colWidths = columns.map((c) => Math.max(sanitizeInline(c).length, ...rows.map((r) => cellText(r[c]).length)));
45
85
  return [
46
- columns.map((c, i) => c.padEnd(colWidths[i] ?? 0)).join(" | "),
86
+ columns.map((c, i) => sanitizeInline(c).padEnd(colWidths[i] ?? 0)).join(" | "),
47
87
  colWidths.map((w) => "-".repeat(w)).join("-|-"),
48
88
  rows.map((r) => columns.map((c, i) => cellText(r[c]).padEnd(colWidths[i] ?? 0)).join(" | ")).join("\n")
49
89
  ].join("\n");
@@ -90,7 +130,7 @@ async function analyzeFileContents(raw, opts = {}) {
90
130
  });
91
131
  }
92
132
  if (opts.level) filtered = filtered.filter((e) => (e["level"] ?? "").toLowerCase() === opts.level.toLowerCase());
93
- const byLevel = {};
133
+ const byLevel = Object.create(null);
94
134
  for (const e of filtered) {
95
135
  const l = (e.level || "info").toUpperCase();
96
136
  byLevel[l] = (byLevel[l] || 0) + 1;
@@ -112,7 +152,7 @@ const analyzeCommand = new commander.Command("analyze").description("Analyze log
112
152
  }
113
153
  const result = await analyzeFileContents(node_fs.default.readFileSync(full, "utf8"), opts);
114
154
  if (opts.format === "json") {
115
- console.log(JSON.stringify(result, null, 2));
155
+ console.log(sanitizeForTerminal(JSON.stringify(result, null, 2)));
116
156
  return;
117
157
  }
118
158
  console.log(result);
@@ -178,10 +218,10 @@ function formatTime(ts) {
178
218
  if (!ts) return " ";
179
219
  try {
180
220
  const d = new Date(String(ts));
181
- if (Number.isNaN(d.getTime())) return String(ts).slice(0, 12).padEnd(12);
221
+ if (Number.isNaN(d.getTime())) return sanitizeInline(String(ts).slice(0, 12)).padEnd(12);
182
222
  return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}:${String(d.getSeconds()).padStart(2, "0")}.${String(d.getMilliseconds()).padStart(3, "0")}`;
183
223
  } catch {
184
- return String(ts).slice(0, 12).padEnd(12);
224
+ return sanitizeInline(String(ts).slice(0, 12)).padEnd(12);
185
225
  }
186
226
  }
187
227
  function syntaxColorJson(line) {
@@ -204,12 +244,12 @@ function buildDetailLines(entry) {
204
244
  if (!entry) return [picocolors.default.dim(" No entry selected")];
205
245
  const lines = [];
206
246
  const { stack,...rest } = entry;
207
- const jsonText = JSON.stringify(rest, null, 2);
247
+ const jsonText = sanitizeForTerminal(JSON.stringify(rest, null, 2));
208
248
  for (const l of jsonText.split("\n")) lines.push(" " + syntaxColorJson(l));
209
249
  if (stack) {
210
250
  lines.push("");
211
251
  lines.push(picocolors.default.bold(picocolors.default.red(" ▼ STACK TRACE")));
212
- for (const sl of String(stack).split("\n")) {
252
+ for (const sl of sanitizeForTerminal(String(stack)).split("\n")) {
213
253
  if (!sl.trim()) continue;
214
254
  if (sl.trim().startsWith("at ")) lines.push(coloriseStackFrame(sl));
215
255
  else lines.push(" " + picocolors.default.red(sl.trim()));
@@ -367,18 +407,18 @@ var TUIExplorer = class {
367
407
  }
368
408
  const entry = this.filteredEntries[entryIdx];
369
409
  const isSelected = entryIdx === this.selectedIndex;
370
- const level = normalizeLevel(entry.level);
410
+ const level = sanitizeInline(normalizeLevel(entry.level));
371
411
  const time = formatTime(entry.timestamp);
372
- const msg = String(entry.message ?? "");
373
- const extraFields = Object.entries(entry).filter(([k]) => ![
412
+ const msg = sanitizeInline(String(entry.message ?? ""));
413
+ const extraFieldsSafe = sanitizeInline(Object.entries(entry).filter(([k]) => ![
374
414
  "timestamp",
375
415
  "level",
376
416
  "message",
377
417
  "stack"
378
- ].includes(k)).map(([k, v]) => `${k}=${typeof v === "object" ? JSON.stringify(v) : String(v)}`).join(" ");
418
+ ].includes(k)).map(([k, v]) => `${k}=${typeof v === "object" ? JSON.stringify(v) : String(v)}`).join(" "));
379
419
  const msgSlot = Math.floor(msgWidth * .65);
380
420
  const msgPart = this.truncate(msg, msgSlot);
381
- const extraPart = this.truncate(extraFields, msgWidth - msgPart.length - 2);
421
+ const extraPart = this.truncate(extraFieldsSafe, msgWidth - msgPart.length - 2);
382
422
  const msgDisplay = msgPart + (extraPart ? picocolors.default.dim(" " + extraPart) : "");
383
423
  const line = ` ${time} ` + levelBadge(level) + " " + this.pad(msgDisplay, msgWidth);
384
424
  if (isSelected) {
@@ -550,10 +590,10 @@ var TUIExplorer = class {
550
590
  if (ext === ".csv") {
551
591
  const entries = this.filteredEntries;
552
592
  const allKeys = [...new Set(entries.flatMap((e) => Object.keys(e)))];
553
- const header = allKeys.join(",");
593
+ const header = allKeys.map((k) => csvField(k)).join(",");
554
594
  const rows = entries.map((e) => allKeys.map((k) => {
555
595
  const v = e[k];
556
- return `"${(typeof v === "string" ? v : JSON.stringify(v ?? "")).replace(/"/g, "\"\"")}"`;
596
+ return csvField(v, true);
557
597
  }).join(","));
558
598
  node_fs.default.writeFileSync(resolved, [header, ...rows].join("\n"), "utf8");
559
599
  } else if (ext === ".ndjson" || ext === ".jsonl") node_fs.default.writeFileSync(resolved, this.filteredEntries.map((e) => JSON.stringify(e)).join("\n"), "utf8");
@@ -653,15 +693,9 @@ function exportAsCSV(entries, fields) {
653
693
  if (entries.length === 0) return "";
654
694
  const columns = fields || Object.keys(entries[0] || {});
655
695
  const lines = [];
656
- lines.push(columns.join(","));
696
+ lines.push(columns.map((c) => csvField(c)).join(","));
657
697
  for (const entry of entries) {
658
- const row = columns.map((col) => {
659
- const value = entry[col];
660
- if (value === void 0 || value === null) return "";
661
- const str = String(value);
662
- if (str.includes(",") || str.includes("\"") || str.includes("\n")) return `"${str.replace(/"/g, "\"\"")}"`;
663
- return str;
664
- });
698
+ const row = columns.map((col) => csvField(entry[col]));
665
699
  lines.push(row.join(","));
666
700
  }
667
701
  return lines.join("\n");
@@ -922,6 +956,21 @@ function parseCondition(expr) {
922
956
  }
923
957
  return null;
924
958
  }
959
+ /** Build a case-insensitive regex for a SQL LIKE pattern; only `%` and `_` are special. */
960
+ function likeToRegExp(pattern) {
961
+ let source = "";
962
+ let lastWasWildcard = false;
963
+ for (const ch of pattern) {
964
+ if (ch === "%") {
965
+ if (!lastWasWildcard) source += ".*";
966
+ lastWasWildcard = true;
967
+ continue;
968
+ }
969
+ lastWasWildcard = false;
970
+ source += ch === "_" ? "." : escapeRegExp(ch);
971
+ }
972
+ return new RegExp(`^${source}$`, "i");
973
+ }
925
974
  function matchesCondition(row, cond) {
926
975
  const rawFieldValue = row[cond.field];
927
976
  if (rawFieldValue === void 0) return false;
@@ -934,14 +983,8 @@ function matchesCondition(row, cond) {
934
983
  case ">=": return Number(rawFieldValue) >= Number(cond.value);
935
984
  case "<": return Number(rawFieldValue) < Number(cond.value);
936
985
  case "<=": return Number(rawFieldValue) <= Number(cond.value);
937
- case "LIKE": {
938
- const pattern = condStr.replace(/%/g, ".*").replace(/_/g, ".");
939
- return new RegExp(`^${pattern}$`, "i").test(fieldStr);
940
- }
941
- case "NOT LIKE": {
942
- const pattern = condStr.replace(/%/g, ".*").replace(/_/g, ".");
943
- return !new RegExp(`^${pattern}$`, "i").test(fieldStr);
944
- }
986
+ case "LIKE": return likeToRegExp(condStr).test(fieldStr);
987
+ case "NOT LIKE": return !likeToRegExp(condStr).test(fieldStr);
945
988
  case "IN": return cond.value.some((v) => String(rawFieldValue) === v);
946
989
  case "NOT IN": return !cond.value.some((v) => String(rawFieldValue) === v);
947
990
  default: return false;
@@ -961,18 +1004,19 @@ function matchesNode(row, node) {
961
1004
  function runAggregation(entries, agg) {
962
1005
  const { fn, field, groupBy } = agg;
963
1006
  if (!groupBy) return picocolors.default.yellow("Aggregation requires a GROUP BY field");
964
- const groups = {};
1007
+ const groups = /* @__PURE__ */ new Map();
965
1008
  for (const entry of entries) {
966
1009
  const key = String(entry[groupBy] ?? "(empty)");
967
- if (!groups[key]) groups[key] = [];
968
- groups[key].push(entry);
1010
+ const bucket = groups.get(key);
1011
+ if (bucket) bucket.push(entry);
1012
+ else groups.set(key, [entry]);
969
1013
  }
970
- if (fn === "COUNT" || fn === "GROUP") return formatAsTable(Object.entries(groups).map(([k, v]) => ({
1014
+ if (fn === "COUNT" || fn === "GROUP") return formatAsTable([...groups.entries()].map(([k, v]) => ({
971
1015
  [groupBy]: k,
972
1016
  count: v.length
973
1017
  })).sort((a, b) => b.count - a.count), [groupBy, "count"]);
974
1018
  if (!field) return picocolors.default.yellow("Numeric aggregation requires a field: AVG(field) BY group");
975
- return formatAsTable(Object.entries(groups).map(([k, v]) => {
1019
+ return formatAsTable([...groups.entries()].map(([k, v]) => {
976
1020
  const nums = v.map((e) => Number(e[field])).filter((n) => !Number.isNaN(n));
977
1021
  let result;
978
1022
  switch (fn) {
@@ -1012,12 +1056,12 @@ function projectRow(row, select) {
1012
1056
  }
1013
1057
  function formatResults(results, format) {
1014
1058
  if (results.length === 0) return picocolors.default.yellow("No results found");
1015
- if (format === "json") return JSON.stringify(results, null, 2);
1016
- if (format === "ndjson") return results.map((r) => JSON.stringify(r)).join("\n");
1059
+ if (format === "json") return sanitizeForTerminal(JSON.stringify(results, null, 2));
1060
+ if (format === "ndjson") return results.map((r) => sanitizeForTerminal(JSON.stringify(r))).join("\n");
1017
1061
  if (format === "table") return formatAsTable(results, Object.keys(results[0] || {}).slice(0, 6));
1018
1062
  return results.map((r) => {
1019
1063
  const level = (r.level || "").toUpperCase();
1020
- const line = JSON.stringify(r);
1064
+ const line = sanitizeForTerminal(JSON.stringify(r));
1021
1065
  if (level === "ERROR" || level === "CRITICAL") return picocolors.default.red(line);
1022
1066
  if (level === "WARN") return picocolors.default.yellow(line);
1023
1067
  if (level === "DEBUG" || level === "VERBOSE") return picocolors.default.gray(line);
@@ -1115,7 +1159,7 @@ const queryCommand = new commander.Command("query").description("Query log files
1115
1159
  });
1116
1160
  if (tailResults.length > 0) console.log(formatResults(tailResults, opts.format || "line"));
1117
1161
  } catch {
1118
- if (!opts.sql) console.log(line);
1162
+ if (!opts.sql) console.log(sanitizeForTerminal(line));
1119
1163
  }
1120
1164
  });
1121
1165
  rs.on("end", () => {
@@ -1148,14 +1192,14 @@ async function searchLogs(raw, opts = {}) {
1148
1192
  });
1149
1193
  }
1150
1194
  function formatSearchResults(results, format, _context) {
1151
- if (format === "json") return JSON.stringify(results, null, 2);
1195
+ if (format === "json") return sanitizeForTerminal(JSON.stringify(results, null, 2));
1152
1196
  if (format === "table") {
1153
1197
  if (results.length === 0) return picocolors.default.yellow("No results found");
1154
1198
  return formatAsTable(results, Object.keys(results[0] || {}).slice(0, 4));
1155
1199
  }
1156
1200
  if (results.length === 0) return picocolors.default.yellow("No results found");
1157
1201
  return results.map((r, idx) => {
1158
- const line = JSON.stringify(r);
1202
+ const line = sanitizeForTerminal(JSON.stringify(r));
1159
1203
  return `${picocolors.default.gray(String(idx + 1) + ":")} ${line}`;
1160
1204
  }).join("\n");
1161
1205
  }
@@ -1179,11 +1223,11 @@ const searchCommand = new commander.Command("search").description("Search log fi
1179
1223
  async function generateStats(raw, opts = {}) {
1180
1224
  const entries = safeParseLogs(raw);
1181
1225
  const group = opts.groupBy || "level";
1182
- const agg = {};
1226
+ const agg = Object.create(null);
1183
1227
  let minTime = null;
1184
1228
  let maxTime = null;
1185
1229
  for (const e of entries) {
1186
- const key = (e[group] || e.level || "unknown").toString().toUpperCase();
1230
+ const key = String(e[group] || e.level || "unknown").toUpperCase();
1187
1231
  agg[key] = (agg[key] || 0) + 1;
1188
1232
  if (e.timestamp) {
1189
1233
  const ts = new Date(e.timestamp);
@@ -1204,14 +1248,15 @@ async function generateStats(raw, opts = {}) {
1204
1248
  function formatStatsOutput(stats, filename) {
1205
1249
  const s = stats;
1206
1250
  const lines = [];
1207
- lines.push(picocolors.default.bold(`\nLog Statistics for ${node_path.default.basename(filename)}`));
1251
+ lines.push(picocolors.default.bold(`\nLog Statistics for ${sanitizeInline(node_path.default.basename(filename))}`));
1208
1252
  lines.push(picocolors.default.bold("=".repeat(50)));
1209
1253
  lines.push(picocolors.default.cyan(`Total Entries: ${s["total"].toLocaleString()}`));
1210
1254
  if (s["timeRange"]) lines.push(picocolors.default.cyan(`Time Range: ${s["timeRange"]["start"].toISOString()} - ${s["timeRange"]["end"].toISOString()}`));
1211
1255
  lines.push("");
1212
- lines.push(picocolors.default.bold(`${s["groupBy"].charAt(0).toUpperCase() + s["groupBy"].slice(1)} Distribution:`));
1256
+ lines.push(picocolors.default.bold(`${sanitizeInline(s["groupBy"].charAt(0).toUpperCase() + s["groupBy"].slice(1))} Distribution:`));
1213
1257
  const sorted = Object.entries(s["distribution"]).sort(([, a], [, b]) => b - a);
1214
- for (const [key, count] of sorted) {
1258
+ for (const [rawKey, count] of sorted) {
1259
+ const key = sanitizeInline(rawKey);
1215
1260
  const percentage = (count / s["total"] * 100).toFixed(1);
1216
1261
  const bar = "█".repeat(Math.floor(count / s["total"] * 30));
1217
1262
  let colorFn = picocolors.default.white;
@@ -1231,7 +1276,7 @@ const statsCommand = new commander.Command("stats").description("Show statistics
1231
1276
  process.exit(2);
1232
1277
  }
1233
1278
  const stats = await generateStats(node_fs.default.readFileSync(full, "utf8"), opts);
1234
- if (opts.format === "json") console.log(JSON.stringify(stats, null, 2));
1279
+ if (opts.format === "json") console.log(sanitizeForTerminal(JSON.stringify(stats, null, 2)));
1235
1280
  else console.log(formatStatsOutput(stats, file));
1236
1281
  });
1237
1282
 
@@ -1256,17 +1301,18 @@ function applyFilter(line, filter) {
1256
1301
  return line.toLowerCase().includes(filter.value.toLowerCase());
1257
1302
  }
1258
1303
  }
1259
- function highlightLine(line, highlight) {
1304
+ function highlightLine(rawLine, highlight) {
1305
+ const line = sanitizeForTerminal(rawLine);
1260
1306
  if (!highlight) return line;
1261
1307
  try {
1262
- const level = (JSON.parse(line).level || "").toUpperCase();
1308
+ const level = (JSON.parse(rawLine).level || "").toUpperCase();
1263
1309
  if (level === "ERROR") return picocolors.default.red(line);
1264
1310
  if (level === "WARN") return picocolors.default.yellow(line);
1265
1311
  if (level === "INFO") return picocolors.default.blue(line);
1266
1312
  if (level === "DEBUG") return picocolors.default.gray(line);
1267
1313
  return line;
1268
1314
  } catch {
1269
- const regex = new RegExp(highlight, "gi");
1315
+ const regex = new RegExp(escapeRegExp(sanitizeForTerminal(highlight)), "gi");
1270
1316
  return line.replace(regex, (match) => picocolors.default.bgYellow(picocolors.default.black(match)));
1271
1317
  }
1272
1318
  }
@@ -1,3 +1,3 @@
1
- import "./transport.types-B5vcyv2k.mjs";
2
- import { n as CloudWatchTransportConfig, t as CloudWatchTransport } from "./cloudwatch.transport-CGHY4buE.mjs";
1
+ import "./transport.types-CtNr3D7S.mjs";
2
+ import { n as CloudWatchTransportConfig, t as CloudWatchTransport } from "./cloudwatch.transport-Blc7Gtxt.mjs";
3
3
  export { CloudWatchTransport, CloudWatchTransportConfig };
@@ -1,3 +1,3 @@
1
- import "./transport.types-BuY84TwO.js";
2
- import { n as CloudWatchTransportConfig, t as CloudWatchTransport } from "./cloudwatch.transport-DK_8cKd3.js";
1
+ import "./transport.types-DqVLZTyf.js";
2
+ import { n as CloudWatchTransportConfig, t as CloudWatchTransport } from "./cloudwatch.transport-B00st6Xv.js";
3
3
  export { CloudWatchTransport, CloudWatchTransportConfig };
@@ -1,6 +1,7 @@
1
1
  require('./internal-log-BUJYMZbE.js');
2
2
  require('./safe-stringify-DWo3aZp_.js');
3
3
  require('./batch-buffer-BY-mRWIK.js');
4
- const require_cloudwatch_transport = require('./cloudwatch.transport-pNBNWJLt.js');
4
+ const require_cloudwatch_transport = require('./cloudwatch.transport-BO1ahxw-.js');
5
+ require('./entry-fields-BvOLFlJ4.js');
5
6
 
6
7
  exports.CloudWatchTransport = require_cloudwatch_transport.CloudWatchTransport;
@@ -1,6 +1,7 @@
1
1
  import "./internal-log-Cm8SOWSV.mjs";
2
2
  import "./safe-stringify-DJ66tW5I.mjs";
3
3
  import "./batch-buffer-PGvSgbmd.mjs";
4
- import { t as CloudWatchTransport } from "./cloudwatch.transport-yBwm4OeO.mjs";
4
+ import { t as CloudWatchTransport } from "./cloudwatch.transport-DZ6ies7g.mjs";
5
+ import "./entry-fields-BJkuPnIg.mjs";
5
6
 
6
7
  export { CloudWatchTransport };
@@ -1,4 +1,4 @@
1
- import { p as TransportLogEntry, s as IAsyncTransport } from "./transport.types-B5vcyv2k.mjs";
1
+ import { p as TransportLogEntry, s as IAsyncTransport } from "./transport.types-DqVLZTyf.js";
2
2
 
3
3
  //#region src/transports/cloudwatch.transport.d.ts
4
4
 
@@ -36,6 +36,12 @@ interface CloudWatchTransportConfig {
36
36
  emf?: boolean;
37
37
  /** CloudWatch Metrics namespace used by EMF. @default 'Logixia' */
38
38
  emfNamespace?: string;
39
+ /**
40
+ * Payload fields published as EMF metrics. When set, only these numeric
41
+ * fields become metrics; when unset every numeric payload field does (up to
42
+ * the EMF limit of 100 per entry).
43
+ */
44
+ emfMetrics?: string[];
39
45
  /** Abort a request that has not finished after this many ms. @default 10000 */
40
46
  requestTimeoutMs?: number;
41
47
  /** Max buffered events; the oldest are dropped past this. @default 10000 */
@@ -62,6 +68,7 @@ declare class CloudWatchTransport implements IAsyncTransport {
62
68
  private readonly logStreamName;
63
69
  private readonly emf;
64
70
  private readonly emfNamespace;
71
+ private readonly emfMetrics;
65
72
  constructor(config: CloudWatchTransportConfig);
66
73
  write(entry: TransportLogEntry): void;
67
74
  flush(): Promise<void>;
@@ -78,4 +85,4 @@ declare class CloudWatchTransport implements IAsyncTransport {
78
85
  }
79
86
  //#endregion
80
87
  export { CloudWatchTransportConfig as n, CloudWatchTransport as t };
81
- //# sourceMappingURL=cloudwatch.transport-CGHY4buE.d.mts.map
88
+ //# sourceMappingURL=cloudwatch.transport-B00st6Xv.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cloudwatch.transport-B00st6Xv.d.ts","names":[],"sources":["../src/transports/cloudwatch.transport.ts"],"sourcesContent":[],"mappings":";;;;UA+CiB,yBAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cA+EJ,mBAAA,YAA+B;;;;;;;;;;;;;;;;;sBAoBtB;eAiCP;WAOJ;;;;;;WASM"}