zero-http 0.2.0 → 0.2.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/README.md +314 -115
- package/documentation/full-server.js +102 -5
- package/documentation/public/data/api.json +154 -33
- package/documentation/public/data/examples.json +35 -11
- package/documentation/public/data/options.json +14 -8
- package/documentation/public/index.html +138 -53
- package/documentation/public/scripts/data-sections.js +23 -4
- package/documentation/public/scripts/helpers.js +4 -4
- package/documentation/public/scripts/playground.js +204 -0
- package/documentation/public/scripts/ui.js +140 -8
- package/documentation/public/scripts/uploads.js +35 -20
- package/documentation/public/styles.css +46 -23
- package/documentation/public/vendor/icons/compress.svg +27 -0
- package/documentation/public/vendor/icons/https.svg +18 -0
- package/documentation/public/vendor/icons/logo.svg +24 -0
- package/documentation/public/vendor/icons/router.svg +27 -0
- package/documentation/public/vendor/icons/sse.svg +22 -0
- package/documentation/public/vendor/icons/websocket.svg +21 -0
- package/index.js +21 -4
- package/lib/app.js +156 -15
- package/lib/body/json.js +3 -0
- package/lib/body/multipart.js +2 -0
- package/lib/body/raw.js +3 -0
- package/lib/body/text.js +3 -0
- package/lib/body/urlencoded.js +3 -0
- package/lib/{fetch.js → fetch/index.js} +30 -1
- package/lib/http/index.js +9 -0
- package/lib/{request.js → http/request.js} +7 -1
- package/lib/{response.js → http/response.js} +70 -1
- package/lib/middleware/compress.js +194 -0
- package/lib/middleware/index.js +12 -0
- package/lib/router/index.js +278 -0
- package/lib/sse/index.js +8 -0
- package/lib/sse/stream.js +322 -0
- package/lib/ws/connection.js +440 -0
- package/lib/ws/handshake.js +122 -0
- package/lib/ws/index.js +12 -0
- package/package.json +1 -1
- package/lib/router.js +0 -87
- /package/lib/{cors.js → middleware/cors.js} +0 -0
- /package/lib/{logger.js → middleware/logger.js} +0 -0
- /package/lib/{rateLimit.js → middleware/rateLimit.js} +0 -0
- /package/lib/{static.js → middleware/static.js} +0 -0
package/lib/app.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @module app
|
|
3
|
-
* @description Express-like HTTP application with middleware pipeline
|
|
4
|
-
* method-based routing
|
|
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('./
|
|
9
|
-
const
|
|
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}
|
|
40
|
-
* @param {Function}
|
|
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,151 @@ 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.
|
|
153
|
+
*
|
|
154
|
+
* @example
|
|
155
|
+
* // Plain HTTP
|
|
156
|
+
* app.listen(3000, () => console.log('HTTP on 3000'));
|
|
129
157
|
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
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
|
-
|
|
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
|
+
const list = this.router.inspect();
|
|
251
|
+
|
|
252
|
+
/* Include WebSocket upgrade handlers */
|
|
253
|
+
for (const [wsPath, { opts }] of this._wsHandlers)
|
|
254
|
+
{
|
|
255
|
+
const entry = { method: 'WS', path: wsPath };
|
|
256
|
+
if (opts && opts.maxPayload !== undefined) entry.maxPayload = opts.maxPayload;
|
|
257
|
+
if (opts && opts.pingInterval !== undefined) entry.pingInterval = opts.pingInterval;
|
|
258
|
+
list.push(entry);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return list;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// -- Route Registration ----------------------------
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Extract an options object from the head of the handlers array when
|
|
268
|
+
* the first argument is a plain object (not a function).
|
|
269
|
+
* @private
|
|
270
|
+
*/
|
|
271
|
+
_extractOpts(fns)
|
|
272
|
+
{
|
|
273
|
+
let opts = {};
|
|
274
|
+
if (fns.length > 0 && typeof fns[0] === 'object' && typeof fns[0] !== 'function')
|
|
275
|
+
{
|
|
276
|
+
opts = fns.shift();
|
|
277
|
+
}
|
|
278
|
+
return opts;
|
|
279
|
+
}
|
|
280
|
+
|
|
140
281
|
/**
|
|
141
282
|
* Register one or more handler functions for a specific HTTP method and path.
|
|
142
283
|
*
|
|
143
284
|
* @param {string} method - HTTP method (GET, POST, etc.) or 'ALL'.
|
|
144
285
|
* @param {string} path - Route pattern (e.g. '/users/:id').
|
|
145
|
-
* @param {...Function} fns
|
|
286
|
+
* @param {...Function|object} fns - Optional options object `{ secure }` followed by handler functions.
|
|
146
287
|
*/
|
|
147
|
-
route(method, path, ...fns) { this.router.add(method, path, fns); }
|
|
288
|
+
route(method, path, ...fns) { const o = this._extractOpts(fns); this.router.add(method, path, fns, o); }
|
|
148
289
|
|
|
149
290
|
/** @see App#route — shortcut for GET requests. */ get(path, ...fns) { this.route('GET', path, ...fns); }
|
|
150
291
|
/** @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
|
package/lib/body/multipart.js
CHANGED
|
@@ -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
|
package/lib/body/urlencoded.js
CHANGED
|
@@ -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
|
-
*
|
|
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')),
|
|
@@ -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
|
/**
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @module response
|
|
2
|
+
* @module http/response
|
|
3
3
|
* @description Lightweight wrapper around Node's `ServerResponse`.
|
|
4
4
|
* Provides chainable helpers for status, headers, and body output.
|
|
5
5
|
*/
|
|
6
|
+
const SSEStream = require('../sse/stream');
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Wrapped HTTP response.
|
|
@@ -160,6 +161,74 @@ class Response
|
|
|
160
161
|
this.set('Location', target);
|
|
161
162
|
this.send('');
|
|
162
163
|
}
|
|
164
|
+
|
|
165
|
+
// -- Server-Sent Events (SSE) ---------------------
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Open a Server-Sent Events stream. Sets the correct headers and
|
|
169
|
+
* returns an SSE controller object with methods for pushing events.
|
|
170
|
+
*
|
|
171
|
+
* The connection stays open until the client disconnects or you call
|
|
172
|
+
* `sse.close()`.
|
|
173
|
+
*
|
|
174
|
+
* @param {object} [opts]
|
|
175
|
+
* @param {number} [opts.retry] - Reconnection interval hint (ms) sent to client.
|
|
176
|
+
* @param {object} [opts.headers] - Additional headers to set on the response.
|
|
177
|
+
* @param {number} [opts.keepAlive=0] - Auto keep-alive interval in ms. `0` to disable.
|
|
178
|
+
* @param {string} [opts.keepAliveComment='ping'] - Comment text for keep-alive messages.
|
|
179
|
+
* @param {boolean} [opts.autoId=false] - Auto-increment event IDs on every `.send()` / `.event()`.
|
|
180
|
+
* @param {number} [opts.startId=1] - Starting value for auto-IDs.
|
|
181
|
+
* @param {number} [opts.pad=0] - Bytes of initial padding (helps flush proxy buffers).
|
|
182
|
+
* @param {number} [opts.status=200] - HTTP status code for the SSE response.
|
|
183
|
+
* @returns {SSEStream} SSE controller.
|
|
184
|
+
*
|
|
185
|
+
* @example
|
|
186
|
+
* app.get('/events', (req, res) => {
|
|
187
|
+
* const sse = res.sse({ retry: 5000, keepAlive: 30000, autoId: true });
|
|
188
|
+
* sse.send('hello'); // id: 1, data: hello
|
|
189
|
+
* sse.event('update', { x: 1 }); // id: 2, event: update
|
|
190
|
+
* sse.comment('debug note'); // : debug note
|
|
191
|
+
* sse.on('close', () => console.log('gone'));
|
|
192
|
+
* });
|
|
193
|
+
*/
|
|
194
|
+
sse(opts = {})
|
|
195
|
+
{
|
|
196
|
+
if (this._sent) return null;
|
|
197
|
+
this._sent = true;
|
|
198
|
+
|
|
199
|
+
const raw = this.raw;
|
|
200
|
+
const statusCode = opts.status || 200;
|
|
201
|
+
raw.writeHead(statusCode, {
|
|
202
|
+
'Content-Type': 'text/event-stream',
|
|
203
|
+
'Cache-Control': 'no-cache',
|
|
204
|
+
'Connection': 'keep-alive',
|
|
205
|
+
'X-Accel-Buffering': 'no',
|
|
206
|
+
...(opts.headers || {}),
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// Initial padding to push past proxy buffers (e.g. 2 KB)
|
|
210
|
+
if (opts.pad && opts.pad > 0)
|
|
211
|
+
{
|
|
212
|
+
raw.write(': ' + ' '.repeat(opts.pad) + '\n\n');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (opts.retry)
|
|
216
|
+
{
|
|
217
|
+
raw.write(`retry: ${opts.retry}\n\n`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Capture the Last-Event-ID header from the request if available
|
|
221
|
+
const lastEventId = this._headers['_sse_last_event_id'] || null;
|
|
222
|
+
|
|
223
|
+
return new SSEStream(raw, {
|
|
224
|
+
keepAlive: opts.keepAlive || 0,
|
|
225
|
+
keepAliveComment: opts.keepAliveComment || 'ping',
|
|
226
|
+
autoId: !!opts.autoId,
|
|
227
|
+
startId: opts.startId || 1,
|
|
228
|
+
lastEventId,
|
|
229
|
+
secure: !!(raw.socket && raw.socket.encrypted),
|
|
230
|
+
});
|
|
231
|
+
}
|
|
163
232
|
}
|
|
164
233
|
|
|
165
234
|
module.exports = Response;
|