@http-client-toolkit/dashboard 0.1.0 → 0.12.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/dist/client/assets/index-6z-6Qs7v.css +598 -1
- package/dist/client/assets/index-C17ynkdl.js +13634 -14
- package/dist/client/index.html +1 -1
- package/lib/index.cjs +420 -295
- package/lib/index.cjs.map +1 -1
- package/lib/index.d.cts +221 -168
- package/lib/index.d.ts +221 -168
- package/lib/index.js +398 -292
- package/lib/index.js.map +1 -1
- package/package.json +9 -9
package/lib/index.js
CHANGED
|
@@ -7,15 +7,15 @@ import { createServer } from 'http';
|
|
|
7
7
|
// src/adapters/cache/generic.ts
|
|
8
8
|
function createGenericCacheAdapter(store) {
|
|
9
9
|
return {
|
|
10
|
-
type:
|
|
10
|
+
type: 'generic',
|
|
11
11
|
capabilities: {
|
|
12
12
|
canList: false,
|
|
13
13
|
canDelete: true,
|
|
14
14
|
canClear: true,
|
|
15
|
-
canGetStats: false
|
|
15
|
+
canGetStats: false,
|
|
16
16
|
},
|
|
17
17
|
async getStats() {
|
|
18
|
-
return { message:
|
|
18
|
+
return { message: 'Stats not available for this store type' };
|
|
19
19
|
},
|
|
20
20
|
async listEntries() {
|
|
21
21
|
return { entries: [] };
|
|
@@ -28,7 +28,7 @@ function createGenericCacheAdapter(store) {
|
|
|
28
28
|
},
|
|
29
29
|
async clearAll() {
|
|
30
30
|
await store.clear();
|
|
31
|
-
}
|
|
31
|
+
},
|
|
32
32
|
};
|
|
33
33
|
}
|
|
34
34
|
|
|
@@ -36,12 +36,12 @@ function createGenericCacheAdapter(store) {
|
|
|
36
36
|
function createMemoryCacheAdapter(store) {
|
|
37
37
|
const memStore = store;
|
|
38
38
|
return {
|
|
39
|
-
type:
|
|
39
|
+
type: 'memory',
|
|
40
40
|
capabilities: {
|
|
41
41
|
canList: true,
|
|
42
42
|
canDelete: true,
|
|
43
43
|
canClear: true,
|
|
44
|
-
canGetStats: true
|
|
44
|
+
canGetStats: true,
|
|
45
45
|
},
|
|
46
46
|
async getStats() {
|
|
47
47
|
return memStore.getStats();
|
|
@@ -59,7 +59,7 @@ function createMemoryCacheAdapter(store) {
|
|
|
59
59
|
},
|
|
60
60
|
async clearAll() {
|
|
61
61
|
await memStore.clear();
|
|
62
|
-
}
|
|
62
|
+
},
|
|
63
63
|
};
|
|
64
64
|
}
|
|
65
65
|
|
|
@@ -67,12 +67,12 @@ function createMemoryCacheAdapter(store) {
|
|
|
67
67
|
function createSqliteCacheAdapter(store) {
|
|
68
68
|
const sqlStore = store;
|
|
69
69
|
return {
|
|
70
|
-
type:
|
|
70
|
+
type: 'sqlite',
|
|
71
71
|
capabilities: {
|
|
72
72
|
canList: true,
|
|
73
73
|
canDelete: true,
|
|
74
74
|
canClear: true,
|
|
75
|
-
canGetStats: true
|
|
75
|
+
canGetStats: true,
|
|
76
76
|
},
|
|
77
77
|
async getStats() {
|
|
78
78
|
return sqlStore.getStats();
|
|
@@ -83,7 +83,7 @@ function createSqliteCacheAdapter(store) {
|
|
|
83
83
|
const entries = results.map((r) => ({
|
|
84
84
|
hash: r.hash,
|
|
85
85
|
expiresAt: r.expiresAt,
|
|
86
|
-
createdAt: r.createdAt
|
|
86
|
+
createdAt: r.createdAt,
|
|
87
87
|
}));
|
|
88
88
|
return { entries };
|
|
89
89
|
},
|
|
@@ -95,27 +95,27 @@ function createSqliteCacheAdapter(store) {
|
|
|
95
95
|
},
|
|
96
96
|
async clearAll() {
|
|
97
97
|
await sqlStore.clear();
|
|
98
|
-
}
|
|
98
|
+
},
|
|
99
99
|
};
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
// src/adapters/dedup/generic.ts
|
|
103
103
|
function createGenericDedupeAdapter(_store) {
|
|
104
104
|
return {
|
|
105
|
-
type:
|
|
105
|
+
type: 'generic',
|
|
106
106
|
capabilities: {
|
|
107
107
|
canList: false,
|
|
108
|
-
canGetStats: false
|
|
108
|
+
canGetStats: false,
|
|
109
109
|
},
|
|
110
110
|
async getStats() {
|
|
111
|
-
return { message:
|
|
111
|
+
return { message: 'Stats not available for this store type' };
|
|
112
112
|
},
|
|
113
113
|
async listJobs() {
|
|
114
114
|
return { jobs: [] };
|
|
115
115
|
},
|
|
116
116
|
async getJob() {
|
|
117
117
|
return void 0;
|
|
118
|
-
}
|
|
118
|
+
},
|
|
119
119
|
};
|
|
120
120
|
}
|
|
121
121
|
|
|
@@ -123,10 +123,10 @@ function createGenericDedupeAdapter(_store) {
|
|
|
123
123
|
function createMemoryDedupeAdapter(store) {
|
|
124
124
|
const memStore = store;
|
|
125
125
|
return {
|
|
126
|
-
type:
|
|
126
|
+
type: 'memory',
|
|
127
127
|
capabilities: {
|
|
128
128
|
canList: true,
|
|
129
|
-
canGetStats: true
|
|
129
|
+
canGetStats: true,
|
|
130
130
|
},
|
|
131
131
|
async getStats() {
|
|
132
132
|
return memStore.getStats();
|
|
@@ -139,7 +139,7 @@ function createMemoryDedupeAdapter(store) {
|
|
|
139
139
|
async getJob(hash) {
|
|
140
140
|
const jobs = memStore.listJobs(0, 1e3);
|
|
141
141
|
return jobs.find((j) => j.hash === hash);
|
|
142
|
-
}
|
|
142
|
+
},
|
|
143
143
|
};
|
|
144
144
|
}
|
|
145
145
|
|
|
@@ -147,10 +147,10 @@ function createMemoryDedupeAdapter(store) {
|
|
|
147
147
|
function createSqliteDedupeAdapter(store) {
|
|
148
148
|
const sqlStore = store;
|
|
149
149
|
return {
|
|
150
|
-
type:
|
|
150
|
+
type: 'sqlite',
|
|
151
151
|
capabilities: {
|
|
152
152
|
canList: true,
|
|
153
|
-
canGetStats: true
|
|
153
|
+
canGetStats: true,
|
|
154
154
|
},
|
|
155
155
|
async getStats() {
|
|
156
156
|
return sqlStore.getStats();
|
|
@@ -163,22 +163,22 @@ function createSqliteDedupeAdapter(store) {
|
|
|
163
163
|
async getJob(hash) {
|
|
164
164
|
const results = await sqlStore.listJobs(0, 1e3);
|
|
165
165
|
return results.find((j) => j.hash === hash);
|
|
166
|
-
}
|
|
166
|
+
},
|
|
167
167
|
};
|
|
168
168
|
}
|
|
169
169
|
|
|
170
170
|
// src/adapters/rate-limit/generic.ts
|
|
171
171
|
function createGenericRateLimitAdapter(store) {
|
|
172
172
|
return {
|
|
173
|
-
type:
|
|
173
|
+
type: 'generic',
|
|
174
174
|
capabilities: {
|
|
175
175
|
canList: false,
|
|
176
176
|
canGetStats: false,
|
|
177
177
|
canUpdateConfig: false,
|
|
178
|
-
canReset: true
|
|
178
|
+
canReset: true,
|
|
179
179
|
},
|
|
180
180
|
async getStats() {
|
|
181
|
-
return { message:
|
|
181
|
+
return { message: 'Stats not available for this store type' };
|
|
182
182
|
},
|
|
183
183
|
async listResources() {
|
|
184
184
|
return [];
|
|
@@ -187,11 +187,11 @@ function createGenericRateLimitAdapter(store) {
|
|
|
187
187
|
return store.getStatus(name);
|
|
188
188
|
},
|
|
189
189
|
async updateResourceConfig() {
|
|
190
|
-
throw new Error(
|
|
190
|
+
throw new Error('Config updates not supported for this store type');
|
|
191
191
|
},
|
|
192
192
|
async resetResource(name) {
|
|
193
193
|
await store.reset(name);
|
|
194
|
-
}
|
|
194
|
+
},
|
|
195
195
|
};
|
|
196
196
|
}
|
|
197
197
|
|
|
@@ -199,12 +199,12 @@ function createGenericRateLimitAdapter(store) {
|
|
|
199
199
|
function createMemoryRateLimitAdapter(store) {
|
|
200
200
|
const memStore = store;
|
|
201
201
|
return {
|
|
202
|
-
type:
|
|
202
|
+
type: 'memory',
|
|
203
203
|
capabilities: {
|
|
204
204
|
canList: true,
|
|
205
205
|
canGetStats: true,
|
|
206
206
|
canUpdateConfig: true,
|
|
207
|
-
canReset: true
|
|
207
|
+
canReset: true,
|
|
208
208
|
},
|
|
209
209
|
async getStats() {
|
|
210
210
|
return memStore.getStats();
|
|
@@ -220,7 +220,7 @@ function createMemoryRateLimitAdapter(store) {
|
|
|
220
220
|
},
|
|
221
221
|
async resetResource(name) {
|
|
222
222
|
await memStore.reset(name);
|
|
223
|
-
}
|
|
223
|
+
},
|
|
224
224
|
};
|
|
225
225
|
}
|
|
226
226
|
|
|
@@ -228,12 +228,12 @@ function createMemoryRateLimitAdapter(store) {
|
|
|
228
228
|
function createSqliteRateLimitAdapter(store) {
|
|
229
229
|
const sqlStore = store;
|
|
230
230
|
return {
|
|
231
|
-
type:
|
|
231
|
+
type: 'sqlite',
|
|
232
232
|
capabilities: {
|
|
233
233
|
canList: true,
|
|
234
234
|
canGetStats: true,
|
|
235
235
|
canUpdateConfig: true,
|
|
236
|
-
canReset: true
|
|
236
|
+
canReset: true,
|
|
237
237
|
},
|
|
238
238
|
async getStats() {
|
|
239
239
|
return sqlStore.getStats();
|
|
@@ -249,40 +249,72 @@ function createSqliteRateLimitAdapter(store) {
|
|
|
249
249
|
},
|
|
250
250
|
async resetResource(name) {
|
|
251
251
|
await sqlStore.reset(name);
|
|
252
|
-
}
|
|
252
|
+
},
|
|
253
253
|
};
|
|
254
254
|
}
|
|
255
255
|
|
|
256
256
|
// src/adapters/detect.ts
|
|
257
257
|
function isMemoryStore(store) {
|
|
258
|
-
if (!store || typeof store !==
|
|
258
|
+
if (!store || typeof store !== 'object') return false;
|
|
259
259
|
const s = store;
|
|
260
|
-
return
|
|
260
|
+
return (
|
|
261
|
+
typeof s.destroy === 'function' &&
|
|
262
|
+
typeof s.getStats === 'function' &&
|
|
263
|
+
typeof s.cleanup === 'function' &&
|
|
264
|
+
typeof s.getLRUItems === 'function'
|
|
265
|
+
);
|
|
261
266
|
}
|
|
262
267
|
function isSqliteStore(store) {
|
|
263
|
-
if (!store || typeof store !==
|
|
268
|
+
if (!store || typeof store !== 'object') return false;
|
|
264
269
|
const s = store;
|
|
265
|
-
return
|
|
270
|
+
return (
|
|
271
|
+
typeof s.destroy === 'function' &&
|
|
272
|
+
typeof s.getStats === 'function' &&
|
|
273
|
+
typeof s.close === 'function'
|
|
274
|
+
);
|
|
266
275
|
}
|
|
267
276
|
function isMemoryDedupeStore(store) {
|
|
268
|
-
if (!store || typeof store !==
|
|
277
|
+
if (!store || typeof store !== 'object') return false;
|
|
269
278
|
const s = store;
|
|
270
|
-
return
|
|
279
|
+
return (
|
|
280
|
+
typeof s.destroy === 'function' &&
|
|
281
|
+
typeof s.getStats === 'function' &&
|
|
282
|
+
typeof s.listJobs === 'function' &&
|
|
283
|
+
typeof s.cleanup === 'function' &&
|
|
284
|
+
!('close' in s && typeof s.close === 'function')
|
|
285
|
+
);
|
|
271
286
|
}
|
|
272
287
|
function isSqliteDedupeStore(store) {
|
|
273
|
-
if (!store || typeof store !==
|
|
288
|
+
if (!store || typeof store !== 'object') return false;
|
|
274
289
|
const s = store;
|
|
275
|
-
return
|
|
290
|
+
return (
|
|
291
|
+
typeof s.destroy === 'function' &&
|
|
292
|
+
typeof s.getStats === 'function' &&
|
|
293
|
+
typeof s.close === 'function' &&
|
|
294
|
+
typeof s.listJobs === 'function'
|
|
295
|
+
);
|
|
276
296
|
}
|
|
277
297
|
function isMemoryRateLimitStore(store) {
|
|
278
|
-
if (!store || typeof store !==
|
|
298
|
+
if (!store || typeof store !== 'object') return false;
|
|
279
299
|
const s = store;
|
|
280
|
-
return
|
|
300
|
+
return (
|
|
301
|
+
typeof s.destroy === 'function' &&
|
|
302
|
+
typeof s.getStats === 'function' &&
|
|
303
|
+
typeof s.listResources === 'function' &&
|
|
304
|
+
typeof s.setResourceConfig === 'function' &&
|
|
305
|
+
!('close' in s && typeof s.close === 'function')
|
|
306
|
+
);
|
|
281
307
|
}
|
|
282
308
|
function isSqliteRateLimitStore(store) {
|
|
283
|
-
if (!store || typeof store !==
|
|
309
|
+
if (!store || typeof store !== 'object') return false;
|
|
284
310
|
const s = store;
|
|
285
|
-
return
|
|
311
|
+
return (
|
|
312
|
+
typeof s.destroy === 'function' &&
|
|
313
|
+
typeof s.getStats === 'function' &&
|
|
314
|
+
typeof s.close === 'function' &&
|
|
315
|
+
typeof s.listResources === 'function' &&
|
|
316
|
+
typeof s.setResourceConfig === 'function'
|
|
317
|
+
);
|
|
286
318
|
}
|
|
287
319
|
function detectCacheAdapter(store) {
|
|
288
320
|
if (isMemoryStore(store)) {
|
|
@@ -312,35 +344,63 @@ function detectRateLimitAdapter(store) {
|
|
|
312
344
|
return createGenericRateLimitAdapter(store);
|
|
313
345
|
}
|
|
314
346
|
var CLIENT_NAME_REGEX = /^[a-zA-Z0-9_-]+$/;
|
|
315
|
-
var ClientConfigSchema = z
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
)
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
);
|
|
347
|
+
var ClientConfigSchema = z
|
|
348
|
+
.object({
|
|
349
|
+
client: z.custom(
|
|
350
|
+
(val) =>
|
|
351
|
+
val != null &&
|
|
352
|
+
typeof val === 'object' &&
|
|
353
|
+
'stores' in val &&
|
|
354
|
+
'get' in val,
|
|
355
|
+
'Must be an HttpClient instance',
|
|
356
|
+
),
|
|
357
|
+
name: z
|
|
358
|
+
.string()
|
|
359
|
+
.min(1, 'Client name must not be empty')
|
|
360
|
+
.regex(CLIENT_NAME_REGEX, 'Client name must be URL-safe (a-z, 0-9, -, _)')
|
|
361
|
+
.optional(),
|
|
362
|
+
})
|
|
363
|
+
.refine(
|
|
364
|
+
(data) => {
|
|
365
|
+
const { stores } = data.client;
|
|
366
|
+
return stores.cache || stores.dedupe || stores.rateLimit;
|
|
367
|
+
},
|
|
368
|
+
{ message: 'HttpClient must have at least one store configured' },
|
|
369
|
+
);
|
|
370
|
+
function resolveClientName(c) {
|
|
371
|
+
return c.name ?? c.client.name;
|
|
372
|
+
}
|
|
373
|
+
var DashboardOptionsSchema = z
|
|
374
|
+
.object({
|
|
375
|
+
clients: z
|
|
376
|
+
.array(ClientConfigSchema)
|
|
377
|
+
.min(1, 'At least one client is required'),
|
|
378
|
+
basePath: z.string().default('/'),
|
|
379
|
+
pollIntervalMs: z.number().int().positive().default(5e3),
|
|
380
|
+
})
|
|
381
|
+
.refine(
|
|
382
|
+
(data) => {
|
|
383
|
+
const names = data.clients.map(resolveClientName);
|
|
384
|
+
return new Set(names).size === names.length;
|
|
385
|
+
},
|
|
386
|
+
{ message: 'Client names must be unique' },
|
|
387
|
+
);
|
|
338
388
|
var StandaloneDashboardOptionsSchema = DashboardOptionsSchema.and(
|
|
339
389
|
z.object({
|
|
340
390
|
port: z.number().int().nonnegative().default(4e3),
|
|
341
|
-
host: z.string().default(
|
|
342
|
-
})
|
|
391
|
+
host: z.string().default('localhost'),
|
|
392
|
+
}),
|
|
343
393
|
);
|
|
394
|
+
function normalizeClient(config) {
|
|
395
|
+
const name = config.name ?? config.client.name;
|
|
396
|
+
const { stores } = config.client;
|
|
397
|
+
return {
|
|
398
|
+
name,
|
|
399
|
+
cacheStore: stores.cache,
|
|
400
|
+
dedupeStore: stores.dedupe,
|
|
401
|
+
rateLimitStore: stores.rateLimit,
|
|
402
|
+
};
|
|
403
|
+
}
|
|
344
404
|
function validateDashboardOptions(options) {
|
|
345
405
|
return DashboardOptionsSchema.parse(options);
|
|
346
406
|
}
|
|
@@ -350,41 +410,41 @@ function validateStandaloneOptions(options) {
|
|
|
350
410
|
|
|
351
411
|
// src/server/request-helpers.ts
|
|
352
412
|
function parseUrl(req, basePath) {
|
|
353
|
-
const raw = req.url ??
|
|
354
|
-
const url = new URL(raw,
|
|
413
|
+
const raw = req.url ?? '/';
|
|
414
|
+
const url = new URL(raw, 'http://localhost');
|
|
355
415
|
let pathname = url.pathname;
|
|
356
|
-
if (basePath !==
|
|
357
|
-
pathname = pathname.slice(basePath.length) ||
|
|
416
|
+
if (basePath !== '/' && pathname.startsWith(basePath)) {
|
|
417
|
+
pathname = pathname.slice(basePath.length) || '/';
|
|
358
418
|
}
|
|
359
419
|
return { pathname, query: url.searchParams };
|
|
360
420
|
}
|
|
361
421
|
function extractParam(pathname, pattern) {
|
|
362
|
-
const patternParts = pattern.split(
|
|
363
|
-
const pathParts = pathname.split(
|
|
422
|
+
const patternParts = pattern.split('/');
|
|
423
|
+
const pathParts = pathname.split('/');
|
|
364
424
|
if (patternParts.length !== pathParts.length) return void 0;
|
|
365
425
|
for (let i = 0; i < patternParts.length; i++) {
|
|
366
426
|
const pp = patternParts[i];
|
|
367
|
-
if (pp.startsWith(
|
|
427
|
+
if (pp.startsWith(':')) continue;
|
|
368
428
|
if (pp !== pathParts[i]) return void 0;
|
|
369
429
|
}
|
|
370
|
-
const paramIndex = patternParts.findIndex((p) => p.startsWith(
|
|
430
|
+
const paramIndex = patternParts.findIndex((p) => p.startsWith(':'));
|
|
371
431
|
if (paramIndex === -1) return void 0;
|
|
372
432
|
return pathParts[paramIndex];
|
|
373
433
|
}
|
|
374
434
|
async function readJsonBody(req) {
|
|
375
435
|
return new Promise((resolve, reject) => {
|
|
376
|
-
let body =
|
|
377
|
-
req.on(
|
|
436
|
+
let body = '';
|
|
437
|
+
req.on('data', (chunk) => {
|
|
378
438
|
body += chunk.toString();
|
|
379
439
|
});
|
|
380
|
-
req.on(
|
|
440
|
+
req.on('end', () => {
|
|
381
441
|
try {
|
|
382
442
|
resolve(JSON.parse(body));
|
|
383
443
|
} catch {
|
|
384
|
-
reject(new Error(
|
|
444
|
+
reject(new Error('Invalid JSON body'));
|
|
385
445
|
}
|
|
386
446
|
});
|
|
387
|
-
req.on(
|
|
447
|
+
req.on('error', reject);
|
|
388
448
|
});
|
|
389
449
|
}
|
|
390
450
|
|
|
@@ -392,9 +452,9 @@ async function readJsonBody(req) {
|
|
|
392
452
|
function sendJson(res, data, status = 200) {
|
|
393
453
|
const body = JSON.stringify(data);
|
|
394
454
|
res.writeHead(status, {
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
455
|
+
'Content-Type': 'application/json',
|
|
456
|
+
'Content-Length': Buffer.byteLength(body),
|
|
457
|
+
'Cache-Control': 'no-store',
|
|
398
458
|
});
|
|
399
459
|
res.end(body);
|
|
400
460
|
}
|
|
@@ -402,10 +462,10 @@ function sendError(res, message, status = 500) {
|
|
|
402
462
|
sendJson(res, { error: message }, status);
|
|
403
463
|
}
|
|
404
464
|
function sendNotFound(res) {
|
|
405
|
-
sendError(res,
|
|
465
|
+
sendError(res, 'Not found', 404);
|
|
406
466
|
}
|
|
407
467
|
function sendMethodNotAllowed(res) {
|
|
408
|
-
sendError(res,
|
|
468
|
+
sendError(res, 'Method not allowed', 405);
|
|
409
469
|
}
|
|
410
470
|
|
|
411
471
|
// src/server/handlers/cache.ts
|
|
@@ -414,22 +474,22 @@ async function handleCacheStats(res, adapter) {
|
|
|
414
474
|
const stats = await adapter.getStats();
|
|
415
475
|
sendJson(res, { stats, capabilities: adapter.capabilities });
|
|
416
476
|
} catch (err) {
|
|
417
|
-
sendError(res, err instanceof Error ? err.message :
|
|
477
|
+
sendError(res, err instanceof Error ? err.message : 'Unknown error');
|
|
418
478
|
}
|
|
419
479
|
}
|
|
420
480
|
async function handleCacheEntries(_req, res, adapter, query) {
|
|
421
481
|
try {
|
|
422
|
-
const page = parseInt(query.get(
|
|
423
|
-
const limit = parseInt(query.get(
|
|
482
|
+
const page = parseInt(query.get('page') ?? '0', 10);
|
|
483
|
+
const limit = parseInt(query.get('limit') ?? '50', 10);
|
|
424
484
|
const result = await adapter.listEntries(page, limit);
|
|
425
485
|
sendJson(res, result);
|
|
426
486
|
} catch (err) {
|
|
427
|
-
sendError(res, err instanceof Error ? err.message :
|
|
487
|
+
sendError(res, err instanceof Error ? err.message : 'Unknown error');
|
|
428
488
|
}
|
|
429
489
|
}
|
|
430
490
|
async function handleCacheEntry(res, adapter, pathname) {
|
|
431
491
|
try {
|
|
432
|
-
const hash = extractParam(pathname,
|
|
492
|
+
const hash = extractParam(pathname, '/cache/entries/:hash');
|
|
433
493
|
if (!hash) {
|
|
434
494
|
sendNotFound(res);
|
|
435
495
|
return;
|
|
@@ -441,12 +501,12 @@ async function handleCacheEntry(res, adapter, pathname) {
|
|
|
441
501
|
}
|
|
442
502
|
sendJson(res, { hash, value: entry });
|
|
443
503
|
} catch (err) {
|
|
444
|
-
sendError(res, err instanceof Error ? err.message :
|
|
504
|
+
sendError(res, err instanceof Error ? err.message : 'Unknown error');
|
|
445
505
|
}
|
|
446
506
|
}
|
|
447
507
|
async function handleDeleteCacheEntry(res, adapter, pathname) {
|
|
448
508
|
try {
|
|
449
|
-
const hash = extractParam(pathname,
|
|
509
|
+
const hash = extractParam(pathname, '/cache/entries/:hash');
|
|
450
510
|
if (!hash) {
|
|
451
511
|
sendNotFound(res);
|
|
452
512
|
return;
|
|
@@ -454,7 +514,7 @@ async function handleDeleteCacheEntry(res, adapter, pathname) {
|
|
|
454
514
|
await adapter.deleteEntry(hash);
|
|
455
515
|
sendJson(res, { deleted: true });
|
|
456
516
|
} catch (err) {
|
|
457
|
-
sendError(res, err instanceof Error ? err.message :
|
|
517
|
+
sendError(res, err instanceof Error ? err.message : 'Unknown error');
|
|
458
518
|
}
|
|
459
519
|
}
|
|
460
520
|
async function handleClearCache(res, adapter) {
|
|
@@ -462,7 +522,7 @@ async function handleClearCache(res, adapter) {
|
|
|
462
522
|
await adapter.clearAll();
|
|
463
523
|
sendJson(res, { cleared: true });
|
|
464
524
|
} catch (err) {
|
|
465
|
-
sendError(res, err instanceof Error ? err.message :
|
|
525
|
+
sendError(res, err instanceof Error ? err.message : 'Unknown error');
|
|
466
526
|
}
|
|
467
527
|
}
|
|
468
528
|
|
|
@@ -472,22 +532,22 @@ async function handleDedupeStats(res, adapter) {
|
|
|
472
532
|
const stats = await adapter.getStats();
|
|
473
533
|
sendJson(res, { stats, capabilities: adapter.capabilities });
|
|
474
534
|
} catch (err) {
|
|
475
|
-
sendError(res, err instanceof Error ? err.message :
|
|
535
|
+
sendError(res, err instanceof Error ? err.message : 'Unknown error');
|
|
476
536
|
}
|
|
477
537
|
}
|
|
478
538
|
async function handleDedupeJobs(_req, res, adapter, query) {
|
|
479
539
|
try {
|
|
480
|
-
const page = parseInt(query.get(
|
|
481
|
-
const limit = parseInt(query.get(
|
|
540
|
+
const page = parseInt(query.get('page') ?? '0', 10);
|
|
541
|
+
const limit = parseInt(query.get('limit') ?? '50', 10);
|
|
482
542
|
const result = await adapter.listJobs(page, limit);
|
|
483
543
|
sendJson(res, result);
|
|
484
544
|
} catch (err) {
|
|
485
|
-
sendError(res, err instanceof Error ? err.message :
|
|
545
|
+
sendError(res, err instanceof Error ? err.message : 'Unknown error');
|
|
486
546
|
}
|
|
487
547
|
}
|
|
488
548
|
async function handleDedupeJob(res, adapter, pathname) {
|
|
489
549
|
try {
|
|
490
|
-
const hash = extractParam(pathname,
|
|
550
|
+
const hash = extractParam(pathname, '/dedup/jobs/:hash');
|
|
491
551
|
if (!hash) {
|
|
492
552
|
sendNotFound(res);
|
|
493
553
|
return;
|
|
@@ -499,19 +559,25 @@ async function handleDedupeJob(res, adapter, pathname) {
|
|
|
499
559
|
}
|
|
500
560
|
sendJson(res, job);
|
|
501
561
|
} catch (err) {
|
|
502
|
-
sendError(res, err instanceof Error ? err.message :
|
|
562
|
+
sendError(res, err instanceof Error ? err.message : 'Unknown error');
|
|
503
563
|
}
|
|
504
564
|
}
|
|
505
565
|
|
|
506
566
|
// src/server/handlers/health.ts
|
|
507
567
|
function clientStoreInfo(client) {
|
|
508
568
|
return {
|
|
509
|
-
cache: client.cache
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
capabilities: client.
|
|
514
|
-
|
|
569
|
+
cache: client.cache
|
|
570
|
+
? { type: client.cache.type, capabilities: client.cache.capabilities }
|
|
571
|
+
: null,
|
|
572
|
+
dedup: client.dedup
|
|
573
|
+
? { type: client.dedup.type, capabilities: client.dedup.capabilities }
|
|
574
|
+
: null,
|
|
575
|
+
rateLimit: client.rateLimit
|
|
576
|
+
? {
|
|
577
|
+
type: client.rateLimit.type,
|
|
578
|
+
capabilities: client.rateLimit.capabilities,
|
|
579
|
+
}
|
|
580
|
+
: null,
|
|
515
581
|
};
|
|
516
582
|
}
|
|
517
583
|
function handleHealth(res, ctx) {
|
|
@@ -520,9 +586,9 @@ function handleHealth(res, ctx) {
|
|
|
520
586
|
clients[name] = clientStoreInfo(client);
|
|
521
587
|
}
|
|
522
588
|
sendJson(res, {
|
|
523
|
-
status:
|
|
589
|
+
status: 'ok',
|
|
524
590
|
clients,
|
|
525
|
-
pollIntervalMs: ctx.pollIntervalMs
|
|
591
|
+
pollIntervalMs: ctx.pollIntervalMs,
|
|
526
592
|
});
|
|
527
593
|
}
|
|
528
594
|
|
|
@@ -532,7 +598,7 @@ async function handleRateLimitStats(res, adapter) {
|
|
|
532
598
|
const stats = await adapter.getStats();
|
|
533
599
|
sendJson(res, { stats, capabilities: adapter.capabilities });
|
|
534
600
|
} catch (err) {
|
|
535
|
-
sendError(res, err instanceof Error ? err.message :
|
|
601
|
+
sendError(res, err instanceof Error ? err.message : 'Unknown error');
|
|
536
602
|
}
|
|
537
603
|
}
|
|
538
604
|
async function handleRateLimitResources(res, adapter) {
|
|
@@ -540,12 +606,12 @@ async function handleRateLimitResources(res, adapter) {
|
|
|
540
606
|
const resources = await adapter.listResources();
|
|
541
607
|
sendJson(res, { resources });
|
|
542
608
|
} catch (err) {
|
|
543
|
-
sendError(res, err instanceof Error ? err.message :
|
|
609
|
+
sendError(res, err instanceof Error ? err.message : 'Unknown error');
|
|
544
610
|
}
|
|
545
611
|
}
|
|
546
612
|
async function handleRateLimitResource(res, adapter, pathname) {
|
|
547
613
|
try {
|
|
548
|
-
const name = extractParam(pathname,
|
|
614
|
+
const name = extractParam(pathname, '/rate-limit/resources/:name');
|
|
549
615
|
if (!name) {
|
|
550
616
|
sendNotFound(res);
|
|
551
617
|
return;
|
|
@@ -553,12 +619,12 @@ async function handleRateLimitResource(res, adapter, pathname) {
|
|
|
553
619
|
const status = await adapter.getResourceStatus(name);
|
|
554
620
|
sendJson(res, { resource: name, ...status });
|
|
555
621
|
} catch (err) {
|
|
556
|
-
sendError(res, err instanceof Error ? err.message :
|
|
622
|
+
sendError(res, err instanceof Error ? err.message : 'Unknown error');
|
|
557
623
|
}
|
|
558
624
|
}
|
|
559
625
|
async function handleUpdateRateLimitConfig(req, res, adapter, pathname) {
|
|
560
626
|
try {
|
|
561
|
-
const name = extractParam(pathname,
|
|
627
|
+
const name = extractParam(pathname, '/rate-limit/resources/:name/config');
|
|
562
628
|
if (!name) {
|
|
563
629
|
sendNotFound(res);
|
|
564
630
|
return;
|
|
@@ -567,12 +633,12 @@ async function handleUpdateRateLimitConfig(req, res, adapter, pathname) {
|
|
|
567
633
|
await adapter.updateResourceConfig(name, body);
|
|
568
634
|
sendJson(res, { updated: true });
|
|
569
635
|
} catch (err) {
|
|
570
|
-
sendError(res, err instanceof Error ? err.message :
|
|
636
|
+
sendError(res, err instanceof Error ? err.message : 'Unknown error');
|
|
571
637
|
}
|
|
572
638
|
}
|
|
573
639
|
async function handleResetRateLimitResource(res, adapter, pathname) {
|
|
574
640
|
try {
|
|
575
|
-
const name = extractParam(pathname,
|
|
641
|
+
const name = extractParam(pathname, '/rate-limit/resources/:name/reset');
|
|
576
642
|
if (!name) {
|
|
577
643
|
sendNotFound(res);
|
|
578
644
|
return;
|
|
@@ -580,7 +646,7 @@ async function handleResetRateLimitResource(res, adapter, pathname) {
|
|
|
580
646
|
await adapter.resetResource(name);
|
|
581
647
|
sendJson(res, { reset: true });
|
|
582
648
|
} catch (err) {
|
|
583
|
-
sendError(res, err instanceof Error ? err.message :
|
|
649
|
+
sendError(res, err instanceof Error ? err.message : 'Unknown error');
|
|
584
650
|
}
|
|
585
651
|
}
|
|
586
652
|
|
|
@@ -591,13 +657,19 @@ function handleClients(res, ctx) {
|
|
|
591
657
|
clients.push({
|
|
592
658
|
name,
|
|
593
659
|
stores: {
|
|
594
|
-
cache: client.cache
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
capabilities: client.
|
|
599
|
-
|
|
600
|
-
|
|
660
|
+
cache: client.cache
|
|
661
|
+
? { type: client.cache.type, capabilities: client.cache.capabilities }
|
|
662
|
+
: null,
|
|
663
|
+
dedup: client.dedup
|
|
664
|
+
? { type: client.dedup.type, capabilities: client.dedup.capabilities }
|
|
665
|
+
: null,
|
|
666
|
+
rateLimit: client.rateLimit
|
|
667
|
+
? {
|
|
668
|
+
type: client.rateLimit.type,
|
|
669
|
+
capabilities: client.rateLimit.capabilities,
|
|
670
|
+
}
|
|
671
|
+
: null,
|
|
672
|
+
},
|
|
601
673
|
});
|
|
602
674
|
}
|
|
603
675
|
sendJson(res, { clients });
|
|
@@ -607,19 +679,19 @@ function handleClients(res, ctx) {
|
|
|
607
679
|
var CLIENT_ROUTE_REGEX = /^\/api\/clients\/([a-zA-Z0-9_-]+)(\/.*)?$/;
|
|
608
680
|
function createApiRouter(ctx) {
|
|
609
681
|
return async (req, res, pathname, query) => {
|
|
610
|
-
const method = req.method?.toUpperCase() ??
|
|
611
|
-
if (pathname ===
|
|
682
|
+
const method = req.method?.toUpperCase() ?? 'GET';
|
|
683
|
+
if (pathname === '/api/health' && method === 'GET') {
|
|
612
684
|
handleHealth(res, ctx);
|
|
613
685
|
return true;
|
|
614
686
|
}
|
|
615
|
-
if (pathname ===
|
|
687
|
+
if (pathname === '/api/clients' && method === 'GET') {
|
|
616
688
|
handleClients(res, ctx);
|
|
617
689
|
return true;
|
|
618
690
|
}
|
|
619
691
|
const clientMatch = pathname.match(CLIENT_ROUTE_REGEX);
|
|
620
692
|
if (clientMatch) {
|
|
621
693
|
const clientName = clientMatch[1];
|
|
622
|
-
const subPath = clientMatch[2] ??
|
|
694
|
+
const subPath = clientMatch[2] ?? '';
|
|
623
695
|
const client = ctx.clients.get(clientName);
|
|
624
696
|
if (!client) {
|
|
625
697
|
sendError(res, `Unknown client: ${clientName}`, 404);
|
|
@@ -627,7 +699,7 @@ function createApiRouter(ctx) {
|
|
|
627
699
|
}
|
|
628
700
|
return routeClientApi(req, res, client, subPath, method, query);
|
|
629
701
|
}
|
|
630
|
-
if (pathname.startsWith(
|
|
702
|
+
if (pathname.startsWith('/api/')) {
|
|
631
703
|
sendNotFound(res);
|
|
632
704
|
return true;
|
|
633
705
|
}
|
|
@@ -635,29 +707,30 @@ function createApiRouter(ctx) {
|
|
|
635
707
|
};
|
|
636
708
|
}
|
|
637
709
|
async function routeClientApi(req, res, client, subPath, method, query) {
|
|
638
|
-
if (subPath.startsWith(
|
|
710
|
+
if (subPath.startsWith('/cache')) {
|
|
639
711
|
if (!client.cache) {
|
|
640
|
-
sendError(res,
|
|
712
|
+
sendError(res, 'Cache store not configured', 404);
|
|
641
713
|
return true;
|
|
642
714
|
}
|
|
643
|
-
if (subPath ===
|
|
715
|
+
if (subPath === '/cache/stats' && method === 'GET') {
|
|
644
716
|
await handleCacheStats(res, client.cache);
|
|
645
717
|
return true;
|
|
646
718
|
}
|
|
647
|
-
if (subPath ===
|
|
719
|
+
if (subPath === '/cache/entries' && method === 'GET') {
|
|
648
720
|
await handleCacheEntries(req, res, client.cache, query);
|
|
649
721
|
return true;
|
|
650
722
|
}
|
|
651
|
-
if (subPath ===
|
|
723
|
+
if (subPath === '/cache/entries' && method === 'DELETE') {
|
|
652
724
|
await handleClearCache(res, client.cache);
|
|
653
725
|
return true;
|
|
654
726
|
}
|
|
655
|
-
const isSingleEntry =
|
|
656
|
-
|
|
727
|
+
const isSingleEntry =
|
|
728
|
+
subPath.startsWith('/cache/entries/') && subPath.split('/').length === 4;
|
|
729
|
+
if (isSingleEntry && method === 'GET') {
|
|
657
730
|
await handleCacheEntry(res, client.cache, subPath);
|
|
658
731
|
return true;
|
|
659
732
|
}
|
|
660
|
-
if (isSingleEntry && method ===
|
|
733
|
+
if (isSingleEntry && method === 'DELETE') {
|
|
661
734
|
await handleDeleteCacheEntry(res, client.cache, subPath);
|
|
662
735
|
return true;
|
|
663
736
|
}
|
|
@@ -666,21 +739,22 @@ async function routeClientApi(req, res, client, subPath, method, query) {
|
|
|
666
739
|
return true;
|
|
667
740
|
}
|
|
668
741
|
}
|
|
669
|
-
if (subPath.startsWith(
|
|
742
|
+
if (subPath.startsWith('/dedup')) {
|
|
670
743
|
if (!client.dedup) {
|
|
671
|
-
sendError(res,
|
|
744
|
+
sendError(res, 'Dedup store not configured', 404);
|
|
672
745
|
return true;
|
|
673
746
|
}
|
|
674
|
-
if (subPath ===
|
|
747
|
+
if (subPath === '/dedup/stats' && method === 'GET') {
|
|
675
748
|
await handleDedupeStats(res, client.dedup);
|
|
676
749
|
return true;
|
|
677
750
|
}
|
|
678
|
-
if (subPath ===
|
|
751
|
+
if (subPath === '/dedup/jobs' && method === 'GET') {
|
|
679
752
|
await handleDedupeJobs(req, res, client.dedup, query);
|
|
680
753
|
return true;
|
|
681
754
|
}
|
|
682
|
-
const isSingleJob =
|
|
683
|
-
|
|
755
|
+
const isSingleJob =
|
|
756
|
+
subPath.startsWith('/dedup/jobs/') && subPath.split('/').length === 4;
|
|
757
|
+
if (isSingleJob && method === 'GET') {
|
|
684
758
|
await handleDedupeJob(res, client.dedup, subPath);
|
|
685
759
|
return true;
|
|
686
760
|
}
|
|
@@ -689,29 +763,31 @@ async function routeClientApi(req, res, client, subPath, method, query) {
|
|
|
689
763
|
return true;
|
|
690
764
|
}
|
|
691
765
|
}
|
|
692
|
-
if (subPath.startsWith(
|
|
766
|
+
if (subPath.startsWith('/rate-limit')) {
|
|
693
767
|
if (!client.rateLimit) {
|
|
694
|
-
sendError(res,
|
|
768
|
+
sendError(res, 'Rate limit store not configured', 404);
|
|
695
769
|
return true;
|
|
696
770
|
}
|
|
697
|
-
if (subPath ===
|
|
771
|
+
if (subPath === '/rate-limit/stats' && method === 'GET') {
|
|
698
772
|
await handleRateLimitStats(res, client.rateLimit);
|
|
699
773
|
return true;
|
|
700
774
|
}
|
|
701
|
-
if (subPath ===
|
|
775
|
+
if (subPath === '/rate-limit/resources' && method === 'GET') {
|
|
702
776
|
await handleRateLimitResources(res, client.rateLimit);
|
|
703
777
|
return true;
|
|
704
778
|
}
|
|
705
|
-
if (subPath.endsWith(
|
|
779
|
+
if (subPath.endsWith('/config') && method === 'PUT') {
|
|
706
780
|
await handleUpdateRateLimitConfig(req, res, client.rateLimit, subPath);
|
|
707
781
|
return true;
|
|
708
782
|
}
|
|
709
|
-
if (subPath.endsWith(
|
|
783
|
+
if (subPath.endsWith('/reset') && method === 'POST') {
|
|
710
784
|
await handleResetRateLimitResource(res, client.rateLimit, subPath);
|
|
711
785
|
return true;
|
|
712
786
|
}
|
|
713
|
-
const isSingleResource =
|
|
714
|
-
|
|
787
|
+
const isSingleResource =
|
|
788
|
+
subPath.startsWith('/rate-limit/resources/') &&
|
|
789
|
+
subPath.split('/').length === 4;
|
|
790
|
+
if (isSingleResource && method === 'GET') {
|
|
715
791
|
await handleRateLimitResource(res, client.rateLimit, subPath);
|
|
716
792
|
return true;
|
|
717
793
|
}
|
|
@@ -724,15 +800,15 @@ async function routeClientApi(req, res, client, subPath, method, query) {
|
|
|
724
800
|
return true;
|
|
725
801
|
}
|
|
726
802
|
var MIME_TYPES = {
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
803
|
+
'.html': 'text/html',
|
|
804
|
+
'.js': 'application/javascript',
|
|
805
|
+
'.css': 'text/css',
|
|
806
|
+
'.json': 'application/json',
|
|
807
|
+
'.png': 'image/png',
|
|
808
|
+
'.svg': 'image/svg+xml',
|
|
809
|
+
'.ico': 'image/x-icon',
|
|
810
|
+
'.woff': 'font/woff',
|
|
811
|
+
'.woff2': 'font/woff2',
|
|
736
812
|
};
|
|
737
813
|
var cachedIndexHtml;
|
|
738
814
|
var clientDir;
|
|
@@ -740,20 +816,20 @@ function getCurrentDir() {
|
|
|
740
816
|
try {
|
|
741
817
|
return dirname(fileURLToPath(import.meta.url));
|
|
742
818
|
} catch {
|
|
743
|
-
return typeof __dirname !==
|
|
819
|
+
return typeof __dirname !== 'undefined' ? __dirname : process.cwd();
|
|
744
820
|
}
|
|
745
821
|
}
|
|
746
822
|
function getClientDir() {
|
|
747
823
|
if (clientDir) return clientDir;
|
|
748
824
|
const currentDir = getCurrentDir();
|
|
749
|
-
clientDir = join(currentDir,
|
|
825
|
+
clientDir = join(currentDir, '..', 'dist', 'client');
|
|
750
826
|
return clientDir;
|
|
751
827
|
}
|
|
752
828
|
function getIndexHtml() {
|
|
753
829
|
if (cachedIndexHtml) return cachedIndexHtml;
|
|
754
|
-
const indexPath = join(getClientDir(),
|
|
830
|
+
const indexPath = join(getClientDir(), 'index.html');
|
|
755
831
|
if (existsSync(indexPath)) {
|
|
756
|
-
cachedIndexHtml = readFileSync(indexPath,
|
|
832
|
+
cachedIndexHtml = readFileSync(indexPath, 'utf-8');
|
|
757
833
|
} else {
|
|
758
834
|
cachedIndexHtml = `<!DOCTYPE html>
|
|
759
835
|
<html lang="en">
|
|
@@ -771,29 +847,30 @@ function getIndexHtml() {
|
|
|
771
847
|
}
|
|
772
848
|
function serveStatic(res, pathname) {
|
|
773
849
|
const dir = getClientDir();
|
|
774
|
-
if (pathname !==
|
|
850
|
+
if (pathname !== '/' && pathname !== '/index.html') {
|
|
775
851
|
const filePath = join(dir, pathname);
|
|
776
852
|
if (existsSync(filePath)) {
|
|
777
853
|
try {
|
|
778
854
|
const content = readFileSync(filePath);
|
|
779
855
|
const ext = extname(pathname);
|
|
780
|
-
const mimeType = MIME_TYPES[ext] ??
|
|
856
|
+
const mimeType = MIME_TYPES[ext] ?? 'application/octet-stream';
|
|
781
857
|
res.writeHead(200, {
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
858
|
+
'Content-Type': mimeType,
|
|
859
|
+
'Content-Length': content.length,
|
|
860
|
+
'Cache-Control': pathname.includes('/assets/')
|
|
861
|
+
? 'public, max-age=31536000, immutable'
|
|
862
|
+
: 'no-cache',
|
|
785
863
|
});
|
|
786
864
|
res.end(content);
|
|
787
865
|
return true;
|
|
788
|
-
} catch {
|
|
789
|
-
}
|
|
866
|
+
} catch {}
|
|
790
867
|
}
|
|
791
868
|
}
|
|
792
869
|
const html = getIndexHtml();
|
|
793
870
|
res.writeHead(200, {
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
871
|
+
'Content-Type': 'text/html',
|
|
872
|
+
'Content-Length': Buffer.byteLength(html),
|
|
873
|
+
'Cache-Control': 'no-cache',
|
|
797
874
|
});
|
|
798
875
|
res.end(html);
|
|
799
876
|
return true;
|
|
@@ -804,33 +881,42 @@ function createDashboard(options) {
|
|
|
804
881
|
const opts = validateDashboardOptions(options);
|
|
805
882
|
const clients = /* @__PURE__ */ new Map();
|
|
806
883
|
for (const clientConfig of opts.clients) {
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
884
|
+
const normalized = normalizeClient(clientConfig);
|
|
885
|
+
clients.set(normalized.name, {
|
|
886
|
+
name: normalized.name,
|
|
887
|
+
cache: normalized.cacheStore
|
|
888
|
+
? detectCacheAdapter(normalized.cacheStore)
|
|
889
|
+
: void 0,
|
|
890
|
+
dedup: normalized.dedupeStore
|
|
891
|
+
? detectDedupeAdapter(normalized.dedupeStore)
|
|
892
|
+
: void 0,
|
|
893
|
+
rateLimit: normalized.rateLimitStore
|
|
894
|
+
? detectRateLimitAdapter(normalized.rateLimitStore)
|
|
895
|
+
: void 0,
|
|
812
896
|
});
|
|
813
897
|
}
|
|
814
898
|
const ctx = {
|
|
815
899
|
clients,
|
|
816
|
-
pollIntervalMs: opts.pollIntervalMs
|
|
900
|
+
pollIntervalMs: opts.pollIntervalMs,
|
|
817
901
|
};
|
|
818
902
|
const apiRouter = createApiRouter(ctx);
|
|
819
903
|
return (req, res, _next) => {
|
|
820
904
|
const { pathname, query } = parseUrl(req, opts.basePath);
|
|
821
|
-
apiRouter(req, res, pathname, query)
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
905
|
+
apiRouter(req, res, pathname, query)
|
|
906
|
+
.then((handled) => {
|
|
907
|
+
if (!handled) {
|
|
908
|
+
serveStatic(res, pathname);
|
|
909
|
+
}
|
|
910
|
+
})
|
|
911
|
+
.catch(() => {
|
|
912
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
913
|
+
res.end(JSON.stringify({ error: 'Internal server error' }));
|
|
914
|
+
});
|
|
829
915
|
};
|
|
830
916
|
}
|
|
831
917
|
var JSON_HEADERS = {
|
|
832
|
-
|
|
833
|
-
|
|
918
|
+
'Content-Type': 'application/json',
|
|
919
|
+
'Cache-Control': 'no-store',
|
|
834
920
|
};
|
|
835
921
|
function jsonResponse(data, status = 200) {
|
|
836
922
|
return new Response(JSON.stringify(data), { status, headers: JSON_HEADERS });
|
|
@@ -839,15 +925,15 @@ function errorResponse(message, status = 500) {
|
|
|
839
925
|
return jsonResponse({ error: message }, status);
|
|
840
926
|
}
|
|
841
927
|
var MIME_TYPES2 = {
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
928
|
+
'.html': 'text/html',
|
|
929
|
+
'.js': 'application/javascript',
|
|
930
|
+
'.css': 'text/css',
|
|
931
|
+
'.json': 'application/json',
|
|
932
|
+
'.png': 'image/png',
|
|
933
|
+
'.svg': 'image/svg+xml',
|
|
934
|
+
'.ico': 'image/x-icon',
|
|
935
|
+
'.woff': 'font/woff',
|
|
936
|
+
'.woff2': 'font/woff2',
|
|
851
937
|
};
|
|
852
938
|
var cachedIndexHtml2;
|
|
853
939
|
var clientDir2;
|
|
@@ -855,20 +941,20 @@ function getCurrentDir2() {
|
|
|
855
941
|
try {
|
|
856
942
|
return dirname(fileURLToPath(import.meta.url));
|
|
857
943
|
} catch {
|
|
858
|
-
return typeof __dirname !==
|
|
944
|
+
return typeof __dirname !== 'undefined' ? __dirname : process.cwd();
|
|
859
945
|
}
|
|
860
946
|
}
|
|
861
947
|
function getClientDir2() {
|
|
862
948
|
if (clientDir2) return clientDir2;
|
|
863
949
|
const currentDir = getCurrentDir2();
|
|
864
|
-
clientDir2 = join(currentDir,
|
|
950
|
+
clientDir2 = join(currentDir, '..', 'dist', 'client');
|
|
865
951
|
return clientDir2;
|
|
866
952
|
}
|
|
867
953
|
function getIndexHtml2() {
|
|
868
954
|
if (cachedIndexHtml2) return cachedIndexHtml2;
|
|
869
|
-
const indexPath = join(getClientDir2(),
|
|
955
|
+
const indexPath = join(getClientDir2(), 'index.html');
|
|
870
956
|
if (existsSync(indexPath)) {
|
|
871
|
-
cachedIndexHtml2 = readFileSync(indexPath,
|
|
957
|
+
cachedIndexHtml2 = readFileSync(indexPath, 'utf-8');
|
|
872
958
|
} else {
|
|
873
959
|
cachedIndexHtml2 = `<!DOCTYPE html>
|
|
874
960
|
<html lang="en">
|
|
@@ -886,59 +972,66 @@ function getIndexHtml2() {
|
|
|
886
972
|
}
|
|
887
973
|
function serveStaticWeb(pathname) {
|
|
888
974
|
const dir = getClientDir2();
|
|
889
|
-
if (pathname !==
|
|
975
|
+
if (pathname !== '/' && pathname !== '/index.html') {
|
|
890
976
|
const filePath = join(dir, pathname);
|
|
891
977
|
if (existsSync(filePath)) {
|
|
892
978
|
try {
|
|
893
979
|
const content = readFileSync(filePath);
|
|
894
980
|
const ext = extname(pathname);
|
|
895
|
-
const mimeType = MIME_TYPES2[ext] ??
|
|
981
|
+
const mimeType = MIME_TYPES2[ext] ?? 'application/octet-stream';
|
|
896
982
|
return new Response(content, {
|
|
897
983
|
status: 200,
|
|
898
984
|
headers: {
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
985
|
+
'Content-Type': mimeType,
|
|
986
|
+
'Content-Length': String(content.length),
|
|
987
|
+
'Cache-Control': pathname.includes('/assets/')
|
|
988
|
+
? 'public, max-age=31536000, immutable'
|
|
989
|
+
: 'no-cache',
|
|
990
|
+
},
|
|
903
991
|
});
|
|
904
|
-
} catch {
|
|
905
|
-
}
|
|
992
|
+
} catch {}
|
|
906
993
|
}
|
|
907
994
|
}
|
|
908
995
|
const html = getIndexHtml2();
|
|
909
996
|
return new Response(html, {
|
|
910
997
|
status: 200,
|
|
911
998
|
headers: {
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
}
|
|
999
|
+
'Content-Type': 'text/html',
|
|
1000
|
+
'Cache-Control': 'no-cache',
|
|
1001
|
+
},
|
|
915
1002
|
});
|
|
916
1003
|
}
|
|
917
1004
|
var CLIENT_ROUTE_REGEX2 = /^\/api\/clients\/([a-zA-Z0-9_-]+)(\/.*)?$/;
|
|
918
1005
|
function clientStoreInfo2(client) {
|
|
919
1006
|
return {
|
|
920
|
-
cache: client.cache
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
capabilities: client.
|
|
925
|
-
|
|
1007
|
+
cache: client.cache
|
|
1008
|
+
? { type: client.cache.type, capabilities: client.cache.capabilities }
|
|
1009
|
+
: null,
|
|
1010
|
+
dedup: client.dedup
|
|
1011
|
+
? { type: client.dedup.type, capabilities: client.dedup.capabilities }
|
|
1012
|
+
: null,
|
|
1013
|
+
rateLimit: client.rateLimit
|
|
1014
|
+
? {
|
|
1015
|
+
type: client.rateLimit.type,
|
|
1016
|
+
capabilities: client.rateLimit.capabilities,
|
|
1017
|
+
}
|
|
1018
|
+
: null,
|
|
926
1019
|
};
|
|
927
1020
|
}
|
|
928
1021
|
async function routeApi(request, pathname, query, ctx) {
|
|
929
1022
|
const method = request.method.toUpperCase();
|
|
930
|
-
if (pathname ===
|
|
1023
|
+
if (pathname === '/api/health' && method === 'GET') {
|
|
931
1024
|
const clients = {};
|
|
932
1025
|
for (const [name, client] of ctx.clients) {
|
|
933
1026
|
clients[name] = clientStoreInfo2(client);
|
|
934
1027
|
}
|
|
935
1028
|
return jsonResponse({
|
|
936
|
-
status:
|
|
1029
|
+
status: 'ok',
|
|
937
1030
|
clients,
|
|
938
|
-
pollIntervalMs: ctx.pollIntervalMs
|
|
1031
|
+
pollIntervalMs: ctx.pollIntervalMs,
|
|
939
1032
|
});
|
|
940
1033
|
}
|
|
941
|
-
if (pathname ===
|
|
1034
|
+
if (pathname === '/api/clients' && method === 'GET') {
|
|
942
1035
|
const clientList = [];
|
|
943
1036
|
for (const [name, client] of ctx.clients) {
|
|
944
1037
|
clientList.push({ name, stores: clientStoreInfo2(client) });
|
|
@@ -948,168 +1041,180 @@ async function routeApi(request, pathname, query, ctx) {
|
|
|
948
1041
|
const clientMatch = pathname.match(CLIENT_ROUTE_REGEX2);
|
|
949
1042
|
if (clientMatch) {
|
|
950
1043
|
const clientName = clientMatch[1];
|
|
951
|
-
const subPath = clientMatch[2] ??
|
|
1044
|
+
const subPath = clientMatch[2] ?? '';
|
|
952
1045
|
const client = ctx.clients.get(clientName);
|
|
953
1046
|
if (!client) {
|
|
954
1047
|
return errorResponse(`Unknown client: ${clientName}`, 404);
|
|
955
1048
|
}
|
|
956
1049
|
return routeClientApi2(request, subPath, method, client, query);
|
|
957
1050
|
}
|
|
958
|
-
if (pathname.startsWith(
|
|
959
|
-
return errorResponse(
|
|
1051
|
+
if (pathname.startsWith('/api/')) {
|
|
1052
|
+
return errorResponse('Not found', 404);
|
|
960
1053
|
}
|
|
961
1054
|
return null;
|
|
962
1055
|
}
|
|
963
1056
|
async function routeClientApi2(request, subPath, method, client, query) {
|
|
964
|
-
if (subPath.startsWith(
|
|
965
|
-
if (!client.cache) return errorResponse(
|
|
1057
|
+
if (subPath.startsWith('/cache')) {
|
|
1058
|
+
if (!client.cache) return errorResponse('Cache store not configured', 404);
|
|
966
1059
|
return routeCache(subPath, method, client.cache, query);
|
|
967
1060
|
}
|
|
968
|
-
if (subPath.startsWith(
|
|
969
|
-
if (!client.dedup) return errorResponse(
|
|
1061
|
+
if (subPath.startsWith('/dedup')) {
|
|
1062
|
+
if (!client.dedup) return errorResponse('Dedup store not configured', 404);
|
|
970
1063
|
return routeDedup(subPath, method, client.dedup, query);
|
|
971
1064
|
}
|
|
972
|
-
if (subPath.startsWith(
|
|
1065
|
+
if (subPath.startsWith('/rate-limit')) {
|
|
973
1066
|
if (!client.rateLimit)
|
|
974
|
-
return errorResponse(
|
|
1067
|
+
return errorResponse('Rate limit store not configured', 404);
|
|
975
1068
|
return routeRateLimit(request, subPath, method, client.rateLimit);
|
|
976
1069
|
}
|
|
977
|
-
return errorResponse(
|
|
1070
|
+
return errorResponse('Not found', 404);
|
|
978
1071
|
}
|
|
979
1072
|
async function routeCache(pathname, method, adapter, query) {
|
|
980
1073
|
try {
|
|
981
|
-
if (pathname ===
|
|
1074
|
+
if (pathname === '/cache/stats' && method === 'GET') {
|
|
982
1075
|
const stats = await adapter.getStats();
|
|
983
1076
|
return jsonResponse({ stats, capabilities: adapter.capabilities });
|
|
984
1077
|
}
|
|
985
|
-
if (pathname ===
|
|
986
|
-
const page = parseInt(query.get(
|
|
987
|
-
const limit = parseInt(query.get(
|
|
1078
|
+
if (pathname === '/cache/entries' && method === 'GET') {
|
|
1079
|
+
const page = parseInt(query.get('page') ?? '0', 10);
|
|
1080
|
+
const limit = parseInt(query.get('limit') ?? '50', 10);
|
|
988
1081
|
return jsonResponse(await adapter.listEntries(page, limit));
|
|
989
1082
|
}
|
|
990
|
-
if (pathname ===
|
|
1083
|
+
if (pathname === '/cache/entries' && method === 'DELETE') {
|
|
991
1084
|
await adapter.clearAll();
|
|
992
1085
|
return jsonResponse({ cleared: true });
|
|
993
1086
|
}
|
|
994
|
-
const isSingleEntry =
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
1087
|
+
const isSingleEntry =
|
|
1088
|
+
pathname.startsWith('/cache/entries/') &&
|
|
1089
|
+
pathname.split('/').length === 4;
|
|
1090
|
+
if (isSingleEntry && method === 'GET') {
|
|
1091
|
+
const hash = extractParam(pathname, '/cache/entries/:hash');
|
|
1092
|
+
if (!hash) return errorResponse('Not found', 404);
|
|
998
1093
|
const entry = await adapter.getEntry(hash);
|
|
999
|
-
if (entry === void 0) return errorResponse(
|
|
1094
|
+
if (entry === void 0) return errorResponse('Not found', 404);
|
|
1000
1095
|
return jsonResponse({ hash, value: entry });
|
|
1001
1096
|
}
|
|
1002
|
-
if (isSingleEntry && method ===
|
|
1003
|
-
const hash = extractParam(pathname,
|
|
1004
|
-
if (!hash) return errorResponse(
|
|
1097
|
+
if (isSingleEntry && method === 'DELETE') {
|
|
1098
|
+
const hash = extractParam(pathname, '/cache/entries/:hash');
|
|
1099
|
+
if (!hash) return errorResponse('Not found', 404);
|
|
1005
1100
|
await adapter.deleteEntry(hash);
|
|
1006
1101
|
return jsonResponse({ deleted: true });
|
|
1007
1102
|
}
|
|
1008
1103
|
if (isSingleEntry) {
|
|
1009
|
-
return errorResponse(
|
|
1104
|
+
return errorResponse('Method not allowed', 405);
|
|
1010
1105
|
}
|
|
1011
1106
|
} catch (err) {
|
|
1012
|
-
return errorResponse(err instanceof Error ? err.message :
|
|
1107
|
+
return errorResponse(err instanceof Error ? err.message : 'Unknown error');
|
|
1013
1108
|
}
|
|
1014
|
-
return errorResponse(
|
|
1109
|
+
return errorResponse('Not found', 404);
|
|
1015
1110
|
}
|
|
1016
1111
|
async function routeDedup(pathname, method, adapter, query) {
|
|
1017
1112
|
try {
|
|
1018
|
-
if (pathname ===
|
|
1113
|
+
if (pathname === '/dedup/stats' && method === 'GET') {
|
|
1019
1114
|
const stats = await adapter.getStats();
|
|
1020
1115
|
return jsonResponse({ stats, capabilities: adapter.capabilities });
|
|
1021
1116
|
}
|
|
1022
|
-
if (pathname ===
|
|
1023
|
-
const page = parseInt(query.get(
|
|
1024
|
-
const limit = parseInt(query.get(
|
|
1117
|
+
if (pathname === '/dedup/jobs' && method === 'GET') {
|
|
1118
|
+
const page = parseInt(query.get('page') ?? '0', 10);
|
|
1119
|
+
const limit = parseInt(query.get('limit') ?? '50', 10);
|
|
1025
1120
|
return jsonResponse(await adapter.listJobs(page, limit));
|
|
1026
1121
|
}
|
|
1027
|
-
const isSingleJob =
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1122
|
+
const isSingleJob =
|
|
1123
|
+
pathname.startsWith('/dedup/jobs/') && pathname.split('/').length === 4;
|
|
1124
|
+
if (isSingleJob && method === 'GET') {
|
|
1125
|
+
const hash = extractParam(pathname, '/dedup/jobs/:hash');
|
|
1126
|
+
if (!hash) return errorResponse('Not found', 404);
|
|
1031
1127
|
const job = await adapter.getJob(hash);
|
|
1032
|
-
if (!job) return errorResponse(
|
|
1128
|
+
if (!job) return errorResponse('Not found', 404);
|
|
1033
1129
|
return jsonResponse(job);
|
|
1034
1130
|
}
|
|
1035
1131
|
if (isSingleJob) {
|
|
1036
|
-
return errorResponse(
|
|
1132
|
+
return errorResponse('Method not allowed', 405);
|
|
1037
1133
|
}
|
|
1038
1134
|
} catch (err) {
|
|
1039
|
-
return errorResponse(err instanceof Error ? err.message :
|
|
1135
|
+
return errorResponse(err instanceof Error ? err.message : 'Unknown error');
|
|
1040
1136
|
}
|
|
1041
|
-
return errorResponse(
|
|
1137
|
+
return errorResponse('Not found', 404);
|
|
1042
1138
|
}
|
|
1043
1139
|
async function routeRateLimit(request, pathname, method, adapter) {
|
|
1044
1140
|
try {
|
|
1045
|
-
if (pathname ===
|
|
1141
|
+
if (pathname === '/rate-limit/stats' && method === 'GET') {
|
|
1046
1142
|
const stats = await adapter.getStats();
|
|
1047
1143
|
return jsonResponse({ stats, capabilities: adapter.capabilities });
|
|
1048
1144
|
}
|
|
1049
|
-
if (pathname ===
|
|
1145
|
+
if (pathname === '/rate-limit/resources' && method === 'GET') {
|
|
1050
1146
|
const resources = await adapter.listResources();
|
|
1051
1147
|
return jsonResponse({ resources });
|
|
1052
1148
|
}
|
|
1053
|
-
if (pathname.endsWith(
|
|
1054
|
-
const name = extractParam(pathname,
|
|
1055
|
-
if (!name) return errorResponse(
|
|
1149
|
+
if (pathname.endsWith('/config') && method === 'PUT') {
|
|
1150
|
+
const name = extractParam(pathname, '/rate-limit/resources/:name/config');
|
|
1151
|
+
if (!name) return errorResponse('Not found', 404);
|
|
1056
1152
|
const body = await request.json();
|
|
1057
1153
|
await adapter.updateResourceConfig(name, body);
|
|
1058
1154
|
return jsonResponse({ updated: true });
|
|
1059
1155
|
}
|
|
1060
|
-
if (pathname.endsWith(
|
|
1061
|
-
const name = extractParam(pathname,
|
|
1062
|
-
if (!name) return errorResponse(
|
|
1156
|
+
if (pathname.endsWith('/reset') && method === 'POST') {
|
|
1157
|
+
const name = extractParam(pathname, '/rate-limit/resources/:name/reset');
|
|
1158
|
+
if (!name) return errorResponse('Not found', 404);
|
|
1063
1159
|
await adapter.resetResource(name);
|
|
1064
1160
|
return jsonResponse({ reset: true });
|
|
1065
1161
|
}
|
|
1066
|
-
const isSingleResource =
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1162
|
+
const isSingleResource =
|
|
1163
|
+
pathname.startsWith('/rate-limit/resources/') &&
|
|
1164
|
+
pathname.split('/').length === 4;
|
|
1165
|
+
if (isSingleResource && method === 'GET') {
|
|
1166
|
+
const name = extractParam(pathname, '/rate-limit/resources/:name');
|
|
1167
|
+
if (!name) return errorResponse('Not found', 404);
|
|
1070
1168
|
const status = await adapter.getResourceStatus(name);
|
|
1071
1169
|
return jsonResponse({ resource: name, ...status });
|
|
1072
1170
|
}
|
|
1073
1171
|
if (isSingleResource) {
|
|
1074
|
-
return errorResponse(
|
|
1172
|
+
return errorResponse('Method not allowed', 405);
|
|
1075
1173
|
}
|
|
1076
1174
|
} catch (err) {
|
|
1077
|
-
return errorResponse(err instanceof Error ? err.message :
|
|
1175
|
+
return errorResponse(err instanceof Error ? err.message : 'Unknown error');
|
|
1078
1176
|
}
|
|
1079
|
-
return errorResponse(
|
|
1177
|
+
return errorResponse('Not found', 404);
|
|
1080
1178
|
}
|
|
1081
1179
|
function createDashboardHandler(options) {
|
|
1082
1180
|
const opts = validateDashboardOptions(options);
|
|
1083
1181
|
const clients = /* @__PURE__ */ new Map();
|
|
1084
1182
|
for (const clientConfig of opts.clients) {
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1183
|
+
const normalized = normalizeClient(clientConfig);
|
|
1184
|
+
clients.set(normalized.name, {
|
|
1185
|
+
name: normalized.name,
|
|
1186
|
+
cache: normalized.cacheStore
|
|
1187
|
+
? detectCacheAdapter(normalized.cacheStore)
|
|
1188
|
+
: void 0,
|
|
1189
|
+
dedup: normalized.dedupeStore
|
|
1190
|
+
? detectDedupeAdapter(normalized.dedupeStore)
|
|
1191
|
+
: void 0,
|
|
1192
|
+
rateLimit: normalized.rateLimitStore
|
|
1193
|
+
? detectRateLimitAdapter(normalized.rateLimitStore)
|
|
1194
|
+
: void 0,
|
|
1090
1195
|
});
|
|
1091
1196
|
}
|
|
1092
1197
|
const ctx = {
|
|
1093
1198
|
clients,
|
|
1094
|
-
pollIntervalMs: opts.pollIntervalMs
|
|
1199
|
+
pollIntervalMs: opts.pollIntervalMs,
|
|
1095
1200
|
};
|
|
1096
1201
|
return async (request) => {
|
|
1097
1202
|
try {
|
|
1098
1203
|
const url = new URL(request.url);
|
|
1099
1204
|
let pathname = url.pathname;
|
|
1100
|
-
if (opts.basePath !==
|
|
1101
|
-
pathname = pathname.slice(opts.basePath.length) ||
|
|
1205
|
+
if (opts.basePath !== '/' && pathname.startsWith(opts.basePath)) {
|
|
1206
|
+
pathname = pathname.slice(opts.basePath.length) || '/';
|
|
1102
1207
|
}
|
|
1103
1208
|
const apiResponse = await routeApi(
|
|
1104
1209
|
request,
|
|
1105
1210
|
pathname,
|
|
1106
1211
|
url.searchParams,
|
|
1107
|
-
ctx
|
|
1212
|
+
ctx,
|
|
1108
1213
|
);
|
|
1109
1214
|
if (apiResponse) return apiResponse;
|
|
1110
1215
|
return serveStaticWeb(pathname);
|
|
1111
1216
|
} catch {
|
|
1112
|
-
return errorResponse(
|
|
1217
|
+
return errorResponse('Internal server error', 500);
|
|
1113
1218
|
}
|
|
1114
1219
|
};
|
|
1115
1220
|
}
|
|
@@ -1120,10 +1225,11 @@ async function startDashboard(options) {
|
|
|
1120
1225
|
middleware(req, res);
|
|
1121
1226
|
});
|
|
1122
1227
|
return new Promise((resolve, reject) => {
|
|
1123
|
-
server.on(
|
|
1228
|
+
server.on('error', reject);
|
|
1124
1229
|
server.listen(opts.port, opts.host, () => {
|
|
1125
1230
|
const addr = server.address();
|
|
1126
|
-
const url =
|
|
1231
|
+
const url =
|
|
1232
|
+
typeof addr === 'string' ? addr : `http://${opts.host}:${opts.port}`;
|
|
1127
1233
|
console.log(`Dashboard running at ${url}`);
|
|
1128
1234
|
resolve({
|
|
1129
1235
|
server,
|
|
@@ -1134,7 +1240,7 @@ async function startDashboard(options) {
|
|
|
1134
1240
|
else res();
|
|
1135
1241
|
});
|
|
1136
1242
|
});
|
|
1137
|
-
}
|
|
1243
|
+
},
|
|
1138
1244
|
});
|
|
1139
1245
|
});
|
|
1140
1246
|
});
|
|
@@ -1142,4 +1248,4 @@ async function startDashboard(options) {
|
|
|
1142
1248
|
|
|
1143
1249
|
export { createDashboard, createDashboardHandler, startDashboard };
|
|
1144
1250
|
//# sourceMappingURL=index.js.map
|
|
1145
|
-
//# sourceMappingURL=index.js.map
|
|
1251
|
+
//# sourceMappingURL=index.js.map
|