zero-http 0.2.5 → 0.3.1

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 (89) hide show
  1. package/README.md +1250 -283
  2. package/documentation/config/db.js +25 -0
  3. package/documentation/config/middleware.js +44 -0
  4. package/documentation/config/tls.js +12 -0
  5. package/documentation/controllers/cookies.js +34 -0
  6. package/documentation/controllers/tasks.js +108 -0
  7. package/documentation/full-server.js +25 -184
  8. package/documentation/models/Task.js +21 -0
  9. package/documentation/public/data/api.json +404 -24
  10. package/documentation/public/data/docs.json +1139 -0
  11. package/documentation/public/data/examples.json +80 -2
  12. package/documentation/public/data/options.json +23 -8
  13. package/documentation/public/index.html +138 -99
  14. package/documentation/public/scripts/app.js +1 -3
  15. package/documentation/public/scripts/custom-select.js +189 -0
  16. package/documentation/public/scripts/data-sections.js +233 -250
  17. package/documentation/public/scripts/playground.js +270 -0
  18. package/documentation/public/scripts/ui.js +4 -3
  19. package/documentation/public/styles.css +56 -5
  20. package/documentation/public/vendor/icons/compress.svg +17 -17
  21. package/documentation/public/vendor/icons/database.svg +21 -0
  22. package/documentation/public/vendor/icons/env.svg +21 -0
  23. package/documentation/public/vendor/icons/fetch.svg +11 -14
  24. package/documentation/public/vendor/icons/security.svg +15 -0
  25. package/documentation/public/vendor/icons/sse.svg +12 -13
  26. package/documentation/public/vendor/icons/static.svg +12 -26
  27. package/documentation/public/vendor/icons/stream.svg +7 -13
  28. package/documentation/public/vendor/icons/validate.svg +17 -0
  29. package/documentation/routes/api.js +41 -0
  30. package/documentation/routes/core.js +20 -0
  31. package/documentation/routes/playground.js +29 -0
  32. package/documentation/routes/realtime.js +49 -0
  33. package/documentation/routes/uploads.js +71 -0
  34. package/index.js +62 -1
  35. package/lib/app.js +200 -8
  36. package/lib/body/json.js +28 -5
  37. package/lib/body/multipart.js +29 -1
  38. package/lib/body/raw.js +1 -1
  39. package/lib/body/sendError.js +1 -0
  40. package/lib/body/text.js +1 -1
  41. package/lib/body/typeMatch.js +6 -2
  42. package/lib/body/urlencoded.js +5 -2
  43. package/lib/debug.js +345 -0
  44. package/lib/env/index.js +440 -0
  45. package/lib/errors.js +231 -0
  46. package/lib/http/request.js +219 -1
  47. package/lib/http/response.js +410 -6
  48. package/lib/middleware/compress.js +39 -6
  49. package/lib/middleware/cookieParser.js +237 -0
  50. package/lib/middleware/cors.js +13 -2
  51. package/lib/middleware/csrf.js +135 -0
  52. package/lib/middleware/errorHandler.js +90 -0
  53. package/lib/middleware/helmet.js +176 -0
  54. package/lib/middleware/index.js +7 -2
  55. package/lib/middleware/rateLimit.js +12 -1
  56. package/lib/middleware/requestId.js +54 -0
  57. package/lib/middleware/static.js +95 -11
  58. package/lib/middleware/timeout.js +72 -0
  59. package/lib/middleware/validator.js +257 -0
  60. package/lib/orm/adapters/json.js +215 -0
  61. package/lib/orm/adapters/memory.js +383 -0
  62. package/lib/orm/adapters/mongo.js +444 -0
  63. package/lib/orm/adapters/mysql.js +272 -0
  64. package/lib/orm/adapters/postgres.js +394 -0
  65. package/lib/orm/adapters/sql-base.js +142 -0
  66. package/lib/orm/adapters/sqlite.js +311 -0
  67. package/lib/orm/index.js +276 -0
  68. package/lib/orm/model.js +895 -0
  69. package/lib/orm/query.js +807 -0
  70. package/lib/orm/schema.js +172 -0
  71. package/lib/router/index.js +136 -47
  72. package/lib/sse/stream.js +15 -3
  73. package/lib/ws/connection.js +19 -3
  74. package/lib/ws/handshake.js +3 -0
  75. package/lib/ws/index.js +3 -1
  76. package/lib/ws/room.js +222 -0
  77. package/package.json +15 -5
  78. package/types/app.d.ts +120 -0
  79. package/types/env.d.ts +80 -0
  80. package/types/errors.d.ts +147 -0
  81. package/types/fetch.d.ts +43 -0
  82. package/types/index.d.ts +135 -0
  83. package/types/middleware.d.ts +292 -0
  84. package/types/orm.d.ts +610 -0
  85. package/types/request.d.ts +99 -0
  86. package/types/response.d.ts +142 -0
  87. package/types/router.d.ts +78 -0
  88. package/types/sse.d.ts +78 -0
  89. package/types/websocket.d.ts +119 -0
