@http-client-toolkit/dashboard 1.0.1 → 2.0.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/lib/index.cjs +88 -14
- package/lib/index.cjs.map +1 -1
- package/lib/index.d.cts +10 -0
- package/lib/index.d.ts +10 -0
- package/lib/index.js +89 -15
- package/lib/index.js.map +1 -1
- package/package.json +7 -7
package/lib/index.cjs
CHANGED
|
@@ -381,6 +381,7 @@ var DashboardOptionsSchema = zod.z
|
|
|
381
381
|
.min(1, 'At least one client is required'),
|
|
382
382
|
basePath: zod.z.string().default('/'),
|
|
383
383
|
pollIntervalMs: zod.z.number().int().positive().default(5e3),
|
|
384
|
+
readonly: zod.z.boolean().default(false),
|
|
384
385
|
})
|
|
385
386
|
.refine(
|
|
386
387
|
(data) => {
|
|
@@ -417,7 +418,11 @@ function parseUrl(req, basePath) {
|
|
|
417
418
|
const raw = req.url ?? '/';
|
|
418
419
|
const url = new URL(raw, 'http://localhost');
|
|
419
420
|
let pathname = url.pathname;
|
|
420
|
-
if (
|
|
421
|
+
if (
|
|
422
|
+
basePath !== '/' &&
|
|
423
|
+
pathname.startsWith(basePath) &&
|
|
424
|
+
(pathname.length === basePath.length || pathname[basePath.length] === '/')
|
|
425
|
+
) {
|
|
421
426
|
pathname = pathname.slice(basePath.length) || '/';
|
|
422
427
|
}
|
|
423
428
|
return { pathname, query: url.searchParams };
|
|
@@ -435,15 +440,23 @@ function extractParam(pathname, pattern) {
|
|
|
435
440
|
if (paramIndex === -1) return void 0;
|
|
436
441
|
return pathParts[paramIndex];
|
|
437
442
|
}
|
|
443
|
+
var MAX_BODY_SIZE = 1024 * 1024;
|
|
438
444
|
async function readJsonBody(req) {
|
|
439
|
-
return new Promise((
|
|
445
|
+
return new Promise((resolve3, reject) => {
|
|
440
446
|
let body = '';
|
|
447
|
+
let size = 0;
|
|
441
448
|
req.on('data', (chunk) => {
|
|
449
|
+
size += chunk.length;
|
|
450
|
+
if (size > MAX_BODY_SIZE) {
|
|
451
|
+
req.destroy();
|
|
452
|
+
reject(new Error('Request body too large'));
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
442
455
|
body += chunk.toString();
|
|
443
456
|
});
|
|
444
457
|
req.on('end', () => {
|
|
445
458
|
try {
|
|
446
|
-
|
|
459
|
+
resolve3(JSON.parse(body));
|
|
447
460
|
} catch {
|
|
448
461
|
reject(new Error('Invalid JSON body'));
|
|
449
462
|
}
|
|
@@ -471,6 +484,9 @@ function sendNotFound(res) {
|
|
|
471
484
|
function sendMethodNotAllowed(res) {
|
|
472
485
|
sendError(res, 'Method not allowed', 405);
|
|
473
486
|
}
|
|
487
|
+
function sendForbidden(res) {
|
|
488
|
+
sendError(res, 'Dashboard is in readonly mode', 403);
|
|
489
|
+
}
|
|
474
490
|
|
|
475
491
|
// src/server/handlers/cache.ts
|
|
476
492
|
async function handleCacheStats(res, adapter) {
|
|
@@ -701,7 +717,15 @@ function createApiRouter(ctx) {
|
|
|
701
717
|
sendError(res, `Unknown client: ${clientName}`, 404);
|
|
702
718
|
return true;
|
|
703
719
|
}
|
|
704
|
-
return routeClientApi(
|
|
720
|
+
return routeClientApi(
|
|
721
|
+
req,
|
|
722
|
+
res,
|
|
723
|
+
client,
|
|
724
|
+
subPath,
|
|
725
|
+
method,
|
|
726
|
+
query,
|
|
727
|
+
ctx.readonly,
|
|
728
|
+
);
|
|
705
729
|
}
|
|
706
730
|
if (pathname.startsWith('/api/')) {
|
|
707
731
|
sendNotFound(res);
|
|
@@ -710,7 +734,22 @@ function createApiRouter(ctx) {
|
|
|
710
734
|
return false;
|
|
711
735
|
};
|
|
712
736
|
}
|
|
713
|
-
|
|
737
|
+
function isMutatingMethod(method) {
|
|
738
|
+
return method === 'DELETE' || method === 'PUT' || method === 'POST';
|
|
739
|
+
}
|
|
740
|
+
async function routeClientApi(
|
|
741
|
+
req,
|
|
742
|
+
res,
|
|
743
|
+
client,
|
|
744
|
+
subPath,
|
|
745
|
+
method,
|
|
746
|
+
query,
|
|
747
|
+
readonly,
|
|
748
|
+
) {
|
|
749
|
+
if (readonly && isMutatingMethod(method)) {
|
|
750
|
+
sendForbidden(res);
|
|
751
|
+
return true;
|
|
752
|
+
}
|
|
714
753
|
if (subPath.startsWith('/cache')) {
|
|
715
754
|
if (!client.cache) {
|
|
716
755
|
sendError(res, 'Cache store not configured', 404);
|
|
@@ -835,7 +874,7 @@ function getCurrentDir() {
|
|
|
835
874
|
function getClientDir() {
|
|
836
875
|
if (clientDir) return clientDir;
|
|
837
876
|
const currentDir = getCurrentDir();
|
|
838
|
-
clientDir = path.
|
|
877
|
+
clientDir = path.resolve(currentDir, '..', 'dist', 'client');
|
|
839
878
|
return clientDir;
|
|
840
879
|
}
|
|
841
880
|
function getIndexHtml() {
|
|
@@ -861,7 +900,12 @@ function getIndexHtml() {
|
|
|
861
900
|
function serveStatic(res, pathname) {
|
|
862
901
|
const dir = getClientDir();
|
|
863
902
|
if (pathname !== '/' && pathname !== '/index.html') {
|
|
864
|
-
const filePath = path.join(dir, pathname);
|
|
903
|
+
const filePath = path.resolve(path.join(dir, pathname));
|
|
904
|
+
if (!filePath.startsWith(dir + '/')) {
|
|
905
|
+
res.writeHead(400, { 'Content-Type': 'text/plain' });
|
|
906
|
+
res.end('Bad request');
|
|
907
|
+
return true;
|
|
908
|
+
}
|
|
865
909
|
if (fs.existsSync(filePath)) {
|
|
866
910
|
try {
|
|
867
911
|
const content = fs.readFileSync(filePath);
|
|
@@ -911,6 +955,7 @@ function createDashboard(options) {
|
|
|
911
955
|
const ctx = {
|
|
912
956
|
clients,
|
|
913
957
|
pollIntervalMs: opts.pollIntervalMs,
|
|
958
|
+
readonly: opts.readonly,
|
|
914
959
|
};
|
|
915
960
|
const apiRouter = createApiRouter(ctx);
|
|
916
961
|
return (req, res, _next) => {
|
|
@@ -969,7 +1014,7 @@ function getCurrentDir2() {
|
|
|
969
1014
|
function getClientDir2() {
|
|
970
1015
|
if (clientDir2) return clientDir2;
|
|
971
1016
|
const currentDir = getCurrentDir2();
|
|
972
|
-
clientDir2 = path.
|
|
1017
|
+
clientDir2 = path.resolve(currentDir, '..', 'dist', 'client');
|
|
973
1018
|
return clientDir2;
|
|
974
1019
|
}
|
|
975
1020
|
function getIndexHtml2() {
|
|
@@ -995,7 +1040,10 @@ function getIndexHtml2() {
|
|
|
995
1040
|
function serveStaticWeb(pathname) {
|
|
996
1041
|
const dir = getClientDir2();
|
|
997
1042
|
if (pathname !== '/' && pathname !== '/index.html') {
|
|
998
|
-
const filePath = path.join(dir, pathname);
|
|
1043
|
+
const filePath = path.resolve(path.join(dir, pathname));
|
|
1044
|
+
if (!filePath.startsWith(dir + '/')) {
|
|
1045
|
+
return new Response('Bad request', { status: 400 });
|
|
1046
|
+
}
|
|
999
1047
|
if (fs.existsSync(filePath)) {
|
|
1000
1048
|
try {
|
|
1001
1049
|
const content = fs.readFileSync(filePath);
|
|
@@ -1068,14 +1116,34 @@ async function routeApi(request, pathname, query, ctx) {
|
|
|
1068
1116
|
if (!client) {
|
|
1069
1117
|
return errorResponse(`Unknown client: ${clientName}`, 404);
|
|
1070
1118
|
}
|
|
1071
|
-
return routeClientApi2(
|
|
1119
|
+
return routeClientApi2(
|
|
1120
|
+
request,
|
|
1121
|
+
subPath,
|
|
1122
|
+
method,
|
|
1123
|
+
client,
|
|
1124
|
+
query,
|
|
1125
|
+
ctx.readonly,
|
|
1126
|
+
);
|
|
1072
1127
|
}
|
|
1073
1128
|
if (pathname.startsWith('/api/')) {
|
|
1074
1129
|
return errorResponse('Not found', 404);
|
|
1075
1130
|
}
|
|
1076
1131
|
return null;
|
|
1077
1132
|
}
|
|
1078
|
-
|
|
1133
|
+
function isMutatingMethod2(method) {
|
|
1134
|
+
return method === 'DELETE' || method === 'PUT' || method === 'POST';
|
|
1135
|
+
}
|
|
1136
|
+
async function routeClientApi2(
|
|
1137
|
+
request,
|
|
1138
|
+
subPath,
|
|
1139
|
+
method,
|
|
1140
|
+
client,
|
|
1141
|
+
query,
|
|
1142
|
+
readonly,
|
|
1143
|
+
) {
|
|
1144
|
+
if (readonly && isMutatingMethod2(method)) {
|
|
1145
|
+
return errorResponse('Dashboard is in readonly mode', 403);
|
|
1146
|
+
}
|
|
1079
1147
|
if (subPath.startsWith('/cache')) {
|
|
1080
1148
|
if (!client.cache) return errorResponse('Cache store not configured', 404);
|
|
1081
1149
|
return routeCache(subPath, method, client.cache, query);
|
|
@@ -1219,12 +1287,18 @@ function createDashboardHandler(options) {
|
|
|
1219
1287
|
const ctx = {
|
|
1220
1288
|
clients,
|
|
1221
1289
|
pollIntervalMs: opts.pollIntervalMs,
|
|
1290
|
+
readonly: opts.readonly,
|
|
1222
1291
|
};
|
|
1223
1292
|
return async (request) => {
|
|
1224
1293
|
try {
|
|
1225
1294
|
const url = new URL(request.url);
|
|
1226
1295
|
let pathname = url.pathname;
|
|
1227
|
-
if (
|
|
1296
|
+
if (
|
|
1297
|
+
opts.basePath !== '/' &&
|
|
1298
|
+
pathname.startsWith(opts.basePath) &&
|
|
1299
|
+
(pathname.length === opts.basePath.length ||
|
|
1300
|
+
pathname[opts.basePath.length] === '/')
|
|
1301
|
+
) {
|
|
1228
1302
|
pathname = pathname.slice(opts.basePath.length) || '/';
|
|
1229
1303
|
}
|
|
1230
1304
|
const apiResponse = await routeApi(
|
|
@@ -1246,14 +1320,14 @@ async function startDashboard(options) {
|
|
|
1246
1320
|
const server = http.createServer((req, res) => {
|
|
1247
1321
|
middleware(req, res);
|
|
1248
1322
|
});
|
|
1249
|
-
return new Promise((
|
|
1323
|
+
return new Promise((resolve3, reject) => {
|
|
1250
1324
|
server.on('error', reject);
|
|
1251
1325
|
server.listen(opts.port, opts.host, () => {
|
|
1252
1326
|
const addr = server.address();
|
|
1253
1327
|
const url =
|
|
1254
1328
|
typeof addr === 'string' ? addr : `http://${opts.host}:${opts.port}`;
|
|
1255
1329
|
console.log(`Dashboard running at ${url}`);
|
|
1256
|
-
|
|
1330
|
+
resolve3({
|
|
1257
1331
|
server,
|
|
1258
1332
|
async close() {
|
|
1259
1333
|
return new Promise((res, rej) => {
|