@jsenv/server 17.1.0 → 17.2.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 +453 -17
- 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/cors/server_plugin_cors.js +26 -13
- 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 +36 -0
- package/src/router/router.js +144 -2
- package/src/start_server.js +106 -0
- package/src/cookies/cookie.js +0 -42
- package/src/cookies/cookie_map.js +0 -18
|
@@ -47,6 +47,7 @@ export const fromNodeRequest = (
|
|
|
47
47
|
});
|
|
48
48
|
|
|
49
49
|
const headers = headersFromObject(nodeRequest.headers);
|
|
50
|
+
const cookies = parseRequestCookieHeader(headers["cookie"]);
|
|
50
51
|
// pause the request body stream to let a chance for other parts of the code to subscribe to the stream
|
|
51
52
|
// Without this the request body readable stream
|
|
52
53
|
// might be closed when we'll try to attach "data" and "end" listeners to it
|
|
@@ -184,6 +185,7 @@ export const fromNodeRequest = (
|
|
|
184
185
|
}),
|
|
185
186
|
method: nodeRequest.method,
|
|
186
187
|
headers,
|
|
188
|
+
cookies,
|
|
187
189
|
body,
|
|
188
190
|
buffer,
|
|
189
191
|
formData,
|
|
@@ -387,6 +389,23 @@ export const readRequestBody = (request, { as }) => {
|
|
|
387
389
|
throw new Error(`unsupported ${as}`);
|
|
388
390
|
};
|
|
389
391
|
|
|
392
|
+
const parseRequestCookieHeader = (cookieHeader) => {
|
|
393
|
+
const map = new Map();
|
|
394
|
+
if (!cookieHeader) {
|
|
395
|
+
return map;
|
|
396
|
+
}
|
|
397
|
+
for (const pair of cookieHeader.split(";")) {
|
|
398
|
+
const eqIndex = pair.indexOf("=");
|
|
399
|
+
if (eqIndex === -1) {
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
const name = pair.slice(0, eqIndex).trim();
|
|
403
|
+
const value = decodeURIComponent(pair.slice(eqIndex + 1).trim());
|
|
404
|
+
map.set(name, value);
|
|
405
|
+
}
|
|
406
|
+
return map;
|
|
407
|
+
};
|
|
408
|
+
|
|
390
409
|
export const applyRedirectionToRequest = (
|
|
391
410
|
request,
|
|
392
411
|
{ resource, pathname, ...rest },
|
|
@@ -56,4 +56,16 @@ const HEADER_NAMES_COMPOSITION = {
|
|
|
56
56
|
"server-timing": composeTwoCommaSeparatedValues,
|
|
57
57
|
// 'content-type', // https://github.com/ninenines/cowboy/issues/1230
|
|
58
58
|
"vary": composeTwoCommaSeparatedValues,
|
|
59
|
+
// set-cookie is special: multiple values must be kept as an array
|
|
60
|
+
"set-cookie": (leftValue, rightValue) => {
|
|
61
|
+
if (!leftValue) {
|
|
62
|
+
return rightValue;
|
|
63
|
+
}
|
|
64
|
+
if (!rightValue) {
|
|
65
|
+
return leftValue;
|
|
66
|
+
}
|
|
67
|
+
const left = Array.isArray(leftValue) ? leftValue : [leftValue];
|
|
68
|
+
const right = Array.isArray(rightValue) ? rightValue : [rightValue];
|
|
69
|
+
return [...left, ...right];
|
|
70
|
+
},
|
|
59
71
|
};
|
|
@@ -0,0 +1,77 @@
|
|
|
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
|
+
if (key === "path") {
|
|
19
|
+
options.path = val;
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if (key === "domain") {
|
|
23
|
+
options.domain = val;
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (key === "secure") {
|
|
27
|
+
options.secure = true;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (key === "httponly") {
|
|
31
|
+
options.httpOnly = true;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return { name, value, options };
|
|
37
|
+
},
|
|
38
|
+
stringify: ({ name, value, options }) => {
|
|
39
|
+
let str = `${name}=${value}`;
|
|
40
|
+
if (options.expires) {
|
|
41
|
+
str += `; Expires=${options.expires.toUTCString()}`;
|
|
42
|
+
}
|
|
43
|
+
if (options.maxAge !== undefined) {
|
|
44
|
+
str += `; Max-Age=${options.maxAge}`;
|
|
45
|
+
}
|
|
46
|
+
if (options.path) {
|
|
47
|
+
str += `; Path=${options.path}`;
|
|
48
|
+
}
|
|
49
|
+
if (options.domain) {
|
|
50
|
+
str += `; Domain=${options.domain}`;
|
|
51
|
+
}
|
|
52
|
+
if (options.sameSite) {
|
|
53
|
+
str += `; SameSite=${options.sameSite}`;
|
|
54
|
+
}
|
|
55
|
+
if (options.secure) {
|
|
56
|
+
str += "; Secure";
|
|
57
|
+
}
|
|
58
|
+
if (options.httpOnly) {
|
|
59
|
+
str += "; HttpOnly";
|
|
60
|
+
}
|
|
61
|
+
return str;
|
|
62
|
+
},
|
|
63
|
+
toSetCookieHeaders: (cookies) => {
|
|
64
|
+
const headers = {};
|
|
65
|
+
for (const cookie of cookies) {
|
|
66
|
+
const value = COOKIE.stringify(cookie);
|
|
67
|
+
if (headers["set-cookie"] === undefined) {
|
|
68
|
+
headers["set-cookie"] = value;
|
|
69
|
+
} else if (Array.isArray(headers["set-cookie"])) {
|
|
70
|
+
headers["set-cookie"].push(value);
|
|
71
|
+
} else {
|
|
72
|
+
headers["set-cookie"] = [headers["set-cookie"], value];
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return headers;
|
|
76
|
+
},
|
|
77
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { COOKIE } from "./cookie.js";
|
|
2
|
+
|
|
3
|
+
export const serverPluginResponseCookies = () => {
|
|
4
|
+
return {
|
|
5
|
+
name: "jsenv:response_cookies",
|
|
6
|
+
augmentRouteFetchSecondArg: (request, fetchSecondArg) => {
|
|
7
|
+
const responseCookies = {
|
|
8
|
+
set: (name, value, options = {}) => {
|
|
9
|
+
fetchSecondArg.injectResponseHeader(
|
|
10
|
+
"set-cookie",
|
|
11
|
+
COOKIE.stringify(COOKIE.from(name, value, options)),
|
|
12
|
+
);
|
|
13
|
+
},
|
|
14
|
+
delete: (name, options = {}) => {
|
|
15
|
+
fetchSecondArg.injectResponseHeader(
|
|
16
|
+
"set-cookie",
|
|
17
|
+
COOKIE.stringify(
|
|
18
|
+
COOKIE.from(name, "", {
|
|
19
|
+
...options,
|
|
20
|
+
expires: new Date(0),
|
|
21
|
+
}),
|
|
22
|
+
),
|
|
23
|
+
);
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
return { responseCookies };
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
};
|
|
@@ -75,17 +75,32 @@ const generateAccessControlHeaders = ({
|
|
|
75
75
|
} = {}) => {
|
|
76
76
|
const vary = [];
|
|
77
77
|
|
|
78
|
-
|
|
79
|
-
if
|
|
80
|
-
|
|
81
|
-
|
|
78
|
+
// Access-Control-Allow-Origin must be a single value (not a list).
|
|
79
|
+
// We reflect back the request's origin if it is in the allowed list.
|
|
80
|
+
// If no origin matches we fall back to "*" (only when not using credentials).
|
|
81
|
+
let allowOrigin = null;
|
|
82
|
+
|
|
83
|
+
const requestOrigin =
|
|
84
|
+
"origin" in headers && headers.origin !== "null"
|
|
85
|
+
? headers.origin
|
|
86
|
+
: "referer" in headers
|
|
87
|
+
? new URL(headers.referer).origin
|
|
88
|
+
: null;
|
|
89
|
+
|
|
90
|
+
if (requestOrigin) {
|
|
91
|
+
if (accessControlAllowedOrigins.includes(requestOrigin)) {
|
|
92
|
+
allowOrigin = requestOrigin;
|
|
93
|
+
vary.push("origin");
|
|
94
|
+
} else if (accessControlAllowRequestOrigin) {
|
|
95
|
+
allowOrigin = requestOrigin;
|
|
82
96
|
vary.push("origin");
|
|
83
|
-
} else if ("referer" in headers) {
|
|
84
|
-
allowedOriginArray.push(new URL(headers.referer).origin);
|
|
85
|
-
vary.push("referer");
|
|
86
|
-
} else {
|
|
87
|
-
allowedOriginArray.push("*");
|
|
88
97
|
}
|
|
98
|
+
} else if (accessControlAllowRequestOrigin) {
|
|
99
|
+
allowOrigin = "*";
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (allowOrigin === null) {
|
|
103
|
+
allowOrigin = accessControlAllowedOrigins[0] ?? "*";
|
|
89
104
|
}
|
|
90
105
|
|
|
91
106
|
const allowedMethodArray = [...accessControlAllowedMethods];
|
|
@@ -119,16 +134,14 @@ const generateAccessControlHeaders = ({
|
|
|
119
134
|
}
|
|
120
135
|
|
|
121
136
|
return {
|
|
122
|
-
"access-control-allow-origin":
|
|
137
|
+
"access-control-allow-origin": allowOrigin,
|
|
123
138
|
"access-control-allow-methods": allowedMethodArray.join(", "),
|
|
124
139
|
"access-control-allow-headers": allowedHeaderArray.join(", "),
|
|
125
140
|
...(accessControlAllowCredentials
|
|
126
141
|
? { "access-control-allow-credentials": true }
|
|
127
142
|
: {}),
|
|
128
143
|
"access-control-max-age": accessControlMaxAge,
|
|
129
|
-
...(timingAllowOrigin
|
|
130
|
-
? { "timing-allow-origin": allowedOriginArray.join(", ") }
|
|
131
|
-
: {}),
|
|
144
|
+
...(timingAllowOrigin ? { "timing-allow-origin": allowOrigin } : {}),
|
|
132
145
|
...(vary.length ? { vary: vary.join(", ") } : {}),
|
|
133
146
|
};
|
|
134
147
|
};
|
|
@@ -113,7 +113,9 @@
|
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
customElements.
|
|
116
|
+
if (!customElements.get("clipboard-copy")) {
|
|
117
|
+
customElements.define("clipboard-copy", ClipboardCopy);
|
|
118
|
+
}
|
|
117
119
|
|
|
118
120
|
const addToClipboard = async (text) => {
|
|
119
121
|
const type = "text/plain";
|
|
@@ -42,6 +42,13 @@
|
|
|
42
42
|
--border-color: #e0e0e0;
|
|
43
43
|
--primary-color: #1976d2;
|
|
44
44
|
--secondary-color: #f5f5f5;
|
|
45
|
+
--forbidden-bg-color: #fff8f0;
|
|
46
|
+
--forbidden-border-color: #cc8800;
|
|
47
|
+
--forbidden-badge-bg: #fff3cc;
|
|
48
|
+
--forbidden-badge-color: #7a5000;
|
|
49
|
+
--admin-badge-bg: #f0f4ff;
|
|
50
|
+
--admin-badge-border-color: #5c7cfa;
|
|
51
|
+
--admin-badge-color: #2a4bbf;
|
|
45
52
|
}
|
|
46
53
|
:root[data-theme="dark"] {
|
|
47
54
|
--background-color: #121212;
|
|
@@ -49,6 +56,13 @@
|
|
|
49
56
|
--border-color: #333;
|
|
50
57
|
--primary-color: #4f9eff;
|
|
51
58
|
--secondary-color: #1e1e1e;
|
|
59
|
+
--forbidden-bg-color: #1e1500;
|
|
60
|
+
--forbidden-border-color: #8a6000;
|
|
61
|
+
--forbidden-badge-bg: #3a2800;
|
|
62
|
+
--forbidden-badge-color: #f0c060;
|
|
63
|
+
--admin-badge-bg: #0d1a3a;
|
|
64
|
+
--admin-badge-border-color: #3b5bdb;
|
|
65
|
+
--admin-badge-color: #91a7ff;
|
|
52
66
|
}
|
|
53
67
|
|
|
54
68
|
:root[data-theme="dark"] a {
|
|
@@ -467,6 +481,42 @@
|
|
|
467
481
|
--method-color: var(--method-delete-color);
|
|
468
482
|
}
|
|
469
483
|
|
|
484
|
+
.route_details[data-forbidden] {
|
|
485
|
+
background: var(--forbidden-bg-color);
|
|
486
|
+
border-color: var(--forbidden-border-color);
|
|
487
|
+
opacity: 0.85;
|
|
488
|
+
}
|
|
489
|
+
.route_details[data-forbidden] .route_method {
|
|
490
|
+
opacity: 0.7;
|
|
491
|
+
filter: grayscale(60%);
|
|
492
|
+
}
|
|
493
|
+
.route_forbidden_badge {
|
|
494
|
+
margin-right: 6px;
|
|
495
|
+
padding: 3px 8px;
|
|
496
|
+
color: var(--forbidden-badge-color);
|
|
497
|
+
font-weight: 600;
|
|
498
|
+
font-size: 12px;
|
|
499
|
+
white-space: nowrap;
|
|
500
|
+
background: var(--forbidden-badge-bg);
|
|
501
|
+
border: 1px solid var(--forbidden-border-color);
|
|
502
|
+
border-radius: 4px;
|
|
503
|
+
}
|
|
504
|
+
.route_admin_badge {
|
|
505
|
+
margin-right: 6px;
|
|
506
|
+
padding: 3px 8px;
|
|
507
|
+
color: var(--admin-badge-color);
|
|
508
|
+
font-weight: 600;
|
|
509
|
+
font-size: 12px;
|
|
510
|
+
white-space: nowrap;
|
|
511
|
+
background: var(--admin-badge-bg);
|
|
512
|
+
border: 1px solid var(--admin-badge-border-color);
|
|
513
|
+
border-radius: 4px;
|
|
514
|
+
}
|
|
515
|
+
.route_forbidden_content {
|
|
516
|
+
color: var(--forbidden-badge-color);
|
|
517
|
+
font-style: italic;
|
|
518
|
+
}
|
|
519
|
+
|
|
470
520
|
.route_owner_name {
|
|
471
521
|
padding: 4px 8px;
|
|
472
522
|
color: #666;
|
|
@@ -1388,6 +1438,8 @@
|
|
|
1388
1438
|
websocket,
|
|
1389
1439
|
clientCodeExample,
|
|
1390
1440
|
declarationSource,
|
|
1441
|
+
forbidden,
|
|
1442
|
+
visibleBecauseAdmin,
|
|
1391
1443
|
}) => {
|
|
1392
1444
|
const [selectedMethod] = useUrlStringParam("method");
|
|
1393
1445
|
const [selectedPackageName] = usePackageUrlStringParam();
|
|
@@ -1400,11 +1452,16 @@
|
|
|
1400
1452
|
return null;
|
|
1401
1453
|
}
|
|
1402
1454
|
|
|
1403
|
-
return html`<details
|
|
1455
|
+
return html`<details
|
|
1456
|
+
class="route_details"
|
|
1457
|
+
data-method="${method}"
|
|
1458
|
+
data-forbidden="${forbidden || undefined}"
|
|
1459
|
+
data-admin-visible="${visibleBecauseAdmin || undefined}"
|
|
1460
|
+
>
|
|
1404
1461
|
<summary class="route_summary_container">
|
|
1405
1462
|
<span class="route_method">${method}</span>
|
|
1406
1463
|
<p class="route_resource">${resource}</p>
|
|
1407
|
-
${method === "GET"
|
|
1464
|
+
${method === "GET" && !forbidden
|
|
1408
1465
|
? html`<a
|
|
1409
1466
|
style="display:inline-flex; margin-left: 5px;"
|
|
1410
1467
|
href="${resource}"
|
|
@@ -1412,25 +1469,45 @@
|
|
|
1412
1469
|
/></a>`
|
|
1413
1470
|
: null}
|
|
1414
1471
|
<span style="flex:1"></span>
|
|
1472
|
+
${visibleBecauseAdmin
|
|
1473
|
+
? html`<span
|
|
1474
|
+
class="route_admin_badge"
|
|
1475
|
+
title="This route is only visible because the server was started with canExposeSensitiveData"
|
|
1476
|
+
>🔒 admin only</span
|
|
1477
|
+
>`
|
|
1478
|
+
: null}
|
|
1479
|
+
${forbidden && !visibleBecauseAdmin
|
|
1480
|
+
? html`<span class="route_forbidden_badge"
|
|
1481
|
+
>🔒 access denied</span
|
|
1482
|
+
>`
|
|
1483
|
+
: null}
|
|
1415
1484
|
<${RouteBadge} meta="${meta}" />
|
|
1416
1485
|
<span class="summary_marker">
|
|
1417
1486
|
<${ArrowDown} />
|
|
1418
1487
|
</span>
|
|
1419
1488
|
</summary>
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1489
|
+
${!forbidden
|
|
1490
|
+
? html`<${RouteDetailsContent}
|
|
1491
|
+
method="${method}"
|
|
1492
|
+
resource="${resource}"
|
|
1493
|
+
description="${description}"
|
|
1494
|
+
availableMediaTypes="${availableMediaTypes}"
|
|
1495
|
+
availableLanguages="${availableLanguages}"
|
|
1496
|
+
availableVersions="${availableVersions}"
|
|
1497
|
+
availableEncodings="${availableEncodings}"
|
|
1498
|
+
acceptedMediaTypes="${acceptedMediaTypes}"
|
|
1499
|
+
websocket="${websocket}"
|
|
1500
|
+
clientCodeExample="${clientCodeExample}"
|
|
1501
|
+
declarationSource="${declarationSource}"
|
|
1502
|
+
meta="${meta}"
|
|
1503
|
+
/>`
|
|
1504
|
+
: html`<div class="route_details_content route_forbidden_content">
|
|
1505
|
+
<p>
|
|
1506
|
+
${visibleBecauseAdmin
|
|
1507
|
+
? "This route is only visible because the server was started with canExposeSensitiveData. Access is denied with the current permissions."
|
|
1508
|
+
: "You do not have the required permissions to access this route."}
|
|
1509
|
+
</p>
|
|
1510
|
+
</div>`}
|
|
1434
1511
|
</details>`;
|
|
1435
1512
|
};
|
|
1436
1513
|
|
|
@@ -1561,26 +1638,26 @@
|
|
|
1561
1638
|
const websocketScheme =
|
|
1562
1639
|
self.location.protocol === "https:" ? "wss" : "ws";
|
|
1563
1640
|
const websocketOrigin = `${websocketScheme}://${self.location.host}`;
|
|
1564
|
-
jsSource =
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1641
|
+
jsSource = `
|
|
1642
|
+
const websocket = new WebSocket("${websocketOrigin}${resource}");
|
|
1643
|
+
websocket.onerror = () => {
|
|
1644
|
+
console.error("Failed to connect to websocket endpoint");
|
|
1645
|
+
};
|
|
1646
|
+
websocket.onopen = () => {
|
|
1647
|
+
console.info("Connected to websocket endpoint");
|
|
1648
|
+
};
|
|
1649
|
+
websocket.onmessage = (event) => {
|
|
1650
|
+
console.log("Received message", event.data);
|
|
1651
|
+
};
|
|
1652
|
+
`;
|
|
1576
1653
|
} else {
|
|
1577
1654
|
let fetchLine;
|
|
1578
1655
|
if (method === "GET") {
|
|
1579
1656
|
fetchLine = `const response = await window.fetch("${resource}");`;
|
|
1580
1657
|
} else {
|
|
1581
1658
|
fetchLine = `const response = await window.fetch("${resource}", {
|
|
1582
|
-
|
|
1583
|
-
|
|
1659
|
+
method: "${method}",
|
|
1660
|
+
}`;
|
|
1584
1661
|
}
|
|
1585
1662
|
let readResponseLine;
|
|
1586
1663
|
if (availableMediaTypes.includes("application/json")) {
|
|
@@ -1589,10 +1666,10 @@
|
|
|
1589
1666
|
readResponseLine = `const text = await response.text();`;
|
|
1590
1667
|
}
|
|
1591
1668
|
|
|
1592
|
-
jsSource =
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1669
|
+
jsSource = `
|
|
1670
|
+
${fetchLine};
|
|
1671
|
+
${readResponseLine};
|
|
1672
|
+
`;
|
|
1596
1673
|
}
|
|
1597
1674
|
|
|
1598
1675
|
return html`<code-block lang="js" data-escaped data-copy-button
|
|
@@ -28,9 +28,10 @@ export const serverPluginRouteInspector = ({ canExposeSensitiveData }) => {
|
|
|
28
28
|
availableMediaTypes: ["application/json"],
|
|
29
29
|
description: "Get the routes available on this server in JSON.",
|
|
30
30
|
declarationSource: import.meta.url,
|
|
31
|
-
fetch: (request, helpers) => {
|
|
32
|
-
const routeJSON = helpers.router.inspect({
|
|
31
|
+
fetch: async (request, helpers) => {
|
|
32
|
+
const routeJSON = await helpers.router.inspect({
|
|
33
33
|
canExposeSensitiveData,
|
|
34
|
+
fetchSecondArg: helpers,
|
|
34
35
|
});
|
|
35
36
|
return Response.json(routeJSON);
|
|
36
37
|
},
|
|
@@ -9,6 +9,7 @@ export const createServerPluginsController = async (serverPlugins) => {
|
|
|
9
9
|
serverListening: { type: "hook" },
|
|
10
10
|
redirectRequest: { type: "hook" },
|
|
11
11
|
augmentRouteFetchSecondArg: { type: "hook" },
|
|
12
|
+
grantPermissions: { type: "hook" },
|
|
12
13
|
handleError: { type: "hook" },
|
|
13
14
|
onResponsePush: { type: "hook" },
|
|
14
15
|
injectResponseProperties: { type: "hook" },
|
|
@@ -297,6 +297,42 @@ export const createPluginsController = async ({
|
|
|
297
297
|
callHooksUntil,
|
|
298
298
|
callAsyncHooks,
|
|
299
299
|
callAsyncHooksUntil,
|
|
300
|
+
// callAsyncHooksWhile(hookName, info, callback)
|
|
301
|
+
// Calls hooks one by one, passing each return value to callback.
|
|
302
|
+
// Stops as soon as callback returns false.
|
|
303
|
+
callAsyncHooksWhile: async (hookName, info, callback) => {
|
|
304
|
+
const hookSet = hookSetMap.get(hookName);
|
|
305
|
+
if (!hookSet) {
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
for (const hook of hookSet) {
|
|
309
|
+
const returnValue = await callAsyncHook(hook, info);
|
|
310
|
+
const shouldContinue = await callback(returnValue);
|
|
311
|
+
if (!shouldContinue) {
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
},
|
|
316
|
+
|
|
317
|
+
// createAsyncHookIterator(hookName, info)
|
|
318
|
+
// Returns an async function that calls the next hook on each invocation.
|
|
319
|
+
// Returns { done: true } when all hooks have been called.
|
|
320
|
+
// Allows callers to maintain a shared cursor across multiple consumers.
|
|
321
|
+
createAsyncHookIterator: (hookName, info) => {
|
|
322
|
+
const hookSet = hookSetMap.get(hookName);
|
|
323
|
+
if (!hookSet) {
|
|
324
|
+
return async () => ({ done: true, value: undefined });
|
|
325
|
+
}
|
|
326
|
+
const iterator = hookSet.values();
|
|
327
|
+
return async () => {
|
|
328
|
+
const { done, value: hook } = iterator.next();
|
|
329
|
+
if (done) {
|
|
330
|
+
return { done: true, value: undefined };
|
|
331
|
+
}
|
|
332
|
+
const value = await callAsyncHook(hook, info);
|
|
333
|
+
return { done: false, value };
|
|
334
|
+
};
|
|
335
|
+
},
|
|
300
336
|
|
|
301
337
|
getPluginMeta: (pluginName) => meta[pluginName],
|
|
302
338
|
getMeta: () => meta,
|