zero-http 0.2.0 → 0.2.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 (42) hide show
  1. package/README.md +314 -115
  2. package/documentation/full-server.js +82 -1
  3. package/documentation/public/data/api.json +154 -33
  4. package/documentation/public/data/examples.json +35 -11
  5. package/documentation/public/data/options.json +14 -8
  6. package/documentation/public/index.html +109 -17
  7. package/documentation/public/scripts/data-sections.js +4 -4
  8. package/documentation/public/scripts/helpers.js +4 -4
  9. package/documentation/public/scripts/playground.js +201 -0
  10. package/documentation/public/scripts/ui.js +6 -6
  11. package/documentation/public/scripts/uploads.js +9 -9
  12. package/documentation/public/styles.css +12 -12
  13. package/documentation/public/vendor/icons/compress.svg +27 -0
  14. package/documentation/public/vendor/icons/https.svg +23 -0
  15. package/documentation/public/vendor/icons/router.svg +29 -0
  16. package/documentation/public/vendor/icons/sse.svg +25 -0
  17. package/documentation/public/vendor/icons/websocket.svg +21 -0
  18. package/index.js +21 -4
  19. package/lib/app.js +145 -15
  20. package/lib/body/json.js +3 -0
  21. package/lib/body/multipart.js +2 -0
  22. package/lib/body/raw.js +3 -0
  23. package/lib/body/text.js +3 -0
  24. package/lib/body/urlencoded.js +3 -0
  25. package/lib/{fetch.js → fetch/index.js} +30 -1
  26. package/lib/http/index.js +9 -0
  27. package/lib/{request.js → http/request.js} +7 -1
  28. package/lib/{response.js → http/response.js} +70 -1
  29. package/lib/middleware/compress.js +194 -0
  30. package/lib/middleware/index.js +12 -0
  31. package/lib/router/index.js +278 -0
  32. package/lib/sse/index.js +8 -0
  33. package/lib/sse/stream.js +322 -0
  34. package/lib/ws/connection.js +440 -0
  35. package/lib/ws/handshake.js +122 -0
  36. package/lib/ws/index.js +12 -0
  37. package/package.json +1 -1
  38. package/lib/router.js +0 -87
  39. /package/lib/{cors.js → middleware/cors.js} +0 -0
  40. /package/lib/{logger.js → middleware/logger.js} +0 -0
  41. /package/lib/{rateLimit.js → middleware/rateLimit.js} +0 -0
  42. /package/lib/{static.js → middleware/static.js} +0 -0
