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.
Files changed (43) hide show
  1. package/README.md +314 -115
  2. package/documentation/full-server.js +102 -5
  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 +138 -53
  7. package/documentation/public/scripts/data-sections.js +23 -4
  8. package/documentation/public/scripts/helpers.js +4 -4
  9. package/documentation/public/scripts/playground.js +204 -0
  10. package/documentation/public/scripts/ui.js +140 -8
  11. package/documentation/public/scripts/uploads.js +35 -20
  12. package/documentation/public/styles.css +46 -23
  13. package/documentation/public/vendor/icons/compress.svg +27 -0
  14. package/documentation/public/vendor/icons/https.svg +18 -0
  15. package/documentation/public/vendor/icons/logo.svg +24 -0
  16. package/documentation/public/vendor/icons/router.svg +27 -0
  17. package/documentation/public/vendor/icons/sse.svg +22 -0
  18. package/documentation/public/vendor/icons/websocket.svg +21 -0
  19. package/index.js +21 -4
  20. package/lib/app.js +156 -15
  21. package/lib/body/json.js +3 -0
  22. package/lib/body/multipart.js +2 -0
  23. package/lib/body/raw.js +3 -0
  24. package/lib/body/text.js +3 -0
  25. package/lib/body/urlencoded.js +3 -0
  26. package/lib/{fetch.js → fetch/index.js} +30 -1
  27. package/lib/http/index.js +9 -0
  28. package/lib/{request.js → http/request.js} +7 -1
  29. package/lib/{response.js → http/response.js} +70 -1
  30. package/lib/middleware/compress.js +194 -0
  31. package/lib/middleware/index.js +12 -0
  32. package/lib/router/index.js +278 -0
  33. package/lib/sse/index.js +8 -0
  34. package/lib/sse/stream.js +322 -0
  35. package/lib/ws/connection.js +440 -0
  36. package/lib/ws/handshake.js +122 -0
  37. package/lib/ws/index.js +12 -0
  38. package/package.json +1 -1
  39. package/lib/router.js +0 -87
  40. /package/lib/{cors.js → middleware/cors.js} +0 -0
  41. /package/lib/{logger.js → middleware/logger.js} +0 -0
  42. /package/lib/{rateLimit.js → middleware/rateLimit.js} +0 -0
  43. /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 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,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
- * @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.
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
+ 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 - Handler functions `(req, res, next) => void`.
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
@@ -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
  /**
@@ -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;