@@ -0,0 +1,237 @@
1
+ /**
2
+ * @module cookieParser
3
+ * @description Cookie parsing middleware.
4
+ * Parses the `Cookie` header and populates `req.cookies`.
5
+ * Supports signed cookies, JSON cookies, secret rotation,
6
+ * and timing-safe signature verification.
7
+ */
8
+ const crypto = require('crypto');
9
+
10
+ // -- Internal helpers ------------------------------------
11
+
12
+ /**
13
+ * Timing-safe HMAC-SHA256 signature comparison.
14
+ * Prevents timing-based side-channel attacks on cookie signatures.
15
+ *
16
+ * @param {string} data - The cookie payload.
17
+ * @param {string} sig - The provided signature (base64, no padding).
18
+ * @param {string} secret - Secret to verify against.
19
+ * @returns {boolean}
20
+ * @private
21
+ */
22
+ function _timingSafeVerify(data, sig, secret)
23
+ {
24
+ try
25
+ {
26
+ const expected = crypto
27
+ .createHmac('sha256', secret)
28
+ .update(data)
29
+ .digest('base64')
30
+ .replace(/=+$/, '');
31
+ if (expected.length !== sig.length) return false;
32
+ return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig));
33
+ }
34
+ catch (e) { return false; }
35
+ }
36
+
37
+ /**
38
+ * Verify and unsign a signed cookie value.
39
+ * Signed cookies have the format: `s:<value>.<signature>`.
40
+ * All secret(s) are attempted to support key rotation.
41
+ *
42
+ * @param {string} val - Raw cookie value.
43
+ * @param {string[]} secrets - Array of secrets to try.
44
+ * @returns {string|false} Unsigned value on success, `false` on failure.
45
+ * @private
46
+ */
47
+ function _unsign(val, secrets)
48
+ {
49
+ if (typeof val !== 'string' || !val.startsWith('s:')) return val;
50
+ const payload = val.slice(2);
51
+ const dotIdx = payload.lastIndexOf('.');
52
+ if (dotIdx === -1) return false;
53
+
54
+ const data = payload.slice(0, dotIdx);
55
+ const sig = payload.slice(dotIdx + 1);
56
+
57
+ for (const s of secrets)
58
+ {
59
+ if (_timingSafeVerify(data, sig, s)) return data;
60
+ }
61
+ return false;
62
+ }
63
+
64
+ /**
65
+ * Try to parse a value as a JSON cookie (prefixed with `j:`).
66
+ *
67
+ * @param {string} val - Cookie value.
68
+ * @returns {*} Parsed value or original string.
69
+ * @private
70
+ */
71
+ function _parseJSONCookie(val)
72
+ {
73
+ if (typeof val !== 'string' || !val.startsWith('j:')) return val;
74
+ try { return JSON.parse(val.slice(2)); }
75
+ catch (e) { return val; }
76
+ }
77
+
78
+ // -- Middleware factory ----------------------------------
79
+
80
+ /**
81
+ * Create a cookie parsing middleware.
82
+ *
83
+ * Features:
84
+ * - Signed cookies with HMAC-SHA256 and timing-safe verification
85
+ * - Secret rotation (array of secrets, newest first)
86
+ * - JSON cookies (`j:` prefix, auto-parsed)
87
+ * - `req.secret` / `req.secrets` exposed for downstream middleware
88
+ * - URI-decode toggle
89
+ *
90
+ * @param {string|string[]} [secret] - Secret(s) for signing / verifying cookies.
91
+ * @param {object} [opts]
92
+ * @param {boolean} [opts.decode=true] - URI-decode cookie values.
93
+ * @returns {Function} Middleware `(req, res, next) => void`.
94
+ *
95
+ * @example
96
+ * app.use(cookieParser());
97
+ * app.use(cookieParser('my-secret'));
98
+ * app.use(cookieParser(['new-secret', 'old-secret'])); // key rotation
99
+ */
100
+ function cookieParser(secret, opts = {})
101
+ {
102
+ const secrets = secret
103
+ ? (Array.isArray(secret) ? secret : [secret])
104
+ : [];
105
+ const decode = opts.decode !== false;
106
+
107
+ return (req, res, next) =>
108
+ {
109
+ const header = req.headers.cookie;
110
+ req.cookies = {};
111
+ req.signedCookies = {};
112
+
113
+ // Expose secret(s) for downstream use (res.cookie signed:true, csrf, etc.)
114
+ if (secrets.length)
115
+ {
116
+ req.secret = secrets[0];
117
+ req.secrets = secrets;
118
+ }
119
+
120
+ if (!header)
121
+ {
122
+ return next();
123
+ }
124
+
125
+ const pairs = header.split(';');
126
+ for (const pair of pairs)
127
+ {
128
+ const eqIdx = pair.indexOf('=');
129
+ if (eqIdx === -1) continue;
130
+
131
+ const name = pair.slice(0, eqIdx).trim();
132
+ let val = pair.slice(eqIdx + 1).trim();
133
+
134
+ // Remove surrounding quotes if any
135
+ if (val.length >= 2 && val[0] === '"' && val[val.length - 1] === '"')
136
+ {
137
+ val = val.slice(1, -1);
138
+ }
139
+
140
+ // URI decode
141
+ if (decode)
142
+ {
143
+ try { val = decodeURIComponent(val); } catch (e) { /* keep raw */ }
144
+ }
145
+
146
+ // Signed cookies → verify, then JSON-parse if j: prefixed
147
+ if (secrets.length > 0 && val.startsWith('s:'))
148
+ {
149
+ const unsigned = _unsign(val, secrets);
150
+ if (unsigned !== false)
151
+ {
152
+ req.signedCookies[name] = _parseJSONCookie(unsigned);
153
+ }
154
+ // Failed-verification signed cookies are silently dropped
155
+ }
156
+ // JSON cookies → auto-parse
157
+ else if (val.startsWith('j:'))
158
+ {
159
+ req.cookies[name] = _parseJSONCookie(val);
160
+ }
161
+ else
162
+ {
163
+ req.cookies[name] = val;
164
+ }
165
+ }
166
+
167
+ next();
168
+ };
169
+ }
170
+
171
+ // -- Static helpers --------------------------------------
172
+
173
+ /**
174
+ * Sign a cookie value with the given secret.
175
+ *
176
+ * @param {string} val - Cookie value to sign.
177
+ * @param {string} secret - Signing secret.
178
+ * @returns {string} Signed value in format `s:<value>.<signature>`.
179
+ *
180
+ * @example
181
+ * const signed = cookieParser.sign('hello', 'my-secret');
182
+ * // => 's:hello.DGDyS...'
183
+ */
184
+ cookieParser.sign = function sign(val, secret)
185
+ {
186
+ const sig = crypto
187
+ .createHmac('sha256', secret)
188
+ .update(String(val))
189
+ .digest('base64')
190
+ .replace(/=+$/, '');
191
+ return `s:${val}.${sig}`;
192
+ };
193
+
194
+ /**
195
+ * Verify and unsign a signed cookie value.
196
+ *
197
+ * @param {string} val - Signed cookie value (`s:data.sig`).
198
+ * @param {string|string[]} secret - Secret or array of secrets (for rotation).
199
+ * @returns {string|false} Unsigned value on success, `false` on failure.
200
+ *
201
+ * @example
202
+ * const value = cookieParser.unsign('s:hello.DGDyS...', 'my-secret');
203
+ * // => 'hello' or false
204
+ */
205
+ cookieParser.unsign = function unsign(val, secret)
206
+ {
207
+ const secrets = Array.isArray(secret) ? secret : [secret];
208
+ return _unsign(val, secrets);
209
+ };
210
+
211
+ /**
212
+ * Serialize a value as a JSON cookie string (prefixed with `j:`).
213
+ *
214
+ * @param {*} val - Value to serialize (object, array, etc.).
215
+ * @returns {string} JSON cookie string.
216
+ *
217
+ * @example
218
+ * const jcookie = cookieParser.jsonCookie({ cart: [1,2,3] });
219
+ * // => 'j:{"cart":[1,2,3]}'
220
+ */
221
+ cookieParser.jsonCookie = function jsonCookie(val)
222
+ {
223
+ return 'j:' + JSON.stringify(val);
224
+ };
225
+
226
+ /**
227
+ * Parse a JSON cookie string (must start with `j:`).
228
+ *
229
+ * @param {string} str - JSON cookie string.
230
+ * @returns {*} Parsed value, or the original string if not a valid JSON cookie.
231
+ */
232
+ cookieParser.parseJSON = function parseJSON(str)
233
+ {
234
+ return _parseJSONCookie(str);
235
+ };
236
+
237
+ module.exports = cookieParser;
@@ -14,7 +14,9 @@
14
14
  * starting with `'.'` for suffix matching.