@@ -0,0 +1,27 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" aria-hidden="true">
2
+ <defs>
3
+ <linearGradient id="g-comp" x1="0" x2="1">
4
+ <stop offset="0" stop-color="#7b61ff"/>
5
+ <stop offset="1" stop-color="#3ec6ff"/>
6
+ </linearGradient>
7
+ </defs>
8
+ <rect width="64" height="64" rx="8" fill="transparent"/>
9
+ <!-- Outer large container (uncompressed) -->
10
+ <rect x="6" y="8" width="24" height="48" rx="5" fill="url(#g-comp)" opacity="0.3"/>
11
+ <!-- Inward arrows implying compression -->
12
+ <path d="M6 20h8" stroke="#ffffff" stroke-width="2" stroke-linecap="round" opacity="0.6"/>
13
+ <path d="M30 20h-8" stroke="#ffffff" stroke-width="2" stroke-linecap="round" opacity="0.6"/>
14
+ <path d="M6 32h8" stroke="#ffffff" stroke-width="2" stroke-linecap="round" opacity="0.6"/>
15
+ <path d="M30 32h-8" stroke="#ffffff" stroke-width="2" stroke-linecap="round" opacity="0.6"/>
16
+ <path d="M6 44h8" stroke="#ffffff" stroke-width="2" stroke-linecap="round" opacity="0.6"/>
17
+ <path d="M30 44h-8" stroke="#ffffff" stroke-width="2" stroke-linecap="round" opacity="0.6"/>
18
+ <!-- Arrow pointing to compressed result -->
19
+ <path d="M34 32h6" stroke="url(#g-comp)" stroke-width="2.5" stroke-linecap="round"/>
20
+ <path d="M38 28l4 4-4 4" fill="none" stroke="url(#g-comp)" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
21
+ <!-- Compact compressed block -->
22
+ <rect x="44" y="20" width="14" height="24" rx="4" fill="url(#g-comp)"/>
23
+ <!-- White highlight lines on compressed block -->
24
+ <line x1="48" y1="27" x2="54" y2="27" stroke="rgba(255,255,255,0.8)" stroke-width="1.5" stroke-linecap="round"/>
25
+ <line x1="48" y1="32" x2="54" y2="32" stroke="rgba(255,255,255,0.55)" stroke-width="1.5" stroke-linecap="round"/>
26
+ <line x1="48" y1="37" x2="52" y2="37" stroke="rgba(255,255,255,0.35)" stroke-width="1.5" stroke-linecap="round"/>
27
+ </svg>
@@ -0,0 +1,23 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" aria-hidden="true">
2
+ <defs>
3
+ <linearGradient id="g-tls" x1="0" x2="1">
4
+ <stop offset="0" stop-color="#7b61ff"/>
5
+ <stop offset="1" stop-color="#3ec6ff"/>
6
+ </linearGradient>
7
+ </defs>
8
+ <rect width="64" height="64" rx="8" fill="transparent"/>
9
+ <!-- Shield shape -->
10
+ <path d="M32 6 L52 16 L52 34 C52 48 32 58 32 58 C32 58 12 48 12 34 L12 16 Z" fill="url(#g-tls)"/>
11
+ <!-- White highlight on shield -->
12
+ <path d="M32 10 L48 18 L48 33 C48 44 32 53 32 53 C32 53 30 52 28 50" fill="rgba(255,255,255,0.08)" stroke="none"/>
13
+ <!-- Lock body -->
14
+ <rect x="24" y="30" width="16" height="14" rx="3" fill="rgba(255,255,255,0.9)"/>
15
+ <!-- Lock shackle -->
16
+ <path d="M27 30 V24 C27 19 37 19 37 24 V30" fill="none" stroke="rgba(255,255,255,0.9)" stroke-width="3" stroke-linecap="round"/>
17
+ <!-- Keyhole -->
18
+ <circle cx="32" cy="36" r="2.5" fill="url(#g-tls)"/>
19
+ <rect x="31" y="36" width="2" height="4" rx="1" fill="url(#g-tls)"/>
20
+ <!-- Sparkle dots -->
21
+ <circle cx="50" cy="10" r="1.5" fill="rgba(255,255,255,0.6)"/>
22
+ <circle cx="54" cy="14" r="1" fill="rgba(255,255,255,0.35)"/>
23
+ </svg>
@@ -0,0 +1,29 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" aria-hidden="true">
2
+ <defs>
3
+ <linearGradient id="g-rtr" x1="0" x2="1">
4
+ <stop offset="0" stop-color="#7b61ff"/>
5
+ <stop offset="1" stop-color="#3ec6ff"/>
6
+ </linearGradient>
7
+ </defs>
8
+ <rect width="64" height="64" rx="8" fill="transparent"/>
9
+ <!-- Central hub node -->
10
+ <circle cx="32" cy="32" r="10" fill="url(#g-rtr)"/>
11
+ <circle cx="32" cy="32" r="4" fill="rgba(255,255,255,0.85)"/>
12
+ <!-- Branch: top-left -->
13
+ <line x1="24" y1="24" x2="14" y2="14" stroke="url(#g-rtr)" stroke-width="2.5" stroke-linecap="round"/>
14
+ <rect x="6" y="6" width="14" height="12" rx="3.5" fill="url(#g-rtr)" opacity="0.7"/>
15
+ <line x1="10" y1="11" x2="16" y2="11" stroke="rgba(255,255,255,0.7)" stroke-width="1.5" stroke-linecap="round"/>
16
+ <line x1="10" y1="14" x2="14" y2="14" stroke="rgba(255,255,255,0.4)" stroke-width="1.5" stroke-linecap="round"/>
17
+ <!-- Branch: top-right -->
18
+ <line x1="40" y1="24" x2="50" y2="14" stroke="url(#g-rtr)" stroke-width="2.5" stroke-linecap="round"/>
19
+ <rect x="44" y="6" width="14" height="12" rx="3.5" fill="url(#g-rtr)" opacity="0.7"/>
20
+ <line x1="48" y1="11" x2="54" y2="11" stroke="rgba(255,255,255,0.7)" stroke-width="1.5" stroke-linecap="round"/>
21
+ <line x1="48" y1="14" x2="52" y2="14" stroke="rgba(255,255,255,0.4)" stroke-width="1.5" stroke-linecap="round"/>
22
+ <!-- Branch: bottom-center -->
23
+ <line x1="32" y1="42" x2="32" y2="50" stroke="url(#g-rtr)" stroke-width="2.5" stroke-linecap="round"/>
24
+ <rect x="22" y="50" width="20" height="10" rx="3.5" fill="url(#g-rtr)" opacity="0.7"/>
25
+ <line x1="26" y1="55" x2="38" y2="55" stroke="rgba(255,255,255,0.7)" stroke-width="1.5" stroke-linecap="round"/>
26
+ <line x1="26" y1="58" x2="34" y2="58" stroke="rgba(255,255,255,0.4)" stroke-width="1.5" stroke-linecap="round"/>
27
+ <!-- Sparkle -->
28
+ <circle cx="56" cy="28" r="1.5" fill="rgba(255,255,255,0.5)"/>
29
+ </svg>
@@ -0,0 +1,25 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" aria-hidden="true">
2
+ <defs>
3
+ <linearGradient id="g-sse" x1="0" x2="1">
4
+ <stop offset="0" stop-color="#7b61ff"/>
5
+ <stop offset="1" stop-color="#3ec6ff"/>
6
+ </linearGradient>
7
+ </defs>
8
+ <rect width="64" height="64" rx="8" fill="transparent"/>
9
+ <!-- Server box -->
10
+ <rect x="8" y="16" width="20" height="32" rx="5" fill="url(#g-sse)"/>
11
+ <!-- White indicator dots on server -->
12
+ <circle cx="18" cy="25" r="2.5" fill="rgba(255,255,255,0.9)"/>
13
+ <circle cx="18" cy="32" r="2.5" fill="rgba(255,255,255,0.55)"/>
14
+ <circle cx="18" cy="39" r="2.5" fill="rgba(255,255,255,0.35)"/>
15
+ <!-- Three streaming arrows flowing right -->
16
+ <line x1="32" y1="24" x2="50" y2="24" stroke="url(#g-sse)" stroke-width="3" stroke-linecap="round"/>
17
+ <path d="M47 20l5 4-5 4" fill="none" stroke="url(#g-sse)" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
18
+ <line x1="34" y1="32" x2="52" y2="32" stroke="url(#g-sse)" stroke-width="3" stroke-linecap="round" opacity="0.7"/>
19
+ <path d="M49 28l5 4-5 4" fill="none" stroke="url(#g-sse)" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" opacity="0.7"/>
20
+ <line x1="36" y1="40" x2="54" y2="40" stroke="url(#g-sse)" stroke-width="3" stroke-linecap="round" opacity="0.45"/>
21
+ <path d="M51 36l5 4-5 4" fill="none" stroke="url(#g-sse)" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" opacity="0.45"/>
22
+ <!-- White sparkle -->
23
+ <circle cx="54" cy="18" r="2" fill="rgba(255,255,255,0.7)"/>
24
+ <circle cx="57" cy="14" r="1" fill="rgba(255,255,255,0.4)"/>
25
+ </svg>
@@ -0,0 +1,21 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" aria-hidden="true">
2
+ <defs>
3
+ <linearGradient id="g-ws" x1="0" x2="1">
4
+ <stop offset="0" stop-color="#7b61ff"/>
5
+ <stop offset="1" stop-color="#3ec6ff"/>
6
+ </linearGradient>
7
+ </defs>
8
+ <rect width="64" height="64" rx="8" fill="transparent"/>
9
+ <!-- Two speech bubbles overlapping to represent bidirectional real-time messaging -->
10
+ <rect x="6" y="10" width="32" height="22" rx="6" fill="url(#g-ws)"/>
11
+ <polygon points="14,32 20,40 24,32" fill="url(#g-ws)"/>
12
+ <!-- White dots in left bubble -->
13
+ <circle cx="15" cy="21" r="2.5" fill="rgba(255,255,255,0.85)"/>
14
+ <circle cx="22" cy="21" r="2.5" fill="rgba(255,255,255,0.85)"/>
15
+ <circle cx="29" cy="21" r="2.5" fill="rgba(255,255,255,0.85)"/>
16
+ <!-- Second bubble (offset) -->
17
+ <rect x="26" y="28" width="32" height="20" rx="6" fill="url(#g-ws)" opacity="0.75"/>
18
+ <polygon points="44,48 48,55 52,48" fill="url(#g-ws)" opacity="0.75"/>
19
+ <!-- Arrow inside second bubble implying response -->
20
+ <path d="M34 38h16M44 34l6 4-6 4" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
21
+ </svg>
package/index.js CHANGED
@@ -4,12 +4,16 @@
4
4
  * Re-exports every middleware, the app factory, and the fetch helper.
