@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.d.ts
CHANGED
|
@@ -67,6 +67,7 @@ declare const DashboardOptionsSchema: z.ZodEffects<
|
|
|
67
67
|
>;
|
|
68
68
|
basePath: z.ZodDefault<z.ZodString>;
|
|
69
69
|
pollIntervalMs: z.ZodDefault<z.ZodNumber>;
|
|
70
|
+
readonly: z.ZodDefault<z.ZodBoolean>;
|
|
70
71
|
},
|
|
71
72
|
'strip',
|
|
72
73
|
z.ZodTypeAny,
|
|
@@ -77,6 +78,7 @@ declare const DashboardOptionsSchema: z.ZodEffects<
|
|
|
77
78
|
}[];
|
|
78
79
|
basePath: string;
|
|
79
80
|
pollIntervalMs: number;
|
|
81
|
+
readonly: boolean;
|
|
80
82
|
},
|
|
81
83
|
{
|
|
82
84
|
clients: {
|
|
@@ -85,6 +87,7 @@ declare const DashboardOptionsSchema: z.ZodEffects<
|
|
|
85
87
|
}[];
|
|
86
88
|
basePath?: string | undefined;
|
|
87
89
|
pollIntervalMs?: number | undefined;
|
|
90
|
+
readonly?: boolean | undefined;
|
|
88
91
|
}
|
|
89
92
|
>,
|
|
90
93
|
{
|
|
@@ -94,6 +97,7 @@ declare const DashboardOptionsSchema: z.ZodEffects<
|
|
|
94
97
|
}[];
|
|
95
98
|
basePath: string;
|
|
96
99
|
pollIntervalMs: number;
|
|
100
|
+
readonly: boolean;
|
|
97
101
|
},
|
|
98
102
|
{
|
|
99
103
|
clients: {
|
|
@@ -102,6 +106,7 @@ declare const DashboardOptionsSchema: z.ZodEffects<
|
|
|
102
106
|
}[];
|
|
103
107
|
basePath?: string | undefined;
|
|
104
108
|
pollIntervalMs?: number | undefined;
|
|
109
|
+
readonly?: boolean | undefined;
|
|
105
110
|
}
|
|
106
111
|
>;
|
|
107
112
|
declare const StandaloneDashboardOptionsSchema: z.ZodIntersection<
|
|
@@ -139,6 +144,7 @@ declare const StandaloneDashboardOptionsSchema: z.ZodIntersection<
|
|
|
139
144
|
>;
|
|
140
145
|
basePath: z.ZodDefault<z.ZodString>;
|
|
141
146
|
pollIntervalMs: z.ZodDefault<z.ZodNumber>;
|
|
147
|
+
readonly: z.ZodDefault<z.ZodBoolean>;
|
|
142
148
|
},
|
|
143
149
|
'strip',
|
|
144
150
|
z.ZodTypeAny,
|
|
@@ -149,6 +155,7 @@ declare const StandaloneDashboardOptionsSchema: z.ZodIntersection<
|
|
|
149
155
|
}[];
|
|
150
156
|
basePath: string;
|
|
151
157
|
pollIntervalMs: number;
|
|
158
|
+
readonly: boolean;
|
|
152
159
|
},
|
|
153
160
|
{
|
|
154
161
|
clients: {
|
|
@@ -157,6 +164,7 @@ declare const StandaloneDashboardOptionsSchema: z.ZodIntersection<
|
|
|
157
164
|
}[];
|
|
158
165
|
basePath?: string | undefined;
|
|
159
166
|
pollIntervalMs?: number | undefined;
|
|
167
|
+
readonly?: boolean | undefined;
|
|
160
168
|
}
|
|
161
169
|
>,
|
|
162
170
|
{
|
|
@@ -166,6 +174,7 @@ declare const StandaloneDashboardOptionsSchema: z.ZodIntersection<
|
|
|
166
174
|
}[];
|
|
167
175
|
basePath: string;
|
|
168
176
|
pollIntervalMs: number;
|
|
177
|
+
readonly: boolean;
|
|
169
178
|
},
|
|
170
179
|
{
|
|
171
180
|
clients: {
|
|
@@ -174,6 +183,7 @@ declare const StandaloneDashboardOptionsSchema: z.ZodIntersection<
|
|
|
174
183
|
}[];
|
|
175
184
|
basePath?: string | undefined;
|
|
176
185
|
pollIntervalMs?: number | undefined;
|
|
186
|
+
readonly?: boolean | undefined;
|
|
177
187
|
}
|
|
178
188
|
>,
|
|
179
189
|
z.ZodObject<
|
package/lib/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { existsSync, readFileSync } from 'fs';
|
|
3
|
-
import { join, extname, dirname } from 'path';
|
|
3
|
+
import { resolve, join, extname, dirname } from 'path';
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
5
5
|
import { createServer } from 'http';
|
|
6
6
|
|
|
@@ -377,6 +377,7 @@ var DashboardOptionsSchema = z
|
|
|
377
377
|
.min(1, 'At least one client is required'),
|
|
378
378
|
basePath: z.string().default('/'),
|
|
379
379
|
pollIntervalMs: z.number().int().positive().default(5e3),
|
|
380
|
+
readonly: z.boolean().default(false),
|
|
380
381
|
})
|
|
381
382
|
.refine(
|
|
382
383
|
(data) => {
|
|
@@ -413,7 +414,11 @@ function parseUrl(req, basePath) {
|
|
|
413
414
|
const raw = req.url ?? '/';
|
|
414
415
|
const url = new URL(raw, 'http://localhost');
|
|
415
416
|
let pathname = url.pathname;
|
|
416
|
-
if (
|
|
417
|
+
if (
|
|
418
|
+
basePath !== '/' &&
|
|
419
|
+
pathname.startsWith(basePath) &&
|
|
420
|
+
(pathname.length === basePath.length || pathname[basePath.length] === '/')
|
|
421
|
+
) {
|
|
417
422
|
pathname = pathname.slice(basePath.length) || '/';
|
|
418
423
|
}
|
|
419
424
|
return { pathname, query: url.searchParams };
|
|
@@ -431,15 +436,23 @@ function extractParam(pathname, pattern) {
|
|
|
431
436
|
if (paramIndex === -1) return void 0;
|
|
432
437
|
return pathParts[paramIndex];
|
|
433
438
|
}
|
|
439
|
+
var MAX_BODY_SIZE = 1024 * 1024;
|
|
434
440
|
async function readJsonBody(req) {
|
|
435
|
-
return new Promise((
|
|
441
|
+
return new Promise((resolve3, reject) => {
|
|
436
442
|
let body = '';
|
|
443
|
+
let size = 0;
|
|
437
444
|
req.on('data', (chunk) => {
|
|
445
|
+
size += chunk.length;
|
|
446
|
+
if (size > MAX_BODY_SIZE) {
|
|
447
|
+
req.destroy();
|
|
448
|
+
reject(new Error('Request body too large'));
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
438
451
|
body += chunk.toString();
|
|
439
452
|
});
|
|
440
453
|
req.on('end', () => {
|
|
441
454
|
try {
|
|
442
|
-
|
|
455
|
+
resolve3(JSON.parse(body));
|
|
443
456
|
} catch {
|
|
444
457
|
reject(new Error('Invalid JSON body'));
|
|
445
458
|
}
|
|
@@ -467,6 +480,9 @@ function sendNotFound(res) {
|
|
|
467
480
|
function sendMethodNotAllowed(res) {
|
|
468
481
|
sendError(res, 'Method not allowed', 405);
|
|
469
482
|
}
|
|
483
|
+
function sendForbidden(res) {
|
|
484
|
+
sendError(res, 'Dashboard is in readonly mode', 403);
|
|
485
|
+
}
|
|
470
486
|
|
|
471
487
|
// src/server/handlers/cache.ts
|
|
472
488
|
async function handleCacheStats(res, adapter) {
|
|
@@ -697,7 +713,15 @@ function createApiRouter(ctx) {
|
|
|
697
713
|
sendError(res, `Unknown client: ${clientName}`, 404);
|
|
698
714
|
return true;
|
|
699
715
|
}
|
|
700
|
-
return routeClientApi(
|
|
716
|
+
return routeClientApi(
|
|
717
|
+
req,
|
|
718
|
+
res,
|
|
719
|
+
client,
|
|
720
|
+
subPath,
|
|
721
|
+
method,
|
|
722
|
+
query,
|
|
723
|
+
ctx.readonly,
|
|
724
|
+
);
|
|
701
725
|
}
|
|
702
726
|
if (pathname.startsWith('/api/')) {
|
|
703
727
|
sendNotFound(res);
|
|
@@ -706,7 +730,22 @@ function createApiRouter(ctx) {
|
|
|
706
730
|
return false;
|
|
707
731
|
};
|
|
708
732
|
}
|
|
709
|
-
|
|
733
|
+
function isMutatingMethod(method) {
|
|
734
|
+
return method === 'DELETE' || method === 'PUT' || method === 'POST';
|
|
735
|
+
}
|
|
736
|
+
async function routeClientApi(
|
|
737
|
+
req,
|
|
738
|
+
res,
|
|
739
|
+
client,
|
|
740
|
+
subPath,
|
|
741
|
+
method,
|
|
742
|
+
query,
|
|
743
|
+
readonly,
|
|
744
|
+
) {
|
|
745
|
+
if (readonly && isMutatingMethod(method)) {
|
|
746
|
+
sendForbidden(res);
|
|
747
|
+
return true;
|
|
748
|
+
}
|
|
710
749
|
if (subPath.startsWith('/cache')) {
|
|
711
750
|
if (!client.cache) {
|
|
712
751
|
sendError(res, 'Cache store not configured', 404);
|
|
@@ -822,7 +861,7 @@ function getCurrentDir() {
|
|
|
822
861
|
function getClientDir() {
|
|
823
862
|
if (clientDir) return clientDir;
|
|
824
863
|
const currentDir = getCurrentDir();
|
|
825
|
-
clientDir =
|
|
864
|
+
clientDir = resolve(currentDir, '..', 'dist', 'client');
|
|
826
865
|
return clientDir;
|
|
827
866
|
}
|
|
828
867
|
function getIndexHtml() {
|
|
@@ -848,7 +887,12 @@ function getIndexHtml() {
|
|
|
848
887
|
function serveStatic(res, pathname) {
|
|
849
888
|
const dir = getClientDir();
|
|
850
889
|
if (pathname !== '/' && pathname !== '/index.html') {
|
|
851
|
-
const filePath = join(dir, pathname);
|
|
890
|
+
const filePath = resolve(join(dir, pathname));
|
|
891
|
+
if (!filePath.startsWith(dir + '/')) {
|
|
892
|
+
res.writeHead(400, { 'Content-Type': 'text/plain' });
|
|
893
|
+
res.end('Bad request');
|
|
894
|
+
return true;
|
|
895
|
+
}
|
|
852
896
|
if (existsSync(filePath)) {
|
|
853
897
|
try {
|
|
854
898
|
const content = readFileSync(filePath);
|
|
@@ -898,6 +942,7 @@ function createDashboard(options) {
|
|
|
898
942
|
const ctx = {
|
|
899
943
|
clients,
|
|
900
944
|
pollIntervalMs: opts.pollIntervalMs,
|
|
945
|
+
readonly: opts.readonly,
|
|
901
946
|
};
|
|
902
947
|
const apiRouter = createApiRouter(ctx);
|
|
903
948
|
return (req, res, _next) => {
|
|
@@ -947,7 +992,7 @@ function getCurrentDir2() {
|
|
|
947
992
|
function getClientDir2() {
|
|
948
993
|
if (clientDir2) return clientDir2;
|
|
949
994
|
const currentDir = getCurrentDir2();
|
|
950
|
-
clientDir2 =
|
|
995
|
+
clientDir2 = resolve(currentDir, '..', 'dist', 'client');
|
|
951
996
|
return clientDir2;
|
|
952
997
|
}
|
|
953
998
|
function getIndexHtml2() {
|
|
@@ -973,7 +1018,10 @@ function getIndexHtml2() {
|
|
|
973
1018
|
function serveStaticWeb(pathname) {
|
|
974
1019
|
const dir = getClientDir2();
|
|
975
1020
|
if (pathname !== '/' && pathname !== '/index.html') {
|
|
976
|
-
const filePath = join(dir, pathname);
|
|
1021
|
+
const filePath = resolve(join(dir, pathname));
|
|
1022
|
+
if (!filePath.startsWith(dir + '/')) {
|
|
1023
|
+
return new Response('Bad request', { status: 400 });
|
|
1024
|
+
}
|
|
977
1025
|
if (existsSync(filePath)) {
|
|
978
1026
|
try {
|
|
979
1027
|
const content = readFileSync(filePath);
|
|
@@ -1046,14 +1094,34 @@ async function routeApi(request, pathname, query, ctx) {
|
|
|
1046
1094
|
if (!client) {
|
|
1047
1095
|
return errorResponse(`Unknown client: ${clientName}`, 404);
|
|
1048
1096
|
}
|
|
1049
|
-
return routeClientApi2(
|
|
1097
|
+
return routeClientApi2(
|
|
1098
|
+
request,
|
|
1099
|
+
subPath,
|
|
1100
|
+
method,
|
|
1101
|
+
client,
|
|
1102
|
+
query,
|
|
1103
|
+
ctx.readonly,
|
|
1104
|
+
);
|
|
1050
1105
|
}
|
|
1051
1106
|
if (pathname.startsWith('/api/')) {
|
|
1052
1107
|
return errorResponse('Not found', 404);
|
|
1053
1108
|
}
|
|
1054
1109
|
return null;
|
|
1055
1110
|
}
|
|
1056
|
-
|
|
1111
|
+
function isMutatingMethod2(method) {
|
|
1112
|
+
return method === 'DELETE' || method === 'PUT' || method === 'POST';
|
|
1113
|
+
}
|
|
1114
|
+
async function routeClientApi2(
|
|
1115
|
+
request,
|
|
1116
|
+
subPath,
|
|
1117
|
+
method,
|
|
1118
|
+
client,
|
|
1119
|
+
query,
|
|
1120
|
+
readonly,
|
|
1121
|
+
) {
|
|
1122
|
+
if (readonly && isMutatingMethod2(method)) {
|
|
1123
|
+
return errorResponse('Dashboard is in readonly mode', 403);
|
|
1124
|
+
}
|
|
1057
1125
|
if (subPath.startsWith('/cache')) {
|
|
1058
1126
|
if (!client.cache) return errorResponse('Cache store not configured', 404);
|
|
1059
1127
|
return routeCache(subPath, method, client.cache, query);
|
|
@@ -1197,12 +1265,18 @@ function createDashboardHandler(options) {
|
|
|
1197
1265
|
const ctx = {
|
|
1198
1266
|
clients,
|
|
1199
1267
|
pollIntervalMs: opts.pollIntervalMs,
|
|
1268
|
+
readonly: opts.readonly,
|
|
1200
1269
|
};
|
|
1201
1270
|
return async (request) => {
|
|
1202
1271
|
try {
|
|
1203
1272
|
const url = new URL(request.url);
|
|
1204
1273
|
let pathname = url.pathname;
|
|
1205
|
-
if (
|
|
1274
|
+
if (
|
|
1275
|
+
opts.basePath !== '/' &&
|
|
1276
|
+
pathname.startsWith(opts.basePath) &&
|
|
1277
|
+
(pathname.length === opts.basePath.length ||
|
|
1278
|
+
pathname[opts.basePath.length] === '/')
|
|
1279
|
+
) {
|
|
1206
1280
|
pathname = pathname.slice(opts.basePath.length) || '/';
|
|
1207
1281
|
}
|
|
1208
1282
|
const apiResponse = await routeApi(
|
|
@@ -1224,14 +1298,14 @@ async function startDashboard(options) {
|
|
|
1224
1298
|
const server = createServer((req, res) => {
|
|
1225
1299
|
middleware(req, res);
|
|
1226
1300
|
});
|
|
1227
|
-
return new Promise((
|
|
1301
|
+
return new Promise((resolve3, reject) => {
|
|
1228
1302
|
server.on('error', reject);
|
|
1229
1303
|
server.listen(opts.port, opts.host, () => {
|
|
1230
1304
|
const addr = server.address();
|
|
1231
1305
|
const url =
|
|
1232
1306
|
typeof addr === 'string' ? addr : `http://${opts.host}:${opts.port}`;
|
|
1233
1307
|
console.log(`Dashboard running at ${url}`);
|
|
1234
|
-
|
|
1308
|
+
resolve3({
|
|
1235
1309
|
server,
|
|
1236
1310
|
async close() {
|
|
1237
1311
|
return new Promise((res, rej) => {
|