15
15
  * @param {string} [options.methods='GET,POST,PUT,DELETE,OPTIONS'] - Allowed HTTP methods.
16
16
  * @param {string} [options.allowedHeaders='Content-Type,Authorization'] - Allowed request headers.
17
- * @param {boolean} [options.credentials=false] - Whether to set `Access-Control-Allow-Credentials`.
17
+ * @param {string} [options.exposedHeaders] - Headers the browser is allowed to read.
18
+ * @param {boolean} [options.credentials=false] - Whether to set `Access-Control-Allow-Credentials`.
19
+ * @param {number} [options.maxAge] - Preflight cache duration in seconds.
18
20
  * @returns {Function} Middleware `(req, res, next) => void`.
19
21
  */
20
22
  function cors(options = {})
@@ -23,6 +25,12 @@ function cors(options = {})
23
25
  const allowMethods = (options.methods || 'GET,POST,PUT,DELETE,OPTIONS');
24
26
  const allowHeaders = (options.allowedHeaders || 'Content-Type,Authorization');
25
27
 
28
+ // RFC 6454: credentials cannot be used with wildcard origin
29
+ if (options.credentials && allowOrigin === '*')
30
+ {
31
+ throw new Error('CORS credentials cannot be used with wildcard origin "*". Specify explicit origins instead.');
32
+ }
33
+
26
34
  /**
27
35
  * Resolve the Origin header value to echo back based on the configured
28
36
  * allow-list. Returns `null` when the origin should not be allowed.
@@ -57,12 +65,15 @@ function cors(options = {})
57
65
  if (originValue)
58
66
  {
59
67
  res.set('Access-Control-Allow-Origin', originValue);
60
- // allow credentials when matching specific origin
68
+ // Set Vary: Origin when not using wildcard (important for caching proxies)
69
+ if (originValue !== '*') res.vary('Origin');
61
70
  if (options.credentials) res.set('Access-Control-Allow-Credentials', 'true');
62
71
  }
63
72
 
64
73
  if (allowMethods) res.set('Access-Control-Allow-Methods', allowMethods);
65
74
  if (allowHeaders) res.set('Access-Control-Allow-Headers', allowHeaders);
75
+ if (options.exposedHeaders) res.set('Access-Control-Expose-Headers', options.exposedHeaders);
76
+ if (options.maxAge !== undefined) res.set('Access-Control-Max-Age', String(options.maxAge));
66
77
 
67
78
  if (req.method === 'OPTIONS') return res.status(204).send();
68
79
  next();
@@ -0,0 +1,135 @@
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-http');
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]
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
+ function generateToken()
49
+ {
50
+ try
51
+ {
52
+ const salt = crypto.randomBytes(saltLen).toString('hex');
53
+ const hash = crypto.createHmac('sha256', secret).update(salt).digest('hex');
54
+ return `${salt}.${hash}`;
55
+ }
56
+ catch (e) { return null; }
57
+ }
58
+
59
+ function verifyToken(token)
60
+ {
61
+ if (!token || typeof token !== 'string') return false;
62
+ const parts = token.split('.');
63
+ if (parts.length !== 2) return false;
64
+ const [salt, hash] = parts;
65
+ try
66
+ {
67
+ const expected = crypto.createHmac('sha256', secret).update(salt).digest('hex');
68
+ // Constant-time comparison
69
+ if (expected.length !== hash.length) return false;
70
+ return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(hash));
71
+ }
72
+ catch (e) { return false; }
73
+ }
74
+
75
+ return function csrfMiddleware(req, res, next)
76
+ {
77
+ // Skip safe methods
78
+ if (ignoreMethods.has(req.method))
79
+ {
80
+ // Ensure a token exists in the cookie for the client to read
81
+ const existing = req.cookies && req.cookies[cookieName];
82
+ if (!existing || !verifyToken(existing))
83
+ {
84
+ const token = generateToken();
85
+ const secure = req.secure ? '; Secure' : '';
86
+ res.set('Set-Cookie',
87
+ `${cookieName}=${token}; Path=/; HttpOnly; SameSite=Strict${secure}`
88
+ );
89
+ req.csrfToken = token;
90
+ }
91
+ else
92
+ {
93
+ req.csrfToken = existing;
94
+ }
95
+ return next();
96
+ }
97
+
98
+ // Skip ignored paths
99
+ const pathname = req.url.split('?')[0];
100
+ for (const prefix of ignorePaths)
101
+ {
102
+ if (pathname.startsWith(prefix)) return next();
103
+ }
104
+
105
+ // Extract the token the client sent
106
+ const clientToken =
107
+ req.headers[headerName] ||
108
+ (req.body && req.body._csrf) ||
109
+ (req.query && req.query._csrf) ||
110
+ null;
111
+
112
+ // Extract the cookie token
113
+ const cookieToken = req.cookies && req.cookies[cookieName];
114
+
115
+ // Both must exist and be valid, and must match
116
+ if (!clientToken || !cookieToken || clientToken !== cookieToken || !verifyToken(clientToken))
117
+ {
118
+ log.warn('CSRF validation failed for %s %s', req.method, pathname);
119
+ if (options.onError) return options.onError(req, res);
120
+ res.status(403).json({ error: 'CSRF token missing or invalid' });
121
+ return;
122
+ }
123
+
124
+ // Rotate token on each state-changing request
125
+ const newToken = generateToken();
126
+ const secure = req.secure ? '; Secure' : '';
127
+ res.set('Set-Cookie',
128
+ `${cookieName}=${newToken}; Path=/; HttpOnly; SameSite=Strict${secure}`
129
+ );
130
+ req.csrfToken = newToken;
131
+ next();
132
+ };
133
+ }
134
+
135
+ module.exports = csrf;
@@ -0,0 +1,90 @@
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
+ * @param {object} [opts]
8
+ * @param {boolean} [opts.stack] - Include stack traces in responses (default: true when NODE_ENV !== 'production').
9
+ * @param {boolean} [opts.log] - Log errors to console (default: true).
10
+ * @param {function} [opts.logger] - Custom log function (default: console.error).
11
+ * @param {function} [opts.formatter] - Custom response formatter: (err, req, isDev) => object.
12
+ * @param {function} [opts.onError] - Callback on every error: (err, req, res) => void.
13
+ * @returns {function} Error-handling middleware `(err, req, res, next) => void`.
14
+ */
15
+ const { HttpError, isHttpError } = require('../errors');
16
+
17
+ function errorHandler(opts = {})
18
+ {
19
+ const isDev = opts.stack !== undefined
20
+ ? opts.stack
21
+ : (process.env.NODE_ENV !== 'production');
22
+
23
+ const shouldLog = opts.log !== undefined ? opts.log : true;
24
+ const logFn = typeof opts.logger === 'function' ? opts.logger : console.error;
25
+ const formatter = typeof opts.formatter === 'function' ? opts.formatter : null;
26
+ const onError = typeof opts.onError === 'function' ? opts.onError : null;
27
+
28
+ return (err, req, res, next) =>
29
+ {
30
+ // Resolve status code
31
+ let statusCode = err.statusCode || err.status || 500;
32
+ if (typeof statusCode !== 'number' || statusCode < 100 || statusCode > 599) statusCode = 500;
33
+
34
+ // Log the error
35
+ if (shouldLog)
36
+ {
37
+ const method = req.method || 'UNKNOWN';
38
+ const url = req.url || req.originalUrl || '/';
39
+ const prefix = `[${method} ${url}]`;
40
+
41
+ if (statusCode >= 500)
42
+ {
43
+ logFn(`${prefix} ${statusCode} - ${err.message}`);
44
+ if (err.stack) logFn(err.stack);
45
+ }
46
+ else
47
+ {
48
+ logFn(`${prefix} ${statusCode} - ${err.message}`);
49
+ }
50
+ }
51
+
52
+ // Callback hook
53
+ if (onError) onError(err, req, res);
54
+
55
+ // Don't send if headers already sent
56
+ if (res.headersSent || (res.raw && res.raw.headersSent))
57
+ {
58
+ return;
59
+ }
60
+
61
+ // Build response body
62
+ let body;
63
+
64
+ if (formatter)
65
+ {
66
+ body = formatter(err, req, isDev);
67
+ }
68
+ else if (isHttpError(err))
69
+ {
70
+ body = err.toJSON ? err.toJSON() : { error: err.message, code: err.code, statusCode };
71
+ if (isDev && err.stack) body.stack = err.stack.split('\n');
72
+ }
73
+ else
74
+ {
75
+ // Generic error
76
+ body = {
77
+ error: statusCode >= 500 && !isDev
78
+ ? 'Internal Server Error' // Hide internal details in production
79
+ : (err.message || 'Internal Server Error'),
80
+ statusCode,
81
+ };
82
+ if (err.code) body.code = err.code;
83
+ if (isDev && err.stack) body.stack = err.stack.split('\n');
84
+ }
85
+
86
+ res.status(statusCode).json(body);
87
+ };
88
+ }
89
+
90
+ module.exports = errorHandler;
@@ -0,0 +1,176 @@
1
+ /**
2
+ * @module helmet
3
+ * @description Security headers middleware.
4
+ * Sets common security-related HTTP response headers to help
5
+ * protect against well-known web vulnerabilities (XSS, clickjacking,
6
+ * MIME sniffing, etc.).
7
+ *
8
+ * Inspired by the `helmet` npm package but zero-dependency.
9
+ */
10
+
11
+ /**
12
+ * Create a security headers middleware.
13
+ *
14
+ * @param {object} [opts]
15
+ * @param {object|false} [opts.contentSecurityPolicy] - CSP directive object or `false` to disable.
16
+ * @param {boolean} [opts.crossOriginEmbedderPolicy=false] - Set COEP header.
17
+ * @param {string|false} [opts.crossOriginOpenerPolicy='same-origin'] - COOP value.
18
+ * @param {string|false} [opts.crossOriginResourcePolicy='same-origin'] - CORP value.
19
+ * @param {boolean} [opts.dnsPrefetchControl=true] - Set X-DNS-Prefetch-Control: off.
20
+ * @param {string|false} [opts.frameguard='deny'] - X-Frame-Options value ('deny' | 'sameorigin').
21
+ * @param {boolean} [opts.hidePoweredBy=true] - Remove X-Powered-By header.
22
+ * @param {boolean|number}[opts.hsts=true] - Set Strict-Transport-Security.
23
+ * @param {number} [opts.hstsMaxAge=15552000] - HSTS max-age in seconds (default ~180 days).
24
+ * @param {boolean} [opts.hstsIncludeSubDomains=true] - HSTS includeSubDomains directive.
25
+ * @param {boolean} [opts.hstsPreload=false] - HSTS preload directive.
26
+ * @param {boolean} [opts.ieNoOpen=true] - Set X-Download-Options: noopen.
27
+ * @param {boolean} [opts.noSniff=true] - Set X-Content-Type-Options: nosniff.
28
+ * @param {string|false} [opts.permittedCrossDomainPolicies='none'] - X-Permitted-Cross-Domain-Policies.
29
+ * @param {string|false} [opts.referrerPolicy='no-referrer'] - Referrer-Policy value.
30
+ * @param {boolean} [opts.xssFilter=false] - Set X-XSS-Protection (legacy, off by default).
31
+ * @returns {Function} Middleware `(req, res, next) => void`.
32
+ *
33
+ * @example
34
+ * app.use(helmet());
35
+ * app.use(helmet({ frameguard: 'sameorigin', hsts: false }));
36
+ * app.use(helmet({
37
+ * contentSecurityPolicy: {
38
+ * directives: {
39
+ * defaultSrc: ["'self'"],
40
+ * scriptSrc: ["'self'", "'unsafe-inline'"],
41
+ * styleSrc: ["'self'", "'unsafe-inline'"],
42
+ * imgSrc: ["'self'", "data:", "https:"],
43
+ * }
44
+ * }
45
+ * }));
46
+ */
47
+ function helmet(opts = {})
48
+ {
49
+ return (req, res, next) =>
50
+ {
51
+ const raw = res.raw || res;
52
+
53
+ // -- Content-Security-Policy --------------------
54
+ if (opts.contentSecurityPolicy !== false)
55
+ {
56
+ const csp = opts.contentSecurityPolicy || {};
57
+ const directives = csp.directives || {
58
+ defaultSrc: ["'self'"],
59
+ baseUri: ["'self'"],
60
+ fontSrc: ["'self'", 'https:', 'data:'],
61
+ formAction: ["'self'"],
62
+ frameAncestors: ["'self'"],
63
+ imgSrc: ["'self'", 'data:'],
64
+ objectSrc: ["'none'"],
65
+ scriptSrc: ["'self'"],
66
+ scriptSrcAttr: ["'none'"],
67
+ styleSrc: ["'self'", "'unsafe-inline'"],
68
+ upgradeInsecureRequests: [],
69
+ };
70
+
71
+ const cspString = Object.entries(directives)
72
+ .map(([key, values]) =>
73
+ {
74
+ const directive = key.replace(/([A-Z])/g, '-$1').toLowerCase();
75
+ if (Array.isArray(values) && values.length === 0) return directive;
76
+ return `${directive} ${Array.isArray(values) ? values.join(' ') : values}`;
77
+ })
78
+ .join('; ');
79
+
80
+ if (cspString)
81
+ {
82
+ try { raw.setHeader('Content-Security-Policy', cspString); } catch (e) { }
83
+ }
84
+ }
85
+
86
+ // -- Cross-Origin-Embedder-Policy ---------------
87
+ if (opts.crossOriginEmbedderPolicy)
88
+ {
89
+ try { raw.setHeader('Cross-Origin-Embedder-Policy', 'require-corp'); } catch (e) { }
90
+ }
91
+
92
+ // -- Cross-Origin-Opener-Policy -----------------
93
+ if (opts.crossOriginOpenerPolicy !== false)
94
+ {
95
+ const coop = opts.crossOriginOpenerPolicy || 'same-origin';
96
+ try { raw.setHeader('Cross-Origin-Opener-Policy', coop); } catch (e) { }
97
+ }
98
+
99
+ // -- Cross-Origin-Resource-Policy ---------------
100
+ if (opts.crossOriginResourcePolicy !== false)
101
+ {
102
+ const corp = opts.crossOriginResourcePolicy || 'same-origin';
103
+ try { raw.setHeader('Cross-Origin-Resource-Policy', corp); } catch (e) { }
104
+ }
105
+
106
+ // -- DNS Prefetch Control -----------------------
107
+ if (opts.dnsPrefetchControl !== false)
108
+ {
109
+ try { raw.setHeader('X-DNS-Prefetch-Control', 'off'); } catch (e) { }
110
+ }
111
+
112
+ // -- Frameguard (X-Frame-Options) ---------------
113
+ if (opts.frameguard !== false)
114
+ {
115
+ const frame = (opts.frameguard || 'deny').toUpperCase();
116
+ try { raw.setHeader('X-Frame-Options', frame); } catch (e) { }
117
+ }
118
+
119
+ // -- Hide X-Powered-By -------------------------
120
+ if (opts.hidePoweredBy !== false)
121
+ {
122
+ try { raw.removeHeader('X-Powered-By'); } catch (e) { }
123
+ }
124
+
125
+ // -- HSTS ---------------------------------------
126
+ if (opts.hsts !== false)
127
+ {
128
+ const maxAge = opts.hstsMaxAge || 15552000;
129
+ let hstsValue = `max-age=${maxAge}`;
130
+ if (opts.hstsIncludeSubDomains !== false) hstsValue += '; includeSubDomains';
131
+ if (opts.hstsPreload) hstsValue += '; preload';
132
+ try { raw.setHeader('Strict-Transport-Security', hstsValue); } catch (e) { }
133
+ }
134
+
135
+ // -- IE No Open --------------------------------
136
+ if (opts.ieNoOpen !== false)
137
+ {
138
+ try { raw.setHeader('X-Download-Options', 'noopen'); } catch (e) { }
139
+ }
140
+
141
+ // -- No Sniff -----------------------------------
142
+ if (opts.noSniff !== false)
143
+ {
144
+ try { raw.setHeader('X-Content-Type-Options', 'nosniff'); } catch (e) { }
145
+ }
146
+
147
+ // -- Permitted Cross Domain Policies ------------
148
+ if (opts.permittedCrossDomainPolicies !== false)
149
+ {
150
+ const pcdp = opts.permittedCrossDomainPolicies || 'none';
151
+ try { raw.setHeader('X-Permitted-Cross-Domain-Policies', pcdp); } catch (e) { }
152
+ }
153
+
154
+ // -- Referrer Policy ----------------------------
155
+ if (opts.referrerPolicy !== false)
156
+ {
157
+ const rp = opts.referrerPolicy || 'no-referrer';
158
+ try { raw.setHeader('Referrer-Policy', rp); } catch (e) { }
159
+ }
160
+
161
+ // -- XSS Filter (legacy) -----------------------
162
+ if (opts.xssFilter)
163
+ {
164
+ try { raw.setHeader('X-XSS-Protection', '1; mode=block'); } catch (e) { }
165
+ }
166
+ else
167
+ {
168
+ // Modern best practice: disable legacy XSS auditor
169
+ try { raw.setHeader('X-XSS-Protection', '0'); } catch (e) { }
170
+ }
171
+
172
+ next();
173
+ };
174
+ }
175
+
176
+ module.exports = helmet;