@fjell/express-router 4.4.83 → 4.4.84
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.
- package/CHANGELOG.md +20 -0
- package/dist/errorHandler.d.ts +5 -0
- package/dist/errorHandler.d.ts.map +1 -1
- package/dist/errorHandler.js +70 -10
- package/dist/errorHandler.js.map +2 -2
- package/package.json +30 -18
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@fjell/express-router` will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
### Security
|
|
11
|
+
- Redact sensitive request headers (`authorization`, `cookie`, etc.) in error logs by default
|
|
12
|
+
- Do not log request bodies by default (`logRequestBody` opt-in); redact common secret fields in query/params/body when logged
|
|
13
|
+
- Avoid echoing full request body into error response `operation.params` unless `logRequestBody` is enabled
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- Bump express to 5.2.x and vitest/eslint toolchain within current majors
|
|
17
|
+
- Exclude `examples/**` from coverage; set src-only thresholds (lines/functions/statements 85, branches 72)
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
- CHANGELOG.md and explicit `package.json` `files` whitelist
|
package/dist/errorHandler.d.ts
CHANGED
|
@@ -3,6 +3,10 @@ import { ErrorInfo } from '@fjell/core';
|
|
|
3
3
|
export interface ErrorHandlerOptions {
|
|
4
4
|
includeStackTrace?: boolean;
|
|
5
5
|
logErrors?: boolean;
|
|
6
|
+
/** When true, include request body in error logs (default: false). */
|
|
7
|
+
logRequestBody?: boolean;
|
|
8
|
+
/** When true, include raw (unredacted) headers in error logs (default: false). */
|
|
9
|
+
logRawHeaders?: boolean;
|
|
6
10
|
customErrorMapper?: (error: any) => ErrorInfo | null;
|
|
7
11
|
}
|
|
8
12
|
export declare class FjellErrorHandler {
|
|
@@ -12,6 +16,7 @@ export declare class FjellErrorHandler {
|
|
|
12
16
|
* Get appropriate HTTP status code for error
|
|
13
17
|
*/
|
|
14
18
|
private getHttpStatusForError;
|
|
19
|
+
private getSafeOperationParams;
|
|
15
20
|
/**
|
|
16
21
|
* Convert generic errors to ErrorInfo structure
|
|
17
22
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errorHandler.d.ts","sourceRoot":"","sources":["../src/errorHandler.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAC1D,OAAO,EAAE,SAAS,EAAiB,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"errorHandler.d.ts","sourceRoot":"","sources":["../src/errorHandler.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAC1D,OAAO,EAAE,SAAS,EAAiB,MAAM,aAAa,CAAC;AA2BvD,MAAM,WAAW,mBAAmB;IAClC,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,sEAAsE;IACtE,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,kFAAkF;IAClF,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,iBAAiB,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,KAAK,SAAS,GAAG,IAAI,CAAC;CACtD;AA+BD,qBAAa,iBAAiB;IAChB,OAAO,CAAC,OAAO;gBAAP,OAAO,GAAE,mBAAwB;IAUrD;;OAEG;IACH,OAAO,CAAC,qBAAqB;IAgB7B,OAAO,CAAC,sBAAsB;IAQ9B;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAiE1B;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAoBxB;;OAEG;IACH,OAAO,CAAC,2BAA2B;IA4BnC;;OAEG;IACH,OAAO,CAAC,eAAe;IAKvB;;OAEG;IACH,OAAO,CAAC,YAAY;IAMpB;;OAEG;IACH,OAAO,CAAC,qBAAqB;IAc7B;;OAEG;IACH,OAAO,CAAC,0BAA0B;IAclC;;OAEG;IACH,MAAM,GAAI,KAAK,GAAG,EAAE,KAAK,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY,KAAG,IAAI,CAyDxE;CACH;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,CAAC,EAAE,mBAAmB,SA/D/C,GAAG,OAAO,OAAO,OAAO,QAAQ,QAAQ,YAAY,KAAG,IAAI,CAkE3E"}
|
package/dist/errorHandler.js
CHANGED
|
@@ -1,15 +1,65 @@
|
|
|
1
1
|
import { isActionError } from "@fjell/core";
|
|
2
2
|
import LibLogger from "./logger.js";
|
|
3
3
|
const logger = LibLogger.get("express-router", "errorHandler");
|
|
4
|
+
const SENSITIVE_HEADER_NAMES = /* @__PURE__ */ new Set([
|
|
5
|
+
"authorization",
|
|
6
|
+
"cookie",
|
|
7
|
+
"set-cookie",
|
|
8
|
+
"x-api-key",
|
|
9
|
+
"x-auth-token",
|
|
10
|
+
"proxy-authorization"
|
|
11
|
+
]);
|
|
12
|
+
const SENSITIVE_PARAM_KEYS = /* @__PURE__ */ new Set([
|
|
13
|
+
"password",
|
|
14
|
+
"passwd",
|
|
15
|
+
"secret",
|
|
16
|
+
"token",
|
|
17
|
+
"access_token",
|
|
18
|
+
"refresh_token",
|
|
19
|
+
"api_key",
|
|
20
|
+
"apikey",
|
|
21
|
+
"authorization",
|
|
22
|
+
"cookie"
|
|
23
|
+
]);
|
|
24
|
+
function redactHeaders(headers) {
|
|
25
|
+
const redacted = {};
|
|
26
|
+
for (const [key, value] of Object.entries(headers || {})) {
|
|
27
|
+
if (SENSITIVE_HEADER_NAMES.has(key.toLowerCase())) {
|
|
28
|
+
redacted[key] = "[REDACTED]";
|
|
29
|
+
} else {
|
|
30
|
+
redacted[key] = value;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return redacted;
|
|
34
|
+
}
|
|
35
|
+
function redactParams(params) {
|
|
36
|
+
if (params == null || typeof params !== "object" || Array.isArray(params)) {
|
|
37
|
+
return params;
|
|
38
|
+
}
|
|
39
|
+
const redacted = {};
|
|
40
|
+
for (const [key, value] of Object.entries(params)) {
|
|
41
|
+
if (SENSITIVE_PARAM_KEYS.has(key.toLowerCase())) {
|
|
42
|
+
redacted[key] = "[REDACTED]";
|
|
43
|
+
} else if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
44
|
+
redacted[key] = redactParams(value);
|
|
45
|
+
} else {
|
|
46
|
+
redacted[key] = value;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return redacted;
|
|
50
|
+
}
|
|
4
51
|
class FjellErrorHandler {
|
|
5
52
|
constructor(options = {}) {
|
|
6
53
|
this.options = options;
|
|
7
54
|
this.options = {
|
|
8
55
|
includeStackTrace: process.env.NODE_ENV === "development",
|
|
9
56
|
logErrors: true,
|
|
57
|
+
logRequestBody: false,
|
|
58
|
+
logRawHeaders: false,
|
|
10
59
|
...options
|
|
11
60
|
};
|
|
12
61
|
}
|
|
62
|
+
options;
|
|
13
63
|
/**
|
|
14
64
|
* Get appropriate HTTP status code for error
|
|
15
65
|
*/
|
|
@@ -27,6 +77,13 @@ class FjellErrorHandler {
|
|
|
27
77
|
};
|
|
28
78
|
return statusMap[code] || 500;
|
|
29
79
|
}
|
|
80
|
+
getSafeOperationParams(req) {
|
|
81
|
+
return redactParams({
|
|
82
|
+
...req.query,
|
|
83
|
+
...req.params,
|
|
84
|
+
...this.options.logRequestBody ? req.body : {}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
30
87
|
/**
|
|
31
88
|
* Convert generic errors to ErrorInfo structure
|
|
32
89
|
*/
|
|
@@ -52,7 +109,7 @@ class FjellErrorHandler {
|
|
|
52
109
|
operation: {
|
|
53
110
|
type: this.getOperationTypeFromRequest(req),
|
|
54
111
|
name: req.path,
|
|
55
|
-
params:
|
|
112
|
+
params: this.getSafeOperationParams(req)
|
|
56
113
|
},
|
|
57
114
|
context: {
|
|
58
115
|
itemType: this.extractItemType(req.path)
|
|
@@ -76,7 +133,7 @@ class FjellErrorHandler {
|
|
|
76
133
|
operation: {
|
|
77
134
|
type: this.getOperationTypeFromRequest(req),
|
|
78
135
|
name: req.path,
|
|
79
|
-
params:
|
|
136
|
+
params: this.getSafeOperationParams(req)
|
|
80
137
|
},
|
|
81
138
|
context: {
|
|
82
139
|
itemType: this.extractItemType(req.path)
|
|
@@ -191,6 +248,16 @@ class FjellErrorHandler {
|
|
|
191
248
|
statusCode = this.getHttpStatusForError(errorInfo.code);
|
|
192
249
|
}
|
|
193
250
|
if (this.options.logErrors) {
|
|
251
|
+
const requestLog = {
|
|
252
|
+
method: req.method,
|
|
253
|
+
path: req.path,
|
|
254
|
+
headers: this.options.logRawHeaders ? req.headers : redactHeaders(req.headers),
|
|
255
|
+
query: redactParams(req.query),
|
|
256
|
+
params: redactParams(req.params)
|
|
257
|
+
};
|
|
258
|
+
if (this.options.logRequestBody) {
|
|
259
|
+
requestLog.body = redactParams(req.body);
|
|
260
|
+
}
|
|
194
261
|
logger.error("HTTP request error", {
|
|
195
262
|
component: "FjellErrorHandler",
|
|
196
263
|
operation: "handle",
|
|
@@ -206,14 +273,7 @@ class FjellErrorHandler {
|
|
|
206
273
|
validOptions: errorInfo.details?.validOptions,
|
|
207
274
|
suggestedAction: errorInfo.details?.suggestedAction,
|
|
208
275
|
retryable: errorInfo.details?.retryable,
|
|
209
|
-
request:
|
|
210
|
-
method: req.method,
|
|
211
|
-
path: req.path,
|
|
212
|
-
headers: req.headers,
|
|
213
|
-
body: req.body,
|
|
214
|
-
query: req.query,
|
|
215
|
-
params: req.params
|
|
216
|
-
},
|
|
276
|
+
request: requestLog,
|
|
217
277
|
suggestion: errorInfo.details?.suggestedAction || "Check request parameters, authentication, and data validation",
|
|
218
278
|
errorInfo
|
|
219
279
|
});
|
package/dist/errorHandler.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/errorHandler.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextFunction, Request, Response } from 'express';\nimport { ErrorInfo, isActionError } from '@fjell/core';\nimport LibLogger from './logger.js';\n\nconst logger = LibLogger.get('express-router', 'errorHandler');\n\nexport interface ErrorHandlerOptions {\n includeStackTrace?: boolean;\n logErrors?: boolean;\n customErrorMapper?: (error: any) => ErrorInfo | null;\n}\n\nexport class FjellErrorHandler {\n constructor(private options: ErrorHandlerOptions = {}) {\n this.options = {\n includeStackTrace: process.env.NODE_ENV === 'development',\n logErrors: true,\n ...options\n };\n }\n\n /**\n * Get appropriate HTTP status code for error\n */\n private getHttpStatusForError(code: string): number {\n const statusMap: Record<string, number> = {\n 'VALIDATION_ERROR': 400,\n 'NOT_FOUND': 404,\n 'PERMISSION_ERROR': 403,\n 'UNAUTHORIZED': 401,\n 'BUSINESS_LOGIC_ERROR': 422,\n 'ACTION_NOT_FOUND': 404,\n 'DUPLICATE_ERROR': 409,\n 'RATE_LIMIT_EXCEEDED': 429,\n 'INTERNAL_ERROR': 500,\n };\n\n return statusMap[code] || 500;\n }\n\n /**\n * Convert generic errors to ErrorInfo structure\n */\n private convertToErrorInfo(error: any, req: Request): ErrorInfo {\n // Check if custom mapper can handle this error\n if (this.options.customErrorMapper) {\n const mapped = this.options.customErrorMapper(error);\n if (mapped) {\n return this.enhanceErrorInfo(mapped, req);\n }\n }\n\n // Handle common HTTP errors\n if (error.status || error.statusCode) {\n const status = error.status || error.statusCode;\n const technical: ErrorInfo['technical'] = {\n timestamp: new Date().toISOString(),\n requestId: this.getRequestId(req)\n };\n \n if (this.options.includeStackTrace && error.stack) {\n technical.stackTrace = error.stack;\n }\n\n return {\n code: this.getErrorCodeForStatus(status),\n message: error.message || this.getDefaultMessageForStatus(status),\n operation: {\n type: this.getOperationTypeFromRequest(req),\n name: req.path,\n params: { ...req.body, ...req.query, ...req.params }\n },\n context: {\n itemType: this.extractItemType(req.path)\n },\n technical\n };\n }\n\n // Default error structure\n const technical: ErrorInfo['technical'] = {\n timestamp: new Date().toISOString(),\n requestId: this.getRequestId(req)\n };\n \n if (this.options.includeStackTrace && error.stack) {\n technical.stackTrace = error.stack;\n }\n \n if (error.cause) {\n technical.cause = error.cause;\n }\n\n return {\n code: 'INTERNAL_ERROR',\n message: error.message || 'An unexpected error occurred',\n operation: {\n type: this.getOperationTypeFromRequest(req),\n name: req.path,\n params: { ...req.body, ...req.query, ...req.params }\n },\n context: {\n itemType: this.extractItemType(req.path)\n },\n technical\n };\n }\n\n /**\n * Enhance error info with request context\n */\n private enhanceErrorInfo(errorInfo: ErrorInfo, req: Request): ErrorInfo {\n const technical: ErrorInfo['technical'] = {\n timestamp: errorInfo.technical?.timestamp || new Date().toISOString(),\n requestId: this.getRequestId(req)\n };\n\n if (this.options.includeStackTrace && errorInfo.technical?.stackTrace) {\n technical.stackTrace = errorInfo.technical.stackTrace;\n }\n\n if (errorInfo.technical?.cause) {\n technical.cause = errorInfo.technical.cause;\n }\n\n return {\n ...errorInfo,\n technical\n };\n }\n\n /**\n * Extract operation type from request\n */\n private getOperationTypeFromRequest(req: Request): ErrorInfo['operation']['type'] {\n const path = req.path.toLowerCase();\n const method = req.method.toLowerCase();\n\n // Pattern matching for operation types\n if (path.includes('/action/')) return 'action';\n if (path.includes('/allaction/')) return 'allAction';\n if (path.includes('/facet/')) return 'facet';\n if (path.includes('/allfacet/')) return 'allFacet';\n if (path.includes('/find/')) return method === 'get' ? 'findOne' : 'find';\n if (path.includes('/upsert')) return 'upsert';\n \n // Method-based detection\n switch (method) {\n case 'get':\n return path.match(/\\/\\d+$/) ? 'get' : 'all';\n case 'post':\n return 'create';\n case 'put':\n case 'patch':\n return 'update';\n case 'delete':\n return 'remove';\n default:\n return 'get';\n }\n }\n\n /**\n * Extract item type from path\n */\n private extractItemType(path: string): string {\n const segments = path.split('/').filter(s => s && !s.match(/^\\d+$/));\n return segments[segments.length - 1] || 'unknown';\n }\n\n /**\n * Get request ID from headers or generate one\n */\n private getRequestId(req: Request): string {\n return (req.headers['x-request-id'] ||\n req.headers['x-trace-id'] ||\n `req-${Date.now()}`) as string;\n }\n\n /**\n * Get error code for HTTP status\n */\n private getErrorCodeForStatus(status: number): string {\n const codeMap: Record<number, string> = {\n 400: 'VALIDATION_ERROR',\n 401: 'UNAUTHORIZED',\n 403: 'PERMISSION_ERROR',\n 404: 'NOT_FOUND',\n 409: 'DUPLICATE_ERROR',\n 422: 'BUSINESS_LOGIC_ERROR',\n 429: 'RATE_LIMIT_EXCEEDED',\n 500: 'INTERNAL_ERROR',\n };\n return codeMap[status] || 'INTERNAL_ERROR';\n }\n\n /**\n * Get default message for HTTP status\n */\n private getDefaultMessageForStatus(status: number): string {\n const messageMap: Record<number, string> = {\n 400: 'Bad Request',\n 401: 'Unauthorized',\n 403: 'Forbidden',\n 404: 'Not Found',\n 409: 'Conflict',\n 422: 'Unprocessable Entity',\n 429: 'Too Many Requests',\n 500: 'Internal Server Error',\n };\n return messageMap[status] || 'Error';\n }\n\n /**\n * Express error handler middleware\n */\n handle = (err: any, req: Request, res: Response, next: NextFunction): void => {\n if (res.headersSent) {\n return next(err);\n }\n\n let errorInfo: ErrorInfo;\n let statusCode: number;\n\n if (isActionError(err)) {\n // This is already a structured Fjell error\n errorInfo = this.enhanceErrorInfo(err.errorInfo, req);\n statusCode = this.getHttpStatusForError(errorInfo.code);\n } else {\n // Convert to ErrorInfo structure\n errorInfo = this.convertToErrorInfo(err, req);\n statusCode = this.getHttpStatusForError(errorInfo.code);\n }\n\n // Log the error if configured - structured for agentic debugging\n if (this.options.logErrors) {\n logger.error('HTTP request error', {\n component: 'FjellErrorHandler',\n operation: 'handle',\n endpoint: req.path,\n method: req.method,\n statusCode,\n errorCode: errorInfo.code,\n errorMessage: errorInfo.message,\n operationType: errorInfo.operation?.type,\n operationName: errorInfo.operation?.name,\n itemType: errorInfo.context?.itemType,\n requestId: this.getRequestId(req),\n validOptions: errorInfo.details?.validOptions,\n suggestedAction: errorInfo.details?.suggestedAction,\n retryable: errorInfo.details?.retryable,\n request: {\n method: req.method,\n path: req.path,\n headers: req.headers,\n body: req.body,\n query: req.query,\n params: req.params\n },\n suggestion: errorInfo.details?.suggestedAction || 'Check request parameters, authentication, and data validation',\n errorInfo\n });\n }\n\n // Send structured error response\n res.status(statusCode).json({\n success: false,\n error: errorInfo\n });\n };\n}\n\n/**\n * Export convenience function\n */\nexport function createErrorHandler(options?: ErrorHandlerOptions) {\n const handler = new FjellErrorHandler(options);\n return handler.handle;\n}\n\n"],
|
|
5
|
-
"mappings": "AACA,SAAoB,qBAAqB;AACzC,OAAO,eAAe;AAEtB,MAAM,SAAS,UAAU,IAAI,kBAAkB,cAAc;
|
|
4
|
+
"sourcesContent": ["import { NextFunction, Request, Response } from 'express';\nimport { ErrorInfo, isActionError } from '@fjell/core';\nimport LibLogger from './logger.js';\n\nconst logger = LibLogger.get('express-router', 'errorHandler');\n\nconst SENSITIVE_HEADER_NAMES = new Set([\n 'authorization',\n 'cookie',\n 'set-cookie',\n 'x-api-key',\n 'x-auth-token',\n 'proxy-authorization',\n]);\n\nconst SENSITIVE_PARAM_KEYS = new Set([\n 'password',\n 'passwd',\n 'secret',\n 'token',\n 'access_token',\n 'refresh_token',\n 'api_key',\n 'apikey',\n 'authorization',\n 'cookie',\n]);\n\nexport interface ErrorHandlerOptions {\n includeStackTrace?: boolean;\n logErrors?: boolean;\n /** When true, include request body in error logs (default: false). */\n logRequestBody?: boolean;\n /** When true, include raw (unredacted) headers in error logs (default: false). */\n logRawHeaders?: boolean;\n customErrorMapper?: (error: any) => ErrorInfo | null;\n}\n\nfunction redactHeaders(headers: Request['headers']): Record<string, unknown> {\n const redacted: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(headers || {})) {\n if (SENSITIVE_HEADER_NAMES.has(key.toLowerCase())) {\n redacted[key] = '[REDACTED]';\n } else {\n redacted[key] = value;\n }\n }\n return redacted;\n}\n\nfunction redactParams(params: unknown): unknown {\n if (params == null || typeof params !== 'object' || Array.isArray(params)) {\n return params;\n }\n const redacted: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(params as Record<string, unknown>)) {\n if (SENSITIVE_PARAM_KEYS.has(key.toLowerCase())) {\n redacted[key] = '[REDACTED]';\n } else if (value && typeof value === 'object' && !Array.isArray(value)) {\n redacted[key] = redactParams(value);\n } else {\n redacted[key] = value;\n }\n }\n return redacted;\n}\n\nexport class FjellErrorHandler {\n constructor(private options: ErrorHandlerOptions = {}) {\n this.options = {\n includeStackTrace: process.env.NODE_ENV === 'development',\n logErrors: true,\n logRequestBody: false,\n logRawHeaders: false,\n ...options\n };\n }\n\n /**\n * Get appropriate HTTP status code for error\n */\n private getHttpStatusForError(code: string): number {\n const statusMap: Record<string, number> = {\n 'VALIDATION_ERROR': 400,\n 'NOT_FOUND': 404,\n 'PERMISSION_ERROR': 403,\n 'UNAUTHORIZED': 401,\n 'BUSINESS_LOGIC_ERROR': 422,\n 'ACTION_NOT_FOUND': 404,\n 'DUPLICATE_ERROR': 409,\n 'RATE_LIMIT_EXCEEDED': 429,\n 'INTERNAL_ERROR': 500,\n };\n\n return statusMap[code] || 500;\n }\n\n private getSafeOperationParams(req: Request): Record<string, unknown> {\n return redactParams({\n ...req.query,\n ...req.params,\n ...(this.options.logRequestBody ? req.body : {}),\n }) as Record<string, unknown>;\n }\n\n /**\n * Convert generic errors to ErrorInfo structure\n */\n private convertToErrorInfo(error: any, req: Request): ErrorInfo {\n // Check if custom mapper can handle this error\n if (this.options.customErrorMapper) {\n const mapped = this.options.customErrorMapper(error);\n if (mapped) {\n return this.enhanceErrorInfo(mapped, req);\n }\n }\n\n // Handle common HTTP errors\n if (error.status || error.statusCode) {\n const status = error.status || error.statusCode;\n const technical: ErrorInfo['technical'] = {\n timestamp: new Date().toISOString(),\n requestId: this.getRequestId(req)\n };\n \n if (this.options.includeStackTrace && error.stack) {\n technical.stackTrace = error.stack;\n }\n\n return {\n code: this.getErrorCodeForStatus(status),\n message: error.message || this.getDefaultMessageForStatus(status),\n operation: {\n type: this.getOperationTypeFromRequest(req),\n name: req.path,\n params: this.getSafeOperationParams(req)\n },\n context: {\n itemType: this.extractItemType(req.path)\n },\n technical\n };\n }\n\n // Default error structure\n const technical: ErrorInfo['technical'] = {\n timestamp: new Date().toISOString(),\n requestId: this.getRequestId(req)\n };\n \n if (this.options.includeStackTrace && error.stack) {\n technical.stackTrace = error.stack;\n }\n \n if (error.cause) {\n technical.cause = error.cause;\n }\n\n return {\n code: 'INTERNAL_ERROR',\n message: error.message || 'An unexpected error occurred',\n operation: {\n type: this.getOperationTypeFromRequest(req),\n name: req.path,\n params: this.getSafeOperationParams(req)\n },\n context: {\n itemType: this.extractItemType(req.path)\n },\n technical\n };\n }\n\n /**\n * Enhance error info with request context\n */\n private enhanceErrorInfo(errorInfo: ErrorInfo, req: Request): ErrorInfo {\n const technical: ErrorInfo['technical'] = {\n timestamp: errorInfo.technical?.timestamp || new Date().toISOString(),\n requestId: this.getRequestId(req)\n };\n\n if (this.options.includeStackTrace && errorInfo.technical?.stackTrace) {\n technical.stackTrace = errorInfo.technical.stackTrace;\n }\n\n if (errorInfo.technical?.cause) {\n technical.cause = errorInfo.technical.cause;\n }\n\n return {\n ...errorInfo,\n technical\n };\n }\n\n /**\n * Extract operation type from request\n */\n private getOperationTypeFromRequest(req: Request): ErrorInfo['operation']['type'] {\n const path = req.path.toLowerCase();\n const method = req.method.toLowerCase();\n\n // Pattern matching for operation types\n if (path.includes('/action/')) return 'action';\n if (path.includes('/allaction/')) return 'allAction';\n if (path.includes('/facet/')) return 'facet';\n if (path.includes('/allfacet/')) return 'allFacet';\n if (path.includes('/find/')) return method === 'get' ? 'findOne' : 'find';\n if (path.includes('/upsert')) return 'upsert';\n \n // Method-based detection\n switch (method) {\n case 'get':\n return path.match(/\\/\\d+$/) ? 'get' : 'all';\n case 'post':\n return 'create';\n case 'put':\n case 'patch':\n return 'update';\n case 'delete':\n return 'remove';\n default:\n return 'get';\n }\n }\n\n /**\n * Extract item type from path\n */\n private extractItemType(path: string): string {\n const segments = path.split('/').filter(s => s && !s.match(/^\\d+$/));\n return segments[segments.length - 1] || 'unknown';\n }\n\n /**\n * Get request ID from headers or generate one\n */\n private getRequestId(req: Request): string {\n return (req.headers['x-request-id'] ||\n req.headers['x-trace-id'] ||\n `req-${Date.now()}`) as string;\n }\n\n /**\n * Get error code for HTTP status\n */\n private getErrorCodeForStatus(status: number): string {\n const codeMap: Record<number, string> = {\n 400: 'VALIDATION_ERROR',\n 401: 'UNAUTHORIZED',\n 403: 'PERMISSION_ERROR',\n 404: 'NOT_FOUND',\n 409: 'DUPLICATE_ERROR',\n 422: 'BUSINESS_LOGIC_ERROR',\n 429: 'RATE_LIMIT_EXCEEDED',\n 500: 'INTERNAL_ERROR',\n };\n return codeMap[status] || 'INTERNAL_ERROR';\n }\n\n /**\n * Get default message for HTTP status\n */\n private getDefaultMessageForStatus(status: number): string {\n const messageMap: Record<number, string> = {\n 400: 'Bad Request',\n 401: 'Unauthorized',\n 403: 'Forbidden',\n 404: 'Not Found',\n 409: 'Conflict',\n 422: 'Unprocessable Entity',\n 429: 'Too Many Requests',\n 500: 'Internal Server Error',\n };\n return messageMap[status] || 'Error';\n }\n\n /**\n * Express error handler middleware\n */\n handle = (err: any, req: Request, res: Response, next: NextFunction): void => {\n if (res.headersSent) {\n return next(err);\n }\n\n let errorInfo: ErrorInfo;\n let statusCode: number;\n\n if (isActionError(err)) {\n // This is already a structured Fjell error\n errorInfo = this.enhanceErrorInfo(err.errorInfo, req);\n statusCode = this.getHttpStatusForError(errorInfo.code);\n } else {\n // Convert to ErrorInfo structure\n errorInfo = this.convertToErrorInfo(err, req);\n statusCode = this.getHttpStatusForError(errorInfo.code);\n }\n\n // Log the error if configured - structured for agentic debugging\n if (this.options.logErrors) {\n const requestLog: Record<string, unknown> = {\n method: req.method,\n path: req.path,\n headers: this.options.logRawHeaders ? req.headers : redactHeaders(req.headers),\n query: redactParams(req.query),\n params: redactParams(req.params),\n };\n if (this.options.logRequestBody) {\n requestLog.body = redactParams(req.body);\n }\n\n logger.error('HTTP request error', {\n component: 'FjellErrorHandler',\n operation: 'handle',\n endpoint: req.path,\n method: req.method,\n statusCode,\n errorCode: errorInfo.code,\n errorMessage: errorInfo.message,\n operationType: errorInfo.operation?.type,\n operationName: errorInfo.operation?.name,\n itemType: errorInfo.context?.itemType,\n requestId: this.getRequestId(req),\n validOptions: errorInfo.details?.validOptions,\n suggestedAction: errorInfo.details?.suggestedAction,\n retryable: errorInfo.details?.retryable,\n request: requestLog,\n suggestion: errorInfo.details?.suggestedAction || 'Check request parameters, authentication, and data validation',\n errorInfo\n });\n }\n\n // Send structured error response\n res.status(statusCode).json({\n success: false,\n error: errorInfo\n });\n };\n}\n\n/**\n * Export convenience function\n */\nexport function createErrorHandler(options?: ErrorHandlerOptions) {\n const handler = new FjellErrorHandler(options);\n return handler.handle;\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["technical"]
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fjell/express-router",
|
|
3
3
|
"description": "Express Router for Fjell",
|
|
4
|
-
"version": "4.4.
|
|
4
|
+
"version": "4.4.84",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"express",
|
|
7
7
|
"router",
|
|
@@ -34,33 +34,45 @@
|
|
|
34
34
|
"docs:test": "cd docs && npm run test"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@fjell/core": "^4.4.
|
|
38
|
-
"@fjell/lib": "^4.4.
|
|
39
|
-
"@fjell/logging": "^4.4.
|
|
40
|
-
"@fjell/registry": "^4.4.
|
|
41
|
-
"@fjell/validation": "^4.4.
|
|
37
|
+
"@fjell/core": "^4.4.80",
|
|
38
|
+
"@fjell/lib": "^4.4.90",
|
|
39
|
+
"@fjell/logging": "^4.4.73",
|
|
40
|
+
"@fjell/registry": "^4.4.87",
|
|
41
|
+
"@fjell/validation": "^4.4.7",
|
|
42
42
|
"deepmerge": "^4.3.1",
|
|
43
|
-
"express": "^5.1
|
|
43
|
+
"express": "^5.2.1"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@eslint/eslintrc": "^3.3.1",
|
|
47
|
-
"@eslint/js": "^9.
|
|
48
|
-
"@fjell/common-config": "^1.1.
|
|
47
|
+
"@eslint/js": "^9.39.4",
|
|
48
|
+
"@fjell/common-config": "^1.1.41",
|
|
49
49
|
"@tsconfig/recommended": "^1.0.10",
|
|
50
|
-
"@types/express": "^5.0.
|
|
51
|
-
"@types/node": "^24.
|
|
52
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
53
|
-
"@typescript-eslint/parser": "^8.
|
|
54
|
-
"@vitest/coverage-v8": "^4.
|
|
55
|
-
"esbuild": "0.
|
|
56
|
-
"eslint": "^9.
|
|
50
|
+
"@types/express": "^5.0.6",
|
|
51
|
+
"@types/node": "^24.13.3",
|
|
52
|
+
"@typescript-eslint/eslint-plugin": "^8.63.0",
|
|
53
|
+
"@typescript-eslint/parser": "^8.63.0",
|
|
54
|
+
"@vitest/coverage-v8": "^4.1.10",
|
|
55
|
+
"esbuild": "0.28.1",
|
|
56
|
+
"eslint": "^9.39.4",
|
|
57
57
|
"nodemon": "^3.1.10",
|
|
58
58
|
"supertest": "^7.1.4",
|
|
59
59
|
"typescript": "^5.9.3",
|
|
60
|
-
"vitest": "^4.
|
|
60
|
+
"vitest": "^4.1.10"
|
|
61
61
|
},
|
|
62
62
|
"repository": {
|
|
63
63
|
"type": "git",
|
|
64
64
|
"url": "git+https://github.com/fjellgrunn/express-router.git"
|
|
65
|
-
}
|
|
65
|
+
},
|
|
66
|
+
"bugs": {
|
|
67
|
+
"url": "https://github.com/fjellgrunn/express-router/issues"
|
|
68
|
+
},
|
|
69
|
+
"homepage": "https://github.com/fjellgrunn/express-router#readme",
|
|
70
|
+
"author": "Fjell Team",
|
|
71
|
+
"files": [
|
|
72
|
+
"dist",
|
|
73
|
+
"README.md",
|
|
74
|
+
"LICENSE",
|
|
75
|
+
"CHANGELOG.md",
|
|
76
|
+
"guide"
|
|
77
|
+
]
|
|
66
78
|
}
|