logixia 1.11.1 → 1.12.0

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 (41) hide show
  1. package/README.md +2 -6
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/{index-CSFeEGLb.d.ts → index-CwFww7VC.d.ts} +15 -1
  4. package/dist/index-CwFww7VC.d.ts.map +1 -0
  5. package/dist/{index-Cw-sN_0_.d.mts → index-DnyeaoZU.d.mts} +15 -1
  6. package/dist/index-DnyeaoZU.d.mts.map +1 -0
  7. package/dist/index.d.mts +2 -2
  8. package/dist/index.d.mts.map +1 -1
  9. package/dist/index.d.ts +2 -2
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +1 -1
  12. package/dist/index.mjs +1 -1
  13. package/dist/{logitron-logger.module-CgxjNLRi.mjs → logitron-logger.module-BIGTm2V6.mjs} +22 -26
  14. package/dist/logitron-logger.module-BIGTm2V6.mjs.map +1 -0
  15. package/dist/{logitron-logger.module-Je6hozHh.d.mts → logitron-logger.module-BLPFKbSa.d.mts} +2 -2
  16. package/dist/logitron-logger.module-BLPFKbSa.d.mts.map +1 -0
  17. package/dist/{logitron-logger.module-VhxcmlZZ.d.ts → logitron-logger.module-BRXjlbAx.d.ts} +2 -2
  18. package/dist/logitron-logger.module-BRXjlbAx.d.ts.map +1 -0
  19. package/dist/{logitron-logger.module-CLfa-Hbx.js → logitron-logger.module-BTxDpK6u.js} +22 -26
  20. package/dist/logitron-logger.module-BTxDpK6u.js.map +1 -0
  21. package/dist/middleware.d.mts +1 -1
  22. package/dist/middleware.d.ts +1 -1
  23. package/dist/middleware.js.map +1 -1
  24. package/dist/middleware.mjs.map +1 -1
  25. package/dist/nest.d.mts +2 -2
  26. package/dist/nest.d.ts +2 -2
  27. package/dist/nest.js +1 -1
  28. package/dist/nest.mjs +1 -1
  29. package/dist/testing.d.mts +1 -1
  30. package/dist/testing.d.ts +1 -1
  31. package/dist/transport.manager-DETpXuaY.mjs.map +1 -1
  32. package/dist/transport.manager-ciG4r5uz.js.map +1 -1
  33. package/dist/transports.d.mts +1 -1
  34. package/dist/transports.d.ts +1 -1
  35. package/package.json +2 -2
  36. package/dist/index-CSFeEGLb.d.ts.map +0 -1
  37. package/dist/index-Cw-sN_0_.d.mts.map +0 -1
  38. package/dist/logitron-logger.module-CLfa-Hbx.js.map +0 -1
  39. package/dist/logitron-logger.module-CgxjNLRi.mjs.map +0 -1
  40. package/dist/logitron-logger.module-Je6hozHh.d.mts.map +0 -1
  41. package/dist/logitron-logger.module-VhxcmlZZ.d.ts.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"middleware.js","names":["out: Record<string, unknown>","fields: Record<string, unknown>"],"sources":["../src/middleware/http-logger.ts"],"sourcesContent":["/**\n * HTTP request/response logging middleware — Morgan replacement.\n *\n * Fixes every documented Morgan bug:\n * - statusCode always captured correctly, even for requests > 20 s\n * - Logs request START (with traceId) and response FINISH (with duration)\n * - Captures errors before and after response\n * - Auto-redacts Authorization / Cookie / Set-Cookie headers\n * - Slow-request warnings\n * - Skip predicates for health-check routes / static assets\n *\n * @example Express\n * ```ts\n * import { createExpressMiddleware } from 'logixia/middleware';\n * app.use(createExpressMiddleware(logger));\n * ```\n *\n * @example Fastify\n * ```ts\n * import { createFastifyPlugin } from 'logixia/middleware';\n * await fastify.register(createFastifyPlugin(logger));\n * ```\n */\n\n/* eslint-disable sonarjs/void-use -- intentional fire-and-forget in sync middleware callbacks */\nimport type { IBaseLogger } from '../types';\n\n// ── Shared types ─────────────────────────────────────────────────────────────\n\nexport interface HttpLoggerOptions {\n /**\n * Skip logging for a request. Called before any I/O.\n * @example `skip: (req) => req.url === '/health'`\n */\n skip?: (req: IncomingRequest) => boolean;\n /**\n * Log request body (POST/PUT/PATCH). Capped at `bodyMaxBytes` (default: 4096).\n * Redaction still applies to the captured body.\n * Default: false.\n */\n logBody?: boolean;\n /** Max bytes of body to capture. Default: 4096. */\n bodyMaxBytes?: number;\n /**\n * Emit a WARN log when a request duration exceeds this threshold (ms).\n * Default: 1000.\n */\n slowRequestThresholdMs?: number;\n /**\n * Additional fields to include in every log entry.\n * @example `extraFields: (req) => ({ tenantId: req.headers['x-tenant-id'] })`\n */\n extraFields?: (req: IncomingRequest) => Record<string, unknown>;\n /**\n * Trace ID header. Default: 'x-trace-id'.\n * If the header is absent, a short random ID is generated.\n */\n traceIdHeader?: string;\n /**\n * Headers to redact from logged output.\n * Default: ['authorization', 'cookie', 'set-cookie', 'x-api-key'].\n */\n redactHeaders?: string[];\n /**\n * Log level for request-start entries. Default: 'debug'.\n * Set to 'silent' to suppress request-start logs entirely.\n */\n requestLevel?: string;\n /** Log level for successful response entries. Default: 'info'. */\n responseLevel?: string;\n /** Log level for error responses (status ≥ 500). Default: 'error'. */\n errorLevel?: string;\n}\n\n// Minimal structural types so we don't need @types/express / fastify in core\nexport interface IncomingRequest {\n method?: string;\n url?: string;\n headers?: Record<string, string | string[] | undefined>;\n body?: unknown;\n socket?: { remoteAddress?: string };\n ip?: string;\n}\n\nexport interface OutgoingResponse {\n statusCode?: number;\n on?: (event: string, cb: () => void) => void;\n once?: (event: string, cb: () => void) => void;\n getHeader?: (name: string) => string | number | string[] | undefined;\n}\n\n// ── Helpers ───────────────────────────────────────────────────────────────────\n\nconst DEFAULT_REDACT_HEADERS = new Set(['authorization', 'cookie', 'set-cookie', 'x-api-key']);\n\nfunction sanitizeHeaders(\n headers: Record<string, string | string[] | undefined> | undefined,\n redactSet: Set<string>\n): Record<string, unknown> {\n if (!headers) return {};\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(headers)) {\n if (k === '__proto__' || k === 'constructor' || k === 'prototype') continue;\n out[k] = redactSet.has(k.toLowerCase()) ? '[REDACTED]' : v;\n }\n return out;\n}\n\nfunction shortId(): string {\n // eslint-disable-next-line sonarjs/pseudo-random -- non-security request ID\n return Math.random().toString(36).slice(2, 10);\n}\n\nfunction buildBaseFields(\n req: IncomingRequest,\n traceId: string,\n options: HttpLoggerOptions\n): Record<string, unknown> {\n const redactSet = options.redactHeaders\n ? new Set(options.redactHeaders.map((h) => h.toLowerCase()))\n : DEFAULT_REDACT_HEADERS;\n\n const fields: Record<string, unknown> = {\n traceId,\n method: req.method?.toUpperCase() ?? 'UNKNOWN',\n url: req.url ?? '/',\n ip: req.ip ?? req.socket?.remoteAddress ?? 'unknown',\n headers: sanitizeHeaders(req.headers, redactSet),\n };\n\n if (options.extraFields) {\n Object.assign(fields, options.extraFields(req));\n }\n\n return fields;\n}\n\n// ── Express middleware ────────────────────────────────────────────────────────\n\n/**\n * Create an Express / Connect compatible middleware that replaces Morgan.\n */\nexport function createExpressMiddleware(\n logger: IBaseLogger,\n options: HttpLoggerOptions = {}\n): (req: IncomingRequest, res: OutgoingResponse, next: () => void) => void {\n const {\n skip,\n logBody,\n bodyMaxBytes,\n traceIdHeader = 'x-trace-id',\n requestLevel = 'debug',\n responseLevel = 'info',\n errorLevel = 'error',\n slowRequestThresholdMs = 1000,\n } = options;\n\n return function logixiaHttpMiddleware(\n req: IncomingRequest,\n res: OutgoingResponse,\n next: () => void\n ): void {\n if (skip?.(req)) {\n next();\n return;\n }\n\n const traceId = (req.headers?.[traceIdHeader] as string | undefined) ?? shortId();\n const startMs = Date.now();\n const baseFields = buildBaseFields(req, traceId, options);\n\n // Log request start\n if (requestLevel !== 'silent') {\n void logger.logLevel(requestLevel, 'request started', {\n ...baseFields,\n ...(logBody && req.body ? { body: truncateBody(req.body, bodyMaxBytes) } : {}),\n });\n }\n\n // Hook into the response 'finish' event — fires after headers + body are sent.\n // This is what Morgan gets wrong for slow requests (it uses 'close' which may\n // fire before the status code is set on some Node versions).\n //\n // BOTH 'finish' and 'close' fire on a normal response, so guard against\n // logging the completion twice (which would duplicate the completed entry and\n // the slow-request warning).\n let completed = false;\n const onFinish = (): void => {\n if (completed) return;\n completed = true;\n\n const duration = Date.now() - startMs;\n const status = res.statusCode ?? 0;\n const level = status >= 500 ? errorLevel : responseLevel;\n\n void logger.logLevel(level, 'request completed', {\n ...baseFields,\n statusCode: status,\n duration,\n });\n\n if (duration > slowRequestThresholdMs) {\n void logger.warn('slow request detected', {\n ...baseFields,\n statusCode: status,\n duration,\n threshold: slowRequestThresholdMs,\n });\n }\n };\n\n res.once?.('finish', onFinish);\n // Fallback: also covers a client that disconnects before 'finish' fires. The\n // `completed` guard ensures a normal finish+close pair logs only once.\n res.once?.('close', onFinish);\n\n next();\n };\n}\n\n// ── Fastify plugin ─────────────────────────────────────────────────────────────\n\nexport interface FastifyInstance {\n addHook: (name: string, fn: (req: unknown, reply: unknown, done: () => void) => void) => void;\n}\n\n/**\n * Create a Fastify plugin (a function you pass to `fastify.register()`).\n *\n * @example\n * ```ts\n * await fastify.register(createFastifyPlugin(logger, { slowRequestThresholdMs: 500 }));\n * ```\n */\nexport function createFastifyPlugin(logger: IBaseLogger, options: HttpLoggerOptions = {}) {\n const {\n skip,\n traceIdHeader = 'x-trace-id',\n requestLevel = 'debug',\n responseLevel = 'info',\n errorLevel = 'error',\n slowRequestThresholdMs = 1000,\n } = options;\n\n return function logixiaFastifyPlugin(\n fastify: FastifyInstance,\n _opts: unknown,\n done: () => void\n ): void {\n fastify.addHook('onRequest', (request: unknown, _reply: unknown, hookDone: () => void) => {\n const req = request as IncomingRequest & { _logixiaStart?: number; _logixiaId?: string };\n if (skip?.(req)) {\n hookDone();\n return;\n }\n\n const traceId = (req.headers?.[traceIdHeader] as string | undefined) ?? shortId();\n req._logixiaStart = Date.now();\n req._logixiaId = traceId;\n\n if (requestLevel !== 'silent') {\n void logger.logLevel(\n requestLevel,\n 'request started',\n buildBaseFields(req, traceId, options)\n );\n }\n hookDone();\n });\n\n fastify.addHook('onResponse', (request: unknown, reply: unknown, hookDone: () => void) => {\n const req = request as IncomingRequest & { _logixiaStart?: number; _logixiaId?: string };\n const rep = reply as { statusCode?: number };\n const duration = Date.now() - (req._logixiaStart ?? Date.now());\n const status = rep.statusCode ?? 0;\n const traceId = req._logixiaId ?? shortId();\n const level = status >= 500 ? errorLevel : responseLevel;\n\n void logger.logLevel(level, 'request completed', {\n ...buildBaseFields(req, traceId, options),\n statusCode: status,\n duration,\n });\n\n if (duration > slowRequestThresholdMs) {\n void logger.warn('slow request detected', {\n traceId,\n url: req.url,\n method: req.method,\n duration,\n threshold: slowRequestThresholdMs,\n });\n }\n\n hookDone();\n });\n\n done();\n };\n}\n\n// ── Internal helpers ──────────────────────────────────────────────────────────\n\nfunction truncateBody(body: unknown, maxBytes = 4096): unknown {\n if (typeof body === 'string') {\n return body.length > maxBytes ? body.slice(0, maxBytes) + '…[truncated]' : body;\n }\n if (body && typeof body === 'object') {\n const str = JSON.stringify(body);\n if (str.length > maxBytes) {\n return str.slice(0, maxBytes) + '…[truncated]';\n }\n }\n return body;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"middleware.js","names":["out: Record<string, unknown>","fields: Record<string, unknown>"],"sources":["../src/middleware/http-logger.ts"],"sourcesContent":["/**\n * HTTP request/response logging middleware — Morgan replacement.\n *\n * Fixes every documented Morgan bug:\n * - statusCode always captured correctly, even for requests > 20 s\n * - Logs request START (with traceId) and response FINISH (with duration)\n * - Captures errors before and after response\n * - Auto-redacts Authorization / Cookie / Set-Cookie headers\n * - Slow-request warnings\n * - Skip predicates for health-check routes / static assets\n *\n * @example Express\n * ```ts\n * import { createExpressMiddleware } from 'logixia/middleware';\n * app.use(createExpressMiddleware(logger));\n * ```\n *\n * @example Fastify\n * ```ts\n * import { createFastifyPlugin } from 'logixia/middleware';\n * await fastify.register(createFastifyPlugin(logger));\n * ```\n */\n\n \nimport type { IBaseLogger } from '../types';\n\n// ── Shared types ─────────────────────────────────────────────────────────────\n\nexport interface HttpLoggerOptions {\n /**\n * Skip logging for a request. Called before any I/O.\n * @example `skip: (req) => req.url === '/health'`\n */\n skip?: (req: IncomingRequest) => boolean;\n /**\n * Log request body (POST/PUT/PATCH). Capped at `bodyMaxBytes` (default: 4096).\n * Redaction still applies to the captured body.\n * Default: false.\n */\n logBody?: boolean;\n /** Max bytes of body to capture. Default: 4096. */\n bodyMaxBytes?: number;\n /**\n * Emit a WARN log when a request duration exceeds this threshold (ms).\n * Default: 1000.\n */\n slowRequestThresholdMs?: number;\n /**\n * Additional fields to include in every log entry.\n * @example `extraFields: (req) => ({ tenantId: req.headers['x-tenant-id'] })`\n */\n extraFields?: (req: IncomingRequest) => Record<string, unknown>;\n /**\n * Trace ID header. Default: 'x-trace-id'.\n * If the header is absent, a short random ID is generated.\n */\n traceIdHeader?: string;\n /**\n * Headers to redact from logged output.\n * Default: ['authorization', 'cookie', 'set-cookie', 'x-api-key'].\n */\n redactHeaders?: string[];\n /**\n * Log level for request-start entries. Default: 'debug'.\n * Set to 'silent' to suppress request-start logs entirely.\n */\n requestLevel?: string;\n /** Log level for successful response entries. Default: 'info'. */\n responseLevel?: string;\n /** Log level for error responses (status ≥ 500). Default: 'error'. */\n errorLevel?: string;\n}\n\n// Minimal structural types so we don't need @types/express / fastify in core\nexport interface IncomingRequest {\n method?: string;\n url?: string;\n headers?: Record<string, string | string[] | undefined>;\n body?: unknown;\n socket?: { remoteAddress?: string };\n ip?: string;\n}\n\nexport interface OutgoingResponse {\n statusCode?: number;\n on?: (event: string, cb: () => void) => void;\n once?: (event: string, cb: () => void) => void;\n getHeader?: (name: string) => string | number | string[] | undefined;\n}\n\n// ── Helpers ───────────────────────────────────────────────────────────────────\n\nconst DEFAULT_REDACT_HEADERS = new Set(['authorization', 'cookie', 'set-cookie', 'x-api-key']);\n\nfunction sanitizeHeaders(\n headers: Record<string, string | string[] | undefined> | undefined,\n redactSet: Set<string>\n): Record<string, unknown> {\n if (!headers) return {};\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(headers)) {\n if (k === '__proto__' || k === 'constructor' || k === 'prototype') continue;\n out[k] = redactSet.has(k.toLowerCase()) ? '[REDACTED]' : v;\n }\n return out;\n}\n\nfunction shortId(): string {\n // eslint-disable-next-line sonarjs/pseudo-random -- non-security request ID\n return Math.random().toString(36).slice(2, 10);\n}\n\nfunction buildBaseFields(\n req: IncomingRequest,\n traceId: string,\n options: HttpLoggerOptions\n): Record<string, unknown> {\n const redactSet = options.redactHeaders\n ? new Set(options.redactHeaders.map((h) => h.toLowerCase()))\n : DEFAULT_REDACT_HEADERS;\n\n const fields: Record<string, unknown> = {\n traceId,\n method: req.method?.toUpperCase() ?? 'UNKNOWN',\n url: req.url ?? '/',\n ip: req.ip ?? req.socket?.remoteAddress ?? 'unknown',\n headers: sanitizeHeaders(req.headers, redactSet),\n };\n\n if (options.extraFields) {\n Object.assign(fields, options.extraFields(req));\n }\n\n return fields;\n}\n\n// ── Express middleware ────────────────────────────────────────────────────────\n\n/**\n * Create an Express / Connect compatible middleware that replaces Morgan.\n */\nexport function createExpressMiddleware(\n logger: IBaseLogger,\n options: HttpLoggerOptions = {}\n): (req: IncomingRequest, res: OutgoingResponse, next: () => void) => void {\n const {\n skip,\n logBody,\n bodyMaxBytes,\n traceIdHeader = 'x-trace-id',\n requestLevel = 'debug',\n responseLevel = 'info',\n errorLevel = 'error',\n slowRequestThresholdMs = 1000,\n } = options;\n\n return function logixiaHttpMiddleware(\n req: IncomingRequest,\n res: OutgoingResponse,\n next: () => void\n ): void {\n if (skip?.(req)) {\n next();\n return;\n }\n\n const traceId = (req.headers?.[traceIdHeader] as string | undefined) ?? shortId();\n const startMs = Date.now();\n const baseFields = buildBaseFields(req, traceId, options);\n\n // Log request start\n if (requestLevel !== 'silent') {\n void logger.logLevel(requestLevel, 'request started', {\n ...baseFields,\n ...(logBody && req.body ? { body: truncateBody(req.body, bodyMaxBytes) } : {}),\n });\n }\n\n // Hook into the response 'finish' event — fires after headers + body are sent.\n // This is what Morgan gets wrong for slow requests (it uses 'close' which may\n // fire before the status code is set on some Node versions).\n //\n // BOTH 'finish' and 'close' fire on a normal response, so guard against\n // logging the completion twice (which would duplicate the completed entry and\n // the slow-request warning).\n let completed = false;\n const onFinish = (): void => {\n if (completed) return;\n completed = true;\n\n const duration = Date.now() - startMs;\n const status = res.statusCode ?? 0;\n const level = status >= 500 ? errorLevel : responseLevel;\n\n void logger.logLevel(level, 'request completed', {\n ...baseFields,\n statusCode: status,\n duration,\n });\n\n if (duration > slowRequestThresholdMs) {\n void logger.warn('slow request detected', {\n ...baseFields,\n statusCode: status,\n duration,\n threshold: slowRequestThresholdMs,\n });\n }\n };\n\n res.once?.('finish', onFinish);\n // Fallback: also covers a client that disconnects before 'finish' fires. The\n // `completed` guard ensures a normal finish+close pair logs only once.\n res.once?.('close', onFinish);\n\n next();\n };\n}\n\n// ── Fastify plugin ─────────────────────────────────────────────────────────────\n\nexport interface FastifyInstance {\n addHook: (name: string, fn: (req: unknown, reply: unknown, done: () => void) => void) => void;\n}\n\n/**\n * Create a Fastify plugin (a function you pass to `fastify.register()`).\n *\n * @example\n * ```ts\n * await fastify.register(createFastifyPlugin(logger, { slowRequestThresholdMs: 500 }));\n * ```\n */\nexport function createFastifyPlugin(logger: IBaseLogger, options: HttpLoggerOptions = {}) {\n const {\n skip,\n traceIdHeader = 'x-trace-id',\n requestLevel = 'debug',\n responseLevel = 'info',\n errorLevel = 'error',\n slowRequestThresholdMs = 1000,\n } = options;\n\n return function logixiaFastifyPlugin(\n fastify: FastifyInstance,\n _opts: unknown,\n done: () => void\n ): void {\n fastify.addHook('onRequest', (request: unknown, _reply: unknown, hookDone: () => void) => {\n const req = request as IncomingRequest & { _logixiaStart?: number; _logixiaId?: string };\n if (skip?.(req)) {\n hookDone();\n return;\n }\n\n const traceId = (req.headers?.[traceIdHeader] as string | undefined) ?? shortId();\n req._logixiaStart = Date.now();\n req._logixiaId = traceId;\n\n if (requestLevel !== 'silent') {\n void logger.logLevel(\n requestLevel,\n 'request started',\n buildBaseFields(req, traceId, options)\n );\n }\n hookDone();\n });\n\n fastify.addHook('onResponse', (request: unknown, reply: unknown, hookDone: () => void) => {\n const req = request as IncomingRequest & { _logixiaStart?: number; _logixiaId?: string };\n const rep = reply as { statusCode?: number };\n const duration = Date.now() - (req._logixiaStart ?? Date.now());\n const status = rep.statusCode ?? 0;\n const traceId = req._logixiaId ?? shortId();\n const level = status >= 500 ? errorLevel : responseLevel;\n\n void logger.logLevel(level, 'request completed', {\n ...buildBaseFields(req, traceId, options),\n statusCode: status,\n duration,\n });\n\n if (duration > slowRequestThresholdMs) {\n void logger.warn('slow request detected', {\n traceId,\n url: req.url,\n method: req.method,\n duration,\n threshold: slowRequestThresholdMs,\n });\n }\n\n hookDone();\n });\n\n done();\n };\n}\n\n// ── Internal helpers ──────────────────────────────────────────────────────────\n\nfunction truncateBody(body: unknown, maxBytes = 4096): unknown {\n if (typeof body === 'string') {\n return body.length > maxBytes ? body.slice(0, maxBytes) + '…[truncated]' : body;\n }\n if (body && typeof body === 'object') {\n const str = JSON.stringify(body);\n if (str.length > maxBytes) {\n return str.slice(0, maxBytes) + '…[truncated]';\n }\n }\n return body;\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"middleware.mjs","names":["out: Record<string, unknown>","fields: Record<string, unknown>"],"sources":["../src/middleware/http-logger.ts"],"sourcesContent":["/**\n * HTTP request/response logging middleware — Morgan replacement.\n *\n * Fixes every documented Morgan bug:\n * - statusCode always captured correctly, even for requests > 20 s\n * - Logs request START (with traceId) and response FINISH (with duration)\n * - Captures errors before and after response\n * - Auto-redacts Authorization / Cookie / Set-Cookie headers\n * - Slow-request warnings\n * - Skip predicates for health-check routes / static assets\n *\n * @example Express\n * ```ts\n * import { createExpressMiddleware } from 'logixia/middleware';\n * app.use(createExpressMiddleware(logger));\n * ```\n *\n * @example Fastify\n * ```ts\n * import { createFastifyPlugin } from 'logixia/middleware';\n * await fastify.register(createFastifyPlugin(logger));\n * ```\n */\n\n/* eslint-disable sonarjs/void-use -- intentional fire-and-forget in sync middleware callbacks */\nimport type { IBaseLogger } from '../types';\n\n// ── Shared types ─────────────────────────────────────────────────────────────\n\nexport interface HttpLoggerOptions {\n /**\n * Skip logging for a request. Called before any I/O.\n * @example `skip: (req) => req.url === '/health'`\n */\n skip?: (req: IncomingRequest) => boolean;\n /**\n * Log request body (POST/PUT/PATCH). Capped at `bodyMaxBytes` (default: 4096).\n * Redaction still applies to the captured body.\n * Default: false.\n */\n logBody?: boolean;\n /** Max bytes of body to capture. Default: 4096. */\n bodyMaxBytes?: number;\n /**\n * Emit a WARN log when a request duration exceeds this threshold (ms).\n * Default: 1000.\n */\n slowRequestThresholdMs?: number;\n /**\n * Additional fields to include in every log entry.\n * @example `extraFields: (req) => ({ tenantId: req.headers['x-tenant-id'] })`\n */\n extraFields?: (req: IncomingRequest) => Record<string, unknown>;\n /**\n * Trace ID header. Default: 'x-trace-id'.\n * If the header is absent, a short random ID is generated.\n */\n traceIdHeader?: string;\n /**\n * Headers to redact from logged output.\n * Default: ['authorization', 'cookie', 'set-cookie', 'x-api-key'].\n */\n redactHeaders?: string[];\n /**\n * Log level for request-start entries. Default: 'debug'.\n * Set to 'silent' to suppress request-start logs entirely.\n */\n requestLevel?: string;\n /** Log level for successful response entries. Default: 'info'. */\n responseLevel?: string;\n /** Log level for error responses (status ≥ 500). Default: 'error'. */\n errorLevel?: string;\n}\n\n// Minimal structural types so we don't need @types/express / fastify in core\nexport interface IncomingRequest {\n method?: string;\n url?: string;\n headers?: Record<string, string | string[] | undefined>;\n body?: unknown;\n socket?: { remoteAddress?: string };\n ip?: string;\n}\n\nexport interface OutgoingResponse {\n statusCode?: number;\n on?: (event: string, cb: () => void) => void;\n once?: (event: string, cb: () => void) => void;\n getHeader?: (name: string) => string | number | string[] | undefined;\n}\n\n// ── Helpers ───────────────────────────────────────────────────────────────────\n\nconst DEFAULT_REDACT_HEADERS = new Set(['authorization', 'cookie', 'set-cookie', 'x-api-key']);\n\nfunction sanitizeHeaders(\n headers: Record<string, string | string[] | undefined> | undefined,\n redactSet: Set<string>\n): Record<string, unknown> {\n if (!headers) return {};\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(headers)) {\n if (k === '__proto__' || k === 'constructor' || k === 'prototype') continue;\n out[k] = redactSet.has(k.toLowerCase()) ? '[REDACTED]' : v;\n }\n return out;\n}\n\nfunction shortId(): string {\n // eslint-disable-next-line sonarjs/pseudo-random -- non-security request ID\n return Math.random().toString(36).slice(2, 10);\n}\n\nfunction buildBaseFields(\n req: IncomingRequest,\n traceId: string,\n options: HttpLoggerOptions\n): Record<string, unknown> {\n const redactSet = options.redactHeaders\n ? new Set(options.redactHeaders.map((h) => h.toLowerCase()))\n : DEFAULT_REDACT_HEADERS;\n\n const fields: Record<string, unknown> = {\n traceId,\n method: req.method?.toUpperCase() ?? 'UNKNOWN',\n url: req.url ?? '/',\n ip: req.ip ?? req.socket?.remoteAddress ?? 'unknown',\n headers: sanitizeHeaders(req.headers, redactSet),\n };\n\n if (options.extraFields) {\n Object.assign(fields, options.extraFields(req));\n }\n\n return fields;\n}\n\n// ── Express middleware ────────────────────────────────────────────────────────\n\n/**\n * Create an Express / Connect compatible middleware that replaces Morgan.\n */\nexport function createExpressMiddleware(\n logger: IBaseLogger,\n options: HttpLoggerOptions = {}\n): (req: IncomingRequest, res: OutgoingResponse, next: () => void) => void {\n const {\n skip,\n logBody,\n bodyMaxBytes,\n traceIdHeader = 'x-trace-id',\n requestLevel = 'debug',\n responseLevel = 'info',\n errorLevel = 'error',\n slowRequestThresholdMs = 1000,\n } = options;\n\n return function logixiaHttpMiddleware(\n req: IncomingRequest,\n res: OutgoingResponse,\n next: () => void\n ): void {\n if (skip?.(req)) {\n next();\n return;\n }\n\n const traceId = (req.headers?.[traceIdHeader] as string | undefined) ?? shortId();\n const startMs = Date.now();\n const baseFields = buildBaseFields(req, traceId, options);\n\n // Log request start\n if (requestLevel !== 'silent') {\n void logger.logLevel(requestLevel, 'request started', {\n ...baseFields,\n ...(logBody && req.body ? { body: truncateBody(req.body, bodyMaxBytes) } : {}),\n });\n }\n\n // Hook into the response 'finish' event — fires after headers + body are sent.\n // This is what Morgan gets wrong for slow requests (it uses 'close' which may\n // fire before the status code is set on some Node versions).\n //\n // BOTH 'finish' and 'close' fire on a normal response, so guard against\n // logging the completion twice (which would duplicate the completed entry and\n // the slow-request warning).\n let completed = false;\n const onFinish = (): void => {\n if (completed) return;\n completed = true;\n\n const duration = Date.now() - startMs;\n const status = res.statusCode ?? 0;\n const level = status >= 500 ? errorLevel : responseLevel;\n\n void logger.logLevel(level, 'request completed', {\n ...baseFields,\n statusCode: status,\n duration,\n });\n\n if (duration > slowRequestThresholdMs) {\n void logger.warn('slow request detected', {\n ...baseFields,\n statusCode: status,\n duration,\n threshold: slowRequestThresholdMs,\n });\n }\n };\n\n res.once?.('finish', onFinish);\n // Fallback: also covers a client that disconnects before 'finish' fires. The\n // `completed` guard ensures a normal finish+close pair logs only once.\n res.once?.('close', onFinish);\n\n next();\n };\n}\n\n// ── Fastify plugin ─────────────────────────────────────────────────────────────\n\nexport interface FastifyInstance {\n addHook: (name: string, fn: (req: unknown, reply: unknown, done: () => void) => void) => void;\n}\n\n/**\n * Create a Fastify plugin (a function you pass to `fastify.register()`).\n *\n * @example\n * ```ts\n * await fastify.register(createFastifyPlugin(logger, { slowRequestThresholdMs: 500 }));\n * ```\n */\nexport function createFastifyPlugin(logger: IBaseLogger, options: HttpLoggerOptions = {}) {\n const {\n skip,\n traceIdHeader = 'x-trace-id',\n requestLevel = 'debug',\n responseLevel = 'info',\n errorLevel = 'error',\n slowRequestThresholdMs = 1000,\n } = options;\n\n return function logixiaFastifyPlugin(\n fastify: FastifyInstance,\n _opts: unknown,\n done: () => void\n ): void {\n fastify.addHook('onRequest', (request: unknown, _reply: unknown, hookDone: () => void) => {\n const req = request as IncomingRequest & { _logixiaStart?: number; _logixiaId?: string };\n if (skip?.(req)) {\n hookDone();\n return;\n }\n\n const traceId = (req.headers?.[traceIdHeader] as string | undefined) ?? shortId();\n req._logixiaStart = Date.now();\n req._logixiaId = traceId;\n\n if (requestLevel !== 'silent') {\n void logger.logLevel(\n requestLevel,\n 'request started',\n buildBaseFields(req, traceId, options)\n );\n }\n hookDone();\n });\n\n fastify.addHook('onResponse', (request: unknown, reply: unknown, hookDone: () => void) => {\n const req = request as IncomingRequest & { _logixiaStart?: number; _logixiaId?: string };\n const rep = reply as { statusCode?: number };\n const duration = Date.now() - (req._logixiaStart ?? Date.now());\n const status = rep.statusCode ?? 0;\n const traceId = req._logixiaId ?? shortId();\n const level = status >= 500 ? errorLevel : responseLevel;\n\n void logger.logLevel(level, 'request completed', {\n ...buildBaseFields(req, traceId, options),\n statusCode: status,\n duration,\n });\n\n if (duration > slowRequestThresholdMs) {\n void logger.warn('slow request detected', {\n traceId,\n url: req.url,\n method: req.method,\n duration,\n threshold: slowRequestThresholdMs,\n });\n }\n\n hookDone();\n });\n\n done();\n };\n}\n\n// ── Internal helpers ──────────────────────────────────────────────────────────\n\nfunction truncateBody(body: unknown, maxBytes = 4096): unknown {\n if (typeof body === 'string') {\n return body.length > maxBytes ? body.slice(0, maxBytes) + '…[truncated]' : body;\n }\n if (body && typeof body === 'object') {\n const str = JSON.stringify(body);\n if (str.length > maxBytes) {\n return str.slice(0, maxBytes) + '…[truncated]';\n }\n }\n return body;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"middleware.mjs","names":["out: Record<string, unknown>","fields: Record<string, unknown>"],"sources":["../src/middleware/http-logger.ts"],"sourcesContent":["/**\n * HTTP request/response logging middleware — Morgan replacement.\n *\n * Fixes every documented Morgan bug:\n * - statusCode always captured correctly, even for requests > 20 s\n * - Logs request START (with traceId) and response FINISH (with duration)\n * - Captures errors before and after response\n * - Auto-redacts Authorization / Cookie / Set-Cookie headers\n * - Slow-request warnings\n * - Skip predicates for health-check routes / static assets\n *\n * @example Express\n * ```ts\n * import { createExpressMiddleware } from 'logixia/middleware';\n * app.use(createExpressMiddleware(logger));\n * ```\n *\n * @example Fastify\n * ```ts\n * import { createFastifyPlugin } from 'logixia/middleware';\n * await fastify.register(createFastifyPlugin(logger));\n * ```\n */\n\n \nimport type { IBaseLogger } from '../types';\n\n// ── Shared types ─────────────────────────────────────────────────────────────\n\nexport interface HttpLoggerOptions {\n /**\n * Skip logging for a request. Called before any I/O.\n * @example `skip: (req) => req.url === '/health'`\n */\n skip?: (req: IncomingRequest) => boolean;\n /**\n * Log request body (POST/PUT/PATCH). Capped at `bodyMaxBytes` (default: 4096).\n * Redaction still applies to the captured body.\n * Default: false.\n */\n logBody?: boolean;\n /** Max bytes of body to capture. Default: 4096. */\n bodyMaxBytes?: number;\n /**\n * Emit a WARN log when a request duration exceeds this threshold (ms).\n * Default: 1000.\n */\n slowRequestThresholdMs?: number;\n /**\n * Additional fields to include in every log entry.\n * @example `extraFields: (req) => ({ tenantId: req.headers['x-tenant-id'] })`\n */\n extraFields?: (req: IncomingRequest) => Record<string, unknown>;\n /**\n * Trace ID header. Default: 'x-trace-id'.\n * If the header is absent, a short random ID is generated.\n */\n traceIdHeader?: string;\n /**\n * Headers to redact from logged output.\n * Default: ['authorization', 'cookie', 'set-cookie', 'x-api-key'].\n */\n redactHeaders?: string[];\n /**\n * Log level for request-start entries. Default: 'debug'.\n * Set to 'silent' to suppress request-start logs entirely.\n */\n requestLevel?: string;\n /** Log level for successful response entries. Default: 'info'. */\n responseLevel?: string;\n /** Log level for error responses (status ≥ 500). Default: 'error'. */\n errorLevel?: string;\n}\n\n// Minimal structural types so we don't need @types/express / fastify in core\nexport interface IncomingRequest {\n method?: string;\n url?: string;\n headers?: Record<string, string | string[] | undefined>;\n body?: unknown;\n socket?: { remoteAddress?: string };\n ip?: string;\n}\n\nexport interface OutgoingResponse {\n statusCode?: number;\n on?: (event: string, cb: () => void) => void;\n once?: (event: string, cb: () => void) => void;\n getHeader?: (name: string) => string | number | string[] | undefined;\n}\n\n// ── Helpers ───────────────────────────────────────────────────────────────────\n\nconst DEFAULT_REDACT_HEADERS = new Set(['authorization', 'cookie', 'set-cookie', 'x-api-key']);\n\nfunction sanitizeHeaders(\n headers: Record<string, string | string[] | undefined> | undefined,\n redactSet: Set<string>\n): Record<string, unknown> {\n if (!headers) return {};\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(headers)) {\n if (k === '__proto__' || k === 'constructor' || k === 'prototype') continue;\n out[k] = redactSet.has(k.toLowerCase()) ? '[REDACTED]' : v;\n }\n return out;\n}\n\nfunction shortId(): string {\n // eslint-disable-next-line sonarjs/pseudo-random -- non-security request ID\n return Math.random().toString(36).slice(2, 10);\n}\n\nfunction buildBaseFields(\n req: IncomingRequest,\n traceId: string,\n options: HttpLoggerOptions\n): Record<string, unknown> {\n const redactSet = options.redactHeaders\n ? new Set(options.redactHeaders.map((h) => h.toLowerCase()))\n : DEFAULT_REDACT_HEADERS;\n\n const fields: Record<string, unknown> = {\n traceId,\n method: req.method?.toUpperCase() ?? 'UNKNOWN',\n url: req.url ?? '/',\n ip: req.ip ?? req.socket?.remoteAddress ?? 'unknown',\n headers: sanitizeHeaders(req.headers, redactSet),\n };\n\n if (options.extraFields) {\n Object.assign(fields, options.extraFields(req));\n }\n\n return fields;\n}\n\n// ── Express middleware ────────────────────────────────────────────────────────\n\n/**\n * Create an Express / Connect compatible middleware that replaces Morgan.\n */\nexport function createExpressMiddleware(\n logger: IBaseLogger,\n options: HttpLoggerOptions = {}\n): (req: IncomingRequest, res: OutgoingResponse, next: () => void) => void {\n const {\n skip,\n logBody,\n bodyMaxBytes,\n traceIdHeader = 'x-trace-id',\n requestLevel = 'debug',\n responseLevel = 'info',\n errorLevel = 'error',\n slowRequestThresholdMs = 1000,\n } = options;\n\n return function logixiaHttpMiddleware(\n req: IncomingRequest,\n res: OutgoingResponse,\n next: () => void\n ): void {\n if (skip?.(req)) {\n next();\n return;\n }\n\n const traceId = (req.headers?.[traceIdHeader] as string | undefined) ?? shortId();\n const startMs = Date.now();\n const baseFields = buildBaseFields(req, traceId, options);\n\n // Log request start\n if (requestLevel !== 'silent') {\n void logger.logLevel(requestLevel, 'request started', {\n ...baseFields,\n ...(logBody && req.body ? { body: truncateBody(req.body, bodyMaxBytes) } : {}),\n });\n }\n\n // Hook into the response 'finish' event — fires after headers + body are sent.\n // This is what Morgan gets wrong for slow requests (it uses 'close' which may\n // fire before the status code is set on some Node versions).\n //\n // BOTH 'finish' and 'close' fire on a normal response, so guard against\n // logging the completion twice (which would duplicate the completed entry and\n // the slow-request warning).\n let completed = false;\n const onFinish = (): void => {\n if (completed) return;\n completed = true;\n\n const duration = Date.now() - startMs;\n const status = res.statusCode ?? 0;\n const level = status >= 500 ? errorLevel : responseLevel;\n\n void logger.logLevel(level, 'request completed', {\n ...baseFields,\n statusCode: status,\n duration,\n });\n\n if (duration > slowRequestThresholdMs) {\n void logger.warn('slow request detected', {\n ...baseFields,\n statusCode: status,\n duration,\n threshold: slowRequestThresholdMs,\n });\n }\n };\n\n res.once?.('finish', onFinish);\n // Fallback: also covers a client that disconnects before 'finish' fires. The\n // `completed` guard ensures a normal finish+close pair logs only once.\n res.once?.('close', onFinish);\n\n next();\n };\n}\n\n// ── Fastify plugin ─────────────────────────────────────────────────────────────\n\nexport interface FastifyInstance {\n addHook: (name: string, fn: (req: unknown, reply: unknown, done: () => void) => void) => void;\n}\n\n/**\n * Create a Fastify plugin (a function you pass to `fastify.register()`).\n *\n * @example\n * ```ts\n * await fastify.register(createFastifyPlugin(logger, { slowRequestThresholdMs: 500 }));\n * ```\n */\nexport function createFastifyPlugin(logger: IBaseLogger, options: HttpLoggerOptions = {}) {\n const {\n skip,\n traceIdHeader = 'x-trace-id',\n requestLevel = 'debug',\n responseLevel = 'info',\n errorLevel = 'error',\n slowRequestThresholdMs = 1000,\n } = options;\n\n return function logixiaFastifyPlugin(\n fastify: FastifyInstance,\n _opts: unknown,\n done: () => void\n ): void {\n fastify.addHook('onRequest', (request: unknown, _reply: unknown, hookDone: () => void) => {\n const req = request as IncomingRequest & { _logixiaStart?: number; _logixiaId?: string };\n if (skip?.(req)) {\n hookDone();\n return;\n }\n\n const traceId = (req.headers?.[traceIdHeader] as string | undefined) ?? shortId();\n req._logixiaStart = Date.now();\n req._logixiaId = traceId;\n\n if (requestLevel !== 'silent') {\n void logger.logLevel(\n requestLevel,\n 'request started',\n buildBaseFields(req, traceId, options)\n );\n }\n hookDone();\n });\n\n fastify.addHook('onResponse', (request: unknown, reply: unknown, hookDone: () => void) => {\n const req = request as IncomingRequest & { _logixiaStart?: number; _logixiaId?: string };\n const rep = reply as { statusCode?: number };\n const duration = Date.now() - (req._logixiaStart ?? Date.now());\n const status = rep.statusCode ?? 0;\n const traceId = req._logixiaId ?? shortId();\n const level = status >= 500 ? errorLevel : responseLevel;\n\n void logger.logLevel(level, 'request completed', {\n ...buildBaseFields(req, traceId, options),\n statusCode: status,\n duration,\n });\n\n if (duration > slowRequestThresholdMs) {\n void logger.warn('slow request detected', {\n traceId,\n url: req.url,\n method: req.method,\n duration,\n threshold: slowRequestThresholdMs,\n });\n }\n\n hookDone();\n });\n\n done();\n };\n}\n\n// ── Internal helpers ──────────────────────────────────────────────────────────\n\nfunction truncateBody(body: unknown, maxBytes = 4096): unknown {\n if (typeof body === 'string') {\n return body.length > maxBytes ? body.slice(0, maxBytes) + '…[truncated]' : body;\n }\n if (body && typeof body === 'object') {\n const str = JSON.stringify(body);\n if (str.length > maxBytes) {\n return str.slice(0, maxBytes) + '…[truncated]';\n }\n }\n return body;\n}\n"],"mappings":"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"}
package/dist/nest.d.mts CHANGED
@@ -1,5 +1,5 @@
1
- import { E as TraceIdConfig } from "./index-Cw-sN_0_.mjs";
2
- import { a as LogixiaOptionsFactory, c as LogixiaLoggerService, i as LogixiaLoggerModule, l as LogixiaServiceWith, n as LOGIXIA_LOGGER_PREFIX, o as WebSocketTraceInterceptor, r as LogixiaAsyncOptions, s as KafkaTraceInterceptor, t as LOGIXIA_LOGGER_CONFIG, u as LogixiaServiceWithLevels } from "./logitron-logger.module-Je6hozHh.mjs";
1
+ import { E as TraceIdConfig } from "./index-DnyeaoZU.mjs";
2
+ import { a as LogixiaOptionsFactory, c as LogixiaLoggerService, i as LogixiaLoggerModule, l as LogixiaServiceWith, n as LOGIXIA_LOGGER_PREFIX, o as WebSocketTraceInterceptor, r as LogixiaAsyncOptions, s as KafkaTraceInterceptor, t as LOGIXIA_LOGGER_CONFIG, u as LogixiaServiceWithLevels } from "./logitron-logger.module-BLPFKbSa.mjs";
3
3
  import { NestMiddleware } from "@nestjs/common";
4
4
  import { NextFunction, Request, Response } from "express";
5
5
 
package/dist/nest.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { E as TraceIdConfig } from "./index-CSFeEGLb.js";
2
- import { a as LogixiaOptionsFactory, c as LogixiaLoggerService, i as LogixiaLoggerModule, l as LogixiaServiceWith, n as LOGIXIA_LOGGER_PREFIX, o as WebSocketTraceInterceptor, r as LogixiaAsyncOptions, s as KafkaTraceInterceptor, t as LOGIXIA_LOGGER_CONFIG, u as LogixiaServiceWithLevels } from "./logitron-logger.module-VhxcmlZZ.js";
1
+ import { E as TraceIdConfig } from "./index-CwFww7VC.js";
2
+ import { a as LogixiaOptionsFactory, c as LogixiaLoggerService, i as LogixiaLoggerModule, l as LogixiaServiceWith, n as LOGIXIA_LOGGER_PREFIX, o as WebSocketTraceInterceptor, r as LogixiaAsyncOptions, s as KafkaTraceInterceptor, t as LOGIXIA_LOGGER_CONFIG, u as LogixiaServiceWithLevels } from "./logitron-logger.module-BRXjlbAx.js";
3
3
  import { NestMiddleware } from "@nestjs/common";
4
4
  import { NextFunction, Request, Response } from "express";
5
5
 
package/dist/nest.js CHANGED
@@ -1,5 +1,5 @@
1
1
  require('./transport.manager-ciG4r5uz.js');
2
- const require_logitron_logger_module = require('./logitron-logger.module-CLfa-Hbx.js');
2
+ const require_logitron_logger_module = require('./logitron-logger.module-BTxDpK6u.js');
3
3
 
4
4
  Object.defineProperty(exports, 'KafkaTraceInterceptor', {
5
5
  enumerable: true,
package/dist/nest.mjs CHANGED
@@ -1,4 +1,4 @@
1
1
  import "./transport.manager-DETpXuaY.mjs";
2
- import { a as TraceMiddleware, c as KafkaTraceInterceptor, i as WebSocketTraceInterceptor, l as LogixiaLoggerService, n as LOGIXIA_LOGGER_PREFIX, r as LogixiaLoggerModule, t as LOGIXIA_LOGGER_CONFIG } from "./logitron-logger.module-CgxjNLRi.mjs";
2
+ import { a as TraceMiddleware, c as KafkaTraceInterceptor, i as WebSocketTraceInterceptor, l as LogixiaLoggerService, n as LOGIXIA_LOGGER_PREFIX, r as LogixiaLoggerModule, t as LOGIXIA_LOGGER_CONFIG } from "./logitron-logger.module-BIGTm2V6.mjs";
3
3
 
4
4
  export { KafkaTraceInterceptor, LOGIXIA_LOGGER_CONFIG, LOGIXIA_LOGGER_PREFIX, LogixiaLoggerModule, LogixiaLoggerService, TraceMiddleware, WebSocketTraceInterceptor };
@@ -1,4 +1,4 @@
1
- import { h as LogEntry, u as IBaseLogger } from "./index-Cw-sN_0_.mjs";
1
+ import { h as LogEntry, u as IBaseLogger } from "./index-DnyeaoZU.mjs";
2
2
 
3
3
  //#region src/testing/mock-logger.d.ts
4
4
 
package/dist/testing.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { h as LogEntry, u as IBaseLogger } from "./index-CSFeEGLb.js";
1
+ import { h as LogEntry, u as IBaseLogger } from "./index-CwFww7VC.js";
2
2
 
3
3
  //#region src/testing/mock-logger.d.ts
4
4