@http-client-toolkit/dashboard 2.0.0 → 3.0.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/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 (basePath !== '/' && pathname.startsWith(basePath)) {
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((resolve, reject) => {
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
- resolve(JSON.parse(body));
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(req, res, client, subPath, method, query);
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
- async function routeClientApi(req, res, client, subPath, method, query) {
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.join(currentDir, '..', 'dist', 'client');
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.join(currentDir, '..', 'dist', 'client');
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(request, subPath, method, client, query);
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
- async function routeClientApi2(request, subPath, method, client, query) {
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 (opts.basePath !== '/' && pathname.startsWith(opts.basePath)) {
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((resolve, reject) => {
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
- resolve({
1330
+ resolve3({
1257
1331
  server,
1258
1332
  async close() {
1259
1333
  return new Promise((res, rej) => {