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.
- package/README.md +314 -115
- package/documentation/full-server.js +82 -1
- 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 +109 -17
- package/documentation/public/scripts/data-sections.js +4 -4
- package/documentation/public/scripts/helpers.js +4 -4
- package/documentation/public/scripts/playground.js +201 -0
- package/documentation/public/scripts/ui.js +6 -6
- package/documentation/public/scripts/uploads.js +9 -9
- package/documentation/public/styles.css +12 -12
- package/documentation/public/vendor/icons/compress.svg +27 -0
- package/documentation/public/vendor/icons/https.svg +23 -0
- package/documentation/public/vendor/icons/router.svg +29 -0
- package/documentation/public/vendor/icons/sse.svg +25 -0
- package/documentation/public/vendor/icons/websocket.svg +21 -0
- package/index.js +21 -4
- package/lib/app.js +145 -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
|
@@ -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
|
|
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
|
|
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,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
|
-
* @
|
|
131
|
-
*
|
|
132
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
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
|
/**
|