@zero-server/sdk 0.9.1 → 0.9.2
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/LICENSE +21 -21
- package/README.md +460 -443
- package/index.js +414 -412
- package/lib/app.js +1172 -1172
- package/lib/auth/authorize.js +399 -399
- package/lib/auth/enrollment.js +367 -367
- package/lib/auth/index.js +57 -57
- package/lib/auth/jwt.js +731 -731
- package/lib/auth/oauth.js +362 -362
- package/lib/auth/session.js +588 -588
- package/lib/auth/trustedDevice.js +409 -409
- package/lib/auth/twoFactor.js +1150 -1150
- package/lib/auth/webauthn.js +946 -946
- package/lib/body/index.js +14 -14
- package/lib/body/json.js +109 -109
- package/lib/body/multipart.js +440 -440
- package/lib/body/raw.js +71 -71
- package/lib/body/rawBuffer.js +160 -160
- package/lib/body/sendError.js +25 -25
- package/lib/body/text.js +75 -75
- package/lib/body/typeMatch.js +41 -41
- package/lib/body/urlencoded.js +235 -235
- package/lib/cli.js +845 -845
- package/lib/cluster.js +666 -666
- package/lib/debug.js +372 -372
- package/lib/env/index.js +465 -465
- package/lib/errors.js +683 -683
- package/lib/fetch/index.js +256 -256
- package/lib/grpc/balancer.js +378 -378
- package/lib/grpc/call.js +708 -708
- package/lib/grpc/client.js +764 -764
- package/lib/grpc/codec.js +1221 -1221
- package/lib/grpc/credentials.js +398 -398
- package/lib/grpc/frame.js +262 -262
- package/lib/grpc/health.js +287 -287
- package/lib/grpc/index.js +121 -121
- package/lib/grpc/metadata.js +461 -461
- package/lib/grpc/proto.js +821 -821
- package/lib/grpc/reflection.js +590 -590
- package/lib/grpc/server.js +445 -445
- package/lib/grpc/status.js +118 -118
- package/lib/grpc/watch.js +173 -173
- package/lib/http/index.js +10 -10
- package/lib/http/request.js +727 -727
- package/lib/http/response.js +799 -799
- package/lib/lifecycle.js +557 -557
- package/lib/middleware/compress.js +230 -230
- package/lib/middleware/cookieParser.js +237 -237
- package/lib/middleware/cors.js +93 -93
- package/lib/middleware/csrf.js +137 -137
- package/lib/middleware/errorHandler.js +101 -101
- package/lib/middleware/helmet.js +175 -175
- package/lib/middleware/index.js +19 -17
- package/lib/middleware/logger.js +74 -74
- package/lib/middleware/rateLimit.js +88 -88
- package/lib/middleware/requestId.js +53 -53
- package/lib/middleware/static.js +326 -326
- package/lib/middleware/timeout.js +71 -71
- package/lib/middleware/validator.js +255 -255
- package/lib/observe/health.js +326 -326
- package/lib/observe/index.js +50 -50
- package/lib/observe/logger.js +359 -359
- package/lib/observe/metrics.js +805 -805
- package/lib/observe/tracing.js +592 -592
- package/lib/orm/adapters/json.js +290 -290
- package/lib/orm/adapters/memory.js +764 -764
- package/lib/orm/adapters/mongo.js +764 -764
- package/lib/orm/adapters/mysql.js +933 -933
- package/lib/orm/adapters/postgres.js +1144 -1144
- package/lib/orm/adapters/redis.js +1534 -1534
- package/lib/orm/adapters/sql-base.js +212 -212
- package/lib/orm/adapters/sqlite.js +858 -858
- package/lib/orm/audit.js +649 -649
- package/lib/orm/cache.js +394 -394
- package/lib/orm/geo.js +387 -387
- package/lib/orm/index.js +784 -784
- package/lib/orm/migrate.js +432 -432
- package/lib/orm/model.js +1706 -1706
- package/lib/orm/plugin.js +375 -375
- package/lib/orm/procedures.js +836 -836
- package/lib/orm/profiler.js +233 -233
- package/lib/orm/query.js +1772 -1772
- package/lib/orm/replicas.js +241 -241
- package/lib/orm/schema.js +307 -307
- package/lib/orm/search.js +380 -380
- package/lib/orm/seed/data/commerce.js +136 -136
- package/lib/orm/seed/data/internet.js +111 -111
- package/lib/orm/seed/data/locations.js +204 -204
- package/lib/orm/seed/data/names.js +338 -338
- package/lib/orm/seed/data/person.js +128 -128
- package/lib/orm/seed/data/phone.js +211 -211
- package/lib/orm/seed/data/words.js +134 -134
- package/lib/orm/seed/factory.js +178 -178
- package/lib/orm/seed/fake.js +1186 -1186
- package/lib/orm/seed/index.js +18 -18
- package/lib/orm/seed/rng.js +70 -70
- package/lib/orm/seed/seeder.js +124 -124
- package/lib/orm/seed/unique.js +68 -68
- package/lib/orm/snapshot.js +366 -366
- package/lib/orm/tenancy.js +605 -605
- package/lib/orm/views.js +350 -350
- package/lib/router/index.js +436 -436
- package/lib/sse/index.js +8 -8
- package/lib/sse/stream.js +349 -349
- package/lib/ws/connection.js +451 -451
- package/lib/ws/handshake.js +125 -125
- package/lib/ws/index.js +14 -14
- package/lib/ws/room.js +223 -223
- package/package.json +73 -73
- package/types/app.d.ts +223 -223
- package/types/auth.d.ts +520 -520
- package/types/cluster.d.ts +75 -75
- package/types/env.d.ts +80 -80
- package/types/errors.d.ts +316 -316
- package/types/fetch.d.ts +43 -43
- package/types/grpc.d.ts +432 -432
- package/types/index.d.ts +384 -384
- package/types/lifecycle.d.ts +60 -60
- package/types/middleware.d.ts +320 -320
- package/types/observe.d.ts +304 -304
- package/types/orm.d.ts +1887 -1887
- package/types/request.d.ts +109 -109
- package/types/response.d.ts +157 -157
- package/types/router.d.ts +78 -78
- package/types/sse.d.ts +78 -78
- package/types/websocket.d.ts +126 -126
package/lib/middleware/csrf.js
CHANGED
|
@@ -1,137 +1,137 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module middleware/csrf
|
|
3
|
-
* @description CSRF (Cross-Site Request Forgery) protection middleware.
|
|
4
|
-
* Uses the double-submit cookie + header/body token pattern.
|
|
5
|
-
*
|
|
6
|
-
* Safe methods (GET, HEAD, OPTIONS) are skipped automatically.
|
|
7
|
-
* For state-changing requests (POST, PUT, PATCH, DELETE), the
|
|
8
|
-
* middleware checks for a matching token in:
|
|
9
|
-
* 1. `req.headers['x-csrf-token']`
|
|
10
|
-
* 2. `req.body._csrf` (if body parsed)
|
|
11
|
-
* 3. `req.query._csrf`
|
|
12
|
-
*
|
|
13
|
-
* @example
|
|
14
|
-
* const { createApp, csrf } = require('@zero-server/sdk');
|
|
15
|
-
* const app = createApp();
|
|
16
|
-
*
|
|
17
|
-
* app.use(csrf()); // default options
|
|
18
|
-
* app.use(csrf({ cookie: 'tok' })); // custom cookie name
|
|
19
|
-
*
|
|
20
|
-
* // In a route, read the token for forms / SPA:
|
|
21
|
-
* app.get('/form', (req, res) => {
|
|
22
|
-
* res.json({ csrfToken: req.csrfToken });
|
|
23
|
-
* });
|
|
24
|
-
*/
|
|
25
|
-
const crypto = require('crypto');
|
|
26
|
-
const log = require('../debug')('zero:csrf');
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* @param {object} [options] - Configuration options.
|
|
30
|
-
* @param {string} [options.cookie='_csrf'] - Name of the double-submit cookie.
|
|
31
|
-
* @param {string} [options.header='x-csrf-token'] - Request header that carries the token.
|
|
32
|
-
* @param {number} [options.saltLength=18] - Bytes of randomness for token generation.
|
|
33
|
-
* @param {string} [options.secret] - HMAC secret. Auto-generated per process if omitted.
|
|
34
|
-
* @param {string[]} [options.ignoreMethods] - HTTP methods to skip. Default: GET, HEAD, OPTIONS.
|
|
35
|
-
* @param {string[]} [options.ignorePaths] - Path prefixes to skip (e.g. ['/api/webhooks']).
|
|
36
|
-
* @param {Function} [options.onError] - Custom error handler `(req, res) => {}`.
|
|
37
|
-
* @returns {Function} Middleware function.
|
|
38
|
-
*/
|
|
39
|
-
function csrf(options = {})
|
|
40
|
-
{
|
|
41
|
-
const cookieName = options.cookie || '_csrf';
|
|
42
|
-
const headerName = (options.header || 'x-csrf-token').toLowerCase();
|
|
43
|
-
const saltLen = options.saltLength || 18;
|
|
44
|
-
const secret = options.secret || crypto.randomBytes(32).toString('hex');
|
|
45
|
-
const ignoreMethods = new Set((options.ignoreMethods || ['GET', 'HEAD', 'OPTIONS']).map(m => m.toUpperCase()));
|
|
46
|
-
const ignorePaths = options.ignorePaths || [];
|
|
47
|
-
|
|
48
|
-
/** @private */
|
|
49
|
-
function generateToken()
|
|
50
|
-
{
|
|
51
|
-
try
|
|
52
|
-
{
|
|
53
|
-
const salt = crypto.randomBytes(saltLen).toString('hex');
|
|
54
|
-
const hash = crypto.createHmac('sha256', secret).update(salt).digest('hex');
|
|
55
|
-
return `${salt}.${hash}`;
|
|
56
|
-
}
|
|
57
|
-
catch (e) { return null; }
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/** @private */
|
|
61
|
-
function verifyToken(token)
|
|
62
|
-
{
|
|
63
|
-
if (!token || typeof token !== 'string') return false;
|
|
64
|
-
const parts = token.split('.');
|
|
65
|
-
if (parts.length !== 2) return false;
|
|
66
|
-
const [salt, hash] = parts;
|
|
67
|
-
try
|
|
68
|
-
{
|
|
69
|
-
const expected = crypto.createHmac('sha256', secret).update(salt).digest('hex');
|
|
70
|
-
// Constant-time comparison
|
|
71
|
-
if (expected.length !== hash.length) return false;
|
|
72
|
-
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(hash));
|
|
73
|
-
}
|
|
74
|
-
catch (e) { return false; }
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
return function csrfMiddleware(req, res, next)
|
|
78
|
-
{
|
|
79
|
-
// Skip safe methods
|
|
80
|
-
if (ignoreMethods.has(req.method))
|
|
81
|
-
{
|
|
82
|
-
// Ensure a token exists in the cookie for the client to read
|
|
83
|
-
const existing = req.cookies && req.cookies[cookieName];
|
|
84
|
-
if (!existing || !verifyToken(existing))
|
|
85
|
-
{
|
|
86
|
-
const token = generateToken();
|
|
87
|
-
const secure = req.secure ? '; Secure' : '';
|
|
88
|
-
res.set('Set-Cookie',
|
|
89
|
-
`${cookieName}=${token}; Path=/; HttpOnly; SameSite=Strict${secure}`
|
|
90
|
-
);
|
|
91
|
-
req.csrfToken = token;
|
|
92
|
-
}
|
|
93
|
-
else
|
|
94
|
-
{
|
|
95
|
-
req.csrfToken = existing;
|
|
96
|
-
}
|
|
97
|
-
return next();
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// Skip ignored paths
|
|
101
|
-
const pathname = req.url.split('?')[0];
|
|
102
|
-
for (const prefix of ignorePaths)
|
|
103
|
-
{
|
|
104
|
-
if (pathname.startsWith(prefix)) return next();
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Extract the token the client sent
|
|
108
|
-
const clientToken =
|
|
109
|
-
req.headers[headerName] ||
|
|
110
|
-
(req.body && req.body._csrf) ||
|
|
111
|
-
(req.query && req.query._csrf) ||
|
|
112
|
-
null;
|
|
113
|
-
|
|
114
|
-
// Extract the cookie token
|
|
115
|
-
const cookieToken = req.cookies && req.cookies[cookieName];
|
|
116
|
-
|
|
117
|
-
// Both must exist and be valid, and must match
|
|
118
|
-
if (!clientToken || !cookieToken || clientToken !== cookieToken || !verifyToken(clientToken))
|
|
119
|
-
{
|
|
120
|
-
log.warn('CSRF validation failed for %s %s', req.method, pathname);
|
|
121
|
-
if (options.onError) return options.onError(req, res);
|
|
122
|
-
res.status(403).json({ error: 'CSRF token missing or invalid' });
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// Rotate token on each state-changing request
|
|
127
|
-
const newToken = generateToken();
|
|
128
|
-
const secure = req.secure ? '; Secure' : '';
|
|
129
|
-
res.set('Set-Cookie',
|
|
130
|
-
`${cookieName}=${newToken}; Path=/; HttpOnly; SameSite=Strict${secure}`
|
|
131
|
-
);
|
|
132
|
-
req.csrfToken = newToken;
|
|
133
|
-
next();
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
module.exports = csrf;
|
|
1
|
+
/**
|
|
2
|
+
* @module middleware/csrf
|
|
3
|
+
* @description CSRF (Cross-Site Request Forgery) protection middleware.
|
|
4
|
+
* Uses the double-submit cookie + header/body token pattern.
|
|
5
|
+
*
|
|
6
|
+
* Safe methods (GET, HEAD, OPTIONS) are skipped automatically.
|
|
7
|
+
* For state-changing requests (POST, PUT, PATCH, DELETE), the
|
|
8
|
+
* middleware checks for a matching token in:
|
|
9
|
+
* 1. `req.headers['x-csrf-token']`
|
|
10
|
+
* 2. `req.body._csrf` (if body parsed)
|
|
11
|
+
* 3. `req.query._csrf`
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* const { createApp, csrf } = require('@zero-server/sdk');
|
|
15
|
+
* const app = createApp();
|
|
16
|
+
*
|
|
17
|
+
* app.use(csrf()); // default options
|
|
18
|
+
* app.use(csrf({ cookie: 'tok' })); // custom cookie name
|
|
19
|
+
*
|
|
20
|
+
* // In a route, read the token for forms / SPA:
|
|
21
|
+
* app.get('/form', (req, res) => {
|
|
22
|
+
* res.json({ csrfToken: req.csrfToken });
|
|
23
|
+
* });
|
|
24
|
+
*/
|
|
25
|
+
const crypto = require('crypto');
|
|
26
|
+
const log = require('../debug')('zero:csrf');
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @param {object} [options] - Configuration options.
|
|
30
|
+
* @param {string} [options.cookie='_csrf'] - Name of the double-submit cookie.
|
|
31
|
+
* @param {string} [options.header='x-csrf-token'] - Request header that carries the token.
|
|
32
|
+
* @param {number} [options.saltLength=18] - Bytes of randomness for token generation.
|
|
33
|
+
* @param {string} [options.secret] - HMAC secret. Auto-generated per process if omitted.
|
|
34
|
+
* @param {string[]} [options.ignoreMethods] - HTTP methods to skip. Default: GET, HEAD, OPTIONS.
|
|
35
|
+
* @param {string[]} [options.ignorePaths] - Path prefixes to skip (e.g. ['/api/webhooks']).
|
|
36
|
+
* @param {Function} [options.onError] - Custom error handler `(req, res) => {}`.
|
|
37
|
+
* @returns {Function} Middleware function.
|
|
38
|
+
*/
|
|
39
|
+
function csrf(options = {})
|
|
40
|
+
{
|
|
41
|
+
const cookieName = options.cookie || '_csrf';
|
|
42
|
+
const headerName = (options.header || 'x-csrf-token').toLowerCase();
|
|
43
|
+
const saltLen = options.saltLength || 18;
|
|
44
|
+
const secret = options.secret || crypto.randomBytes(32).toString('hex');
|
|
45
|
+
const ignoreMethods = new Set((options.ignoreMethods || ['GET', 'HEAD', 'OPTIONS']).map(m => m.toUpperCase()));
|
|
46
|
+
const ignorePaths = options.ignorePaths || [];
|
|
47
|
+
|
|
48
|
+
/** @private */
|
|
49
|
+
function generateToken()
|
|
50
|
+
{
|
|
51
|
+
try
|
|
52
|
+
{
|
|
53
|
+
const salt = crypto.randomBytes(saltLen).toString('hex');
|
|
54
|
+
const hash = crypto.createHmac('sha256', secret).update(salt).digest('hex');
|
|
55
|
+
return `${salt}.${hash}`;
|
|
56
|
+
}
|
|
57
|
+
catch (e) { return null; }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** @private */
|
|
61
|
+
function verifyToken(token)
|
|
62
|
+
{
|
|
63
|
+
if (!token || typeof token !== 'string') return false;
|
|
64
|
+
const parts = token.split('.');
|
|
65
|
+
if (parts.length !== 2) return false;
|
|
66
|
+
const [salt, hash] = parts;
|
|
67
|
+
try
|
|
68
|
+
{
|
|
69
|
+
const expected = crypto.createHmac('sha256', secret).update(salt).digest('hex');
|
|
70
|
+
// Constant-time comparison
|
|
71
|
+
if (expected.length !== hash.length) return false;
|
|
72
|
+
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(hash));
|
|
73
|
+
}
|
|
74
|
+
catch (e) { return false; }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return function csrfMiddleware(req, res, next)
|
|
78
|
+
{
|
|
79
|
+
// Skip safe methods
|
|
80
|
+
if (ignoreMethods.has(req.method))
|
|
81
|
+
{
|
|
82
|
+
// Ensure a token exists in the cookie for the client to read
|
|
83
|
+
const existing = req.cookies && req.cookies[cookieName];
|
|
84
|
+
if (!existing || !verifyToken(existing))
|
|
85
|
+
{
|
|
86
|
+
const token = generateToken();
|
|
87
|
+
const secure = req.secure ? '; Secure' : '';
|
|
88
|
+
res.set('Set-Cookie',
|
|
89
|
+
`${cookieName}=${token}; Path=/; HttpOnly; SameSite=Strict${secure}`
|
|
90
|
+
);
|
|
91
|
+
req.csrfToken = token;
|
|
92
|
+
}
|
|
93
|
+
else
|
|
94
|
+
{
|
|
95
|
+
req.csrfToken = existing;
|
|
96
|
+
}
|
|
97
|
+
return next();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Skip ignored paths
|
|
101
|
+
const pathname = req.url.split('?')[0];
|
|
102
|
+
for (const prefix of ignorePaths)
|
|
103
|
+
{
|
|
104
|
+
if (pathname.startsWith(prefix)) return next();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Extract the token the client sent
|
|
108
|
+
const clientToken =
|
|
109
|
+
req.headers[headerName] ||
|
|
110
|
+
(req.body && req.body._csrf) ||
|
|
111
|
+
(req.query && req.query._csrf) ||
|
|
112
|
+
null;
|
|
113
|
+
|
|
114
|
+
// Extract the cookie token
|
|
115
|
+
const cookieToken = req.cookies && req.cookies[cookieName];
|
|
116
|
+
|
|
117
|
+
// Both must exist and be valid, and must match
|
|
118
|
+
if (!clientToken || !cookieToken || clientToken !== cookieToken || !verifyToken(clientToken))
|
|
119
|
+
{
|
|
120
|
+
log.warn('CSRF validation failed for %s %s', req.method, pathname);
|
|
121
|
+
if (options.onError) return options.onError(req, res);
|
|
122
|
+
res.status(403).json({ error: 'CSRF token missing or invalid' });
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Rotate token on each state-changing request
|
|
127
|
+
const newToken = generateToken();
|
|
128
|
+
const secure = req.secure ? '; Secure' : '';
|
|
129
|
+
res.set('Set-Cookie',
|
|
130
|
+
`${cookieName}=${newToken}; Path=/; HttpOnly; SameSite=Strict${secure}`
|
|
131
|
+
);
|
|
132
|
+
req.csrfToken = newToken;
|
|
133
|
+
next();
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
module.exports = csrf;
|
|
@@ -1,101 +1,101 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module middleware/errorHandler
|
|
3
|
-
* @description Configurable error-handling middleware that formats error responses
|
|
4
|
-
* based on environment (dev vs production), supports custom formatters,
|
|
5
|
-
* and integrates with HttpError classes.
|
|
6
|
-
*/
|
|
7
|
-
const { HttpError, isHttpError } = require('../errors');
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Create an error-handling middleware.
|
|
11
|
-
*
|
|
12
|
-
* @param {object} [opts] - Configuration options.
|
|
13
|
-
* @param {boolean} [opts.stack] - Include stack traces in responses (default: true when NODE_ENV !== 'production').
|
|
14
|
-
* @param {boolean} [opts.log] - Log errors to console (default: true).
|
|
15
|
-
* @param {function} [opts.logger] - Custom log function (default: console.error).
|
|
16
|
-
* @param {function} [opts.formatter] - Custom response formatter: (err, req, isDev) => object.
|
|
17
|
-
* @param {function} [opts.onError] - Callback on every error: (err, req, res) => void.
|
|
18
|
-
* @returns {Function} Error-handling middleware `(err, req, res, next) => void`.
|
|
19
|
-
*
|
|
20
|
-
* @example
|
|
21
|
-
* app.use(errorHandler()); // dev-friendly by default
|
|
22
|
-
* app.use(errorHandler({ stack: false })); // hide stack traces
|
|
23
|
-
* app.use(errorHandler({
|
|
24
|
-
* formatter: (err, req, isDev) => ({ message: err.message }),
|
|
25
|
-
* onError: (err) => metrics.increment('errors'),
|
|
26
|
-
* }));
|
|
27
|
-
*/
|
|
28
|
-
function errorHandler(opts = {})
|
|
29
|
-
{
|
|
30
|
-
const isDev = opts.stack !== undefined
|
|
31
|
-
? opts.stack
|
|
32
|
-
: (process.env.NODE_ENV !== 'production');
|
|
33
|
-
|
|
34
|
-
const shouldLog = opts.log !== undefined ? opts.log : true;
|
|
35
|
-
const logFn = typeof opts.logger === 'function' ? opts.logger : console.error;
|
|
36
|
-
const formatter = typeof opts.formatter === 'function' ? opts.formatter : null;
|
|
37
|
-
const onError = typeof opts.onError === 'function' ? opts.onError : null;
|
|
38
|
-
|
|
39
|
-
return (err, req, res, next) =>
|
|
40
|
-
{
|
|
41
|
-
// Resolve status code
|
|
42
|
-
let statusCode = err.statusCode || err.status || 500;
|
|
43
|
-
if (typeof statusCode !== 'number' || statusCode < 100 || statusCode > 599) statusCode = 500;
|
|
44
|
-
|
|
45
|
-
// Log the error
|
|
46
|
-
if (shouldLog)
|
|
47
|
-
{
|
|
48
|
-
const method = req.method || 'UNKNOWN';
|
|
49
|
-
const url = req.url || req.originalUrl || '/';
|
|
50
|
-
const prefix = `[${method} ${url}]`;
|
|
51
|
-
|
|
52
|
-
if (statusCode >= 500)
|
|
53
|
-
{
|
|
54
|
-
logFn(`${prefix} ${statusCode} - ${err.message}`);
|
|
55
|
-
if (err.stack) logFn(err.stack);
|
|
56
|
-
}
|
|
57
|
-
else
|
|
58
|
-
{
|
|
59
|
-
logFn(`${prefix} ${statusCode} - ${err.message}`);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Callback hook
|
|
64
|
-
if (onError) onError(err, req, res);
|
|
65
|
-
|
|
66
|
-
// Don't send if headers already sent
|
|
67
|
-
if (res.headersSent || (res.raw && res.raw.headersSent))
|
|
68
|
-
{
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Build response body
|
|
73
|
-
let body;
|
|
74
|
-
|
|
75
|
-
if (formatter)
|
|
76
|
-
{
|
|
77
|
-
body = formatter(err, req, isDev);
|
|
78
|
-
}
|
|
79
|
-
else if (isHttpError(err))
|
|
80
|
-
{
|
|
81
|
-
body = err.toJSON ? err.toJSON() : { error: err.message, code: err.code, statusCode };
|
|
82
|
-
if (isDev && err.stack) body.stack = err.stack.split('\n');
|
|
83
|
-
}
|
|
84
|
-
else
|
|
85
|
-
{
|
|
86
|
-
// Generic error
|
|
87
|
-
body = {
|
|
88
|
-
error: statusCode >= 500 && !isDev
|
|
89
|
-
? 'Internal Server Error' // Hide internal details in production
|
|
90
|
-
: (err.message || 'Internal Server Error'),
|
|
91
|
-
statusCode,
|
|
92
|
-
};
|
|
93
|
-
if (err.code) body.code = err.code;
|
|
94
|
-
if (isDev && err.stack) body.stack = err.stack.split('\n');
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
res.status(statusCode).json(body);
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
module.exports = errorHandler;
|
|
1
|
+
/**
|
|
2
|
+
* @module middleware/errorHandler
|
|
3
|
+
* @description Configurable error-handling middleware that formats error responses
|
|
4
|
+
* based on environment (dev vs production), supports custom formatters,
|
|
5
|
+
* and integrates with HttpError classes.
|
|
6
|
+
*/
|
|
7
|
+
const { HttpError, isHttpError } = require('../errors');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Create an error-handling middleware.
|
|
11
|
+
*
|
|
12
|
+
* @param {object} [opts] - Configuration options.
|
|
13
|
+
* @param {boolean} [opts.stack] - Include stack traces in responses (default: true when NODE_ENV !== 'production').
|
|
14
|
+
* @param {boolean} [opts.log] - Log errors to console (default: true).
|
|
15
|
+
* @param {function} [opts.logger] - Custom log function (default: console.error).
|
|
16
|
+
* @param {function} [opts.formatter] - Custom response formatter: (err, req, isDev) => object.
|
|
17
|
+
* @param {function} [opts.onError] - Callback on every error: (err, req, res) => void.
|
|
18
|
+
* @returns {Function} Error-handling middleware `(err, req, res, next) => void`.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* app.use(errorHandler()); // dev-friendly by default
|
|
22
|
+
* app.use(errorHandler({ stack: false })); // hide stack traces
|
|
23
|
+
* app.use(errorHandler({
|
|
24
|
+
* formatter: (err, req, isDev) => ({ message: err.message }),
|
|
25
|
+
* onError: (err) => metrics.increment('errors'),
|
|
26
|
+
* }));
|
|
27
|
+
*/
|
|
28
|
+
function errorHandler(opts = {})
|
|
29
|
+
{
|
|
30
|
+
const isDev = opts.stack !== undefined
|
|
31
|
+
? opts.stack
|
|
32
|
+
: (process.env.NODE_ENV !== 'production');
|
|
33
|
+
|
|
34
|
+
const shouldLog = opts.log !== undefined ? opts.log : true;
|
|
35
|
+
const logFn = typeof opts.logger === 'function' ? opts.logger : console.error;
|
|
36
|
+
const formatter = typeof opts.formatter === 'function' ? opts.formatter : null;
|
|
37
|
+
const onError = typeof opts.onError === 'function' ? opts.onError : null;
|
|
38
|
+
|
|
39
|
+
return (err, req, res, next) =>
|
|
40
|
+
{
|
|
41
|
+
// Resolve status code
|
|
42
|
+
let statusCode = err.statusCode || err.status || 500;
|
|
43
|
+
if (typeof statusCode !== 'number' || statusCode < 100 || statusCode > 599) statusCode = 500;
|
|
44
|
+
|
|
45
|
+
// Log the error
|
|
46
|
+
if (shouldLog)
|
|
47
|
+
{
|
|
48
|
+
const method = req.method || 'UNKNOWN';
|
|
49
|
+
const url = req.url || req.originalUrl || '/';
|
|
50
|
+
const prefix = `[${method} ${url}]`;
|
|
51
|
+
|
|
52
|
+
if (statusCode >= 500)
|
|
53
|
+
{
|
|
54
|
+
logFn(`${prefix} ${statusCode} - ${err.message}`);
|
|
55
|
+
if (err.stack) logFn(err.stack);
|
|
56
|
+
}
|
|
57
|
+
else
|
|
58
|
+
{
|
|
59
|
+
logFn(`${prefix} ${statusCode} - ${err.message}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Callback hook
|
|
64
|
+
if (onError) onError(err, req, res);
|
|
65
|
+
|
|
66
|
+
// Don't send if headers already sent
|
|
67
|
+
if (res.headersSent || (res.raw && res.raw.headersSent))
|
|
68
|
+
{
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Build response body
|
|
73
|
+
let body;
|
|
74
|
+
|
|
75
|
+
if (formatter)
|
|
76
|
+
{
|
|
77
|
+
body = formatter(err, req, isDev);
|
|
78
|
+
}
|
|
79
|
+
else if (isHttpError(err))
|
|
80
|
+
{
|
|
81
|
+
body = err.toJSON ? err.toJSON() : { error: err.message, code: err.code, statusCode };
|
|
82
|
+
if (isDev && err.stack) body.stack = err.stack.split('\n');
|
|
83
|
+
}
|
|
84
|
+
else
|
|
85
|
+
{
|
|
86
|
+
// Generic error
|
|
87
|
+
body = {
|
|
88
|
+
error: statusCode >= 500 && !isDev
|
|
89
|
+
? 'Internal Server Error' // Hide internal details in production
|
|
90
|
+
: (err.message || 'Internal Server Error'),
|
|
91
|
+
statusCode,
|
|
92
|
+
};
|
|
93
|
+
if (err.code) body.code = err.code;
|
|
94
|
+
if (isDev && err.stack) body.stack = err.stack.split('\n');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
res.status(statusCode).json(body);
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
module.exports = errorHandler;
|