5
5
  */
6
6
  const App = require('./lib/app');
7
- const cors = require('./lib/cors');
7
+ const Router = require('./lib/router');
8
+ const cors = require('./lib/middleware/cors');
8
9
  const fetch = require('./lib/fetch');
9
10
  const body = require('./lib/body');
10
- const serveStatic = require('./lib/static');
11
- const rateLimit = require('./lib/rateLimit');
12
- const logger = require('./lib/logger');
11
+ const serveStatic = require('./lib/middleware/static');
12
+ const rateLimit = require('./lib/middleware/rateLimit');
13
+ const logger = require('./lib/middleware/logger');
14
+ const compress = require('./lib/middleware/compress');
15
+ const { WebSocketConnection } = require('./lib/ws');
16
+ const { SSEStream } = require('./lib/sse');
13
17
 
14
18
  module.exports = {
15
19
  /**
@@ -17,6 +21,12 @@ module.exports = {
17
21
  * @returns {import('./lib/app')} Fresh App with an empty middleware stack.
18
22
  */
19
23
  createApp: () => new App(),
24
+ /**
25
+ * Create a standalone Router for modular route grouping.
26
+ * Mount on an App with `app.use('/prefix', router)`.
27
+ * @returns {import('./lib/router')} Fresh Router instance.
28
+ */
29
+ Router: () => new Router(),
20
30
  /** @see module:cors */
21
31
  cors,
22
32
  /** @see module:fetch */
@@ -40,4 +50,11 @@ module.exports = {
40
50
  rateLimit,
41
51
  /** @see module:logger */
42
52
  logger,
53
+ /** @see module:compress */
54
+ compress,
55
+ // classes (for advanced / direct usage)
56
+ /** @see module:ws/connection */
57
+ WebSocketConnection,
58
+ /** @see module:sse/stream */
59
+ SSEStream,
43
60
  };
package/lib/app.js CHANGED
@@ -1,12 +1,15 @@
1
1
  /**
2
2
  * @module app
3
- * @description Express-like HTTP application with middleware pipeline and
4
- * method-based routing. Created via `createApp()` in the public API.
3
+ * @description Express-like HTTP application with middleware pipeline,
4
+ * method-based routing, HTTPS support, built-in WebSocket
5
+ * upgrade handling, and route introspection.
6
+ * Created via `createApp()` in the public API.
5
7
  */
6
8
  const http = require('http');
9
+ const https = require('https');
7
10
  const Router = require('./router');
8
- const Request = require('./request');
9
- const Response = require('./response');
11
+ const { Request, Response } = require('./http');
12
+ const { handleUpgrade } = require('./ws');
10
13
 
11
14
  class App
12
15
  {
@@ -25,19 +28,26 @@ class App
25
28
  this.middlewares = [];
26
29
  /** @type {Function|null} */
27
30
  this._errorHandler = null;
31
+ /** @type {Map<string, { handler: Function, opts: object }>} WebSocket upgrade handlers keyed by path */
32
+ this._wsHandlers = new Map();
33
+ /** @type {import('http').Server|import('https').Server|null} */
34
+ this._server = null;
28
35
 
29
36
  // Bind for use as `http.createServer(app.handler)`
30
37
  this.handler = (req, res) => this.handle(req, res);
31
38
  }
32
39
 
40
+ // -- Middleware -------------------------------------
41
+
33
42
  /**
34
- * Register middleware.
43
+ * Register middleware or mount a sub-router.
35
44
  * - `use(fn)` — global middleware applied to every request.
36
45
  * - `use('/prefix', fn)` — path-scoped middleware (strips the prefix
37
46
  * before calling `fn` so downstream sees relative paths).
47
+ * - `use('/prefix', router)` — mount a Router sub-app at the given prefix.
38
48
  *
39
- * @param {string|Function} pathOrFn - A path prefix string, or middleware function.
40
- * @param {Function} [fn] - Middleware function when first arg is a path.
49
+ * @param {string|Function} pathOrFn - A path prefix string, or middleware function.
50
+ * @param {Function|Router} [fn] - Middleware function or Router when first arg is a path.
41
51
  */
42
52
  use(pathOrFn, fn)
43
53
  {
@@ -45,6 +55,11 @@ class App
45
55
  {
46
56
  this.middlewares.push(pathOrFn);
47
57
  }
58
+ else if (typeof pathOrFn === 'string' && fn instanceof Router)
59
+ {
60
+ // Mount a sub-router
61
+ this.router.use(pathOrFn, fn);
62
+ }
48
63
  else if (typeof pathOrFn === 'string' && typeof fn === 'function')
49
64
  {
50
65
  const prefix = pathOrFn.endsWith('/') ? pathOrFn.slice(0, -1) : pathOrFn;
@@ -78,6 +93,8 @@ class App
78
93
  this._errorHandler = fn;
79
94
  }
80
95
 
96
+ // -- Request Handling ------------------------------
97
+
81
98
  /**
82
99
  * Core request handler. Wraps the raw Node `req`/`res` in
83
100
  * {@link Request}/{@link Response} wrappers, runs the middleware
@@ -124,27 +141,140 @@ class App
124
141
  run();
125
142
  }
126
143
 
144
+ // -- Server Lifecycle ------------------------------
145
+
127
146
  /**
128
- * Start listening for HTTP connections.
147
+ * Start listening for HTTP or HTTPS connections.
148
+ *
149
+ * @param {number} [port=3000] - Port number to bind.
150
+ * @param {object|Function} [opts] - TLS options `{ key, cert, ... }` for HTTPS, or a callback.
151
+ * @param {Function} [cb] - Callback invoked once the server is listening.
152
+ * @returns {import('http').Server|import('https').Server} The underlying server.
129
153
  *
130
- * @param {number} [port=3000] - Port number to bind.
131
- * @param {Function} [cb] - Callback invoked once the server is listening.
132
- * @returns {import('http').Server} The underlying Node HTTP server.
154
+ * @example
155
+ * // Plain HTTP
156
+ * app.listen(3000, () => console.log('HTTP on 3000'));
157
+ *
158
+ * // HTTPS
159
+ * app.listen(443, { key: fs.readFileSync('key.pem'), cert: fs.readFileSync('cert.pem') },
160
+ * () => console.log('HTTPS on 443'));
133
161
  */
134
- listen(port = 3000, cb)
162
+ listen(port = 3000, opts, cb)
135
163
  {
136
- const server = http.createServer(this.handler);
164
+ // Normalise arguments — allow `listen(port, cb)` without opts
165
+ if (typeof opts === 'function') { cb = opts; opts = undefined; }
166
+
167
+ const isHTTPS = opts && (opts.key || opts.pfx || opts.cert);
168
+ const server = isHTTPS
169
+ ? https.createServer(opts, this.handler)
170
+ : http.createServer(this.handler);
171
+
172
+ this._server = server;
173
+
174
+ // Always attach WebSocket upgrade handling so ws() works
175
+ // regardless of registration order (before or after listen).
176
+ server.on('upgrade', (req, socket, head) =>
177
+ {
178
+ if (this._wsHandlers.size > 0)
179
+ handleUpgrade(req, socket, head, this._wsHandlers);
180
+ else
181
+ socket.destroy();
182
+ });
183
+
137
184
  return server.listen(port, cb);
138
185
  }
139
186
 
187
+ /**
188
+ * Gracefully close the server, stopping new connections.
189
+ *
190
+ * @param {Function} [cb] - Callback invoked once the server has closed.
191
+ */
192
+ close(cb)
193
+ {
194
+ if (this._server) this._server.close(cb);
195
+ }
196
+
197
+ // -- WebSocket Support -----------------------------
198
+
199
+ /**
200
+ * Register a WebSocket upgrade handler for a path.
201
+ *
202
+ * The handler receives `(ws, req)` where `ws` is a rich WebSocket
203
+ * connection object. See {@link WebSocketConnection} for the full API.
204
+ *
205
+ * @param {string} path - URL path to listen for upgrade requests.
206
+ * @param {object|Function} [opts] - Options object, or the handler function directly.
207
+ * @param {number} [opts.maxPayload=1048576] - Maximum incoming frame size in bytes (default 1 MB).
208
+ * @param {number} [opts.pingInterval=30000] - Auto-ping interval in ms. Set `0` to disable.
209
+ * @param {Function} [opts.verifyClient] - `(req) => boolean` — return false to reject the upgrade.
210
+ * @param {Function} handler - `(ws, req) => void`.
211
+ *
212
+ * @example
213
+ * // Simple
214
+ * app.ws('/chat', (ws, req) => {
215
+ * ws.on('message', data => ws.send('echo: ' + data));
216
+ * });
217
+ *
218
+ * // With options
219
+ * app.ws('/feed', { maxPayload: 64 * 1024, pingInterval: 15000 }, (ws, req) => {
220
+ * console.log('client', ws.id, 'from', ws.ip);
221
+ * ws.sendJSON({ hello: 'world' });
222
+ * });
223
+ */
224
+ ws(path, opts, handler)
225
+ {
226
+ // Normalise arguments: ws(path, handler) or ws(path, opts, handler)
227
+ if (typeof opts === 'function') { handler = opts; opts = {}; }
228
+ if (!opts) opts = {};
229
+
230
+ this._wsHandlers.set(path, { handler, opts });
231
+ }
232
+
233
+ // -- Route Introspection ---------------------------
234
+
235
+ /**
236
+ * Return a flat list of all registered routes across the router tree,
237
+ * including mounted sub-routers. Useful for debugging, auto-generated
238
+ * docs, or CLI tooling.
239
+ *
240
+ * @returns {{ method: string, path: string }[]}
241
+ *
242
+ * @example
243
+ * app.routes().forEach(r => console.log(r.method, r.path));
244
+ * // GET /users
245
+ * // POST /users
246
+ * // GET /api/v1/items/:id
247
+ */
248
+ routes()
249
+ {
250
+ return this.router.inspect();
251
+ }
252
+
253
+ // -- Route Registration ----------------------------
254
+
255
+ /**
256
+ * Extract an options object from the head of the handlers array when
257
+ * the first argument is a plain object (not a function).
258
+ * @private
259
+ */
260
+ _extractOpts(fns)
261
+ {
262
+ let opts = {};
263
+ if (fns.length > 0 && typeof fns[0] === 'object' && typeof fns[0] !== 'function')
264
+ {
265
+ opts = fns.shift();
266
+ }
267
+ return opts;
268
+ }
269
+
140
270
  /**
141
271
  * Register one or more handler functions for a specific HTTP method and path.
142
272
  *
143
273
  * @param {string} method - HTTP method (GET, POST, etc.) or 'ALL'.
144
274
  * @param {string} path - Route pattern (e.g. '/users/:id').
145
- * @param {...Function} fns - Handler functions `(req, res, next) => void`.
275
+ * @param {...Function|object} fns - Optional options object `{ secure }` followed by handler functions.
146
276
  */
147
- route(method, path, ...fns) { this.router.add(method, path, fns); }
277
+ route(method, path, ...fns) { const o = this._extractOpts(fns); this.router.add(method, path, fns, o); }
148
278
 
149
279
  /** @see App#route — shortcut for GET requests. */ get(path, ...fns) { this.route('GET', path, ...fns); }
150
280
  /** @see App#route — shortcut for POST requests. */ post(path, ...fns) { this.route('POST', path, ...fns); }
package/lib/body/json.js CHANGED
@@ -15,6 +15,7 @@ const sendError = require('./sendError');
15
15
  * @param {Function} [options.reviver] - `JSON.parse` reviver function.
16
16
  * @param {boolean} [options.strict=true] - When true, reject non-object/array roots.
17
17
  * @param {string|Function} [options.type='application/json'] - Content-Type to match.
18
+ * @param {boolean} [options.requireSecure=false] - When true, reject non-HTTPS requests with 403.
18
19
  * @returns {Function} Async middleware `(req, res, next) => void`.
19
20
  */
20
21
  function json(options = {})
@@ -24,9 +25,11 @@ function json(options = {})
24
25
  const reviver = opts.reviver;
25
26
  const strict = (opts.hasOwnProperty('strict')) ? !!opts.strict : true;
26
27
  const typeOpt = opts.type || 'application/json';
28
+ const requireSecure = !!opts.requireSecure;
27
29
 
28
30
  return async (req, res, next) =>
29
31
  {
32
+ if (requireSecure && !req.secure) return sendError(res, 403, 'HTTPS required');
30
33
  const ct = (req.headers['content-type'] || '');
31
34
  if (!isTypeMatch(ct, typeOpt)) return next();
32
35
  try
@@ -77,12 +77,14 @@ function parseContentDisposition(cd)
77
77
  * @param {object} [opts]
78
78
  * @param {string} [opts.dir] - Upload directory (default: OS temp dir).
79
79
  * @param {number} [opts.maxFileSize] - Maximum file size in bytes.
80
+ * @param {boolean} [opts.requireSecure=false] - When true, reject non-HTTPS requests with 403.
80
81
  * @returns {Function} Async middleware `(req, res, next) => void`.
81
82
  */
82
83
  function multipart(opts = {})
83
84
  {
84
85
  return async (req, res, next) =>
85
86
  {
87
+ if (opts.requireSecure && !req.secure) return sendError(res, 403, 'HTTPS required');
86
88
  const ct = req.headers['content-type'] || '';
87
89
  const m = /boundary=(?:"([^"]+)"|([^;\s]+))/i.exec(ct);
88
90
  if (!m) return next();
package/lib/body/raw.js CHANGED
@@ -13,6 +13,7 @@ const sendError = require('./sendError');
13
13
  * @param {object} [options]
14
14
  * @param {string|number} [options.limit] - Max body size.
15
15
  * @param {string|Function} [options.type='application/octet-stream'] - Content-Type to match.
16
+ * @param {boolean} [options.requireSecure=false] - When true, reject non-HTTPS requests with 403.
16
17
  * @returns {Function} Async middleware `(req, res, next) => void`.
17
18
  */
18
19
  function raw(options = {})
@@ -20,9 +21,11 @@ function raw(options = {})
20
21
  const opts = options || {};
21
22
  const limit = opts.limit || null;
22
23
  const typeOpt = opts.type || 'application/octet-stream';
24
+ const requireSecure = !!opts.requireSecure;
23
25
 
24
26
  return async (req, res, next) =>
25
27
  {
28
+ if (requireSecure && !req.secure) return sendError(res, 403, 'HTTPS required');
26
29
  const ct = (req.headers['content-type'] || '');
27
30
  if (!isTypeMatch(ct, typeOpt)) return next();
28
31
  try
package/lib/body/text.js CHANGED
@@ -14,6 +14,7 @@ const sendError = require('./sendError');
14
14
  * @param {string|number} [options.limit] - Max body size.
15
15
  * @param {string} [options.encoding='utf8'] - Character encoding.
16
16
  * @param {string|Function} [options.type='text/*'] - Content-Type to match.
17
+ * @param {boolean} [options.requireSecure=false] - When true, reject non-HTTPS requests with 403.
17
18
  * @returns {Function} Async middleware `(req, res, next) => void`.
18
19
  */
19
20
  function text(options = {})
@@ -22,9 +23,11 @@ function text(options = {})
22
23
  const limit = opts.limit || null;
23
24
  const encoding = opts.encoding || 'utf8';
24
25
  const typeOpt = opts.type || 'text/*';
26
+ const requireSecure = !!opts.requireSecure;
25
27
 
26
28
  return async (req, res, next) =>
27
29
  {
30
+ if (requireSecure && !req.secure) return sendError(res, 403, 'HTTPS required');
28
31
  const ct = (req.headers['content-type'] || '');
29
32
  if (!isTypeMatch(ct, typeOpt)) return next();
30
33
  try
@@ -30,6 +30,7 @@ function appendValue(prev, val)
30
30
  * @param {string|number} [options.limit] - Max body size (e.g. `'10kb'`).
31
31
  * @param {string|Function} [options.type='application/x-www-form-urlencoded'] - Content-Type to match.
32
32
  * @param {boolean} [options.extended=false] - Use nested bracket parsing (e.g. `a[b][c]=1`).
33
+ * @param {boolean} [options.requireSecure=false] - When true, reject non-HTTPS requests with 403.
33
34
  * @returns {Function} Async middleware `(req, res, next) => void`.
34
35
  */
35
36
  function urlencoded(options = {})
@@ -38,9 +39,11 @@ function urlencoded(options = {})
38
39
  const limit = opts.limit || null;
39
40
  const typeOpt = opts.type || 'application/x-www-form-urlencoded';
40
41
  const extended = !!opts.extended;
42
+ const requireSecure = !!opts.requireSecure;
41
43
 
42
44
  return async (req, res, next) =>
43
45
  {
46
+ if (requireSecure && !req.secure) return sendError(res, 403, 'HTTPS required');
44
47
  const ct = (req.headers['content-type'] || '');
45
48
  if (!isTypeMatch(ct, typeOpt)) return next();
46
49
  try
@@ -23,7 +23,21 @@ const STATUS_CODES = http.STATUS_CODES;
23
23
  * @param {import('http').Agent} [opts.agent] - Custom HTTP agent.
24
24
  * @param {Function} [opts.onDownloadProgress] - `({ loaded, total }) => void` callback.
25
25
  * @param {Function} [opts.onUploadProgress] - `({ loaded, total }) => void` callback.
26
- * @returns {Promise<{ status: number, statusText: string, ok: boolean, headers: object, arrayBuffer: Function, text: Function, json: Function }>}
26
+ *
27
+ * TLS / HTTPS options (passed through to `https.request()` when the URL is `https:`):
28
+ * @param {boolean} [opts.rejectUnauthorized] - Reject connections with unverified certs (default: Node default `true`).
29
+ * @param {string|Buffer|Array} [opts.ca] - Override default CA certificates.
30
+ * @param {string|Buffer} [opts.cert] - Client certificate (PEM) for mutual TLS.
31
+ * @param {string|Buffer} [opts.key] - Private key (PEM) for mutual TLS.
32
+ * @param {string|Buffer} [opts.pfx] - PFX / PKCS12 bundle (alternative to cert+key).
33
+ * @param {string} [opts.passphrase] - Passphrase for the key or PFX.
34
+ * @param {string} [opts.servername] - SNI server name override.
35
+ * @param {string} [opts.ciphers] - Colon-separated cipher list.
36
+ * @param {string} [opts.secureProtocol] - SSL/TLS protocol method name.
37
+ * @param {string} [opts.minVersion] - Minimum TLS version (`'TLSv1.2'`, etc.).
38
+ * @param {string} [opts.maxVersion] - Maximum TLS version.
39
+ *
40
+ * @returns {Promise<{ status: number, statusText: string, ok: boolean, secure: boolean, url: string, headers: object, arrayBuffer: Function, text: Function, json: Function }>}
27
41
  */
28
42
  function miniFetch(url, opts = {})
29
43
  {
@@ -66,6 +80,19 @@ function miniFetch(url, opts = {})
66
80
  const options = { method, headers };
67
81
  if (opts.agent) options.agent = opts.agent;
68
82
 
83
+ // Pass through TLS options for HTTPS requests
84
+ if (lib === https)
85
+ {
86
+ const tlsKeys = [
87
+ 'rejectUnauthorized', 'ca', 'cert', 'key', 'pfx', 'passphrase',
88
+ 'servername', 'ciphers', 'secureProtocol', 'minVersion', 'maxVersion'
89
+ ];
90
+ for (const k of tlsKeys)
91
+ {
92
+ if (opts[k] !== undefined) options[k] = opts[k];
93
+ }
94
+ }
95
+
69
96
  const req = lib.request(u, options, (res) =>
70
97
  {
71
98
  const chunks = [];
@@ -101,6 +128,8 @@ function miniFetch(url, opts = {})
101
128
  status,
102
129
  statusText: STATUS_CODES[status] || '',
103
130
  ok: status >= 200 && status < 300,
131
+ secure: u.protocol === 'https:',
132
+ url: u.href,
104
133
  headers: responseHeaders,
105
134
  arrayBuffer: () => Promise.resolve(buf),
106
135
  text: () => Promise.resolve(buf.toString('utf8')),
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @module http
3
+ * @description HTTP request/response wrappers for zero-http.
4
+ * Exports Request and Response classes.
5
+ */
6
+ const Request = require('./request');
7
+ const Response = require('./response');
8
+
9
+ module.exports = { Request, Response };
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @module request
2
+ * @module http/request
3
3
  * @description Lightweight wrapper around Node's `IncomingMessage`.
4
4
  * Provides parsed query string, params, body, and convenience helpers.
5
5
  */
@@ -31,6 +31,12 @@ class Request
31
31
  this.params = {};
32
32
  this.body = null;
33
33
  this.ip = req.socket ? req.socket.remoteAddress : null;
34
+
35
+ /** `true` when the connection is over TLS (HTTPS). */
36
+ this.secure = !!(req.socket && req.socket.encrypted);
37
+
38
+ /** Protocol string — `'https'` or `'http'`. */
39
+ this.protocol = this.secure ? 'https' : 'http';
34
40
  }
35
41
 
36
42
  /**