@jsenv/server 17.1.1 → 17.3.0
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/dist/client/route_inspector/route_inspector.html +118 -35
- package/dist/jsenv_server.js +435 -4
- package/package.json +13 -13
- package/src/interfacing_with_node/from_node_request.js +19 -0
- package/src/internal/headers_composition.js +12 -0
- package/src/plugins/cookies/cookie.js +77 -0
- package/src/plugins/cookies/server_plugin_response_cookies.js +30 -0
- package/src/plugins/route_inspector/client/code_block.js +3 -1
- package/src/plugins/route_inspector/client/route_inspector.html +111 -34
- package/src/plugins/route_inspector/server_plugin_route_inspector.js +3 -2
- package/src/plugins/server_plugins_controller.js +1 -0
- package/src/plugins_controller.js +43 -0
- package/src/router/router.js +144 -2
- package/src/start_server.js +107 -0
- package/src/cookies/cookie.js +0 -42
- package/src/cookies/cookie_map.js +0 -18
package/src/start_server.js
CHANGED
|
@@ -24,6 +24,7 @@ import { createPolyglotServer } from "./internal/server-polyglot.js";
|
|
|
24
24
|
import { trackServerPendingConnections } from "./internal/trackServerPendingConnections.js";
|
|
25
25
|
import { trackServerPendingRequests } from "./internal/trackServerPendingRequests.js";
|
|
26
26
|
import { serverPluginAutoreloadOnRestart } from "./plugins/autoreload_on_server_restart/server_plugin_autoreload_on_server_restart.js";
|
|
27
|
+
import { serverPluginResponseCookies } from "./plugins/cookies/server_plugin_response_cookies.js";
|
|
27
28
|
import { serverPluginDefaultBody4xx5xx } from "./plugins/default_body_4xx_5xx/server_plugin_default_body_4xx_5xx.js";
|
|
28
29
|
import { serverPluginInternalClientFiles } from "./plugins/internal_client_files/server_plugin_internal_client_files.js";
|
|
29
30
|
import { serverPluginOpenFile } from "./plugins/open_file/server_plugin_open_file.js";
|
|
@@ -50,6 +51,68 @@ const TIMING_NOOP = () => {
|
|
|
50
51
|
return { end: () => {} };
|
|
51
52
|
};
|
|
52
53
|
|
|
54
|
+
const permissionsSatisfy = (permissionsSet, permissionsRequired) => {
|
|
55
|
+
for (const p of permissionsRequired) {
|
|
56
|
+
if (!permissionsSet.has(p)) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return true;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Starts an HTTP (or HTTPS/HTTP2) server.
|
|
65
|
+
*
|
|
66
|
+
* @param {Object} params
|
|
67
|
+
*
|
|
68
|
+
* @param {Array<Object>} [params.routes=[]] - Route definitions for the server.
|
|
69
|
+
* Each route is an object with the following properties:
|
|
70
|
+
* - `endpoint` {string} — Required. A string like `"GET /users/:id"` combining the HTTP method
|
|
71
|
+
* and the resource path. Use `"*"` as the method to match all methods, and URL patterns
|
|
72
|
+
* such as `:param` for named segments or `*` for wildcards.
|
|
73
|
+
* - `fetch` {Function} — Required. `(request, helpers) => response`. Must return a `Response`,
|
|
74
|
+
* a response-like object `{ status, headers, body }`, or `null`/`undefined` to skip the route.
|
|
75
|
+
* - `description` {string} — Optional human-readable description shown in the route inspector.
|
|
76
|
+
* - `permissionsRequired` {Array<string>} — Optional. Permissions the client must hold to
|
|
77
|
+
* access the route. An empty array means anyone can access. When omitted the route is hidden
|
|
78
|
+
* by default (404 for everyone).
|
|
79
|
+
* - `permissionsToSee` {Array<string>} — Optional. Permissions needed to know the route exists
|
|
80
|
+
* (403 instead of 404 when access is denied). An empty array means the route is visible to
|
|
81
|
+
* everyone even when access is denied.
|
|
82
|
+
* - `availableMediaTypes` {Array<string>} — Content-types this route can produce (drives
|
|
83
|
+
* `Accept` content negotiation).
|
|
84
|
+
* - `availableLanguages` {Array<string>} — Languages this route can respond with.
|
|
85
|
+
* - `availableEncodings` {Array<string>} — Encodings this route supports.
|
|
86
|
+
* - `acceptedMediaTypes` {Array<string>} — Content-types accepted for request bodies
|
|
87
|
+
* (POST/PATCH/PUT).
|
|
88
|
+
* - `clientCodeExample` {string|Function} — Optional code snippet displayed in the route
|
|
89
|
+
* inspector as a usage example.
|
|
90
|
+
* - `declarationSource` {string} — Optional file URL of where the route is declared, shown
|
|
91
|
+
* in the route inspector when `canExposeSensitiveData` is enabled.
|
|
92
|
+
* - `headers` {Object} — Optional header pattern that must match for the route to be selected.
|
|
93
|
+
*
|
|
94
|
+
* @param {Array<Object>} [params.plugins=[]] - Server plugins that extend behaviour (hooks such
|
|
95
|
+
* as `grantPermissions`, `redirectRequest`, `handleError`, etc.). See plugin documentation for
|
|
96
|
+
* the exact shape; plugins are not described here in detail.
|
|
97
|
+
*
|
|
98
|
+
* @param {number} [params.port=0] - Port to listen on. Defaults to `0` which lets the OS assign
|
|
99
|
+
* a random available port — useful in tests to avoid port conflicts. A fixed port such as
|
|
100
|
+
* `3000` can be passed for a predictable address.
|
|
101
|
+
*
|
|
102
|
+
* @param {boolean} [params.acceptAnyIp=false] - When `true` the server binds to all network
|
|
103
|
+
* interfaces (`0.0.0.0` / `::`), making it reachable from other machines on the network.
|
|
104
|
+
* When `false` (default) it only listens on the configured `hostname`.
|
|
105
|
+
*
|
|
106
|
+
* @param {boolean} [params.canExposeSensitiveData=false] - Unlocks developer-facing features that
|
|
107
|
+
* should never be enabled in production:
|
|
108
|
+
* - Declaration source links in the route inspector (shows where each route is defined).
|
|
109
|
+
* - The `/.internal/open_file` endpoint that can open files on the server machine.
|
|
110
|
+
* - Auto-reload client notification when the server restarts.
|
|
111
|
+
* - All routes are visible in the route inspector regardless of permission configuration.
|
|
112
|
+
* Set to `true` during local development for a better DX.
|
|
113
|
+
*
|
|
114
|
+
* @returns {Promise<Object>} Resolves to the running server object with `{ origin, port, stop, … }`.
|
|
115
|
+
*/
|
|
53
116
|
export const startServer = async ({
|
|
54
117
|
signal = new AbortController().signal,
|
|
55
118
|
logLevel,
|
|
@@ -160,6 +223,7 @@ export const startServer = async ({
|
|
|
160
223
|
|
|
161
224
|
plugins = [
|
|
162
225
|
...(canExposeSensitiveData ? [serverPluginOpenFile()] : []),
|
|
226
|
+
serverPluginResponseCookies(),
|
|
163
227
|
serverPluginDefaultBody4xx5xx(),
|
|
164
228
|
serverPluginRouteInspector({ canExposeSensitiveData }),
|
|
165
229
|
...(canExposeSensitiveData ? [serverPluginInternalClientFiles()] : []),
|
|
@@ -344,6 +408,7 @@ export const startServer = async ({
|
|
|
344
408
|
stopCallbackSet.clear();
|
|
345
409
|
await Promise.all(promises);
|
|
346
410
|
serverPluginsController.callHooks("serverStopped", { reason });
|
|
411
|
+
await serverPluginsController.destroyAllPlugins();
|
|
347
412
|
status = "stopped";
|
|
348
413
|
stoppedResolve(reason);
|
|
349
414
|
});
|
|
@@ -571,6 +636,48 @@ export const startServer = async ({
|
|
|
571
636
|
}
|
|
572
637
|
},
|
|
573
638
|
);
|
|
639
|
+
// Build permission helpers, lazily per request.
|
|
640
|
+
// A single shared iterator ensures each plugin is called at most once.
|
|
641
|
+
// hasPermissions stops early once the rule is satisfied;
|
|
642
|
+
// getAllPermissions drains all remaining plugins.
|
|
643
|
+
const permissionsSet = new Set();
|
|
644
|
+
const nextPermissionsHook =
|
|
645
|
+
serverPluginsController.createAsyncHookIterator(
|
|
646
|
+
"grantPermissions",
|
|
647
|
+
request,
|
|
648
|
+
);
|
|
649
|
+
const drainPermissionsUntil = async (permissionsRequired) => {
|
|
650
|
+
for (;;) {
|
|
651
|
+
if (
|
|
652
|
+
permissionsRequired !== undefined &&
|
|
653
|
+
permissionsSatisfy(permissionsSet, permissionsRequired)
|
|
654
|
+
) {
|
|
655
|
+
return true;
|
|
656
|
+
}
|
|
657
|
+
const { done, value } = await nextPermissionsHook();
|
|
658
|
+
if (done) {
|
|
659
|
+
break;
|
|
660
|
+
}
|
|
661
|
+
if (Array.isArray(value)) {
|
|
662
|
+
for (const p of value) {
|
|
663
|
+
permissionsSet.add(p);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
return false;
|
|
668
|
+
};
|
|
669
|
+
const getAllPermissions = async () => {
|
|
670
|
+
await drainPermissionsUntil(undefined);
|
|
671
|
+
return permissionsSet;
|
|
672
|
+
};
|
|
673
|
+
const hasPermissions = async (permissionsRequired) => {
|
|
674
|
+
if (permissionsRequired.length === 0) {
|
|
675
|
+
return true;
|
|
676
|
+
}
|
|
677
|
+
return drainPermissionsUntil(permissionsRequired);
|
|
678
|
+
};
|
|
679
|
+
fetchSecondArg.getAllPermissions = getAllPermissions;
|
|
680
|
+
fetchSecondArg.hasPermissions = hasPermissions;
|
|
574
681
|
const routerResponseProperties = await router.match(
|
|
575
682
|
request,
|
|
576
683
|
fetchSecondArg,
|
package/src/cookies/cookie.js
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
// https://bun.sh/docs/api/cookie
|
|
2
|
-
|
|
3
|
-
export const COOKIE = {
|
|
4
|
-
from: (name, value, options = {}) => {
|
|
5
|
-
return { name, value, options };
|
|
6
|
-
},
|
|
7
|
-
parse: (cookieString) => {
|
|
8
|
-
const parts = cookieString.split("; ");
|
|
9
|
-
const [nameValue, ...attributes] = parts;
|
|
10
|
-
const [name, value] = nameValue.split("=");
|
|
11
|
-
const options = {};
|
|
12
|
-
for (const attr of attributes) {
|
|
13
|
-
const [key, val] = attr.split("=");
|
|
14
|
-
if (key === "expires") {
|
|
15
|
-
options.expires = new Date(val);
|
|
16
|
-
continue;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
if (key === "path") {
|
|
20
|
-
options.path = val;
|
|
21
|
-
continue;
|
|
22
|
-
}
|
|
23
|
-
if (key === "domain") {
|
|
24
|
-
options.domain = val;
|
|
25
|
-
continue;
|
|
26
|
-
}
|
|
27
|
-
if (key === "secure") {
|
|
28
|
-
options.secure = true;
|
|
29
|
-
continue;
|
|
30
|
-
}
|
|
31
|
-
if (key === "httponly") {
|
|
32
|
-
options.httpOnly = true;
|
|
33
|
-
continue;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
return { name, value, options };
|
|
38
|
-
},
|
|
39
|
-
stringify: ({ name, value, options }) => {
|
|
40
|
-
return `${name}=${value}${options.expires ? `; Expires=${options.expires.toUTCString()}` : ""}${options.path ? `; Path=${options.path}` : ""}${options.domain ? `; Domain=${options.domain}` : ""}${options.secure ? "; Secure" : ""}${options.httpOnly ? "; HttpOnly" : ""}`;
|
|
41
|
-
},
|
|
42
|
-
};
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { COOKIE } from "./cookie.js";
|
|
2
|
-
|
|
3
|
-
export const createCookieMap = (setCookieString) => {
|
|
4
|
-
const map = new Map();
|
|
5
|
-
|
|
6
|
-
if (setCookieString) {
|
|
7
|
-
const cookies = setCookieString.split(", ");
|
|
8
|
-
for (const cookie of cookies) {
|
|
9
|
-
const parsedCookie = COOKIE.parse(cookie);
|
|
10
|
-
map.set(parsedCookie.name, parsedCookie);
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
return {
|
|
15
|
-
map,
|
|
16
|
-
toSetCookieHeaders: () => {},
|
|
17
|
-
};
|
|
18
|
-
};
|